import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import LoadingScreen from "./components/LoadingScreen"; import CaptioningView from "./components/CaptioningView"; import WelcomeScreen from "./components/WelcomeScreen"; import WebcamPermissionDialog from "./components/WebcamPermissionDialog"; import type { AppState } from "./types"; export default function App() { const [appState, setAppState] = useState("welcome"); const [mediaStream, setMediaStream] = useState(null); const [fileUrl, setFileUrl] = useState(null); const [isVideoReady, setIsVideoReady] = useState(false); const videoRef = useRef(null); const handlePermissionGranted = useCallback((stream: MediaStream) => { // Clean up any previous source setFileUrl((prev: string | null) => { if (prev) URL.revokeObjectURL(prev); return null; }); stopStreamTracks(mediaStream); setMediaStream(stream); setAppState("welcome"); }, []); const handleStart = useCallback(() => { setAppState("loading"); }, []); const handleLoadingComplete = useCallback(() => { setAppState("captioning"); }, []); const playVideo = useCallback(async (video: HTMLVideoElement) => { try { await video.play(); } catch (error) { console.error("Failed to play video:", error); } }, []); const setupVideo = useCallback( (video: HTMLVideoElement, source: { stream?: MediaStream; url?: string | null }) => { // Reset current src/srcObject first video.pause(); video.src = ""; (video as HTMLVideoElement & { srcObject: MediaStream | null }).srcObject = null; if (source.stream) { (video as HTMLVideoElement & { srcObject: MediaStream | null }).srcObject = source.stream; } else if (source.url) { video.src = source.url; } const handleCanPlay = () => { setIsVideoReady(true); playVideo(video); }; video.addEventListener("canplay", handleCanPlay, { once: true }); return () => { video.removeEventListener("canplay", handleCanPlay); }; }, [playVideo], ); useEffect(() => { const video = videoRef.current; if (!video) return; const cleanup = setupVideo(video, { stream: mediaStream ?? undefined, url: fileUrl }); return cleanup; }, [mediaStream, fileUrl, setupVideo]); const stopStreamTracks = (stream: MediaStream | null) => { stream?.getTracks().forEach((t) => { try { t.stop(); } catch (e) { // ignore } }); }; const handleSelectCamera = useCallback(() => { // Move to permission flow; component will request permission setAppState("requesting-permission"); }, []); const handleSelectDisplay = useCallback(async () => { try { // Clean up previous stopStreamTracks(mediaStream); setMediaStream(null); setFileUrl((prev: string | null) => { if (prev) URL.revokeObjectURL(prev); return null; }); if (!navigator.mediaDevices?.getDisplayMedia) { throw new Error("Screen capture not supported in this browser"); } const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false, }); setMediaStream(stream); // Ensure we are on welcome where Start is visible setAppState("welcome"); } catch (e) { console.error("Failed to start display capture:", e); // Stay on welcome; user can retry } }, [mediaStream]); const handleSelectFile = useCallback((file: File) => { // Clean up previous sources stopStreamTracks(mediaStream); setMediaStream(null); setFileUrl((prev: string | null) => { if (prev) URL.revokeObjectURL(prev); return null; }); const url = URL.createObjectURL(file); setFileUrl(url); setAppState("welcome"); }, [mediaStream]); const videoBlurState = useMemo(() => { switch (appState) { case "requesting-permission": return "blur(20px) brightness(0.2) saturate(0.5)"; case "welcome": return "blur(12px) brightness(0.3) saturate(0.7)"; case "loading": return "blur(8px) brightness(0.4) saturate(0.8)"; case "captioning": return "none"; default: return "blur(20px) brightness(0.2) saturate(0.5)"; } }, [appState]); return (