@hexar/biometric-identity-sdk-react-native 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA8vBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA80BtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
@@ -116,6 +116,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
116
116
  const frameCaptureInterval = (0, react_1.useRef)(null);
117
117
  const videoRecordingRef = (0, react_1.useRef)(null);
118
118
  const isRecordingRef = (0, react_1.useRef)(false);
119
+ const recordingTimeoutRef = (0, react_1.useRef)(null);
119
120
  const minDurationMs = 8000;
120
121
  const totalDuration = duration || Math.max(minDurationMs, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
121
122
  // Check camera permissions
@@ -274,12 +275,14 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
274
275
  react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
275
276
  }, [onCancel]);
276
277
  const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
277
- console.log('handleVideoComplete called with video:', video?.path);
278
- if (!isRecordingRef.current && phase !== 'processing') {
279
- console.log('Video already processed, ignoring duplicate callback');
278
+ console.log('handleVideoComplete called with video:', video?.path, 'current phase:', phase, 'isRecording:', isRecordingRef.current);
279
+ // Don't process if we're already done (result screen or error)
280
+ if (phase === 'loading' && !video) {
281
+ console.log('Video completion in loading phase with no video, ignoring');
280
282
  return;
281
283
  }
282
284
  try {
285
+ console.log('Setting phase to processing');
283
286
  setPhase('processing');
284
287
  setOverallProgress(100);
285
288
  const actualDuration = Date.now() - recordingStartTime.current;
@@ -369,24 +372,36 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
369
372
  }
370
373
  }, [device]);
