@hexar/biometric-identity-sdk-react-native 1.21.0 → 1.23.0
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;AAexE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,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;AA2CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAexE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,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;AA2CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqiCtD,CAAC;AA4PF,eAAe,aAAa,CAAC"}
|
|
@@ -101,12 +101,21 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
101
101
|
const framesRef = (0, react_1.useRef)([]);
|
|
102
102
|
const [guidanceText, setGuidanceText] = (0, react_1.useState)(null);
|
|
103
103
|
const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
|
|
104
|
+
const [isCameraReady, setIsCameraReady] = (0, react_1.useState)(false);
|
|
105
|
+
// Controlled via stopRecording — set to false before calling onComplete so
|
|
106
|
+
// VisionCamera tears down the native session and any in-flight takePhoto()
|
|
107
|
+
// calls fail gracefully instead of crashing with IllegalViewOperationException
|
|
108
|
+
// when the parent unmounts the <Camera> view underneath them.
|
|
109
|
+
const [isCameraActive, setIsCameraActive] = (0, react_1.useState)(true);
|
|
104
110
|
const cameraRef = (0, react_1.useRef)(null);
|
|
105
111
|
const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
|
|
106
112
|
// Prefer wide-angle (main) front camera — some Android devices expose
|
|
107
113
|
// multiple front sensors and the default may lack autofocus.
|
|
114
|
+
// Only request wide-angle — ultra-wide opens a multi-physical-camera session
|
|
115
|
+
// that is slow to release, causing CameraViewModule.findCameraView to throw
|
|
116
|
+
// IllegalViewOperationException when the component unmounts after recording.
|
|
108
117
|
const device = (0, react_native_vision_camera_1.useCameraDevice)('front', {
|
|
109
|
-
physicalDevices: ['wide-angle-camera'
|
|
118
|
+
physicalDevices: ['wide-angle-camera'],
|
|
110
119
|
});
|
|
111
120
|
const [deviceReady, setDeviceReady] = (0, react_1.useState)(!!device);
|
|
112
121
|
const fadeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
@@ -233,7 +242,11 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
233
242
|
initChallenges();
|
|
234
243
|
}, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
|
|
235
244
|
(0, react_1.useEffect)(() => {
|
|
236
|
-
|
|
245
|
+
// Don't start countdown until the native camera session is ready.
|
|
246
|
+
// On Motorola and other slow-to-initialize Android OEMs, isActive becoming
|
|
247
|
+
// true before the native view is registered causes IllegalViewOperationException
|
|
248
|
+
// inside CameraViewModule.findCameraView. We wait for onInitialized to fire.
|
|
249
|
+
if (phase !== 'countdown' || !isCameraReady)
|
|
237
250
|
return;
|
|
238
251
|
// Trigger focus at the start of countdown so the camera has ~3s to lock focus
|
|
239
252
|
if (countdown === 3)
|
|
@@ -257,7 +270,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
257
270
|
else {
|
|
258
271
|
startRecording();
|
|
259
272
|
}
|
|
260
|
-
}, [countdown, phase]);
|
|
273
|
+
}, [countdown, phase, isCameraReady]);
|
|
261
274
|
(0, react_1.useEffect)(() => {
|
|
262
275
|
if (phase === 'recording') {
|
|
263
276
|
react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
@@ -302,6 +315,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
302
315
|
setChallengeProgress(0);
|
|
303
316
|
setOverallProgress(0);
|
|
304
317
|
recordingStartTime.current = 0;
|
|
318
|
+
setIsCameraActive(true);
|
|
305
319
|
setPhase('countdown');
|
|
306
320
|
setCountdown(3);
|
|
307
321
|
}, []);
|
|
@@ -309,6 +323,19 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
309
323
|
setPhase('loading');
|
|
310
324
|
react_native_1.Alert.alert('Recording Error', 'Failed to record video. Please try again.', [{ text: 'OK', onPress: onCancel }]);
|
|
311
325
|
}, [onCancel]);
|
|
326
|
+
// Safety timeout: if the camera session never fires onInitialized within
|
|
327
|
+
// 10 seconds, treat it as a hardware failure and send the user back.
|
|
328
|
+
(0, react_1.useEffect)(() => {
|
|
329
|
+
if (isCameraReady)
|
|
330
|
+
return;
|
|
331
|
+
const timeout = setTimeout(() => {
|
|
332
|
+
if (!isCameraReady) {
|
|
333
|
+
biometric_identity_sdk_core_1.logger.error('Camera onInitialized timed out — hardware may be unavailable');
|
|
334
|
+
handleRecordingError(new Error('Camera initialization timed out'));
|
|
335
|
+
}
|
|
336
|
+
}, 10000);
|
|
337
|
+
return () => clearTimeout(timeout);
|
|
338
|
+
}, [isCameraReady, handleRecordingError]);
|
|
312
339
|
const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
|
|
313
340
|
if (phase === 'loading' && !video) {
|
|
314
341
|
return;
|
|
@@ -362,11 +389,11 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
362
389
|
};
|
|
363
390
|
biometric_identity_sdk_core_1.logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
|
|
364
391
|
isRecordingRef.current = false;
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
|
|
392
|
+
// Deactivate the camera first so VisionCamera tears down its native
|
|
393
|
+
// session. Any in-flight takePhoto() calls will then fail gracefully
|
|
394
|
+
// rather than crashing with IllegalViewOperationException when the
|
|
395
|
+
// parent unmounts the <Camera> view underneath them.
|
|
396
|
+
setIsCameraActive(false);
|
|
370
397
|
setTimeout(() => onComplete(result), 250);
|
|
371
398
|
}
|
|
372
399
|
catch (error) {
|
|
@@ -518,6 +545,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
518
545
|
sessionId,
|
|
519
546
|
};
|
|
520
547
|
setPhase('processing');
|
|
548
|
+
setIsCameraActive(false);
|
|
521
549
|
setTimeout(() => onComplete(result), 250);
|
|
522
550
|
}
|
|
523
551
|
else {
|
|
@@ -531,6 +559,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
531
559
|
sessionId,
|
|
532
560
|
};
|
|
533
561
|
setPhase('processing');
|
|
562
|
+
setIsCameraActive(false);
|
|
534
563
|
setTimeout(() => onComplete(result), 250);
|
|
535
564
|
}
|
|
536
565
|
}
|
|
@@ -562,6 +591,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
562
591
|
challengesCompleted: currentCompletedChallenges,
|
|
563
592
|
sessionId,
|
|
564
593
|
};
|
|
594
|
+
setIsCameraActive(false);
|
|
565
595
|
setTimeout(() => {
|
|
566
596
|
onComplete(result);
|
|
567
597
|
}, 250);
|
|
@@ -608,6 +638,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
608
638
|
sessionId,
|
|
609
639
|
};
|
|
610
640
|
setPhase('processing');
|
|
641
|
+
setIsCameraActive(false);
|
|
611
642
|
setTimeout(() => onComplete(result), 250);
|
|
612
643
|
}
|
|
613
644
|
}
|
|
@@ -789,7 +820,10 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
789
820
|
}
|
|
790
821
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
791
822
|
react_1.default.createElement(react_native_1.View, { style: styles.cameraContainer },
|
|
792
|
-
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive:
|
|
823
|
+
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: cameraRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isCameraActive, video: true, photo: true, audio: false, onInitialized: () => setIsCameraReady(true), onError: (error) => {
|
|
824
|
+
biometric_identity_sdk_core_1.logger.error('Camera hardware error:', error);
|
|
825
|
+
handleRecordingError(error);
|
|
826
|
+
} }),
|
|
793
827
|
react_1.default.createElement(react_native_1.View, { style: styles.overlay },
|
|
794
828
|
react_1.default.createElement(react_native_1.View, { style: [
|
|
795
829
|
styles.faceOval,
|
package/package.json
CHANGED
|
@@ -130,12 +130,21 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
130
130
|
const framesRef = useRef<string[]>([]);
|
|
131
131
|
const [guidanceText, setGuidanceText] = useState<string | null>(null);
|
|
132
132
|
const [hasPermission, setHasPermission] = useState(false);
|
|
133
|
+
const [isCameraReady, setIsCameraReady] = useState(false);
|
|
134
|
+
// Controlled via stopRecording — set to false before calling onComplete so
|
|
135
|
+
// VisionCamera tears down the native session and any in-flight takePhoto()
|
|
136
|
+
// calls fail gracefully instead of crashing with IllegalViewOperationException
|
|
137
|
+
// when the parent unmounts the <Camera> view underneath them.
|
|
138
|
+
const [isCameraActive, setIsCameraActive] = useState(true);
|
|
133
139
|
const cameraRef = useRef<Camera>(null);
|
|
134
140
|
const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
|
|
135
141
|
// Prefer wide-angle (main) front camera — some Android devices expose
|
|
136
142
|
// multiple front sensors and the default may lack autofocus.
|
|
143
|
+
// Only request wide-angle — ultra-wide opens a multi-physical-camera session
|
|
144
|
+
// that is slow to release, causing CameraViewModule.findCameraView to throw
|
|
145
|
+
// IllegalViewOperationException when the component unmounts after recording.
|
|
137
146
|
const device = useCameraDevice('front', {
|
|
138
|
-
physicalDevices: ['wide-angle-camera'
|
|
147
|
+
physicalDevices: ['wide-angle-camera'],
|
|
139
148
|
});
|
|
140
149
|
const [deviceReady, setDeviceReady] = useState(!!device);
|
|
141
150
|
|
|
@@ -274,7 +283,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
274
283
|
}, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
|
|
275
284
|
|
|
276
285
|
useEffect(() => {
|
|
277
|
-
|
|
286
|
+
// Don't start countdown until the native camera session is ready.
|
|
287
|
+
// On Motorola and other slow-to-initialize Android OEMs, isActive becoming
|
|
288
|
+
// true before the native view is registered causes IllegalViewOperationException
|
|
289
|
+
// inside CameraViewModule.findCameraView. We wait for onInitialized to fire.
|
|
290
|
+
if (phase !== 'countdown' || !isCameraReady) return;
|
|
278
291
|
|
|
279
292
|
// Trigger focus at the start of countdown so the camera has ~3s to lock focus
|
|
280
293
|
if (countdown === 3) triggerFocus();
|
|
@@ -298,7 +311,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
298
311
|
} else {
|
|
299
312
|
startRecording();
|
|
300
313
|
}
|
|
301
|
-
}, [countdown, phase]);
|
|
314
|
+
}, [countdown, phase, isCameraReady]);
|
|
302
315
|
|
|
303
316
|
useEffect(() => {
|
|
304
317
|
if (phase === 'recording') {
|
|
@@ -351,6 +364,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
351
364
|
setChallengeProgress(0);
|
|
352
365
|
setOverallProgress(0);
|
|
353
366
|
recordingStartTime.current = 0;
|
|
367
|
+
setIsCameraActive(true);
|
|
354
368
|
setPhase('countdown');
|
|
355
369
|
setCountdown(3);
|
|
356
370
|
}, []);
|
|
@@ -364,6 +378,19 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
364
378
|
);
|
|
365
379
|
}, [onCancel]);
|
|
366
380
|
|
|
381
|
+
// Safety timeout: if the camera session never fires onInitialized within
|
|
382
|
+
// 10 seconds, treat it as a hardware failure and send the user back.
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (isCameraReady) return;
|
|
385
|
+
const timeout = setTimeout(() => {
|
|
386
|
+
if (!isCameraReady) {
|
|
387
|
+
logger.error('Camera onInitialized timed out — hardware may be unavailable');
|
|
388
|
+
handleRecordingError(new Error('Camera initialization timed out'));
|
|
389
|
+
}
|
|
390
|
+
}, 10000);
|
|
391
|
+
return () => clearTimeout(timeout);
|
|
392
|
+
}, [isCameraReady, handleRecordingError]);
|
|
393
|
+
|
|
367
394
|
const handleVideoComplete = useCallback(async (video: any) => {
|
|
368
395
|
if (phase === 'loading' && !video) {
|
|
369
396
|
return;
|
|
@@ -432,11 +459,11 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
432
459
|
logger.info('Video recording completed:', result.frames.length, 'frames,', (actualDuration / 1000).toFixed(1) + 's', 'challenges:', currentCompletedChallenges.length);
|
|
433
460
|
|
|
434
461
|
isRecordingRef.current = false;
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
462
|
+
// Deactivate the camera first so VisionCamera tears down its native
|
|
463
|
+
// session. Any in-flight takePhoto() calls will then fail gracefully
|
|
464
|
+
// rather than crashing with IllegalViewOperationException when the
|
|
465
|
+
// parent unmounts the <Camera> view underneath them.
|
|
466
|
+
setIsCameraActive(false);
|
|
440
467
|
setTimeout(() => onComplete(result), 250);
|
|
441
468
|
} catch (error) {
|
|
442
469
|
logger.error('Error processing video:', error);
|
|
@@ -604,6 +631,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
604
631
|
sessionId,
|
|
605
632
|
};
|
|
606
633
|
setPhase('processing');
|
|
634
|
+
setIsCameraActive(false);
|
|
607
635
|
setTimeout(() => onComplete(result), 250);
|
|
608
636
|
} else {
|
|
609
637
|
logger.info('Stopping recording with frames (fallback):', currentFrames.length);
|
|
@@ -616,6 +644,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
616
644
|
sessionId,
|
|
617
645
|
};
|
|
618
646
|
setPhase('processing');
|
|
647
|
+
setIsCameraActive(false);
|
|
619
648
|
setTimeout(() => onComplete(result), 250);
|
|
620
649
|
}
|
|
621
650
|
}
|
|
@@ -654,6 +683,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
654
683
|
sessionId,
|
|
655
684
|
};
|
|
656
685
|
|
|
686
|
+
setIsCameraActive(false);
|
|
657
687
|
setTimeout(() => {
|
|
658
688
|
onComplete(result);
|
|
659
689
|
}, 250);
|
|
@@ -705,6 +735,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
705
735
|
sessionId,
|
|
706
736
|
};
|
|
707
737
|
setPhase('processing');
|
|
738
|
+
setIsCameraActive(false);
|
|
708
739
|
setTimeout(() => onComplete(result), 250);
|
|
709
740
|
}
|
|
710
741
|
}
|
|
@@ -960,10 +991,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
960
991
|
ref={cameraRef}
|
|
961
992
|
style={StyleSheet.absoluteFill}
|
|
962
993
|
device={device}
|
|
963
|
-
isActive={
|
|
994
|
+
isActive={isCameraActive}
|
|
964
995
|
video={true}
|
|
965
996
|
photo={true}
|
|
966
997
|
audio={false}
|
|
998
|
+
onInitialized={() => setIsCameraReady(true)}
|
|
999
|
+
onError={(error) => {
|
|
1000
|
+
logger.error('Camera hardware error:', error);
|
|
1001
|
+
handleRecordingError(error);
|
|
1002
|
+
}}
|
|
967
1003
|
/>
|
|
968
1004
|
|
|
969
1005
|
{/* Face Oval Overlay */}
|