react-native-biometric-verifier 0.0.66 → 0.0.67
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
|
@@ -7,15 +7,18 @@ import {
|
|
|
7
7
|
ActivityIndicator,
|
|
8
8
|
Animated,
|
|
9
9
|
Dimensions,
|
|
10
|
+
Platform,
|
|
10
11
|
} from 'react-native';
|
|
11
12
|
import {
|
|
12
13
|
Camera,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
useCameraDevices,
|
|
15
|
+
useCameraPermission,
|
|
16
|
+
usePhotoOutput,
|
|
16
17
|
} from 'react-native-vision-camera';
|
|
18
|
+
import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner';
|
|
17
19
|
import { Global } from '../utils/Global';
|
|
18
20
|
import { useFaceDetectionFrameProcessor } from '../hooks/useFaceDetectionFrameProcessor';
|
|
21
|
+
import DeviceBrightness from '@adrianso/react-native-device-brightness';
|
|
19
22
|
|
|
20
23
|
const CaptureImageWithoutEdit = React.memo(
|
|
21
24
|
({
|
|
@@ -33,6 +36,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
33
36
|
const [cameraInitialized, setCameraInitialized] = useState(false);
|
|
34
37
|
const [currentCameraType, setCurrentCameraType] = useState(cameraType);
|
|
35
38
|
const [isInitializing, setIsInitializing] = useState(true);
|
|
39
|
+
const [layout, setLayout] = useState({ width: 0, height: 0 });
|
|
36
40
|
|
|
37
41
|
const [faces, setFaces] = useState([]);
|
|
38
42
|
const [livenessStep, setLivenessStep] = useState(0);
|
|
@@ -50,6 +54,43 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
50
54
|
const instructionAnim = useRef(new Animated.Value(1)).current;
|
|
51
55
|
const liveIndicatorAnim = useRef(new Animated.Value(0)).current;
|
|
52
56
|
|
|
57
|
+
const originalBrightnessRef = useRef(null);
|
|
58
|
+
|
|
59
|
+
// Camera hooks - NEW API
|
|
60
|
+
const devices = useCameraDevices();
|
|
61
|
+
const { hasPermission, requestPermission } = useCameraPermission();
|
|
62
|
+
const photoOutput = usePhotoOutput();
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const adjustBrightness = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const currentBr = await DeviceBrightness.getBrightnessLevel();
|
|
68
|
+
originalBrightnessRef.current = currentBr;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.log('Error getting brightness:', e);
|
|
71
|
+
originalBrightnessRef.current = 0.6;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await DeviceBrightness.setBrightnessLevel(1.0);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log('Error setting brightness:', e);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
adjustBrightness();
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
if (originalBrightnessRef.current !== null) {
|
|
85
|
+
try {
|
|
86
|
+
DeviceBrightness.setBrightnessLevel(originalBrightnessRef.current);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.log('Error restoring brightness in cleanup:', e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
53
94
|
const resetCaptureState = useCallback(() => {
|
|
54
95
|
captured.current = false;
|
|
55
96
|
setFaces([]);
|
|
@@ -63,18 +104,20 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
63
104
|
setHasSingleFace(false);
|
|
64
105
|
}, []);
|
|
65
106
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
onCapture(codes[0].value);
|
|
72
|
-
}
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.error('Error processing scanned code:', error);
|
|
107
|
+
// CodeScanner component - NEW API
|
|
108
|
+
const handleBarcodeScanned = useCallback((barcodes) => {
|
|
109
|
+
try {
|
|
110
|
+
if (showCodeScanner && barcodes && barcodes[0]?.rawValue && !isLoading) {
|
|
111
|
+
onCapture(barcodes[0].rawValue);
|
|
75
112
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Error processing scanned code:', error);
|
|
115
|
+
}
|
|
116
|
+
}, [showCodeScanner, isLoading, onCapture]);
|
|
117
|
+
|
|
118
|
+
const handleScannerError = useCallback((error) => {
|
|
119
|
+
console.error('Barcode scanner error:', error);
|
|
120
|
+
}, []);
|
|
78
121
|
|
|
79
122
|
const onStableFaceDetected = useCallback(
|
|
80
123
|
async (faceRect) => {
|
|
@@ -89,18 +132,20 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
89
132
|
throw new Error('Camera ref not available');
|
|
90
133
|
}
|
|
91
134
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
135
|
+
// Use photoOutput.capturePhotoToFile - NEW API
|
|
136
|
+
const photo = await photoOutput.capturePhotoToFile(
|
|
137
|
+
{
|
|
138
|
+
flashMode: 'off',
|
|
139
|
+
qualityPrioritization: 'quality',
|
|
140
|
+
},
|
|
141
|
+
{}
|
|
142
|
+
);
|
|
98
143
|
|
|
99
|
-
if (!photo || !photo.
|
|
144
|
+
if (!photo || !photo.filePath) {
|
|
100
145
|
throw new Error('Failed to capture photo - no path returned');
|
|
101
146
|
}
|
|
102
147
|
|
|
103
|
-
const photopath = `file://${photo.
|
|
148
|
+
const photopath = `file://${photo.filePath}`;
|
|
104
149
|
const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
|
|
105
150
|
const photoData = {
|
|
106
151
|
uri: photopath,
|
|
@@ -115,7 +160,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
115
160
|
resetCaptureState();
|
|
116
161
|
}
|
|
117
162
|
},
|
|
118
|
-
[onCapture, resetCaptureState]
|
|
163
|
+
[onCapture, resetCaptureState, photoOutput]
|
|
119
164
|
);
|
|
120
165
|
|
|
121
166
|
const onFacesUpdate = useCallback((payload) => {
|
|
@@ -125,7 +170,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
125
170
|
setFaceCount(count);
|
|
126
171
|
setProgress(progress);
|
|
127
172
|
|
|
128
|
-
// Update anti-spoof related states
|
|
129
173
|
if (antiSpoofState) {
|
|
130
174
|
setIsFaceLive(antiSpoofState.isLive || false);
|
|
131
175
|
setAntiSpoofConfidence(antiSpoofState.confidence || 0);
|
|
@@ -164,7 +208,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
164
208
|
const onAntiSpoofUpdate = useCallback((result) => {
|
|
165
209
|
if (!isMounted.current) return;
|
|
166
210
|
try {
|
|
167
|
-
// Animate live indicator when face becomes live
|
|
168
211
|
if (result?.isLive && !isFaceLive) {
|
|
169
212
|
Animated.spring(liveIndicatorAnim, {
|
|
170
213
|
toValue: 1,
|
|
@@ -189,7 +232,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
189
232
|
}, [isFaceLive, liveIndicatorAnim]);
|
|
190
233
|
|
|
191
234
|
const {
|
|
192
|
-
|
|
235
|
+
outputs: faceOutputs,
|
|
193
236
|
forceResetCaptureState,
|
|
194
237
|
updateShowCodeScanner,
|
|
195
238
|
updateIsActive,
|
|
@@ -204,6 +247,9 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
204
247
|
isActive: showCamera && cameraInitialized,
|
|
205
248
|
livenessLevel: livenessLevel,
|
|
206
249
|
antispooflevel,
|
|
250
|
+
cameraFacing: currentCameraType,
|
|
251
|
+
windowWidth: layout.width,
|
|
252
|
+
windowHeight: layout.height,
|
|
207
253
|
});
|
|
208
254
|
|
|
209
255
|
useEffect(() => {
|
|
@@ -214,68 +260,55 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
214
260
|
}
|
|
215
261
|
}, [capturedSV?.value]);
|
|
216
262
|
|
|
217
|
-
|
|
263
|
+
// Camera permission and device selection - NEW API
|
|
264
|
+
const initializeCamera = useCallback(async () => {
|
|
218
265
|
try {
|
|
219
266
|
if (!isMounted.current) return;
|
|
220
267
|
|
|
221
268
|
setIsInitializing(true);
|
|
222
269
|
setShowCamera(false);
|
|
223
270
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
let devices = await Camera?.getAvailableCameraDevices();
|
|
271
|
+
const permission = await requestPermission();
|
|
272
|
+
console.log('Camera permission:', permission);
|
|
227
273
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
devices = await Camera?.getAvailableCameraDevices();
|
|
232
|
-
}
|
|
274
|
+
if (permission === true || permission === 'granted') {
|
|
275
|
+
// Find device by position
|
|
276
|
+
const device = devices.find((d) => d.position === currentCameraType) || devices[0];
|
|
233
277
|
|
|
234
|
-
if (!
|
|
235
|
-
throw new Error(
|
|
278
|
+
if (!device) {
|
|
279
|
+
throw new Error(`No ${currentCameraType} camera available`);
|
|
236
280
|
}
|
|
237
281
|
|
|
238
|
-
|
|
239
|
-
if (!device) throw new Error(`No ${currentCameraType} camera available`);
|
|
240
|
-
|
|
282
|
+
console.log('Selected device:', device);
|
|
241
283
|
setCameraDevice(device);
|
|
242
284
|
setShowCamera(true);
|
|
243
285
|
} else {
|
|
244
286
|
console.warn('Camera permission not granted');
|
|
287
|
+
setShowCamera(false);
|
|
245
288
|
}
|
|
246
289
|
} catch (error) {
|
|
247
|
-
console.error('Camera
|
|
290
|
+
console.error('Camera initialization error:', error);
|
|
248
291
|
setShowCamera(false);
|
|
249
292
|
} finally {
|
|
250
293
|
if (isMounted.current) {
|
|
251
294
|
setIsInitializing(false);
|
|
252
295
|
}
|
|
253
296
|
}
|
|
254
|
-
}, [currentCameraType]);
|
|
255
|
-
|
|
256
|
-
const initializeCamera = useCallback(async () => {
|
|
257
|
-
await getPermission();
|
|
258
|
-
}, [getPermission]);
|
|
297
|
+
}, [currentCameraType, devices, requestPermission]);
|
|
259
298
|
|
|
260
299
|
useEffect(() => {
|
|
261
300
|
isMounted.current = true;
|
|
262
301
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (error) {
|
|
267
|
-
console.error('Failed to initialize camera on mount:', error);
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
initOnMount();
|
|
302
|
+
if (devices && devices.length > 0) {
|
|
303
|
+
initializeCamera();
|
|
304
|
+
}
|
|
272
305
|
|
|
273
306
|
return () => {
|
|
274
307
|
isMounted.current = false;
|
|
275
308
|
setShowCamera(false);
|
|
276
309
|
forceResetCaptureState();
|
|
277
310
|
};
|
|
278
|
-
}, [initializeCamera, forceResetCaptureState]);
|
|
311
|
+
}, [devices, initializeCamera, forceResetCaptureState]);
|
|
279
312
|
|
|
280
313
|
useEffect(() => {
|
|
281
314
|
updateIsActive(showCamera && cameraInitialized);
|
|
@@ -284,13 +317,11 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
284
317
|
useEffect(() => {
|
|
285
318
|
if (cameraType !== currentCameraType) {
|
|
286
319
|
setCurrentCameraType(cameraType);
|
|
287
|
-
|
|
320
|
+
if (devices && devices.length > 0) {
|
|
321
|
+
initializeCamera();
|
|
322
|
+
}
|
|
288
323
|
}
|
|
289
|
-
}, [cameraType, currentCameraType, initializeCamera]);
|
|
290
|
-
|
|
291
|
-
const format = useCameraFormat(cameraDevice, [
|
|
292
|
-
{ fps: 30 },
|
|
293
|
-
]);
|
|
324
|
+
}, [cameraType, currentCameraType, initializeCamera, devices]);
|
|
294
325
|
|
|
295
326
|
useEffect(() => {
|
|
296
327
|
try {
|
|
@@ -315,11 +346,13 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
315
346
|
setCameraInitialized(false);
|
|
316
347
|
forceResetCaptureState();
|
|
317
348
|
resetCaptureState();
|
|
318
|
-
|
|
349
|
+
if (devices && devices.length > 0) {
|
|
350
|
+
await initializeCamera();
|
|
351
|
+
}
|
|
319
352
|
} catch (error) {
|
|
320
353
|
console.error('Retry failed:', error);
|
|
321
354
|
}
|
|
322
|
-
}, [initializeCamera, resetCaptureState, forceResetCaptureState]);
|
|
355
|
+
}, [initializeCamera, resetCaptureState, forceResetCaptureState, devices]);
|
|
323
356
|
|
|
324
357
|
const getInstruction = useCallback(() => {
|
|
325
358
|
if (faceCount > 1) {
|
|
@@ -424,35 +457,95 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
424
457
|
|
|
425
458
|
const stepConfig = getStepConfig();
|
|
426
459
|
|
|
460
|
+
const getOverlayHeights = () => {
|
|
461
|
+
const containerHeight = layout.height || 400;
|
|
462
|
+
const overlayHeight = containerHeight * 0.15;
|
|
463
|
+
return { overlayHeight };
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const { overlayHeight } = getOverlayHeights();
|
|
467
|
+
const confidencePercent = Math.round(
|
|
468
|
+
Math.max(0, Math.min(1, Number(antiSpoofConfidence) || 0)) * 100
|
|
469
|
+
);
|
|
470
|
+
const confidenceColor = confidencePercent >= 70
|
|
471
|
+
? Global.AppTheme.success
|
|
472
|
+
: confidencePercent >= 40
|
|
473
|
+
? Global.AppTheme.warning
|
|
474
|
+
: Global.AppTheme.error;
|
|
475
|
+
|
|
476
|
+
const scannerOutput = useBarcodeScannerOutput({
|
|
477
|
+
barcodeFormats: [
|
|
478
|
+
'qr-code',
|
|
479
|
+
'ean-13',
|
|
480
|
+
'code-128',
|
|
481
|
+
'code-39',
|
|
482
|
+
'code-93',
|
|
483
|
+
],
|
|
484
|
+
onBarcodeScanned: handleBarcodeScanned,
|
|
485
|
+
onError: handleScannerError,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const cameraOutputs = showCodeScanner
|
|
489
|
+
? [photoOutput, scannerOutput]
|
|
490
|
+
: [photoOutput, ...faceOutputs];
|
|
491
|
+
|
|
427
492
|
return (
|
|
428
493
|
<View style={styles.container}>
|
|
429
|
-
<View
|
|
494
|
+
<View
|
|
495
|
+
style={styles.cameraContainer}
|
|
496
|
+
onLayout={(event) => {
|
|
497
|
+
const { width, height } = event.nativeEvent.layout;
|
|
498
|
+
setLayout({ width, height });
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
430
501
|
{!isInitializing && showCamera && cameraDevice ? (
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
502
|
+
<View style={StyleSheet.absoluteFill}>
|
|
503
|
+
<Camera
|
|
504
|
+
ref={cameraRef}
|
|
505
|
+
style={styles.camera}
|
|
506
|
+
device={cameraDevice}
|
|
507
|
+
isActive={showCamera && !isLoading}
|
|
508
|
+
outputs={cameraOutputs}
|
|
509
|
+
enableNativeZoomGesture={false}
|
|
510
|
+
onStarted={() => {
|
|
511
|
+
setCameraInitialized(true);
|
|
512
|
+
}}
|
|
513
|
+
onStopped={() => {
|
|
514
|
+
setCameraInitialized(false);
|
|
515
|
+
}}
|
|
516
|
+
onError={(error) => {
|
|
517
|
+
console.error('Camera error:', error);
|
|
518
|
+
}}
|
|
519
|
+
exposure={1.2}
|
|
520
|
+
/>
|
|
521
|
+
{currentCameraType === 'front' && (
|
|
522
|
+
<View style={StyleSheet.absoluteFillObject} pointerEvents="none">
|
|
523
|
+
{/* Top Translucent White Overlay (Header) */}
|
|
524
|
+
<View style={{
|
|
525
|
+
position: 'absolute',
|
|
526
|
+
top: 0,
|
|
527
|
+
left: 0,
|
|
528
|
+
right: 0,
|
|
529
|
+
height: overlayHeight,
|
|
530
|
+
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
531
|
+
borderBottomWidth: 1,
|
|
532
|
+
borderBottomColor: 'rgba(255, 255, 255, 0.8)',
|
|
533
|
+
}} />
|
|
534
|
+
|
|
535
|
+
{/* Bottom Translucent White Overlay (Footer) */}
|
|
536
|
+
<View style={{
|
|
537
|
+
position: 'absolute',
|
|
538
|
+
bottom: 0,
|
|
539
|
+
left: 0,
|
|
540
|
+
right: 0,
|
|
541
|
+
height: overlayHeight,
|
|
542
|
+
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
543
|
+
borderTopWidth: 1,
|
|
544
|
+
borderTopColor: 'rgba(255, 255, 255, 0.8)',
|
|
545
|
+
}} />
|
|
546
|
+
</View>
|
|
547
|
+
)}
|
|
548
|
+
</View>
|
|
456
549
|
) : (
|
|
457
550
|
<View style={styles.placeholderContainer}>
|
|
458
551
|
{isInitializing && (
|
|
@@ -545,19 +638,15 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
545
638
|
{isFaceCentered && (
|
|
546
639
|
<View style={styles.confidenceContainer}>
|
|
547
640
|
<Text style={styles.confidenceText}>
|
|
548
|
-
Confidence: {
|
|
641
|
+
Confidence: {confidencePercent}%
|
|
549
642
|
</Text>
|
|
550
643
|
<View style={styles.confidenceBar}>
|
|
551
644
|
<View
|
|
552
645
|
style={[
|
|
553
646
|
styles.confidenceProgress,
|
|
554
647
|
{
|
|
555
|
-
width: `${
|
|
556
|
-
backgroundColor:
|
|
557
|
-
? Global.AppTheme.success
|
|
558
|
-
: antiSpoofConfidence * 100 > 20
|
|
559
|
-
? Global.AppTheme.warning
|
|
560
|
-
: Global.AppTheme.error
|
|
648
|
+
width: `${confidencePercent}%`,
|
|
649
|
+
backgroundColor: confidenceColor,
|
|
561
650
|
}
|
|
562
651
|
]}
|
|
563
652
|
/>
|
|
@@ -670,7 +759,6 @@ const styles = StyleSheet.create({
|
|
|
670
759
|
color: 'white',
|
|
671
760
|
fontWeight: 'bold',
|
|
672
761
|
},
|
|
673
|
-
// Live Indicator
|
|
674
762
|
liveIndicator: {
|
|
675
763
|
position: 'absolute',
|
|
676
764
|
top: 20,
|
|
@@ -704,7 +792,6 @@ const styles = StyleSheet.create({
|
|
|
704
792
|
fontSize: 12,
|
|
705
793
|
fontWeight: 'bold',
|
|
706
794
|
},
|
|
707
|
-
// Status Overview
|
|
708
795
|
statusOverview: {
|
|
709
796
|
position: 'absolute',
|
|
710
797
|
bottom: 20,
|
|
@@ -758,7 +845,6 @@ const styles = StyleSheet.create({
|
|
|
758
845
|
height: '100%',
|
|
759
846
|
borderRadius: 2,
|
|
760
847
|
},
|
|
761
|
-
// Existing styles
|
|
762
848
|
blinkProgressContainer: {
|
|
763
849
|
flexDirection: 'row',
|
|
764
850
|
marginVertical: 8,
|
|
@@ -1,620 +1,207 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const runOnAntiSpoof = useMemo(
|
|
209
|
-
() =>
|
|
210
|
-
Worklets.createRunOnJS((result) => {
|
|
211
|
-
const now = Date.now();
|
|
212
|
-
const callbacks = callbacksRef.current;
|
|
213
|
-
|
|
214
|
-
if (now - callbacks.lastAntiSpoofEventTime > ANTI_SPOOF_EVENT_INTERVAL_MS) {
|
|
215
|
-
callbacks.lastAntiSpoofEventTime = now;
|
|
216
|
-
onAntiSpoofUpdate?.(result);
|
|
217
|
-
}
|
|
218
|
-
}),
|
|
219
|
-
[onAntiSpoofUpdate]
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// Optimized face centering check - inlined for performance
|
|
223
|
-
const isFaceCenteredInFrame = Worklets.createRunOnJS((faceBounds, frameWidth, frameHeight) => {
|
|
224
|
-
'worklet';
|
|
225
|
-
|
|
226
|
-
if (!faceBounds || frameWidth === 0 || frameHeight === 0) return false;
|
|
227
|
-
|
|
228
|
-
const faceCenterX = faceBounds.x + faceBounds.width / 2;
|
|
229
|
-
const faceCenterY = faceBounds.y + faceBounds.height / 2;
|
|
230
|
-
const frameCenterX = frameWidth / 2;
|
|
231
|
-
const frameCenterY = frameHeight / 2;
|
|
232
|
-
|
|
233
|
-
return (
|
|
234
|
-
Math.abs(faceCenterX - frameCenterX) <= frameWidth * FACE_CENTER_THRESHOLD_X &&
|
|
235
|
-
Math.abs(faceCenterY - frameCenterY) <= frameHeight * FACE_CENTER_THRESHOLD_Y
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Fast early exit conditions check
|
|
240
|
-
const shouldProcessFrame = Worklets.createRunOnJS((state, now, isLoading) => {
|
|
241
|
-
'worklet';
|
|
242
|
-
return !(
|
|
243
|
-
state.flags.showCodeScanner ||
|
|
244
|
-
state.flags.captured ||
|
|
245
|
-
isLoading ||
|
|
246
|
-
!state.flags.isActive ||
|
|
247
|
-
(now - state.lastProcessedTime < FRAME_PROCESSOR_MIN_INTERVAL_MS)
|
|
248
|
-
);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Optimized frame processor
|
|
253
|
-
const frameProcessor = useFrameProcessor(
|
|
254
|
-
(frame) => {
|
|
255
|
-
'worklet';
|
|
256
|
-
|
|
257
|
-
// Performance monitoring
|
|
258
|
-
const processingStart = Date.now();
|
|
259
|
-
|
|
260
|
-
const state = sharedState.value;
|
|
261
|
-
const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now();
|
|
262
|
-
|
|
263
|
-
// Fast early exit
|
|
264
|
-
if (!shouldProcessFrame(state, now, isLoading)) {
|
|
265
|
-
frame.release?.();
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Performance guard - don't process if taking too long
|
|
270
|
-
if (processingStart - frameProcessingStartTime.current < MAX_FRAME_PROCESSING_TIME_MS) {
|
|
271
|
-
frame.release?.();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
frameProcessingStartTime.current = processingStart;
|
|
276
|
-
|
|
277
|
-
let detected = null;
|
|
278
|
-
let antiSpoofResult = null;
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
// Initialize frame dimensions once
|
|
282
|
-
if (state.centering.frameWidth === 0) {
|
|
283
|
-
state.centering.frameWidth = frame.width;
|
|
284
|
-
state.centering.frameHeight = frame.height;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Detect faces
|
|
288
|
-
detected = detectFaces?.(frame);
|
|
289
|
-
|
|
290
|
-
// Fast path for no faces
|
|
291
|
-
if (!detected || detected.length === 0) {
|
|
292
|
-
state.faceTracking.stableCount = 0;
|
|
293
|
-
state.antiSpoof.consecutiveLiveFrames = 0;
|
|
294
|
-
state.flags.hasSingleFace = false;
|
|
295
|
-
state.centering.centeredFrames = 0;
|
|
296
|
-
state.flags.isFaceCentered = false;
|
|
297
|
-
state.lastProcessedTime = now;
|
|
298
|
-
|
|
299
|
-
runOnFaces(0, 0, state.liveness.step, false, {
|
|
300
|
-
isLive: false,
|
|
301
|
-
confidence: 0,
|
|
302
|
-
consecutiveLiveFrames: 0,
|
|
303
|
-
isFaceCentered: false,
|
|
304
|
-
hasSingleFace: false,
|
|
305
|
-
});
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Process single face scenario
|
|
310
|
-
if (detected.length === 1 && !state.flags.captured) {
|
|
311
|
-
const face = detected[0];
|
|
312
|
-
if (!face?.bounds) {
|
|
313
|
-
runOnFaces(0, 0, state.liveness.step, false, {
|
|
314
|
-
isLive: false,
|
|
315
|
-
confidence: 0,
|
|
316
|
-
consecutiveLiveFrames: 0,
|
|
317
|
-
isFaceCentered: false,
|
|
318
|
-
hasSingleFace: false,
|
|
319
|
-
});
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const bounds = face.bounds;
|
|
324
|
-
const x = Math.max(0, bounds.x);
|
|
325
|
-
const y = Math.max(0, bounds.y);
|
|
326
|
-
const width = Math.max(0, bounds.width);
|
|
327
|
-
const height = Math.max(0, bounds.height);
|
|
328
|
-
|
|
329
|
-
// Local state snapshot for performance
|
|
330
|
-
const localState = {
|
|
331
|
-
livenessLevel: state.liveness.level,
|
|
332
|
-
isLive: state.antiSpoof.isLive,
|
|
333
|
-
consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
|
|
334
|
-
isFaceCentered: state.flags.isFaceCentered,
|
|
335
|
-
antiSpoofConfidence: state.antiSpoof.confidence,
|
|
336
|
-
livenessStep: state.liveness.step,
|
|
337
|
-
blinkCount: state.liveness.blinkCount,
|
|
338
|
-
eyeClosed: state.flags.eyeClosed,
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
// Update single face state
|
|
342
|
-
state.flags.hasSingleFace = true;
|
|
343
|
-
|
|
344
|
-
// Face centering check
|
|
345
|
-
const centered = isFaceCenteredInFrame(
|
|
346
|
-
bounds,
|
|
347
|
-
state.centering.frameWidth,
|
|
348
|
-
state.centering.frameHeight
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
if (centered) {
|
|
352
|
-
state.centering.centeredFrames = Math.min(
|
|
353
|
-
MIN_FACE_CENTERED_FRAMES,
|
|
354
|
-
state.centering.centeredFrames + 1
|
|
355
|
-
);
|
|
356
|
-
} else {
|
|
357
|
-
state.centering.centeredFrames = 0;
|
|
358
|
-
}
|
|
359
|
-
state.flags.isFaceCentered = state.centering.centeredFrames >= MIN_FACE_CENTERED_FRAMES;
|
|
360
|
-
|
|
361
|
-
// Anti-spoof detection only when face is centered and single
|
|
362
|
-
if (state.flags.isFaceCentered) {
|
|
363
|
-
try {
|
|
364
|
-
antiSpoofResult = faceAntiSpoofFrameProcessor?.(frame);
|
|
365
|
-
if (antiSpoofResult != null) {
|
|
366
|
-
state.antiSpoof.lastResult = antiSpoofResult;
|
|
367
|
-
|
|
368
|
-
const { laplacianScore = 0, confidence = 0, combinedScore = 0 } = antiSpoofResult;
|
|
369
|
-
|
|
370
|
-
if (laplacianScore > REAL_LAPLACIAN_THRESHOLD &&
|
|
371
|
-
confidence > antispooflevel &&
|
|
372
|
-
combinedScore > antispooflevel) {
|
|
373
|
-
state.antiSpoof.consecutiveLiveFrames = Math.min(
|
|
374
|
-
REQUIRED_CONSECUTIVE_LIVE_FRAMES,
|
|
375
|
-
state.antiSpoof.consecutiveLiveFrames + 1
|
|
376
|
-
);
|
|
377
|
-
} else {
|
|
378
|
-
state.antiSpoof.consecutiveLiveFrames = Math.max(0, state.antiSpoof.consecutiveLiveFrames - 1);
|
|
379
|
-
}
|
|
380
|
-
state.antiSpoof.isLive = state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES;
|
|
381
|
-
state.antiSpoof.confidence = confidence;
|
|
382
|
-
|
|
383
|
-
// Batch anti-spoof updates
|
|
384
|
-
if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) {
|
|
385
|
-
runOnAntiSpoof({
|
|
386
|
-
isLive: state.antiSpoof.isLive,
|
|
387
|
-
confidence: state.antiSpoof.confidence,
|
|
388
|
-
rawResult: antiSpoofResult,
|
|
389
|
-
consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
|
|
390
|
-
isFaceCentered: state.flags.isFaceCentered,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
} catch (antiSpoofError) {
|
|
395
|
-
// Silent error handling
|
|
396
|
-
}
|
|
397
|
-
} else {
|
|
398
|
-
// Reset anti-spoof if face not centered
|
|
399
|
-
state.antiSpoof.consecutiveLiveFrames = 0;
|
|
400
|
-
state.antiSpoof.isLive = false;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Liveness logic - optimized
|
|
404
|
-
let newLivenessStep = localState.livenessStep;
|
|
405
|
-
let newBlinkCount = localState.blinkCount;
|
|
406
|
-
let newEyeClosed = localState.eyeClosed;
|
|
407
|
-
|
|
408
|
-
if (localState.livenessLevel === 1) {
|
|
409
|
-
if (newLivenessStep === 0) {
|
|
410
|
-
newLivenessStep = 1;
|
|
411
|
-
runOnLiveness(newLivenessStep);
|
|
412
|
-
}
|
|
413
|
-
else if (newLivenessStep === 1) {
|
|
414
|
-
const leftEye = face.leftEyeOpenProbability ?? 1;
|
|
415
|
-
const rightEye = face.rightEyeOpenProbability ?? 1;
|
|
416
|
-
const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD;
|
|
417
|
-
|
|
418
|
-
if (eyesClosed && !newEyeClosed) {
|
|
419
|
-
newBlinkCount++;
|
|
420
|
-
newEyeClosed = true;
|
|
421
|
-
runOnLiveness(newLivenessStep, { blinkCount: newBlinkCount });
|
|
422
|
-
} else if (!eyesClosed && newEyeClosed) {
|
|
423
|
-
newEyeClosed = false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (newBlinkCount >= REQUIRED_BLINKS) {
|
|
427
|
-
newLivenessStep = 2;
|
|
428
|
-
runOnLiveness(newLivenessStep);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Face stability check - optimized
|
|
434
|
-
let newStableCount = state.faceTracking.stableCount;
|
|
435
|
-
if (state.faceTracking.lastX === 0 && state.faceTracking.lastY === 0) {
|
|
436
|
-
newStableCount = 1;
|
|
437
|
-
} else {
|
|
438
|
-
const dx = Math.abs(x - state.faceTracking.lastX);
|
|
439
|
-
const dy = Math.abs(y - state.faceTracking.lastY);
|
|
440
|
-
newStableCount = (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD)
|
|
441
|
-
? state.faceTracking.stableCount + 1
|
|
442
|
-
: 1;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Batch state updates
|
|
446
|
-
state.lastProcessedTime = now;
|
|
447
|
-
state.faceTracking.lastX = x;
|
|
448
|
-
state.faceTracking.lastY = y;
|
|
449
|
-
state.faceTracking.lastW = width;
|
|
450
|
-
state.faceTracking.lastH = height;
|
|
451
|
-
state.faceTracking.stableCount = newStableCount;
|
|
452
|
-
state.liveness.step = newLivenessStep;
|
|
453
|
-
state.liveness.blinkCount = newBlinkCount;
|
|
454
|
-
state.flags.eyeClosed = newEyeClosed;
|
|
455
|
-
state.performance.batchCounter++;
|
|
456
|
-
|
|
457
|
-
const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100);
|
|
458
|
-
|
|
459
|
-
// Batch face updates
|
|
460
|
-
if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) {
|
|
461
|
-
runOnFaces(1, progress, newLivenessStep, state.flags.isFaceCentered, {
|
|
462
|
-
isLive: state.antiSpoof.isLive,
|
|
463
|
-
confidence: state.antiSpoof.confidence,
|
|
464
|
-
consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
|
|
465
|
-
isFaceCentered: state.flags.isFaceCentered,
|
|
466
|
-
hasSingleFace: true,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Capture condition - optimized
|
|
471
|
-
const shouldCapture = !state.flags.captured && (
|
|
472
|
-
newStableCount >= FACE_STABILITY_THRESHOLD &&
|
|
473
|
-
state.antiSpoof.isLive &&
|
|
474
|
-
state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES &&
|
|
475
|
-
state.flags.isFaceCentered &&
|
|
476
|
-
(localState.livenessLevel === 0 || (
|
|
477
|
-
localState.livenessLevel === 1 &&
|
|
478
|
-
newLivenessStep === 2 &&
|
|
479
|
-
newBlinkCount >= REQUIRED_BLINKS
|
|
480
|
-
))
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
if (shouldCapture) {
|
|
484
|
-
state.flags.captured = true;
|
|
485
|
-
runOnStable(
|
|
486
|
-
{ x, y, width, height },
|
|
487
|
-
state.antiSpoof.lastResult
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
} else {
|
|
491
|
-
// Multiple faces - reset states
|
|
492
|
-
state.faceTracking.stableCount = 0;
|
|
493
|
-
state.lastProcessedTime = now;
|
|
494
|
-
state.antiSpoof.consecutiveLiveFrames = 0;
|
|
495
|
-
state.flags.hasSingleFace = false;
|
|
496
|
-
state.centering.centeredFrames = 0;
|
|
497
|
-
state.flags.isFaceCentered = false;
|
|
498
|
-
|
|
499
|
-
runOnFaces(detected.length, 0, state.liveness.step, false, {
|
|
500
|
-
isLive: false,
|
|
501
|
-
confidence: 0,
|
|
502
|
-
consecutiveLiveFrames: 0,
|
|
503
|
-
isFaceCentered: false,
|
|
504
|
-
hasSingleFace: false,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
} catch (err) {
|
|
508
|
-
// Error boundary - ensure frame is released
|
|
509
|
-
} finally {
|
|
510
|
-
frame.release?.();
|
|
511
|
-
}
|
|
512
|
-
},
|
|
513
|
-
[detectFaces, isLoading]
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
// Optimized reset functions
|
|
517
|
-
const resetCaptureState = useCallback(() => {
|
|
518
|
-
const state = sharedState.value;
|
|
519
|
-
state.lastProcessedTime = 0;
|
|
520
|
-
state.faceTracking.lastX = 0;
|
|
521
|
-
state.faceTracking.lastY = 0;
|
|
522
|
-
state.faceTracking.lastW = 0;
|
|
523
|
-
state.faceTracking.lastH = 0;
|
|
524
|
-
state.faceTracking.stableCount = 0;
|
|
525
|
-
state.flags.captured = false;
|
|
526
|
-
state.liveness.step = 0;
|
|
527
|
-
state.liveness.blinkCount = 0;
|
|
528
|
-
state.flags.eyeClosed = false;
|
|
529
|
-
state.antiSpoof.consecutiveLiveFrames = 0;
|
|
530
|
-
state.antiSpoof.lastResult = null;
|
|
531
|
-
state.antiSpoof.isLive = false;
|
|
532
|
-
state.antiSpoof.confidence = 0;
|
|
533
|
-
state.flags.hasSingleFace = false;
|
|
534
|
-
state.centering.centeredFrames = 0;
|
|
535
|
-
state.flags.isFaceCentered = false;
|
|
536
|
-
state.centering.frameWidth = 0;
|
|
537
|
-
state.centering.frameHeight = 0;
|
|
538
|
-
state.performance.batchCounter = 0;
|
|
539
|
-
}, [sharedState]);
|
|
540
|
-
|
|
541
|
-
const forceResetCaptureState = useCallback(() => {
|
|
542
|
-
const current = sharedState.value;
|
|
543
|
-
|
|
544
|
-
sharedState.value = {
|
|
545
|
-
lastProcessedTime: 0,
|
|
546
|
-
faceTracking: {
|
|
547
|
-
lastX: 0, lastY: 0, lastW: 0, lastH: 0, stableCount: 0
|
|
548
|
-
},
|
|
549
|
-
flags: {
|
|
550
|
-
captured: false,
|
|
551
|
-
showCodeScanner: current.flags.showCodeScanner,
|
|
552
|
-
isActive: current.flags.isActive,
|
|
553
|
-
hasSingleFace: false,
|
|
554
|
-
isFaceCentered: false,
|
|
555
|
-
eyeClosed: false,
|
|
556
|
-
},
|
|
557
|
-
liveness: {
|
|
558
|
-
level: current.liveness.level,
|
|
559
|
-
step: 0,
|
|
560
|
-
blinkCount: 0,
|
|
561
|
-
},
|
|
562
|
-
antiSpoof: {
|
|
563
|
-
consecutiveLiveFrames: 0,
|
|
564
|
-
lastResult: null,
|
|
565
|
-
isLive: false,
|
|
566
|
-
confidence: 0,
|
|
567
|
-
},
|
|
568
|
-
centering: {
|
|
569
|
-
centeredFrames: 0,
|
|
570
|
-
frameWidth: 0,
|
|
571
|
-
frameHeight: 0,
|
|
572
|
-
},
|
|
573
|
-
performance: {
|
|
574
|
-
batchCounter: 0,
|
|
575
|
-
lastBatchUpdate: 0,
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
}, [sharedState]);
|
|
579
|
-
|
|
580
|
-
const updateShowCodeScanner = useCallback(
|
|
581
|
-
(value) => {
|
|
582
|
-
sharedState.value.flags.showCodeScanner = !!value;
|
|
583
|
-
},
|
|
584
|
-
[sharedState]
|
|
585
|
-
);
|
|
586
|
-
|
|
587
|
-
const updateIsActive = useCallback(
|
|
588
|
-
(active) => {
|
|
589
|
-
sharedState.value.flags.isActive = !!active;
|
|
590
|
-
if (!active) sharedState.value.flags.captured = false;
|
|
591
|
-
},
|
|
592
|
-
[sharedState]
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
useEffect(() => {
|
|
596
|
-
isMounted.current = true;
|
|
597
|
-
return () => {
|
|
598
|
-
isMounted.current = false;
|
|
599
|
-
forceResetCaptureState();
|
|
600
|
-
};
|
|
601
|
-
}, [forceResetCaptureState]);
|
|
602
|
-
|
|
603
|
-
return {
|
|
604
|
-
frameProcessor,
|
|
605
|
-
resetCaptureState,
|
|
606
|
-
forceResetCaptureState,
|
|
607
|
-
updateShowCodeScanner,
|
|
608
|
-
updateIsActive,
|
|
609
|
-
initializeAntiSpoof,
|
|
610
|
-
capturedSV: { value: sharedState.value.flags.captured },
|
|
611
|
-
antiSpoofState: {
|
|
612
|
-
isLive: sharedState.value.antiSpoof.isLive,
|
|
613
|
-
confidence: sharedState.value.antiSpoof.confidence,
|
|
614
|
-
consecutiveLiveFrames: sharedState.value.antiSpoof.consecutiveLiveFrames,
|
|
615
|
-
lastResult: sharedState.value.antiSpoof.lastResult,
|
|
616
|
-
hasSingleFace: sharedState.value.flags.hasSingleFace,
|
|
617
|
-
isFaceCentered: sharedState.value.flags.isFaceCentered,
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
};
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useFaceDetectorOutput } from 'react-native-vision-camera-face-detector';
|
|
3
|
+
import { useFaceAntiSpoofFrameOutput } from 'react-native-vision-camera-spoof-detector';
|
|
4
|
+
|
|
5
|
+
const STABLE_FRAMES = 3;
|
|
6
|
+
const CENTER_FRAMES = 2;
|
|
7
|
+
const REQUIRED_BLINKS = 3;
|
|
8
|
+
const REQUIRED_LIVE_FRAMES = 3;
|
|
9
|
+
const BLINK_THRESHOLD = 0.3;
|
|
10
|
+
const MOVEMENT_THRESHOLD = 15;
|
|
11
|
+
const LAPLACIAN_THRESHOLD = 2500;
|
|
12
|
+
const CENTER_X = 0.2;
|
|
13
|
+
const CENTER_Y = 0.15;
|
|
14
|
+
|
|
15
|
+
export const useFaceDetectionFrameProcessor = ({
|
|
16
|
+
onStableFaceDetected = () => { },
|
|
17
|
+
onFacesUpdate = () => { },
|
|
18
|
+
onLivenessUpdate = () => { },
|
|
19
|
+
onAntiSpoofUpdate = () => { },
|
|
20
|
+
showCodeScanner = false,
|
|
21
|
+
isLoading = false,
|
|
22
|
+
isActive = true,
|
|
23
|
+
livenessLevel = 0,
|
|
24
|
+
antispooflevel = 0.35,
|
|
25
|
+
cameraFacing = 'front',
|
|
26
|
+
windowWidth = 0,
|
|
27
|
+
windowHeight = 0,
|
|
28
|
+
}) => {
|
|
29
|
+
const stateRef = useRef({});
|
|
30
|
+
const callbacksRef = useRef({
|
|
31
|
+
onStableFaceDetected,
|
|
32
|
+
onFacesUpdate,
|
|
33
|
+
onLivenessUpdate,
|
|
34
|
+
onAntiSpoofUpdate,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const resetState = useCallback(() => {
|
|
38
|
+
stateRef.current = {
|
|
39
|
+
captured: false,
|
|
40
|
+
hasSingleFace: false,
|
|
41
|
+
isFaceCentered: false,
|
|
42
|
+
stableCount: 0,
|
|
43
|
+
centeredFrames: 0,
|
|
44
|
+
lastX: 0,
|
|
45
|
+
lastY: 0,
|
|
46
|
+
isLive: false,
|
|
47
|
+
confidence: 0,
|
|
48
|
+
consecutiveLiveFrames: 0,
|
|
49
|
+
lastResult: null,
|
|
50
|
+
livenessStep: 0,
|
|
51
|
+
blinkCount: 0,
|
|
52
|
+
eyeClosed: false,
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
if (!stateRef.current.captured && !stateRef.current.hasSingleFace) {
|
|
57
|
+
resetState();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
callbacksRef.current = {
|
|
62
|
+
onStableFaceDetected,
|
|
63
|
+
onFacesUpdate,
|
|
64
|
+
onLivenessUpdate,
|
|
65
|
+
onAntiSpoofUpdate,
|
|
66
|
+
};
|
|
67
|
+
}, [onStableFaceDetected, onFacesUpdate, onLivenessUpdate, onAntiSpoofUpdate]);
|
|
68
|
+
|
|
69
|
+
const emitFaces = useCallback((count, progress, state) => {
|
|
70
|
+
callbacksRef.current.onFacesUpdate({
|
|
71
|
+
count,
|
|
72
|
+
progress,
|
|
73
|
+
step: state.livenessStep,
|
|
74
|
+
isCentered: state.isFaceCentered,
|
|
75
|
+
antiSpoofState: {
|
|
76
|
+
isLive: state.isLive,
|
|
77
|
+
confidence: state.confidence,
|
|
78
|
+
consecutiveLiveFrames: state.consecutiveLiveFrames,
|
|
79
|
+
isFaceCentered: state.isFaceCentered,
|
|
80
|
+
hasSingleFace: state.hasSingleFace,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const faceDetectorOutput = useFaceDetectorOutput({
|
|
86
|
+
cameraFacing,
|
|
87
|
+
autoMode: true,
|
|
88
|
+
windowWidth,
|
|
89
|
+
windowHeight,
|
|
90
|
+
performanceMode: 'fast',
|
|
91
|
+
trackingEnabled: true,
|
|
92
|
+
landmarkMode: 'none',
|
|
93
|
+
contourMode: 'none',
|
|
94
|
+
classificationMode: livenessLevel === 1 ? 'all' : 'none',
|
|
95
|
+
minFaceSize: 0.2,
|
|
96
|
+
onFacesDetected: (faces) => {
|
|
97
|
+
const state = stateRef.current;
|
|
98
|
+
if (showCodeScanner || isLoading || !isActive || state.captured) return;
|
|
99
|
+
|
|
100
|
+
if (!faces || faces.length !== 1 || !faces[0]?.bounds) {
|
|
101
|
+
state.hasSingleFace = false;
|
|
102
|
+
state.isFaceCentered = false;
|
|
103
|
+
state.centeredFrames = 0;
|
|
104
|
+
state.stableCount = 0;
|
|
105
|
+
state.consecutiveLiveFrames = 0;
|
|
106
|
+
state.isLive = false;
|
|
107
|
+
emitFaces(faces?.length || 0, 0, state);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { bounds } = faces[0];
|
|
112
|
+
const x = Math.max(0, bounds.x);
|
|
113
|
+
const y = Math.max(0, bounds.y);
|
|
114
|
+
const width = Math.max(0, bounds.width);
|
|
115
|
+
const height = Math.max(0, bounds.height);
|
|
116
|
+
const centered =
|
|
117
|
+
Math.abs(x + width / 2 - windowWidth / 2) <= windowWidth * CENTER_X &&
|
|
118
|
+
Math.abs(y + height / 2 - windowHeight / 2) <= windowHeight * CENTER_Y;
|
|
119
|
+
|
|
120
|
+
state.hasSingleFace = true;
|
|
121
|
+
state.centeredFrames = centered ? Math.min(CENTER_FRAMES, state.centeredFrames + 1) : 0;
|
|
122
|
+
state.isFaceCentered = state.centeredFrames >= CENTER_FRAMES;
|
|
123
|
+
state.stableCount =
|
|
124
|
+
Math.abs(x - state.lastX) < MOVEMENT_THRESHOLD &&
|
|
125
|
+
Math.abs(y - state.lastY) < MOVEMENT_THRESHOLD
|
|
126
|
+
? state.stableCount + 1
|
|
127
|
+
: 1;
|
|
128
|
+
state.lastX = x;
|
|
129
|
+
state.lastY = y;
|
|
130
|
+
|
|
131
|
+
if (livenessLevel === 1) {
|
|
132
|
+
const eyesClosed =
|
|
133
|
+
(faces[0].leftEyeOpenProbability ?? 1) < BLINK_THRESHOLD &&
|
|
134
|
+
(faces[0].rightEyeOpenProbability ?? 1) < BLINK_THRESHOLD;
|
|
135
|
+
if (state.livenessStep === 0) {
|
|
136
|
+
state.livenessStep = 1;
|
|
137
|
+
callbacksRef.current.onLivenessUpdate(1);
|
|
138
|
+
} else if (eyesClosed && !state.eyeClosed) {
|
|
139
|
+
state.blinkCount += 1;
|
|
140
|
+
callbacksRef.current.onLivenessUpdate(1, { blinkCount: state.blinkCount });
|
|
141
|
+
}
|
|
142
|
+
state.eyeClosed = eyesClosed;
|
|
143
|
+
if (state.blinkCount >= REQUIRED_BLINKS) state.livenessStep = 2;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
emitFaces(1, Math.min(100, (state.stableCount / STABLE_FRAMES) * 100), state);
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
state.stableCount >= STABLE_FRAMES &&
|
|
150
|
+
state.isLive &&
|
|
151
|
+
state.consecutiveLiveFrames >= REQUIRED_LIVE_FRAMES &&
|
|
152
|
+
state.isFaceCentered &&
|
|
153
|
+
(livenessLevel === 0 || state.livenessStep === 2)
|
|
154
|
+
) {
|
|
155
|
+
state.captured = true;
|
|
156
|
+
callbacksRef.current.onStableFaceDetected({ x, y, width, height }, state.lastResult);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
onError: (error) => console.warn('[useFaceDetection] face detector error:', error?.message),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const antiSpoofOutput = useFaceAntiSpoofFrameOutput({
|
|
163
|
+
onResult: (result) => {
|
|
164
|
+
const state = stateRef.current;
|
|
165
|
+
if (showCodeScanner || isLoading || !isActive || !state.isFaceCentered || !result) return;
|
|
166
|
+
console.log('Kkkkkkkkkkkkkk', JSON.stringify(result))
|
|
167
|
+
|
|
168
|
+
const { laplacianScore = 0, confidence = 0, combinedScore = 0 } = result;
|
|
169
|
+
const isLiveFrame =
|
|
170
|
+
laplacianScore > LAPLACIAN_THRESHOLD &&
|
|
171
|
+
confidence > antispooflevel &&
|
|
172
|
+
combinedScore > antispooflevel;
|
|
173
|
+
state.consecutiveLiveFrames = isLiveFrame
|
|
174
|
+
? Math.min(REQUIRED_LIVE_FRAMES, state.consecutiveLiveFrames + 1)
|
|
175
|
+
: Math.max(0, state.consecutiveLiveFrames - 1);
|
|
176
|
+
state.isLive = state.consecutiveLiveFrames >= REQUIRED_LIVE_FRAMES;
|
|
177
|
+
state.confidence = confidence;
|
|
178
|
+
state.lastResult = result;
|
|
179
|
+
callbacksRef.current.onAntiSpoofUpdate({
|
|
180
|
+
isLive: state.isLive,
|
|
181
|
+
confidence,
|
|
182
|
+
rawResult: result,
|
|
183
|
+
consecutiveLiveFrames: state.consecutiveLiveFrames,
|
|
184
|
+
isFaceCentered: state.isFaceCentered,
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
useEffect(() => () => resetState(), [resetState]);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
outputs: showCodeScanner ? [] : [faceDetectorOutput, antiSpoofOutput].filter(Boolean),
|
|
193
|
+
resetCaptureState: resetState,
|
|
194
|
+
forceResetCaptureState: resetState,
|
|
195
|
+
updateShowCodeScanner: () => { },
|
|
196
|
+
updateIsActive: () => { },
|
|
197
|
+
capturedSV: { value: stateRef.current.captured },
|
|
198
|
+
antiSpoofState: {
|
|
199
|
+
isLive: stateRef.current.isLive,
|
|
200
|
+
confidence: stateRef.current.confidence,
|
|
201
|
+
consecutiveLiveFrames: stateRef.current.consecutiveLiveFrames,
|
|
202
|
+
lastResult: stateRef.current.lastResult,
|
|
203
|
+
hasSingleFace: stateRef.current.hasSingleFace,
|
|
204
|
+
isFaceCentered: stateRef.current.isFaceCentered,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
};
|