371
374
  const stopRecording = (0, react_1.useCallback)(async () => {
372
- console.log('Stopping recording...');
375
+ console.log('stopRecording called, isRecording:', isRecordingRef.current, 'hasVideoRef:', !!videoRecordingRef.current);
376
+ // Clear any pending timeouts
377
+ if (recordingTimeoutRef.current) {
378
+ clearTimeout(recordingTimeoutRef.current);
379
+ recordingTimeoutRef.current = null;
380
+ }
373
381
  if (frameCaptureInterval.current) {
374
382
  clearInterval(frameCaptureInterval.current);
375
383
  frameCaptureInterval.current = null;
376
384
  }
385
+ // Prevent multiple calls
386
+ if (!isRecordingRef.current) {
387
+ console.log('Recording already stopped, ignoring');
388
+ return;
389
+ }
390
+ // Mark as not recording first to prevent re-entry
391
+ isRecordingRef.current = false;
377
392
  if (videoRecordingRef.current) {
378
393
  try {
379
- console.log('Stopping video recording');
394
+ console.log('Stopping video recording, current ref exists');
380
395
  const recording = videoRecordingRef.current;
381
396
  videoRecordingRef.current = null;
382
- isRecordingRef.current = false;
383
397
  await recording.stop();
384
398
  console.log('Video recording stopped - waiting for onRecordingFinished callback');
399
+ // onRecordingFinished callback will handle completion
385
400
  }
386
401
  catch (error) {
387
402
  console.error('Error stopping video recording:', error);
388
- isRecordingRef.current = false;
389
403
  const actualDuration = Date.now() - recordingStartTime.current;
404
+ console.log('Error duration check:', actualDuration, 'minDurationMs:', minDurationMs, 'frames:', frames.length);
390
405
  if (actualDuration >= minDurationMs && frames.length > 0) {
391
406
  console.log('Video stopped with error, using captured frames');
392
407
  const result = {
@@ -399,32 +414,76 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
399
414
  };
400
415
  onComplete(result);
401
416
  }
417
+ else {
418
+ // Even if we don't have enough frames, try to complete with what we have
419
+ console.log('Completing with available data despite error');
420
+ const result = {
421
+ frames: frames.length > 0 ? frames : [],
422
+ duration: actualDuration,
423
+ instructionsFollowed: completedChallenges.length === challenges.length,
424
+ qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 0,
425
+ challengesCompleted: completedChallenges,
426
+ sessionId,
427
+ };
428
+ onComplete(result);
429
+ }
402
430
  }
403
431
  }
404
432
  else {
405
- isRecordingRef.current = false;
433
+ console.log('No video recording ref - completing with captured frames');
434
+ const actualDuration = Date.now() - recordingStartTime.current;
435
+ // Set phase to processing immediately
436
+ setPhase('processing');
437
+ setOverallProgress(100);
438
+ // Complete with frames we have
439
+ const result = {
440
+ frames: frames.length > 0 ? frames : [],
441
+ duration: actualDuration,
442
+ instructionsFollowed: completedChallenges.length === challenges.length,
443
+ qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 0,
444
+ challengesCompleted: completedChallenges,
445
+ sessionId,
446
+ };
447
+ console.log('Completing without video ref:', {
448
+ frames: result.frames.length,
449
+ duration: (actualDuration / 1000).toFixed(1) + 's',
450
+ challenges: completedChallenges.length
451
+ });
452
+ // Small delay to ensure UI updates
453
+ setTimeout(() => {
454
+ onComplete(result);
455
+ }, 100);
406
456
  }
407
457
  }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
408
458
  const runChallenge = (0, react_1.useCallback)((index) => {
409
459
  if (index >= challenges.length) {
410
- console.log('All challenges completed, stopping recording');
460
+ console.log('All challenges completed, checking duration');
411
461
  setOverallProgress(100);
462
+ // Clear the totalDuration timeout since we're handling it manually
463
+ if (recordingTimeoutRef.current) {
464
+ clearTimeout(recordingTimeoutRef.current);
465
+ recordingTimeoutRef.current = null;
466
+ console.log('Cleared totalDuration timeout');
467
+ }
412
468
  if (isRecordingRef.current) {
413
469
  const elapsed = Date.now() - recordingStartTime.current;
414
470
  console.log('Checking duration - Elapsed:', (elapsed / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's');
415
471
  if (elapsed < minDurationMs) {
416
472
  const remaining = minDurationMs - elapsed;
417
473
  console.log('Waiting additional', (remaining / 1000).toFixed(1), 's to meet minimum duration');
418
- setTimeout(() => {
474
+ recordingTimeoutRef.current = setTimeout(() => {
475
+ console.log('Minimum duration wait completed, stopping recording');
419
476
  if (isRecordingRef.current) {
420
477
  stopRecording();
421
478
  }
422
479
  }, remaining);
423
480
  return;
424
481
  }
482
+ console.log('Duration requirement met, stopping immediately');
425
483
  stopRecording();
426
484
  }
427
485
  else {
486
+ console.log('Recording already stopped, checking if we can complete');
428
487
  const actualDuration = Date.now() - recordingStartTime.current;
429
488
  if (actualDuration >= minDurationMs && frames.length > 0) {
430
489
  console.log('Recording already stopped, completing with frames');
@@ -489,8 +548,15 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
489
548
  videoRecordingRef.current = await cameraRef.current.startRecording({
490
549
  flash: 'off',
491
550
  onRecordingFinished: (video) => {
492
- console.log('Video recording finished callback called, path:', video?.path || 'N/A');
493
- handleVideoComplete(video);
551
+ console.log('Video recording finished callback called, path:', video?.path || 'N/A', 'phase:', phase);
552
+ // Ensure we're in the right state to process
553
+ if (phase === 'recording' || phase === 'processing') {
554
+ handleVideoComplete(video);
555
+ }
556
+ else {
557
+ console.warn('Received onRecordingFinished but phase is', phase, '- calling handleVideoComplete anyway');
558
+ handleVideoComplete(video);
559
+ }
494
560
  },
495
561
  onRecordingError: (error) => {
496
562
  console.error('Recording error:', error);
@@ -509,16 +575,24 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
509
575
  startFrameCapture();
510
576
  }
511
577
  runChallenge(0);
512
- const timeoutId = setTimeout(() => {
513
- console.log('Recording timeout reached, stopping recording');
578
+ // Set a safety timeout that's longer than totalDuration to ensure we stop eventually
579
+ // This will be cleared if challenges complete early
580
+ const maxDuration = Math.max(totalDuration, minDurationMs + 2000); // Add 2s buffer
581
+ recordingTimeoutRef.current = setTimeout(() => {
582
+ console.log('Safety timeout reached (max duration), stopping recording');
514
583
  if (isRecordingRef.current) {
515
584
  stopRecording().catch(err => {
516
- console.error('Error stopping recording on timeout:', err);
585
+ console.error('Error stopping recording on safety timeout:', err);
517
586
  });
518
587
  }
519
- }, totalDuration);
520
- return () => clearTimeout(timeoutId);
521
- }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
588
+ }, maxDuration);
589
+ return () => {
590
+ if (recordingTimeoutRef.current) {
591
+ clearTimeout(recordingTimeoutRef.current);
592
+ recordingTimeoutRef.current = null;
593
+ }
594
+ };
595
+ }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
522
596
  // Current challenge
523
597
  const currentChallenge = challenges[currentChallengeIndex];
524
598
  // Get direction arrow for the current challenge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -147,6 +147,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
147
147
  const frameCaptureInterval = useRef<NodeJS.Timeout | null>(null);
148
148
  const videoRecordingRef = useRef<any>(null);
149
149
  const isRecordingRef = useRef<boolean>(false);
150
+ const recordingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
150
151
  const minDurationMs = 8000;
151
152
  const totalDuration = duration || Math.max(
152
153
  minDurationMs,
@@ -328,14 +329,16 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
328
329
  }, [onCancel]);
329
330
 
330
331
  const handleVideoComplete = useCallback(async (video: any) => {
331
- console.log('handleVideoComplete called with video:', video?.path);
332
+ console.log('handleVideoComplete called with video:', video?.path, 'current phase:', phase, 'isRecording:', isRecordingRef.current);
332
333
 
333
- if (!isRecordingRef.current && phase !== 'processing') {
334
- console.log('Video already processed, ignoring duplicate callback');
334
+ // Don't process if we're already done (result screen or error)
335
+ if (phase === 'loading' && !video) {
336
+ console.log('Video completion in loading phase with no video, ignoring');
335
337
  return;
336
338
  }
337
339
 
338
340
  try {
341
+ console.log('Setting phase to processing');
339
342
  setPhase('processing');
340
343
  setOverallProgress(100);
341
344
 
@@ -436,27 +439,42 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
436
439
  }, [device]);
437
440
 
438
441
  const stopRecording = useCallback(async () => {
439
- console.log('Stopping recording...');
442
+ console.log('stopRecording called, isRecording:', isRecordingRef.current, 'hasVideoRef:', !!videoRecordingRef.current);
443
+
444
+ // Clear any pending timeouts
445
+ if (recordingTimeoutRef.current) {
446
+ clearTimeout(recordingTimeoutRef.current);
447
+ recordingTimeoutRef.current = null;
448
+ }
440
449
 
441
450
  if (frameCaptureInterval.current) {
442
451
  clearInterval(frameCaptureInterval.current);
443
452
  frameCaptureInterval.current = null;
444
453
  }
445
454
 
455
+ // Prevent multiple calls
456
+ if (!isRecordingRef.current) {
457
+ console.log('Recording already stopped, ignoring');
458
+ return;
459
+ }
460
+
461
+ // Mark as not recording first to prevent re-entry
462
+ isRecordingRef.current = false;
463
+
446
464
  if (videoRecordingRef.current) {
447
465
  try {
448
- console.log('Stopping video recording');
466
+ console.log('Stopping video recording, current ref exists');
449
467
  const recording = videoRecordingRef.current;
450
468
  videoRecordingRef.current = null;
451
- isRecordingRef.current = false;
452
469
 
453
470
  await recording.stop();
454
471
  console.log('Video recording stopped - waiting for onRecordingFinished callback');
472
+ // onRecordingFinished callback will handle completion
455
473
  } catch (error) {
456
474
  console.error('Error stopping video recording:', error);
457
- isRecordingRef.current = false;
458
475
 
459
476
  const actualDuration = Date.now() - recordingStartTime.current;
477
+ console.log('Error duration check:', actualDuration, 'minDurationMs:', minDurationMs, 'frames:', frames.length);
460
478
  if (actualDuration >= minDurationMs && frames.length > 0) {
461
479
  console.log('Video stopped with error, using captured frames');
462
480
  const result: VideoRecordingResult = {
@@ -468,18 +486,63 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
468
486
  sessionId,
469
487
  };
470
488
  onComplete(result);
489
+ } else {
490
+ // Even if we don't have enough frames, try to complete with what we have
491
+ console.log('Completing with available data despite error');
492
+ const result: VideoRecordingResult = {
493
+ frames: frames.length > 0 ? frames : [],
494
+ duration: actualDuration,
495
+ instructionsFollowed: completedChallenges.length === challenges.length,
496
+ qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 0,
497
+ challengesCompleted: completedChallenges,
498
+ sessionId,
499
+ };
500
+ onComplete(result);
471
501
  }
472
502
  }
473
503
  } else {
474
- isRecordingRef.current = false;
504
+ console.log('No video recording ref - completing with captured frames');
505
+ const actualDuration = Date.now() - recordingStartTime.current;
506
+
507
+ // Set phase to processing immediately
508
+ setPhase('processing');
509
+ setOverallProgress(100);
510
+
511
+ // Complete with frames we have
512
+ const result: VideoRecordingResult = {
513
+ frames: frames.length > 0 ? frames : [],
514
+ duration: actualDuration,
515
+ instructionsFollowed: completedChallenges.length === challenges.length,
516
+ qualityScore: frames.length > 0 ? Math.min(100, (frames.length / 30) * 100) : 0,
517
+ challengesCompleted: completedChallenges,
518
+ sessionId,
519
+ };
520
+
521
+ console.log('Completing without video ref:', {
522
+ frames: result.frames.length,
523
+ duration: (actualDuration / 1000).toFixed(1) + 's',
524
+ challenges: completedChallenges.length
525
+ });
526
+
527
+ // Small delay to ensure UI updates
528
+ setTimeout(() => {
529
+ onComplete(result);
530
+ }, 100);
475
531
  }
476
532
  }, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
477
533
 
478
534
  const runChallenge = useCallback((index: number) => {
479
535
  if (index >= challenges.length) {
480
- console.log('All challenges completed, stopping recording');
536
+ console.log('All challenges completed, checking duration');
481
537
  setOverallProgress(100);
482
538
 
539
+ // Clear the totalDuration timeout since we're handling it manually
540
+ if (recordingTimeoutRef.current) {
541
+ clearTimeout(recordingTimeoutRef.current);
542
+ recordingTimeoutRef.current = null;
543
+ console.log('Cleared totalDuration timeout');
544
+ }
545
+
483
546
  if (isRecordingRef.current) {
484
547
  const elapsed = Date.now() - recordingStartTime.current;
485
548
  console.log('Checking duration - Elapsed:', (elapsed / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's');
@@ -487,7 +550,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
487
550
  if (elapsed < minDurationMs) {
488
551
  const remaining = minDurationMs - elapsed;
489
552
  console.log('Waiting additional', (remaining / 1000).toFixed(1), 's to meet minimum duration');
490
- setTimeout(() => {
553
+ recordingTimeoutRef.current = setTimeout(() => {
554
+ console.log('Minimum duration wait completed, stopping recording');
491
555
  if (isRecordingRef.current) {
492
556
  stopRecording();
493
557
  }
@@ -495,8 +559,10 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
495
559
  return;
496
560
  }
497
561
 
562
+ console.log('Duration requirement met, stopping immediately');
498
563
  stopRecording();
499
564
  } else {
565
+ console.log('Recording already stopped, checking if we can complete');
500
566
  const actualDuration = Date.now() - recordingStartTime.current;
501
567
  if (actualDuration >= minDurationMs && frames.length > 0) {
502
568
  console.log('Recording already stopped, completing with frames');
@@ -573,8 +639,14 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
573
639
  videoRecordingRef.current = await cameraRef.current.startRecording({
574
640
  flash: 'off',
575
641
  onRecordingFinished: (video: any) => {
576
- console.log('Video recording finished callback called, path:', video?.path || 'N/A');
577
- handleVideoComplete(video);
642
+ console.log('Video recording finished callback called, path:', video?.path || 'N/A', 'phase:', phase);
643
+ // Ensure we're in the right state to process
644
+ if (phase === 'recording' || phase === 'processing') {
645
+ handleVideoComplete(video);
646
+ } else {
647
+ console.warn('Received onRecordingFinished but phase is', phase, '- calling handleVideoComplete anyway');
648
+ handleVideoComplete(video);
649
+ }
578
650
  },
579
651
  onRecordingError: (error: any) => {
580
652
  console.error('Recording error:', error);
@@ -593,17 +665,25 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
593
665
 
594
666
  runChallenge(0);
595
667
 
596
- const timeoutId = setTimeout(() => {
597
- console.log('Recording timeout reached, stopping recording');
668
+ // Set a safety timeout that's longer than totalDuration to ensure we stop eventually
669
+ // This will be cleared if challenges complete early
670
+ const maxDuration = Math.max(totalDuration, minDurationMs + 2000); // Add 2s buffer
671
+ recordingTimeoutRef.current = setTimeout(() => {
672
+ console.log('Safety timeout reached (max duration), stopping recording');
598
673
  if (isRecordingRef.current) {
599
674
  stopRecording().catch(err => {
600
- console.error('Error stopping recording on timeout:', err);
675
+ console.error('Error stopping recording on safety timeout:', err);
601
676
  });
602
677
  }
603
- }, totalDuration);
678
+ }, maxDuration);
604
679
 
605
- return () => clearTimeout(timeoutId);
606
- }, [device, totalDuration, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
680
+ return () => {
681
+ if (recordingTimeoutRef.current) {
682
+ clearTimeout(recordingTimeoutRef.current);
683
+ recordingTimeoutRef.current = null;
684
+ }
685
+ };
686
+ }, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
607
687
 
608
688
  // Current challenge
609
689
  const currentChallenge = challenges[currentChallengeIndex];