react-native-biometric-verifier 0.0.17 → 0.0.18
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.
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useState, useCallback
|
|
1
|
+
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
TouchableOpacity,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Platform,
|
|
9
9
|
AppState,
|
|
10
10
|
BackHandler,
|
|
11
|
+
Animated,
|
|
11
12
|
} from 'react-native';
|
|
12
13
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
13
14
|
import {
|
|
@@ -39,6 +40,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
39
40
|
const [faces, setFaces] = useState([]);
|
|
40
41
|
const [singleFaceDetected, setSingleFaceDetected] = useState(false);
|
|
41
42
|
const [cameraError, setCameraError] = useState(null);
|
|
43
|
+
const [livenessStep, setLivenessStep] = useState(0);
|
|
42
44
|
|
|
43
45
|
const captured = useRef(false);
|
|
44
46
|
const appState = useRef(AppState.currentState);
|
|
@@ -46,11 +48,15 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
46
48
|
const initializationAttempts = useRef(0);
|
|
47
49
|
const maxInitializationAttempts = 3;
|
|
48
50
|
|
|
51
|
+
// Animation values
|
|
52
|
+
const instructionAnim = useRef(new Animated.Value(1)).current;
|
|
53
|
+
|
|
49
54
|
// Reset capture state
|
|
50
55
|
const resetCaptureState = useCallback(() => {
|
|
51
56
|
captured.current = false;
|
|
52
57
|
setSingleFaceDetected(false);
|
|
53
58
|
setFaces([]);
|
|
59
|
+
setLivenessStep(0);
|
|
54
60
|
}, []);
|
|
55
61
|
|
|
56
62
|
// Code scanner
|
|
@@ -132,6 +138,18 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
132
138
|
}
|
|
133
139
|
}, []);
|
|
134
140
|
|
|
141
|
+
const onLivenessUpdate = useCallback((step) => {
|
|
142
|
+
setLivenessStep(step);
|
|
143
|
+
|
|
144
|
+
// Animate instruction change
|
|
145
|
+
instructionAnim.setValue(0);
|
|
146
|
+
Animated.timing(instructionAnim, {
|
|
147
|
+
toValue: 1,
|
|
148
|
+
duration: 300,
|
|
149
|
+
useNativeDriver: true,
|
|
150
|
+
}).start();
|
|
151
|
+
}, [instructionAnim]);
|
|
152
|
+
|
|
135
153
|
// Use the face detection frame processor hook
|
|
136
154
|
const {
|
|
137
155
|
frameProcessor,
|
|
@@ -143,6 +161,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
143
161
|
} = useFaceDetectionFrameProcessor({
|
|
144
162
|
onStableFaceDetected,
|
|
145
163
|
onFacesUpdate,
|
|
164
|
+
onLivenessUpdate,
|
|
146
165
|
showCodeScanner,
|
|
147
166
|
isLoading,
|
|
148
167
|
isActive: showCamera && cameraInitialized, // Pass active state
|
|
@@ -343,6 +362,28 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
343
362
|
}, 500);
|
|
344
363
|
}, [initializeCamera, resetCaptureState, forceResetCaptureState]);
|
|
345
364
|
|
|
365
|
+
// Render liveness instructions
|
|
366
|
+
const renderLivenessInstruction = () => {
|
|
367
|
+
if (faces.length === 0) return 'Position your face in the frame';
|
|
368
|
+
|
|
369
|
+
switch (livenessStep) {
|
|
370
|
+
case 0:
|
|
371
|
+
return 'Look straight into the camera';
|
|
372
|
+
case 1:
|
|
373
|
+
return 'Turn your face to the right';
|
|
374
|
+
case 2:
|
|
375
|
+
return 'Now turn your face to the left';
|
|
376
|
+
case 3:
|
|
377
|
+
if (captured.current) {
|
|
378
|
+
return 'Capturing...';
|
|
379
|
+
} else {
|
|
380
|
+
return 'Perfect! Hold still for capture...';
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
return 'Position your face in the frame';
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
346
387
|
// camera placeholder / UI
|
|
347
388
|
const renderCameraPlaceholder = () => (
|
|
348
389
|
<View style={styles.cameraContainer}>
|
|
@@ -430,32 +471,65 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
430
471
|
renderCameraPlaceholder()
|
|
431
472
|
)}
|
|
432
473
|
|
|
433
|
-
{/*
|
|
474
|
+
{/* Liveness Detection UI */}
|
|
434
475
|
{!showCodeScanner && shouldRenderCamera && (
|
|
476
|
+
<View style={styles.livenessContainer}>
|
|
477
|
+
{/* Instructions with animation */}
|
|
478
|
+
<Animated.View
|
|
479
|
+
style={[
|
|
480
|
+
styles.instructionContainer,
|
|
481
|
+
{
|
|
482
|
+
opacity: instructionAnim,
|
|
483
|
+
transform: [
|
|
484
|
+
{
|
|
485
|
+
translateY: instructionAnim.interpolate({
|
|
486
|
+
inputRange: [0, 1],
|
|
487
|
+
outputRange: [10, 0],
|
|
488
|
+
}),
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
]}
|
|
493
|
+
>
|
|
494
|
+
<Text style={styles.instructionText}>{renderLivenessInstruction()}</Text>
|
|
495
|
+
</Animated.View>
|
|
496
|
+
|
|
497
|
+
{/* Step Indicators */}
|
|
498
|
+
<View style={styles.stepsContainer}>
|
|
499
|
+
{[0, 1, 2].map((step) => (
|
|
500
|
+
<React.Fragment key={step}>
|
|
501
|
+
<View style={[
|
|
502
|
+
styles.stepIndicator,
|
|
503
|
+
livenessStep > step ? styles.stepCompleted :
|
|
504
|
+
livenessStep === step ? styles.stepCurrent : styles.stepPending
|
|
505
|
+
]}>
|
|
506
|
+
<Text style={styles.stepText}>{step + 1}</Text>
|
|
507
|
+
</View>
|
|
508
|
+
{step < 2 && (
|
|
509
|
+
<View style={[
|
|
510
|
+
styles.stepConnector,
|
|
511
|
+
livenessStep > step ? styles.connectorCompleted : {}
|
|
512
|
+
]} />
|
|
513
|
+
)}
|
|
514
|
+
</React.Fragment>
|
|
515
|
+
))}
|
|
516
|
+
</View>
|
|
517
|
+
|
|
518
|
+
{/* Step Labels */}
|
|
519
|
+
<View style={styles.stepLabelsContainer}>
|
|
520
|
+
<Text style={styles.stepLabel}>Center</Text>
|
|
521
|
+
<Text style={styles.stepLabel}>Right</Text>
|
|
522
|
+
<Text style={styles.stepLabel}>Left</Text>
|
|
523
|
+
</View>
|
|
524
|
+
</View>
|
|
525
|
+
)}
|
|
526
|
+
|
|
527
|
+
{/* Face detection status */}
|
|
528
|
+
{!showCodeScanner && shouldRenderCamera && faces.length > 1 && (
|
|
435
529
|
<View style={styles.faceDetectionStatus}>
|
|
436
|
-
{
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
</Text>
|
|
440
|
-
)}
|
|
441
|
-
{faces.length > 1 && (
|
|
442
|
-
<Text style={[styles.faceDetectionText, styles.multipleFacesText]}>
|
|
443
|
-
Multiple faces detected. Please ensure only one person is in the frame.
|
|
444
|
-
</Text>
|
|
445
|
-
)}
|
|
446
|
-
{singleFaceDetected && !captured.current && (
|
|
447
|
-
<Text style={[styles.faceDetectionText, styles.singleFaceText]}>
|
|
448
|
-
Face detected. Hold still...
|
|
449
|
-
</Text>
|
|
450
|
-
)}
|
|
451
|
-
{captured.current && (
|
|
452
|
-
<View style={styles.capturingContainer}>
|
|
453
|
-
<ActivityIndicator size="small" color={COLORS.light} />
|
|
454
|
-
<Text style={[styles.faceDetectionText, styles.capturingText]}>
|
|
455
|
-
Capturing...
|
|
456
|
-
</Text>
|
|
457
|
-
</View>
|
|
458
|
-
)}
|
|
530
|
+
<Text style={[styles.faceDetectionText, styles.multipleFacesText]}>
|
|
531
|
+
Multiple faces detected. Please ensure only one person is in the frame.
|
|
532
|
+
</Text>
|
|
459
533
|
</View>
|
|
460
534
|
)}
|
|
461
535
|
|
|
@@ -465,10 +539,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
465
539
|
<TouchableOpacity
|
|
466
540
|
style={[
|
|
467
541
|
styles.flipButton,
|
|
468
|
-
(isLoading || captured.current) && styles.flipButtonDisabled,
|
|
542
|
+
(isLoading || captured.current || livenessStep > 0) && styles.flipButtonDisabled,
|
|
469
543
|
]}
|
|
470
544
|
onPress={onToggleCamera}
|
|
471
|
-
disabled={isLoading || captured.current}
|
|
545
|
+
disabled={isLoading || captured.current || livenessStep > 0}
|
|
472
546
|
accessibilityLabel="Flip camera"
|
|
473
547
|
>
|
|
474
548
|
<Icon name="flip-camera-ios" size={28} color={COLORS.light} />
|
|
@@ -579,19 +653,80 @@ const styles = StyleSheet.create({
|
|
|
579
653
|
multipleFacesText: {
|
|
580
654
|
backgroundColor: 'rgba(255,0,0,0.7)',
|
|
581
655
|
},
|
|
582
|
-
|
|
583
|
-
|
|
656
|
+
// Liveness detection styles
|
|
657
|
+
livenessContainer: {
|
|
658
|
+
position: 'absolute',
|
|
659
|
+
top: 40,
|
|
660
|
+
left: 0,
|
|
661
|
+
right: 0,
|
|
662
|
+
alignItems: 'center',
|
|
663
|
+
paddingHorizontal: 20,
|
|
584
664
|
},
|
|
585
|
-
|
|
586
|
-
backgroundColor: 'rgba(0,
|
|
665
|
+
instructionContainer: {
|
|
666
|
+
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
667
|
+
paddingHorizontal: 20,
|
|
668
|
+
paddingVertical: 12,
|
|
669
|
+
borderRadius: 8,
|
|
670
|
+
marginBottom: 20,
|
|
587
671
|
},
|
|
588
|
-
|
|
672
|
+
instructionText: {
|
|
673
|
+
color: 'white',
|
|
674
|
+
fontSize: 16,
|
|
675
|
+
fontWeight: 'bold',
|
|
676
|
+
textAlign: 'center',
|
|
677
|
+
},
|
|
678
|
+
stepsContainer: {
|
|
589
679
|
flexDirection: 'row',
|
|
590
680
|
alignItems: 'center',
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
681
|
+
marginBottom: 8,
|
|
682
|
+
},
|
|
683
|
+
stepIndicator: {
|
|
684
|
+
width: 30,
|
|
685
|
+
height: 30,
|
|
686
|
+
borderRadius: 15,
|
|
687
|
+
justifyContent: 'center',
|
|
688
|
+
alignItems: 'center',
|
|
689
|
+
borderWidth: 2,
|
|
690
|
+
},
|
|
691
|
+
stepCompleted: {
|
|
692
|
+
backgroundColor: COLORS.primary,
|
|
693
|
+
borderColor: COLORS.primary,
|
|
694
|
+
},
|
|
695
|
+
stepCurrent: {
|
|
696
|
+
backgroundColor: COLORS.primary,
|
|
697
|
+
borderColor: COLORS.primary,
|
|
698
|
+
opacity: 0.7,
|
|
699
|
+
},
|
|
700
|
+
stepPending: {
|
|
701
|
+
backgroundColor: 'transparent',
|
|
702
|
+
borderColor: 'rgba(255,255,255,0.5)',
|
|
703
|
+
},
|
|
704
|
+
stepText: {
|
|
705
|
+
color: 'white',
|
|
706
|
+
fontSize: 12,
|
|
707
|
+
fontWeight: 'bold',
|
|
708
|
+
},
|
|
709
|
+
stepConnector: {
|
|
710
|
+
flex: 1,
|
|
711
|
+
height: 2,
|
|
712
|
+
backgroundColor: 'rgba(255,255,255,0.3)',
|
|
713
|
+
marginHorizontal: 4,
|
|
714
|
+
},
|
|
715
|
+
connectorCompleted: {
|
|
716
|
+
backgroundColor: COLORS.primary,
|
|
717
|
+
},
|
|
718
|
+
stepLabelsContainer: {
|
|
719
|
+
flexDirection: 'row',
|
|
720
|
+
justifyContent: 'space-between',
|
|
721
|
+
width: '100%',
|
|
722
|
+
paddingHorizontal: 10,
|
|
723
|
+
},
|
|
724
|
+
stepLabel: {
|
|
725
|
+
color: 'white',
|
|
726
|
+
fontSize: 12,
|
|
727
|
+
opacity: 0.8,
|
|
728
|
+
textAlign: 'center',
|
|
729
|
+
flex: 1,
|
|
595
730
|
},
|
|
596
731
|
});
|
|
597
732
|
|
package/src/components/Loader.js
CHANGED
|
@@ -16,7 +16,7 @@ export default function Loader({
|
|
|
16
16
|
overlayColor = 'rgba(0,0,0,0.4)',
|
|
17
17
|
loaderColor = 'lightblue',
|
|
18
18
|
size = 50,
|
|
19
|
-
gifSource = {uri
|
|
19
|
+
gifSource = {uri: `http://emr.amalaims.org:9393/file/getCommonFile/image/heartpulse.gif`},
|
|
20
20
|
message = '',
|
|
21
21
|
messageStyle = {},
|
|
22
22
|
animationType = 'fade',
|
|
@@ -27,6 +27,19 @@ export default function Loader({
|
|
|
27
27
|
const [rotation] = useState(new Animated.Value(0));
|
|
28
28
|
const [pulse] = useState(new Animated.Value(1));
|
|
29
29
|
const [fade] = useState(new Animated.Value(0));
|
|
30
|
+
const [imageSource, setImageSource] = useState(gifSource);
|
|
31
|
+
|
|
32
|
+
// Reset imageSource whenever gifSource prop changes
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setImageSource(gifSource);
|
|
35
|
+
}, [gifSource]);
|
|
36
|
+
|
|
37
|
+
const handleImageError = () => {
|
|
38
|
+
// Fallback to default heartpulse GIF when image fails to load
|
|
39
|
+
setImageSource({
|
|
40
|
+
uri: "http://emr.amalaims.org:9393/file/getCommonFile/image/heartpulse.gif"
|
|
41
|
+
});
|
|
42
|
+
};
|
|
30
43
|
|
|
31
44
|
// Rotation animation
|
|
32
45
|
useEffect(() => {
|
|
@@ -73,7 +86,9 @@ export default function Loader({
|
|
|
73
86
|
const loaderContent = gifSource ? (
|
|
74
87
|
<FastImage
|
|
75
88
|
style={[styles.icon_style, { width: normalize(size), height: normalize(size) }]}
|
|
76
|
-
source={
|
|
89
|
+
source={imageSource}
|
|
90
|
+
resizeMode={FastImage.resizeMode.contain}
|
|
91
|
+
onError={handleImageError}
|
|
77
92
|
/>
|
|
78
93
|
) : (
|
|
79
94
|
<Animated.View style={[
|
|
@@ -120,7 +135,6 @@ export default function Loader({
|
|
|
120
135
|
...(shadow && styles.shadowStyle)
|
|
121
136
|
}
|
|
122
137
|
]}>
|
|
123
|
-
{console.log('loadersource--------------------',JSON.stringify(gifSource))}
|
|
124
138
|
{loaderContent}
|
|
125
139
|
{message ? (
|
|
126
140
|
<Text style={[styles.messageText, messageStyle]}>
|
|
@@ -3,20 +3,25 @@ import { Worklets } from 'react-native-worklets-core';
|
|
|
3
3
|
import { useFrameProcessor } from 'react-native-vision-camera';
|
|
4
4
|
import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
5
5
|
|
|
6
|
-
// Tuned constants
|
|
6
|
+
// Tuned constants for liveness detection
|
|
7
7
|
const FACE_STABILITY_THRESHOLD = 3;
|
|
8
8
|
const FACE_MOVEMENT_THRESHOLD = 15;
|
|
9
|
-
const FRAME_PROCESSOR_MIN_INTERVAL_MS =
|
|
9
|
+
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 800;
|
|
10
10
|
const MIN_FACE_SIZE = 0.2;
|
|
11
11
|
|
|
12
|
+
// Liveness detection constants
|
|
13
|
+
const YAW_LEFT_THRESHOLD = -15; // face turned left (negative yaw)
|
|
14
|
+
const YAW_RIGHT_THRESHOLD = 15; // face turned right (positive yaw)
|
|
15
|
+
const YAW_CENTER_THRESHOLD = 5; // face centered
|
|
16
|
+
|
|
12
17
|
export const useFaceDetectionFrameProcessor = ({
|
|
13
18
|
onStableFaceDetected = () => {},
|
|
14
19
|
onFacesUpdate = () => {},
|
|
20
|
+
onLivenessUpdate = () => {},
|
|
15
21
|
showCodeScanner = false,
|
|
16
22
|
isLoading = false,
|
|
17
|
-
isActive = true,
|
|
23
|
+
isActive = true,
|
|
18
24
|
}) => {
|
|
19
|
-
// Face detector (fast mode only)
|
|
20
25
|
const { detectFaces } = useFaceDetector({
|
|
21
26
|
performanceMode: 'fast',
|
|
22
27
|
landmarkMode: 'none',
|
|
@@ -25,23 +30,26 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
25
30
|
minFaceSize: MIN_FACE_SIZE,
|
|
26
31
|
});
|
|
27
32
|
|
|
28
|
-
// Use ref to track component mount state
|
|
29
33
|
const isMounted = useRef(true);
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
const sharedState = useMemo(() =>
|
|
36
|
+
Worklets.createSharedValue({
|
|
37
|
+
lastProcessedTime: 0,
|
|
38
|
+
lastX: 0,
|
|
39
|
+
lastY: 0,
|
|
40
|
+
lastW: 0,
|
|
41
|
+
lastH: 0,
|
|
42
|
+
stableCount: 0,
|
|
43
|
+
captured: false,
|
|
44
|
+
showCodeScanner: showCodeScanner,
|
|
45
|
+
isActive: isActive,
|
|
46
|
+
livenessStep: 0, // 0=look straight, 1=left done, 2=right done, 3=ready for capture
|
|
47
|
+
leftTurnVerified: false,
|
|
48
|
+
rightTurnVerified: false,
|
|
49
|
+
currentYaw: 0,
|
|
50
|
+
yawStableCount: 0,
|
|
51
|
+
}), []);
|
|
43
52
|
|
|
44
|
-
// Update states when props change
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
if (!isMounted.current) return;
|
|
47
55
|
|
|
@@ -51,58 +59,55 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
51
59
|
isActive: isActive
|
|
52
60
|
};
|
|
53
61
|
|
|
54
|
-
// Reset
|
|
62
|
+
// Reset when becoming active again
|
|
55
63
|
if (isActive && sharedState.value.captured) {
|
|
56
64
|
sharedState.value = {
|
|
57
65
|
...sharedState.value,
|
|
58
66
|
captured: false,
|
|
59
67
|
stableCount: 0,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
livenessStep: 0,
|
|
69
|
+
leftTurnVerified: false,
|
|
70
|
+
rightTurnVerified: false,
|
|
71
|
+
yawStableCount: 0,
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
|
-
}, [showCodeScanner, isActive]);
|
|
74
|
+
}, [showCodeScanner, isActive, sharedState]);
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
width: Math.max(0, Math.round(width)),
|
|
77
|
-
height: Math.max(0, Math.round(height)),
|
|
78
|
-
});
|
|
79
|
-
} catch (error) {
|
|
80
|
-
if (__DEV__) console.log('runOnStable error:', error);
|
|
81
|
-
}
|
|
82
|
-
}),
|
|
83
|
-
[onStableFaceDetected]
|
|
76
|
+
const runOnStable = useMemo(() =>
|
|
77
|
+
Worklets.createRunOnJS((faceRect) => {
|
|
78
|
+
try {
|
|
79
|
+
onStableFaceDetected?.(faceRect);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error in runOnStable:', error);
|
|
82
|
+
}
|
|
83
|
+
}), [onStableFaceDetected]
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
-
const runOnFaces = useMemo(
|
|
87
|
-
() =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
const runOnFaces = useMemo(() =>
|
|
87
|
+
Worklets.createRunOnJS((count, progress, step) => {
|
|
88
|
+
try {
|
|
89
|
+
onFacesUpdate?.({ count, progress, step });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error in runOnFaces:', error);
|
|
92
|
+
}
|
|
93
|
+
}), [onFacesUpdate]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const runOnLiveness = useMemo(() =>
|
|
97
|
+
Worklets.createRunOnJS((step) => {
|
|
98
|
+
try {
|
|
99
|
+
onLivenessUpdate?.(step);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Error in runOnLiveness:', error);
|
|
102
|
+
}
|
|
103
|
+
}), [onLivenessUpdate]
|
|
96
104
|
);
|
|
97
105
|
|
|
98
|
-
// Frame processor with memory optimization
|
|
99
106
|
const frameProcessor = useFrameProcessor((frame) => {
|
|
100
107
|
'worklet';
|
|
101
|
-
|
|
102
|
-
// Get current state
|
|
103
108
|
const state = sharedState.value;
|
|
104
|
-
|
|
105
|
-
//
|
|
109
|
+
|
|
110
|
+
// Early return conditions
|
|
106
111
|
if (state.showCodeScanner || state.captured || isLoading || !state.isActive) {
|
|
107
112
|
frame.release?.();
|
|
108
113
|
return;
|
|
@@ -117,7 +122,6 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
117
122
|
let detected;
|
|
118
123
|
try {
|
|
119
124
|
detected = detectFaces?.(frame);
|
|
120
|
-
// Release frame immediately after face detection to prevent memory leaks
|
|
121
125
|
frame.release?.();
|
|
122
126
|
} catch (error) {
|
|
123
127
|
frame.release?.();
|
|
@@ -125,76 +129,146 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
if (!detected || detected.length === 0) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
sharedState.value = {
|
|
133
|
+
...state,
|
|
134
|
+
stableCount: 0,
|
|
135
|
+
lastProcessedTime: now,
|
|
136
|
+
yawStableCount: 0
|
|
133
137
|
};
|
|
134
|
-
runOnFaces(0, 0);
|
|
138
|
+
runOnFaces(0, 0, state.livenessStep);
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
if (detected.length === 1 && !state.captured) {
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
runOnFaces(0, 0);
|
|
143
|
+
const face = detected[0];
|
|
144
|
+
if (!face?.bounds || face.yawAngle === undefined) {
|
|
145
|
+
runOnFaces(0, 0, state.livenessStep);
|
|
142
146
|
return;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
const x = Math.max(0,
|
|
147
|
-
const y = Math.max(0,
|
|
148
|
-
const width = Math.max(0,
|
|
149
|
-
const height = Math.max(0,
|
|
149
|
+
const yaw = face.yawAngle;
|
|
150
|
+
const x = Math.max(0, face.bounds.x);
|
|
151
|
+
const y = Math.max(0, face.bounds.y);
|
|
152
|
+
const width = Math.max(0, face.bounds.width);
|
|
153
|
+
const height = Math.max(0, face.bounds.height);
|
|
154
|
+
|
|
155
|
+
let newLivenessStep = state.livenessStep;
|
|
156
|
+
let newLeftTurnVerified = state.leftTurnVerified;
|
|
157
|
+
let newRightTurnVerified = state.rightTurnVerified;
|
|
158
|
+
let newYawStableCount = state.yawStableCount;
|
|
150
159
|
|
|
151
|
-
|
|
160
|
+
// Liveness detection logic
|
|
161
|
+
if (newLivenessStep === 0) {
|
|
162
|
+
// Step 0: Wait for face to be centered and stable
|
|
163
|
+
if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
|
|
164
|
+
newYawStableCount++;
|
|
165
|
+
if (newYawStableCount >= 2) {
|
|
166
|
+
newLivenessStep = 1; // Ready for left turn
|
|
167
|
+
runOnLiveness(newLivenessStep);
|
|
168
|
+
newYawStableCount = 0;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
newYawStableCount = 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (newLivenessStep === 1 && !newLeftTurnVerified) {
|
|
175
|
+
// Step 1: Detect left turn
|
|
176
|
+
if (yaw < YAW_LEFT_THRESHOLD) {
|
|
177
|
+
newYawStableCount++;
|
|
178
|
+
if (newYawStableCount >= 2) {
|
|
179
|
+
newLeftTurnVerified = true;
|
|
180
|
+
newLivenessStep = 2; // Ready for right turn
|
|
181
|
+
runOnLiveness(newLivenessStep);
|
|
182
|
+
newYawStableCount = 0;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
newYawStableCount = Math.max(0, newYawStableCount - 0.5);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (newLivenessStep === 2 && newLeftTurnVerified && !newRightTurnVerified) {
|
|
189
|
+
// Step 2: Detect right turn
|
|
190
|
+
if (yaw > YAW_RIGHT_THRESHOLD) {
|
|
191
|
+
newYawStableCount++;
|
|
192
|
+
if (newYawStableCount >= 2) {
|
|
193
|
+
newRightTurnVerified = true;
|
|
194
|
+
newLivenessStep = 3; // Ready for capture
|
|
195
|
+
runOnLiveness(newLivenessStep);
|
|
196
|
+
newYawStableCount = 0;
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
newYawStableCount = Math.max(0, newYawStableCount - 0.5);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (newLivenessStep === 3 && newLeftTurnVerified && newRightTurnVerified) {
|
|
203
|
+
// Step 3: Wait for face to return to center for capture
|
|
204
|
+
if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
|
|
205
|
+
newYawStableCount++;
|
|
206
|
+
} else {
|
|
207
|
+
newYawStableCount = 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Calculate face stability for capture
|
|
212
|
+
let newStableCount = state.stableCount;
|
|
152
213
|
if (state.lastX === 0 && state.lastY === 0) {
|
|
153
|
-
// First detection
|
|
154
214
|
newStableCount = 1;
|
|
155
215
|
} else {
|
|
156
216
|
const dx = Math.abs(x - state.lastX);
|
|
157
217
|
const dy = Math.abs(y - state.lastY);
|
|
158
|
-
|
|
159
218
|
if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
|
|
160
219
|
newStableCount = state.stableCount + 1;
|
|
161
220
|
} else {
|
|
162
|
-
newStableCount = 1;
|
|
221
|
+
newStableCount = 1;
|
|
163
222
|
}
|
|
164
223
|
}
|
|
165
224
|
|
|
166
225
|
// Update shared state
|
|
167
226
|
sharedState.value = {
|
|
168
227
|
...state,
|
|
228
|
+
lastProcessedTime: now,
|
|
169
229
|
lastX: x,
|
|
170
230
|
lastY: y,
|
|
171
231
|
lastW: width,
|
|
172
232
|
lastH: height,
|
|
173
233
|
stableCount: newStableCount,
|
|
174
|
-
|
|
234
|
+
livenessStep: newLivenessStep,
|
|
235
|
+
leftTurnVerified: newLeftTurnVerified,
|
|
236
|
+
rightTurnVerified: newRightTurnVerified,
|
|
237
|
+
currentYaw: yaw,
|
|
238
|
+
yawStableCount: newYawStableCount,
|
|
175
239
|
};
|
|
176
240
|
|
|
177
|
-
|
|
241
|
+
// Calculate progress for UI
|
|
242
|
+
const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100);
|
|
243
|
+
runOnFaces(1, progress, newLivenessStep);
|
|
244
|
+
|
|
245
|
+
// Capture conditions: liveness complete + face stable + face centered
|
|
246
|
+
if (newLivenessStep === 3 &&
|
|
247
|
+
newLeftTurnVerified &&
|
|
248
|
+
newRightTurnVerified &&
|
|
249
|
+
newStableCount >= FACE_STABILITY_THRESHOLD &&
|
|
250
|
+
Math.abs(yaw) < YAW_CENTER_THRESHOLD &&
|
|
251
|
+
newYawStableCount >= 2) {
|
|
252
|
+
|
|
178
253
|
sharedState.value = {
|
|
179
254
|
...sharedState.value,
|
|
180
255
|
captured: true
|
|
181
256
|
};
|
|
182
|
-
runOnStable(x, y, width, height);
|
|
183
|
-
} else {
|
|
184
|
-
runOnFaces(1, newStableCount);
|
|
257
|
+
runOnStable({ x, y, width, height });
|
|
185
258
|
}
|
|
259
|
+
|
|
186
260
|
} else {
|
|
187
|
-
// Multiple faces
|
|
188
|
-
sharedState.value = {
|
|
189
|
-
...state,
|
|
190
|
-
stableCount: 0,
|
|
191
|
-
lastProcessedTime: now
|
|
261
|
+
// Multiple faces or other conditions
|
|
262
|
+
sharedState.value = {
|
|
263
|
+
...state,
|
|
264
|
+
stableCount: 0,
|
|
265
|
+
lastProcessedTime: now,
|
|
266
|
+
yawStableCount: 0
|
|
192
267
|
};
|
|
193
|
-
runOnFaces(detected.length, 0);
|
|
268
|
+
runOnFaces(detected.length, 0, state.livenessStep);
|
|
194
269
|
}
|
|
195
270
|
}, [detectFaces, isLoading]);
|
|
196
271
|
|
|
197
|
-
// Reset everything safely
|
|
198
272
|
const resetCaptureState = useCallback(() => {
|
|
199
273
|
sharedState.value = {
|
|
200
274
|
...sharedState.value,
|
|
@@ -204,11 +278,15 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
204
278
|
lastW: 0,
|
|
205
279
|
lastH: 0,
|
|
206
280
|
stableCount: 0,
|
|
207
|
-
captured: false
|
|
281
|
+
captured: false,
|
|
282
|
+
livenessStep: 0,
|
|
283
|
+
leftTurnVerified: false,
|
|
284
|
+
rightTurnVerified: false,
|
|
285
|
+
currentYaw: 0,
|
|
286
|
+
yawStableCount: 0,
|
|
208
287
|
};
|
|
209
288
|
}, [sharedState]);
|
|
210
289
|
|
|
211
|
-
// Force reset capture state (for external calls)
|
|
212
290
|
const forceResetCaptureState = useCallback(() => {
|
|
213
291
|
sharedState.value = {
|
|
214
292
|
lastProcessedTime: 0,
|
|
@@ -219,31 +297,34 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
219
297
|
stableCount: 0,
|
|
220
298
|
captured: false,
|
|
221
299
|
showCodeScanner: sharedState.value.showCodeScanner,
|
|
222
|
-
isActive: sharedState.value.isActive
|
|
300
|
+
isActive: sharedState.value.isActive,
|
|
301
|
+
livenessStep: 0,
|
|
302
|
+
leftTurnVerified: false,
|
|
303
|
+
rightTurnVerified: false,
|
|
304
|
+
currentYaw: 0,
|
|
305
|
+
yawStableCount: 0,
|
|
223
306
|
};
|
|
224
307
|
}, [sharedState]);
|
|
225
308
|
|
|
226
|
-
// Update QR/Scanner toggle
|
|
227
309
|
const updateShowCodeScanner = useCallback(
|
|
228
310
|
(value) => {
|
|
229
|
-
sharedState.value = {
|
|
230
|
-
...sharedState.value,
|
|
231
|
-
showCodeScanner: !!value
|
|
311
|
+
sharedState.value = {
|
|
312
|
+
...sharedState.value,
|
|
313
|
+
showCodeScanner: !!value
|
|
232
314
|
};
|
|
233
|
-
},
|
|
315
|
+
},
|
|
234
316
|
[sharedState]
|
|
235
317
|
);
|
|
236
318
|
|
|
237
|
-
// Update active state
|
|
238
319
|
const updateIsActive = useCallback(
|
|
239
320
|
(active) => {
|
|
240
|
-
sharedState.value = {
|
|
241
|
-
...sharedState.value,
|
|
321
|
+
sharedState.value = {
|
|
322
|
+
...sharedState.value,
|
|
242
323
|
isActive: active,
|
|
243
|
-
// Reset capture state when
|
|
244
|
-
captured: active ?
|
|
324
|
+
// Reset capture state when deactivating
|
|
325
|
+
captured: active ? sharedState.value.captured : false
|
|
245
326
|
};
|
|
246
|
-
},
|
|
327
|
+
},
|
|
247
328
|
[sharedState]
|
|
248
329
|
);
|
|
249
330
|
|
|
@@ -253,7 +334,6 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
253
334
|
|
|
254
335
|
return () => {
|
|
255
336
|
isMounted.current = false;
|
|
256
|
-
// Reset state when component unmounts
|
|
257
337
|
forceResetCaptureState();
|
|
258
338
|
};
|
|
259
339
|
}, [forceResetCaptureState]);
|
|
@@ -261,7 +341,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
261
341
|
return {
|
|
262
342
|
frameProcessor,
|
|
263
343
|
resetCaptureState,
|
|
264
|
-
forceResetCaptureState,
|
|
344
|
+
forceResetCaptureState,
|
|
265
345
|
updateShowCodeScanner,
|
|
266
346
|
updateIsActive,
|
|
267
347
|
capturedSV: { value: sharedState.value.captured },
|