fdaudens commited on
Commit
782289e
·
1 Parent(s): ff324d9

added screen recording and upload options

Browse files
Files changed (5) hide show
  1. .gitignore +15 -0
  2. package-lock.json +0 -0
  3. package.json +2 -2
  4. src/App.tsx +103 -27
  5. src/components/WelcomeScreen.tsx +66 -17
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dependencies
2
+ node_modules/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.pyc
7
+
8
+ # OS files
9
+ .DS_Store
10
+
11
+ # Large binaries
12
+ *.onnx
13
+ *.bin
14
+ *.pt
15
+ *.mp4
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -19,8 +19,8 @@
19
  },
20
  "devDependencies": {
21
  "@eslint/js": "^9.29.0",
22
- "@types/react": "^19.1.8",
23
- "@types/react-dom": "^19.1.6",
24
  "@vitejs/plugin-react": "^4.5.2",
25
  "@webgpu/types": "^0.1.63",
26
  "eslint": "^9.29.0",
 
19
  },
20
  "devDependencies": {
21
  "@eslint/js": "^9.29.0",
22
+ "@types/react": "^19.1.12",
23
+ "@types/react-dom": "^19.1.9",
24
  "@vitejs/plugin-react": "^4.5.2",
25
  "@webgpu/types": "^0.1.63",
26
  "eslint": "^9.29.0",
src/App.tsx CHANGED
@@ -6,13 +6,21 @@ import WebcamPermissionDialog from "./components/WebcamPermissionDialog";
6
  import type { AppState } from "./types";
7
 
8
  export default function App() {
9
- const [appState, setAppState] = useState<AppState>("requesting-permission");
10
- const [webcamStream, setWebcamStream] = useState<MediaStream | null>(null);
 
11
  const [isVideoReady, setIsVideoReady] = useState(false);
12
  const videoRef = useRef<HTMLVideoElement | null>(null);
13
 
14
  const handlePermissionGranted = useCallback((stream: MediaStream) => {
15
- setWebcamStream(stream);
 
 
 
 
 
 
 
16
  setAppState("welcome");
17
  }, []);
18
 
@@ -33,8 +41,17 @@ export default function App() {
33
  }, []);
34
 
35
  const setupVideo = useCallback(
36
- (video: HTMLVideoElement, stream: MediaStream) => {
37
- video.srcObject = stream;
 
 
 
 
 
 
 
 
 
38
 
39
  const handleCanPlay = () => {
40
  setIsVideoReady(true);
@@ -51,16 +68,68 @@ export default function App() {
51
  );
52
 
53
  useEffect(() => {
54
- if (webcamStream && videoRef.current) {
55
- const video = videoRef.current;
56
-
57
- video.srcObject = null;
58
- video.load();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- const cleanup = setupVideo(video, webcamStream);
61
- return cleanup;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
- }, [webcamStream, setupVideo]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  const videoBlurState = useMemo(() => {
66
  switch (appState) {
@@ -81,25 +150,32 @@ export default function App() {
81
  <div className="App relative h-screen overflow-hidden">
82
  <div className="absolute inset-0 bg-gray-900" />
83
 
84
- {webcamStream && (
85
- <video
86
- ref={videoRef}
87
- autoPlay
88
- muted
89
- playsInline
90
- className="absolute inset-0 w-full h-full object-cover transition-all duration-1000 ease-out"
91
- style={{
92
- filter: videoBlurState,
93
- opacity: isVideoReady ? 1 : 0,
94
- }}
95
- />
96
- )}
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
 
 
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 [fileUrl, setFileUrl] = useState<string | null>(null);
12
  const [isVideoReady, setIsVideoReady] = useState(false);
13
  const videoRef = useRef<HTMLVideoElement | null>(null);
14
 
15
  const handlePermissionGranted = useCallback((stream: MediaStream) => {
16
+ // Clean up any previous source
17
+ setFileUrl((prev: string | null) => {
18
+ if (prev) URL.revokeObjectURL(prev);
19
+ return null;
20
+ });
21
+ stopStreamTracks(mediaStream);
22
+
23
+ setMediaStream(stream);
24
  setAppState("welcome");
25
  }, []);
26
 
 
41
  }, []);
42
 
43
  const setupVideo = useCallback(
44
+ (video: HTMLVideoElement, source: { stream?: MediaStream; url?: string | null }) => {
45
+ // Reset current src/srcObject first
46
+ video.pause();
47
+ video.src = "";
48
+ (video as HTMLVideoElement & { srcObject: MediaStream | null }).srcObject = null;
49
+
50
+ if (source.stream) {
51
+ (video as HTMLVideoElement & { srcObject: MediaStream | null }).srcObject = source.stream;
52
+ } else if (source.url) {
53
+ video.src = source.url;
54
+ }
55
 
56
  const handleCanPlay = () => {
57
  setIsVideoReady(true);
 
68
  );
69
 
70
  useEffect(() => {
71
+ const video = videoRef.current;
72
+ if (!video) return;
73
+
74
+ const cleanup = setupVideo(video, { stream: mediaStream ?? undefined, url: fileUrl });
75
+ return cleanup;
76
+ }, [mediaStream, fileUrl, setupVideo]);
77
+
78
+ const stopStreamTracks = (stream: MediaStream | null) => {
79
+ stream?.getTracks().forEach((t) => {
80
+ try {
81
+ t.stop();
82
+ } catch (e) {
83
+ // ignore
84
+ }
85
+ });
86
+ };
87
+
88
+ const handleSelectCamera = useCallback(() => {
89
+ // Move to permission flow; component will request permission
90
+ setAppState("requesting-permission");
91
+ }, []);
92
 
93
+ const handleSelectDisplay = useCallback(async () => {
94
+ try {
95
+ // Clean up previous
96
+ stopStreamTracks(mediaStream);
97
+ setMediaStream(null);
98
+ setFileUrl((prev: string | null) => {
99
+ if (prev) URL.revokeObjectURL(prev);
100
+ return null;
101
+ });
102
+
103
+ if (!navigator.mediaDevices?.getDisplayMedia) {
104
+ throw new Error("Screen capture not supported in this browser");
105
+ }
106
+
107
+ const stream = await navigator.mediaDevices.getDisplayMedia({
108
+ video: true,
109
+ audio: false,
110
+ });
111
+ setMediaStream(stream);
112
+ // Ensure we are on welcome where Start is visible
113
+ setAppState("welcome");
114
+ } catch (e) {
115
+ console.error("Failed to start display capture:", e);
116
+ // Stay on welcome; user can retry
117
  }
118
+ }, [mediaStream]);
119
+
120
+ const handleSelectFile = useCallback((file: File) => {
121
+ // Clean up previous sources
122
+ stopStreamTracks(mediaStream);
123
+ setMediaStream(null);
124
+ setFileUrl((prev: string | null) => {
125
+ if (prev) URL.revokeObjectURL(prev);
126
+ return null;
127
+ });
128
+
129
+ const url = URL.createObjectURL(file);
130
+ setFileUrl(url);
131
+ setAppState("welcome");
132
+ }, [mediaStream]);
133
 
134
  const videoBlurState = useMemo(() => {
135
  switch (appState) {
 
150
  <div className="App relative h-screen overflow-hidden">
151
  <div className="absolute inset-0 bg-gray-900" />
152
 
153
+ <video
154
+ ref={videoRef}
155
+ autoPlay
156
+ muted
157
+ playsInline
158
+ loop={Boolean(fileUrl)}
159
+ className="absolute inset-0 w-full h-full object-cover transition-all duration-1000 ease-out"
160
+ style={{
161
+ filter: videoBlurState,
162
+ opacity: isVideoReady ? 1 : 0,
163
+ }}
164
+ />
 
165
 
166
  {appState !== "captioning" && <div className="absolute inset-0 bg-gray-900/80 backdrop-blur-sm" />}
167
 
168
  {appState === "requesting-permission" && <WebcamPermissionDialog onPermissionGranted={handlePermissionGranted} />}
169
 
170
+ {appState === "welcome" && (
171
+ <WelcomeScreen
172
+ onStart={handleStart}
173
+ onSelectCamera={handleSelectCamera}
174
+ onSelectDisplay={handleSelectDisplay}
175
+ onSelectFile={handleSelectFile}
176
+ isSourceReady={Boolean(mediaStream) || Boolean(fileUrl)}
177
+ />
178
+ )}
179
 
180
  {appState === "loading" && <LoadingScreen onComplete={handleLoadingComplete} />}
181
 
src/components/WelcomeScreen.tsx CHANGED
@@ -2,12 +2,24 @@ import HfIcon from "./HfIcon";
2
  import GlassContainer from "./GlassContainer";
3
  import GlassButton from "./GlassButton";
4
  import { GLASS_EFFECTS } from "../constants";
 
5
 
6
  interface WelcomeScreenProps {
7
  onStart: () => void;
 
 
 
 
8
  }
9
 
10
- export default function WelcomeScreen({ onStart }: WelcomeScreenProps) {
 
 
 
 
 
 
 
11
  return (
12
  <div className="absolute inset-0 text-white flex items-center justify-center p-8">
13
  <div className="max-w-2xl w-full space-y-8">
@@ -33,18 +45,64 @@ 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
 
@@ -104,16 +162,7 @@ export default function WelcomeScreen({ onStart }: WelcomeScreenProps) {
104
  </div>
105
  </GlassContainer>
106
 
107
- {/* Start Button */}
108
  <div className="flex flex-col items-center space-y-4">
109
- <GlassButton
110
- onClick={onStart}
111
- className="px-8 py-4 rounded-2xl"
112
- aria-label="Start live captioning with AI model"
113
- >
114
- <span className="font-semibold text-lg">Start Live Captioning</span>
115
- </GlassButton>
116
-
117
  <p className="text-sm text-gray-400 opacity-80">AI model will load when you click start</p>
118
  </div>
119
  </div>
 
2
  import GlassContainer from "./GlassContainer";
3
  import GlassButton from "./GlassButton";
4
  import { GLASS_EFFECTS } from "../constants";
5
+ import type React from "react";
6
 
7
  interface WelcomeScreenProps {
8
  onStart: () => void;
9
+ onSelectCamera?: () => void;
10
+ onSelectDisplay?: () => void;
11
+ onSelectFile?: (file: File) => void;
12
+ isSourceReady?: boolean;
13
  }
14
 
15
+ export default function WelcomeScreen({ onStart, onSelectCamera, onSelectDisplay, onSelectFile, isSourceReady }: WelcomeScreenProps) {
16
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
17
+ const file = e.target.files?.[0];
18
+ if (file && onSelectFile) onSelectFile(file);
19
+ // Reset input so selecting the same file again re-triggers change
20
+ e.currentTarget.value = "";
21
+ };
22
+
23
  return (
24
  <div className="absolute inset-0 text-white flex items-center justify-center p-8">
25
  <div className="max-w-2xl w-full space-y-8">
 
45
  </div>
46
  </GlassContainer>
47
 
48
+ {/* Source Selection Card */}
49
  <GlassContainer
50
  bgColor={GLASS_EFFECTS.COLORS.SUCCESS_BG}
51
  className="rounded-2xl shadow-2xl hover:scale-105 transition-transform duration-200"
52
+ role="region"
53
+ aria-label="Video source selection"
54
  >
55
+ <div className="p-6 flex flex-col items-center gap-4">
56
+ <div className="flex flex-wrap items-center justify-center gap-3">
57
+ <GlassButton
58
+ onClick={(e) => {
59
+ e.preventDefault();
60
+ onSelectCamera?.();
61
+ }}
62
+ className="px-4 py-3 rounded-2xl"
63
+ aria-label="Use Camera"
64
+ >
65
+ Use Camera
66
+ </GlassButton>
67
+ <GlassButton
68
+ onClick={(e) => {
69
+ e.preventDefault();
70
+ onSelectDisplay?.();
71
+ }}
72
+ className="px-4 py-3 rounded-2xl"
73
+ aria-label="Share Tab or Screen"
74
+ >
75
+ Share Tab/Screen
76
+ </GlassButton>
77
+ <div>
78
+ <input
79
+ id="video-file-input"
80
+ type="file"
81
+ accept="video/*"
82
+ onChange={handleFileChange}
83
+ className="hidden"
84
+ />
85
+ <GlassButton
86
+ onClick={(e) => {
87
+ e.preventDefault();
88
+ document.getElementById("video-file-input")?.click();
89
+ }}
90
+ className="px-4 py-3 rounded-2xl"
91
+ aria-label="Upload Video"
92
+ >
93
+ Upload Video
94
+ </GlassButton>
95
+ </div>
96
  </div>
97
+
98
+ <GlassButton
99
+ onClick={onStart}
100
+ className="px-8 py-4 rounded-2xl"
101
+ aria-label="Start live captioning with AI model"
102
+ disabled={!isSourceReady}
103
+ >
104
+ <span className="font-semibold text-lg">Start Live Captioning</span>
105
+ </GlassButton>
106
  </div>
107
  </GlassContainer>
108
 
 
162
  </div>
163
  </GlassContainer>
164
 
 
165
  <div className="flex flex-col items-center space-y-4">
 
 
 
 
 
 
 
 
166
  <p className="text-sm text-gray-400 opacity-80">AI model will load when you click start</p>
167
  </div>
168
  </div>