Skip to content

Commit 33ab70c

Browse files
committed
added provision for running inference on recorded video
1 parent 2338672 commit 33ab70c

File tree

4 files changed

+202
-15
lines changed

4 files changed

+202
-15
lines changed

src/App.tsx

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,6 @@ const App: React.FC = () => {
226226
});
227227
};
228228

229-
// Handler for initialization errors
230-
const handleInitializationError = (error: unknown) => {
231-
setStatusMessage({
232-
message: `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'
233-
}`,
234-
type: 'error'
235-
});
236-
};
237-
238229
// Start monitoring buffer progress
239230
const startMonitoring = useCallback(() => {
240231
if (progressIntervalRef.current) {
@@ -368,6 +359,89 @@ const App: React.FC = () => {
368359
}, [isInitialized]); // Add isInitialized to dependency array
369360

370361

362+
// Handler for initialization errors
363+
const handleInitializationError = (error: unknown) => {
364+
setStatusMessage({
365+
message: `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'
366+
}`,
367+
type: 'error'
368+
});
369+
};
370+
371+
372+
// Add this handler function with the other handlers
373+
const handleVideoFileSelected = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
374+
if (!videoProcessorRef.current || !inferenceWorkerRef.current) {
375+
setStatusMessage({
376+
message: 'System components not ready. Please refresh the page.',
377+
type: 'error'
378+
});
379+
return;
380+
}
381+
382+
const file = event.target.files?.[0];
383+
if (!file) {
384+
return;
385+
}
386+
387+
try {
388+
setStatusMessage({
389+
message: 'Loading video file...',
390+
type: 'info'
391+
});
392+
393+
// Add a callback for video completion
394+
videoProcessorRef.current.setOnVideoComplete(() => {
395+
console.log('[App] Video processing complete');
396+
setIsCapturing(false);
397+
setStatusMessage({
398+
message: 'Video processing complete. Data ready for export.',
399+
type: 'success'
400+
});
401+
});
402+
403+
// Reset data and frame collection
404+
resetData();
405+
frameCollectionRef.current = {
406+
frames: [],
407+
initialCollectionComplete: false,
408+
framesSinceLastInference: 0
409+
};
410+
411+
// Start the worker capture BEFORE loading the video
412+
// This is the critical step that was missing
413+
console.log('[App] Starting inference worker capture for video processing');
414+
inferenceWorkerRef.current.postMessage({
415+
type: 'startCapture'
416+
});
417+
418+
// Load the video file
419+
await videoProcessorRef.current.loadVideoFile(file);
420+
421+
// Set capturing state
422+
setIsCapturing(true);
423+
424+
// Start monitoring
425+
startMonitoring();
426+
427+
setStatusMessage({
428+
message: `Processing video: ${file.name}`,
429+
type: 'success'
430+
});
431+
} catch (error) {
432+
setIsCapturing(false);
433+
setStatusMessage({
434+
message: `Failed to load video file: ${error instanceof Error ? error.message : 'Unknown error'}`,
435+
type: 'error'
436+
});
437+
}
438+
439+
// Reset the file input
440+
event.target.value = '';
441+
}, [resetData, startMonitoring]);
442+
443+
444+
371445
// Start capture handler
372446
const handleStartCapture = useCallback(async () => {
373447
if (!videoProcessorRef.current || !inferenceWorkerRef.current) {
@@ -673,7 +747,8 @@ const App: React.FC = () => {
673747
onStart={handleStartCapture}
674748
onStop={handleStopCapture}
675749
onExport={handleExport}
676-
/>
750+
onVideoFileSelected={handleVideoFileSelected}
751+
/>
677752

678753
<VideoDisplay
679754
videoProcessor={videoProcessorRef.current}

src/components/Controls/Controls.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,27 @@ const Controls: React.FC<ControlsProps> = ({
66
isInitialized,
77
onStart,
88
onStop,
9-
onExport
9+
onExport,
10+
onVideoFileSelected
1011
}) => {
1112
return (
1213
<div className="controls">
14+
<button
15+
className="control-button"
16+
onClick={() => document.getElementById('video-file-input')?.click()}
17+
disabled={isCapturing}
18+
title="Load video file for analysis"
19+
>
20+
<span className="button-icon">📁</span>
21+
Load Video
22+
</button>
23+
<input
24+
id="video-file-input"
25+
type="file"
26+
accept="video/*"
27+
style={{ display: 'none' }}
28+
onChange={onVideoFileSelected}
29+
/>
1330
<button
1431
className="btn primary"
1532
onClick={onStart}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface ControlsProps {
7373
onStart: () => void;
7474
onStop: () => void;
7575
onExport: () => void;
76+
onVideoFileSelected: (event: React.ChangeEvent<HTMLInputElement>) => void;
7677
}
7778

7879
export type StatusMessage = {

src/utils/videoProcessor.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export class VideoProcessor {
3434
private readonly FACE_DISTANCE_THRESHOLD = 0.15; // 15% of frame width threshold
3535
private faceBoxHistory: FaceBox[] = [];
3636
private readonly FACE_HISTORY_SIZE = 5;
37+
private isVideoFileSource: boolean = false;
38+
private onVideoComplete: (() => void) | null = null;
3739

3840
constructor() {
3941
// Initialize video element
@@ -42,6 +44,8 @@ export class VideoProcessor {
4244
this.videoElement.muted = true;
4345
this.videoElement.autoplay = true;
4446

47+
this.setupVideoEventListeners();
48+
4549
// Initialize canvases with optimized settings
4650
this.croppedCanvas = this.createOptimizedCanvas(256, 256);
4751

@@ -100,6 +104,60 @@ export class VideoProcessor {
100104
this.croppedCtx.imageSmoothingQuality = 'high';
101105
}
102106

107+
async loadVideoFile(file: File): Promise<void> {
108+
// Stop any existing capture
109+
await this.stopCapture();
110+
111+
// Set flag to indicate we're using a video file
112+
this.isVideoFileSource = true;
113+
114+
// Create a URL for the file
115+
const videoURL = URL.createObjectURL(file);
116+
117+
// Reset buffers
118+
this.frameBuffer = [];
119+
this.newFramesBuffer = [];
120+
this.frameCount = 0;
121+
122+
// Set the video element source
123+
this.videoElement.src = videoURL;
124+
this.videoElement.muted = true;
125+
126+
// Wait for video metadata to load
127+
return new Promise<void>((resolve, reject) => {
128+
const timeout = setTimeout(() => {
129+
reject(new Error('Video loading timeout'));
130+
}, 10000);
131+
132+
this.videoElement.onloadedmetadata = () => {
133+
clearTimeout(timeout);
134+
135+
// Initialize face detector if needed
136+
if (!this.faceDetector.isInitialized) {
137+
this.faceDetector.initialize().then(() => {
138+
// Load config and start processing
139+
this.loadConfigSettings().then(() => {
140+
this.startFrameProcessing();
141+
resolve();
142+
}).catch(reject);
143+
}).catch(reject);
144+
} else {
145+
// Just load config and start processing
146+
this.loadConfigSettings().then(() => {
147+
this.startFrameProcessing();
148+
resolve();
149+
}).catch(reject);
150+
}
151+
};
152+
153+
this.videoElement.onerror = () => {
154+
clearTimeout(timeout);
155+
URL.revokeObjectURL(videoURL);
156+
reject(new Error('Failed to load video file'));
157+
};
158+
});
159+
}
160+
103161
async startCapture(): Promise<void> {
104162
try {
105163
// Clear frame buffer
@@ -113,6 +171,7 @@ export class VideoProcessor {
113171
this.onFrameProcessed = null;
114172
}
115173

174+
this.isVideoFileSource = false;
116175
// Load frame dimensions from config
117176
await this.loadConfigSettings();
118177

@@ -292,34 +351,60 @@ export class VideoProcessor {
292351
}
293352
}
294353

354+
private setupVideoEventListeners(): void {
355+
this.videoElement.addEventListener('ended', () => {
356+
console.log('[VideoProcessor] Video playback complete');
357+
// Notify any listeners that processing is complete
358+
if (this.onVideoComplete) {
359+
this.onVideoComplete();
360+
}
361+
});
362+
}
363+
295364
private startFrameProcessing(): void {
296365
// Process frames at target FPS
297366
this._isShuttingDown = false; // Reset flag when starting
298367

368+
if (this.isVideoFileSource) {
369+
// Start playing the video
370+
this.videoElement.play().catch(error => {
371+
console.error('Failed to play video:', error);
372+
});
373+
}
374+
299375
const processFrame = async () => {
300376
// First check if we're shutting down
301377
if (this._isShuttingDown) {
302378
console.log('[VideoProcessor] Aborting frame processing loop - shutdown in progress');
303379
return; // Don't request a new frame
304380
}
305381
const now = performance.now();
306-
if (now - this.lastFrameTime >= this.frameInterval) {
382+
383+
// For video files, we don't throttle by frame rate
384+
if (this.isVideoFileSource || (now - this.lastFrameTime >= this.frameInterval)) {
307385
// Update face detection if needed
308386
await this.updateFaceDetection();
309387

310388
// Process frame
311389
this.processVideoFrame(now);
312390
this.lastFrameTime = now;
313391
}
314-
// Only request next frame if not shutting down
315-
if (!this._isShuttingDown) {
392+
393+
// Only request next frame if not shutting down and (for video files) if video is still playing
394+
if (!this._isShuttingDown && (!this.isVideoFileSource || !this.videoElement.ended)) {
316395
this.processingFrameId = requestAnimationFrame(processFrame);
396+
} else if (this.isVideoFileSource && this.videoElement.ended) {
397+
console.log('[VideoProcessor] Video playback complete');
317398
}
318399
};
319400

320401
this.processingFrameId = requestAnimationFrame(processFrame);
321402
}
322403

404+
isVideoComplete(): boolean {
405+
return this.isVideoFileSource && this.videoElement.ended;
406+
}
407+
323408
private processVideoFrame(timestamp: number): void {
324409
try {
325410
// Immediately exit if we're shutting down
@@ -494,6 +579,9 @@ export class VideoProcessor {
494579
return frames;
495580
}
496581

582+
public setOnVideoComplete(callback: () => void): void {
583+
this.onVideoComplete = callback;
584+
}
497585

498586
getCurrentFaceBox(): FaceBox | null {
499587
return this.currentFaceBox;
@@ -520,7 +608,13 @@ export class VideoProcessor {
520608
}
521609

522610
isCapturing(): boolean {
523-
// Return false if explicitly shutting down or media stream is inactive
611+
// Check if we're using a video file source
612+
if (this.isVideoFileSource) {
613+
// For video files, we're capturing if not shutting down and video isn't ended
614+
return !this._isShuttingDown && !this.videoElement.ended && this.videoElement.readyState >= 2;
615+
}
616+
617+
// For camera capture, check media stream
524618
return !this._isShuttingDown && !!this.mediaStream?.active;
525619
}
526620
}

0 commit comments

Comments
 (0)