import { useCallback, useEffect, useState } from "react"; import { prepareStream } from "@/backend/extension/streams"; import { Button } from "@/components/buttons/Button"; import { Toggle } from "@/components/buttons/Toggle"; import { Dropdown } from "@/components/form/Dropdown"; import { Icon, Icons } from "@/components/Icon"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { convertProviderCaption } from "@/components/player/utils/captions"; import { Title } from "@/components/text/Title"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { TextInputControl } from "@/components/text-inputs/TextInputControl"; import { Divider } from "@/components/utils/Divider"; import { PlaybackErrorPart } from "@/pages/parts/player/PlaybackErrorPart"; import { PlayerPart } from "@/pages/parts/player/PlayerPart"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { SourceSliceSource, StreamType } from "@/stores/player/utils/qualities"; import { type ExtensionStatus, getExtensionState } from "@/utils/extension"; const testMeta: PlayerMeta = { releaseYear: 2010, title: "Sintel", tmdbId: "45745", type: "movie", poster: "https://image.tmdb.org/t/p/w342//4BMG9hk9NvSBeQvC82sVmVRK140.jpg", }; const testStreams: Record = { hls: "https://alpha-charlott.github.io/video-openh264/Sintel_master.m3u8", mp4: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4", }; const streamTypes: Record = { hls: "HLS", mp4: "MP4", }; export default function VideoTesterView() { const { status, playMedia, setMeta, reset } = usePlayer(); const [selected, setSelected] = useState("mp4"); const [inputSource, setInputSource] = useState(""); const [extensionState, setExtensionState] = useState("unknown"); const [headersEnabled, setHeadersEnabled] = useState(false); const [headers, setHeaders] = useState>( [{ key: "", value: "" }], ); // Check extension state on mount useEffect(() => { getExtensionState().then(setExtensionState); }, []); // Header management functions const addHeader = useCallback(() => { setHeaders((prev) => [...prev, { key: "", value: "" }]); }, []); const updateHeader = useCallback( (index: number, field: "key" | "value", value: string) => { setHeaders((prev) => prev.map((header, i) => i === index ? { ...header, [field]: value } : header, ), ); }, [], ); const removeHeader = useCallback((index: number) => { setHeaders((prev) => prev.filter((_, i) => i !== index)); }, []); const toggleHeaders = useCallback(() => { const newEnabled = !headersEnabled; setHeadersEnabled(newEnabled); if (!newEnabled) { setHeaders([{ key: "", value: "" }]); } }, [headersEnabled]); const start = useCallback( async (url: string, type: StreamType) => { // Build headers object from enabled headers const headersObj: Record = {}; if (headersEnabled) { headers.forEach(({ key, value }) => { if (key.trim() && value.trim()) { headersObj[key.trim()] = value.trim(); } }); } let source: SourceSliceSource; if (type === "hls") { source = { type: "hls", url, ...(Object.keys(headersObj).length > 0 && { headers: headersObj }), }; } else if (type === "mp4") { source = { type: "file", qualities: { unknown: { type: "mp4", url, }, }, ...(Object.keys(headersObj).length > 0 && { headers: headersObj }), }; } else throw new Error("Invalid type"); // Prepare stream headers if extension is active and headers are present if (extensionState === "success" && Object.keys(headersObj).length > 0) { // Create a mock Stream object for prepareStream const mockStream: any = { type: type === "hls" ? "hls" : "file", ...(type === "hls" ? { playlist: url } : { qualities: { unknown: { type: "mp4", url, }, }, }), headers: headersObj, }; try { await prepareStream(mockStream); } catch (error) { console.warn("Failed to prepare stream headers:", error); } } setMeta(testMeta); playMedia(source, [], null); }, [playMedia, setMeta, headersEnabled, headers, extensionState], ); const startFromCli = useCallback(async () => { try { const clipboardText = await navigator.clipboard.readText(); // Parse JavaScript object notation by evaluating it safely let cliData; try { // Try to parse as JSON first (in case it's already valid JSON) cliData = JSON.parse(clipboardText); } catch { // If JSON parsing fails, try to evaluate as JavaScript object try { // Use Function constructor to safely evaluate the JavaScript object // eslint-disable-next-line no-new-func cliData = new Function(`return (${clipboardText})`)(); } catch { throw new Error( "Invalid JavaScript object format. Please ensure the CLI output is properly formatted.", ); } } if ( !cliData.stream || !Array.isArray(cliData.stream) || cliData.stream.length === 0 ) { throw new Error("Invalid CLI output: no stream data found"); } const streamData = cliData.stream[0]; // Take the first stream let source: SourceSliceSource; if (streamData.type === "hls") { source = { type: "hls", url: streamData.playlist, ...(streamData.headers && { headers: streamData.headers }), }; } else if (streamData.type === "file") { // Handle file type streams const qualities = streamData.qualities || {}; const qualityKeys = Object.keys(qualities); if (qualityKeys.length === 0) { throw new Error("Invalid file stream: no qualities found"); } source = { type: "file", qualities, ...(streamData.headers && { headers: streamData.headers }), }; } else { throw new Error(`Unsupported stream type: ${streamData.type}`); } // Convert captions const captions = streamData.captions ? convertProviderCaption(streamData.captions) : []; // Prepare stream headers if extension is active and headers are present if ( extensionState === "success" && streamData.headers && Object.keys(streamData.headers).length > 0 ) { try { await prepareStream(streamData); } catch (error) { console.warn("Failed to prepare stream headers:", error); } } setMeta(testMeta); playMedia(source, captions, streamData.id); } catch (error) { console.error("Failed to parse CLI data:", error); let errorMessage = error instanceof Error ? error.message : "Unknown error"; // Check for common JSON/JavaScript formatting issues if ( errorMessage.includes("Expected property name") || errorMessage.includes("Unexpected token") ) { errorMessage += "\n\nThe CLI output should be in JavaScript object format. Make sure you're copying the complete output from your CLI tool."; } // eslint-disable-next-line no-alert alert(`Failed to parse CLI data: ${errorMessage}`); } }, [playMedia, setMeta, extensionState]); // player meta and streams carry over, so reset on mount useEffect(() => { if (status !== playerStatus.IDLE) { reset(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( {status === playerStatus.IDLE ? (
Custom stream
({ id: v[0], name: v[1], }))} selectedItem={{ id: selected, name: streamTypes[selected as StreamType], }} setSelectedItem={(item) => setSelected(item.id)} />
{extensionState === "success" && (

Headers

{headersEnabled && ( <>
{headers.length === 0 ? (

No headers configured.

) : ( headers.map((header, index) => (
updateHeader(index, "key", value) } placeholder="Key" /> updateHeader(index, "value", value) } placeholder="Value" />
)) )}
)}
)}
Preset tests
) : null} {status === playerStatus.PLAYBACK_ERROR ? : null}
); }