Spaces:
Running
Running
| import { useState, useEffect, useRef, useCallback, useMemo } from "react"; | |
| import LoadingScreen from "./components/LoadingScreen"; | |
| import CaptioningView from "./components/CaptioningView"; | |
| import WelcomeScreen from "./components/WelcomeScreen"; | |
| import SourceSelector from "./components/SourceSelector"; | |
| import type { AppState, VideoSource } from "./types"; | |
| export default function App() { | |
| const [appState, setAppState] = useState<AppState>("source-selection"); | |
| const [videoSource, setVideoSource] = useState<VideoSource | null>(null); | |
| const [isVideoReady, setIsVideoReady] = useState(false); | |
| const videoRef = useRef<HTMLVideoElement | null>(null); | |
| const handleSourceSelected = useCallback((source: VideoSource) => { | |
| setVideoSource(source); | |
| setAppState("welcome"); | |
| }, []); | |
| const handleBackToSourceSelection = useCallback(() => { | |
| // Clean up current source | |
| if (videoSource?.stream) { | |
| videoSource.stream.getTracks().forEach(track => track.stop()); | |
| } | |
| if (videoSource?.url && videoSource.type === "upload") { | |
| URL.revokeObjectURL(videoSource.url); | |
| } | |
| setVideoSource(null); | |
| setIsVideoReady(false); | |
| setAppState("source-selection"); | |
| }, [videoSource]); | |
| 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: VideoSource) => { | |
| // Reset video element | |
| video.srcObject = null; | |
| video.src = ""; | |
| video.load(); | |
| if (source.stream) { | |
| // Stream-based sources (webcam, screen share) | |
| video.srcObject = source.stream; | |
| } else if (source.url) { | |
| // URL-based sources (upload, examples) | |
| video.src = source.url; | |
| video.loop = true; // Loop for uploaded/example videos | |
| } | |
| const handleCanPlay = () => { | |
| setIsVideoReady(true); | |
| playVideo(video); | |
| }; | |
| video.addEventListener("canplay", handleCanPlay, { once: true }); | |
| // Handle stream ended (e.g., user stops screen sharing) | |
| if (source.stream) { | |
| const track = source.stream.getVideoTracks()[0]; | |
| if (track) { | |
| track.onended = () => { | |
| handleBackToSourceSelection(); | |
| }; | |
| } | |
| } | |
| return () => { | |
| video.removeEventListener("canplay", handleCanPlay); | |
| }; | |
| }, | |
| [playVideo, handleBackToSourceSelection], | |
| ); | |
| useEffect(() => { | |
| if (videoSource && videoRef.current) { | |
| const video = videoRef.current; | |
| const cleanup = setupVideo(video, videoSource); | |
| return cleanup; | |
| } | |
| }, [videoSource, setupVideo]); | |
| const videoBlurState = useMemo(() => { | |
| switch (appState) { | |
| case "source-selection": | |
| 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]); | |
| const getSourceLabel = () => { | |
| if (!videoSource) return null; | |
| switch (videoSource.type) { | |
| case "webcam": | |
| return "Webcam"; | |
| case "screen": | |
| return "Screen Share"; | |
| case "upload": | |
| return videoSource.name || "Uploaded Video"; | |
| case "example": | |
| return videoSource.name || "Example Video"; | |
| } | |
| }; | |
| return ( | |
| <div className="App relative h-screen overflow-hidden bg-[#FFFAEB]"> | |
| <div className="absolute inset-0 bg-[#FFFAEB]" /> | |
| {videoSource && ( | |
| <video | |
| ref={videoRef} | |
| autoPlay | |
| muted | |
| playsInline | |
| className="absolute inset-0 w-full h-full object-cover transition-all duration-1000 ease-out" | |
| style={{ | |
| filter: videoBlurState, | |
| opacity: isVideoReady ? 1 : 0, | |
| }} | |
| /> | |
| )} | |
| {appState !== "captioning" && appState !== "source-selection" && ( | |
| <div className="absolute inset-0 bg-[#FFFAEB]/90 backdrop-blur-sm" /> | |
| )} | |
| {appState === "source-selection" && ( | |
| <SourceSelector onSourceSelected={handleSourceSelected} /> | |
| )} | |
| {appState === "welcome" && ( | |
| <WelcomeScreen | |
| onStart={handleStart} | |
| onBack={handleBackToSourceSelection} | |
| sourceLabel={getSourceLabel() ?? undefined} | |
| /> | |
| )} | |
| {appState === "loading" && ( | |
| <LoadingScreen onComplete={handleLoadingComplete} /> | |
| )} | |
| {appState === "captioning" && ( | |
| <CaptioningView | |
| videoRef={videoRef} | |
| videoSource={videoSource} | |
| onChangeSource={handleBackToSourceSelection} | |
| /> | |
| )} | |
| </div> | |
| ); | |
| } | |