Spaces:
Running
Running
add ability to screenshare or upload local video
Browse files- src/App.tsx +30 -17
- src/components/CaptioningView.tsx +19 -5
- src/components/WelcomeScreen.tsx +0 -14
- src/constants/index.ts +1 -0
- src/types/index.ts +1 -1
src/App.tsx
CHANGED
@@ -2,28 +2,31 @@ import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
2 |
import LoadingScreen from "./components/LoadingScreen";
|
3 |
import CaptioningView from "./components/CaptioningView";
|
4 |
import WelcomeScreen from "./components/WelcomeScreen";
|
5 |
-
import
|
6 |
import type { AppState } from "./types";
|
7 |
|
8 |
export default function App() {
|
9 |
-
const [appState, setAppState] = useState<AppState>("
|
10 |
-
const [
|
|
|
11 |
const [isVideoReady, setIsVideoReady] = useState(false);
|
12 |
const videoRef = useRef<HTMLVideoElement | null>(null);
|
13 |
|
14 |
-
const
|
15 |
-
|
16 |
-
|
|
|
17 |
}, []);
|
18 |
|
19 |
const handleStart = useCallback(() => {
|
20 |
-
setAppState("
|
21 |
}, []);
|
22 |
|
23 |
const handleLoadingComplete = useCallback(() => {
|
24 |
setAppState("captioning");
|
25 |
}, []);
|
26 |
|
|
|
27 |
const playVideo = useCallback(async (video: HTMLVideoElement) => {
|
28 |
try {
|
29 |
await video.play();
|
@@ -34,7 +37,17 @@ export default function App() {
|
|
34 |
|
35 |
const setupVideo = useCallback(
|
36 |
(video: HTMLVideoElement, stream: MediaStream) => {
|
37 |
-
video
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
const handleCanPlay = () => {
|
40 |
setIsVideoReady(true);
|
@@ -51,23 +64,23 @@ export default function App() {
|
|
51 |
);
|
52 |
|
53 |
useEffect(() => {
|
54 |
-
if (
|
55 |
const video = videoRef.current;
|
56 |
|
57 |
video.srcObject = null;
|
58 |
video.load();
|
59 |
|
60 |
-
const cleanup = setupVideo(video,
|
61 |
return cleanup;
|
62 |
}
|
63 |
-
}, [
|
64 |
|
65 |
const videoBlurState = useMemo(() => {
|
66 |
switch (appState) {
|
67 |
-
case "requesting-permission":
|
68 |
-
return "blur(20px) brightness(0.2) saturate(0.5)";
|
69 |
case "welcome":
|
70 |
return "blur(12px) brightness(0.3) saturate(0.7)";
|
|
|
|
|
71 |
case "loading":
|
72 |
return "blur(8px) brightness(0.4) saturate(0.8)";
|
73 |
case "captioning":
|
@@ -81,7 +94,7 @@ export default function App() {
|
|
81 |
<div className="App relative h-screen overflow-hidden">
|
82 |
<div className="absolute inset-0 bg-gray-900" />
|
83 |
|
84 |
-
{
|
85 |
<video
|
86 |
ref={videoRef}
|
87 |
autoPlay
|
@@ -97,13 +110,13 @@ export default function App() {
|
|
97 |
|
98 |
{appState !== "captioning" && <div className="absolute inset-0 bg-gray-900/80 backdrop-blur-sm" />}
|
99 |
|
100 |
-
{appState === "requesting-permission" && <WebcamPermissionDialog onPermissionGranted={handlePermissionGranted} />}
|
101 |
-
|
102 |
{appState === "welcome" && <WelcomeScreen onStart={handleStart} />}
|
103 |
|
|
|
|
|
104 |
{appState === "loading" && <LoadingScreen onComplete={handleLoadingComplete} />}
|
105 |
|
106 |
-
{appState === "captioning" && <CaptioningView videoRef={videoRef} />}
|
107 |
</div>
|
108 |
);
|
109 |
}
|
|
|
2 |
import LoadingScreen from "./components/LoadingScreen";
|
3 |
import CaptioningView from "./components/CaptioningView";
|
4 |
import WelcomeScreen from "./components/WelcomeScreen";
|
5 |
+
import InputSourceDialog from "./components/InputSourceDialog";
|
6 |
import type { AppState } from "./types";
|
7 |
|
8 |
export default function App() {
|
9 |
+
const [appState, setAppState] = useState<AppState>("welcome");
|
10 |
+
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
|
11 |
+
const [sourceType, setSourceType] = useState<'webcam' | 'screen' | 'file' | null>(null);
|
12 |
const [isVideoReady, setIsVideoReady] = useState(false);
|
13 |
const videoRef = useRef<HTMLVideoElement | null>(null);
|
14 |
|
15 |
+
const handleSourceSelected = useCallback((stream: MediaStream, type: 'webcam' | 'screen' | 'file') => {
|
16 |
+
setMediaStream(stream);
|
17 |
+
setSourceType(type);
|
18 |
+
setAppState("loading");
|
19 |
}, []);
|
20 |
|
21 |
const handleStart = useCallback(() => {
|
22 |
+
setAppState("source-selection");
|
23 |
}, []);
|
24 |
|
25 |
const handleLoadingComplete = useCallback(() => {
|
26 |
setAppState("captioning");
|
27 |
}, []);
|
28 |
|
29 |
+
|
30 |
const playVideo = useCallback(async (video: HTMLVideoElement) => {
|
31 |
try {
|
32 |
await video.play();
|
|
|
37 |
|
38 |
const setupVideo = useCallback(
|
39 |
(video: HTMLVideoElement, stream: MediaStream) => {
|
40 |
+
// Check if this is a video file (mock stream with videoFileUrl)
|
41 |
+
const videoFileUrl = (stream as any).videoFileUrl;
|
42 |
+
|
43 |
+
if (videoFileUrl) {
|
44 |
+
// For video files, use the file URL directly
|
45 |
+
video.src = videoFileUrl;
|
46 |
+
video.srcObject = null;
|
47 |
+
} else {
|
48 |
+
// For webcam/screen, use the stream
|
49 |
+
video.srcObject = stream;
|
50 |
+
}
|
51 |
|
52 |
const handleCanPlay = () => {
|
53 |
setIsVideoReady(true);
|
|
|
64 |
);
|
65 |
|
66 |
useEffect(() => {
|
67 |
+
if (mediaStream && videoRef.current) {
|
68 |
const video = videoRef.current;
|
69 |
|
70 |
video.srcObject = null;
|
71 |
video.load();
|
72 |
|
73 |
+
const cleanup = setupVideo(video, mediaStream);
|
74 |
return cleanup;
|
75 |
}
|
76 |
+
}, [mediaStream, setupVideo]);
|
77 |
|
78 |
const videoBlurState = useMemo(() => {
|
79 |
switch (appState) {
|
|
|
|
|
80 |
case "welcome":
|
81 |
return "blur(12px) brightness(0.3) saturate(0.7)";
|
82 |
+
case "source-selection":
|
83 |
+
return "blur(20px) brightness(0.2) saturate(0.5)";
|
84 |
case "loading":
|
85 |
return "blur(8px) brightness(0.4) saturate(0.8)";
|
86 |
case "captioning":
|
|
|
94 |
<div className="App relative h-screen overflow-hidden">
|
95 |
<div className="absolute inset-0 bg-gray-900" />
|
96 |
|
97 |
+
{mediaStream && (
|
98 |
<video
|
99 |
ref={videoRef}
|
100 |
autoPlay
|
|
|
110 |
|
111 |
{appState !== "captioning" && <div className="absolute inset-0 bg-gray-900/80 backdrop-blur-sm" />}
|
112 |
|
|
|
|
|
113 |
{appState === "welcome" && <WelcomeScreen onStart={handleStart} />}
|
114 |
|
115 |
+
{appState === "source-selection" && <InputSourceDialog onSourceSelected={handleSourceSelected} />}
|
116 |
+
|
117 |
{appState === "loading" && <LoadingScreen onComplete={handleLoadingComplete} />}
|
118 |
|
119 |
+
{appState === "captioning" && <CaptioningView videoRef={videoRef} sourceType={sourceType} />}
|
120 |
</div>
|
121 |
);
|
122 |
}
|
src/components/CaptioningView.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import { useState, useRef, useEffect, useCallback } from "react";
|
2 |
import WebcamCapture from "./WebcamCapture";
|
|
|
3 |
import DraggableContainer from "./DraggableContainer";
|
4 |
import PromptInput from "./PromptInput";
|
5 |
import LiveCaption from "./LiveCaption";
|
@@ -8,6 +9,7 @@ import { PROMPTS, TIMING } from "../constants";
|
|
8 |
|
9 |
interface CaptioningViewProps {
|
10 |
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
|
11 |
}
|
12 |
|
13 |
function useCaptioningLoop(
|
@@ -67,7 +69,7 @@ function useCaptioningLoop(
|
|
67 |
}, [isRunning, isLoaded, runInference, promptRef, videoRef]);
|
68 |
}
|
69 |
|
70 |
-
export default function CaptioningView({ videoRef }: CaptioningViewProps) {
|
71 |
const [caption, setCaption] = useState<string>("");
|
72 |
const [isLoopRunning, setIsLoopRunning] = useState<boolean>(true);
|
73 |
const [currentPrompt, setCurrentPrompt] = useState<string>(PROMPTS.default);
|
@@ -108,13 +110,25 @@ export default function CaptioningView({ videoRef }: CaptioningViewProps) {
|
|
108 |
<div className="relative w-full h-full">
|
109 |
<WebcamCapture isRunning={isLoopRunning} onToggleRunning={handleToggleLoop} error={error} />
|
110 |
|
111 |
-
{/*
|
112 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
<PromptInput onPromptChange={handlePromptChange} />
|
114 |
</DraggableContainer>
|
115 |
|
116 |
-
{/* Draggable Live Caption - Bottom Right */}
|
117 |
-
<DraggableContainer
|
|
|
|
|
|
|
118 |
<LiveCaption caption={caption} isRunning={isLoopRunning} error={error} />
|
119 |
</DraggableContainer>
|
120 |
</div>
|
|
|
1 |
import { useState, useRef, useEffect, useCallback } from "react";
|
2 |
import WebcamCapture from "./WebcamCapture";
|
3 |
+
import VideoScrubber from "./VideoScrubber";
|
4 |
import DraggableContainer from "./DraggableContainer";
|
5 |
import PromptInput from "./PromptInput";
|
6 |
import LiveCaption from "./LiveCaption";
|
|
|
9 |
|
10 |
interface CaptioningViewProps {
|
11 |
videoRef: React.RefObject<HTMLVideoElement | null>;
|
12 |
+
sourceType?: 'webcam' | 'screen' | 'file' | null;
|
13 |
}
|
14 |
|
15 |
function useCaptioningLoop(
|
|
|
69 |
}, [isRunning, isLoaded, runInference, promptRef, videoRef]);
|
70 |
}
|
71 |
|
72 |
+
export default function CaptioningView({ videoRef, sourceType }: CaptioningViewProps) {
|
73 |
const [caption, setCaption] = useState<string>("");
|
74 |
const [isLoopRunning, setIsLoopRunning] = useState<boolean>(true);
|
75 |
const [currentPrompt, setCurrentPrompt] = useState<string>(PROMPTS.default);
|
|
|
110 |
<div className="relative w-full h-full">
|
111 |
<WebcamCapture isRunning={isLoopRunning} onToggleRunning={handleToggleLoop} error={error} />
|
112 |
|
113 |
+
{/* Video Scrubber - Only show for video files */}
|
114 |
+
<VideoScrubber
|
115 |
+
videoRef={videoRef}
|
116 |
+
isVisible={sourceType === 'file'}
|
117 |
+
/>
|
118 |
+
|
119 |
+
{/* Draggable Prompt Input - Bottom Left (above scrubber) */}
|
120 |
+
<DraggableContainer
|
121 |
+
initialPosition={sourceType === 'file' ? { x: 20, y: window.innerHeight - 200 } : "bottom-left"}
|
122 |
+
className="z-[150]"
|
123 |
+
>
|
124 |
<PromptInput onPromptChange={handlePromptChange} />
|
125 |
</DraggableContainer>
|
126 |
|
127 |
+
{/* Draggable Live Caption - Bottom Right (above scrubber) */}
|
128 |
+
<DraggableContainer
|
129 |
+
initialPosition={sourceType === 'file' ? { x: window.innerWidth - 170, y: window.innerHeight - 200 } : "bottom-right"}
|
130 |
+
className="z-[150]"
|
131 |
+
>
|
132 |
<LiveCaption caption={caption} isRunning={isLoopRunning} error={error} />
|
133 |
</DraggableContainer>
|
134 |
</div>
|
src/components/WelcomeScreen.tsx
CHANGED
@@ -33,20 +33,6 @@ export default function WelcomeScreen({ onStart }: WelcomeScreenProps) {
|
|
33 |
</div>
|
34 |
</GlassContainer>
|
35 |
|
36 |
-
{/* Webcam Status Card */}
|
37 |
-
<GlassContainer
|
38 |
-
bgColor={GLASS_EFFECTS.COLORS.SUCCESS_BG}
|
39 |
-
className="rounded-2xl shadow-2xl hover:scale-105 transition-transform duration-200"
|
40 |
-
role="status"
|
41 |
-
aria-label="Camera status"
|
42 |
-
>
|
43 |
-
<div className="p-4">
|
44 |
-
<div className="flex items-center justify-center space-x-2">
|
45 |
-
<div className="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
|
46 |
-
<p className="text-green-400 font-medium">Camera ready</p>
|
47 |
-
</div>
|
48 |
-
</div>
|
49 |
-
</GlassContainer>
|
50 |
|
51 |
{/* How It Works Card */}
|
52 |
<GlassContainer
|
|
|
33 |
</div>
|
34 |
</GlassContainer>
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
37 |
{/* How It Works Card */}
|
38 |
<GlassContainer
|
src/constants/index.ts
CHANGED
@@ -16,6 +16,7 @@ export const LAYOUT = {
|
|
16 |
MARGINS: {
|
17 |
DEFAULT: 20,
|
18 |
BOTTOM: 20,
|
|
|
19 |
},
|
20 |
DIMENSIONS: {
|
21 |
PROMPT_WIDTH: 420,
|
|
|
16 |
MARGINS: {
|
17 |
DEFAULT: 20,
|
18 |
BOTTOM: 20,
|
19 |
+
BOTTOM_WITH_SCRUBBER: 100,
|
20 |
},
|
21 |
DIMENSIONS: {
|
22 |
PROMPT_WIDTH: 420,
|
src/types/index.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
export type AppState = "
|
2 |
|
3 |
export interface GlassEffectProps {
|
4 |
baseFrequency?: number;
|
|
|
1 |
+
export type AppState = "welcome" | "source-selection" | "loading" | "captioning";
|
2 |
|
3 |
export interface GlassEffectProps {
|
4 |
baseFrequency?: number;
|