fdaudens's picture
added screen recording and upload options
782289e
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<AppState>("welcome");
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
const [fileUrl, setFileUrl] = useState<string | null>(null);
const [isVideoReady, setIsVideoReady] = useState(false);
const videoRef = useRef<HTMLVideoElement | null>(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 (
<div className="App relative h-screen overflow-hidden">
<div className="absolute inset-0 bg-gray-900" />
<video
ref={videoRef}
autoPlay
muted
playsInline
loop={Boolean(fileUrl)}
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" && <div className="absolute inset-0 bg-gray-900/80 backdrop-blur-sm" />}
{appState === "requesting-permission" && <WebcamPermissionDialog onPermissionGranted={handlePermissionGranted} />}
{appState === "welcome" && (
<WelcomeScreen
onStart={handleStart}
onSelectCamera={handleSelectCamera}
onSelectDisplay={handleSelectDisplay}
onSelectFile={handleSelectFile}
isSourceReady={Boolean(mediaStream) || Boolean(fileUrl)}
/>
)}
{appState === "loading" && <LoadingScreen onComplete={handleLoadingComplete} />}
{appState === "captioning" && <CaptioningView videoRef={videoRef} />}
</div>
);
}