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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-biometric-verifier",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "description": "A React Native module for biometric verification with face recognition and QR code scanning",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -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
- getCameraDevice,
14
- useCodeScanner,
15
- useCameraFormat,
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
- const codeScanner = useCodeScanner({
67
- codeTypes: ['qr', 'ean-13'],
68
- onCodeScanned: (codes) => {
69
- try {
70
- if (showCodeScanner && codes && codes[0]?.value && !isLoading) {
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
- const photo = await cameraRef.current.takePhoto({
93
- flash: 'off',
94
- qualityPrioritization: 'quality',
95
- enableShutterSound: false,
96
- skipMetadata: true,
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.path) {
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.path}`;
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
- frameProcessor,
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
- const getPermission = useCallback(async () => {
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 newCameraPermission = await Camera?.requestCameraPermission();
225
- if (newCameraPermission === 'granted') {
226
- let devices = await Camera?.getAvailableCameraDevices();
271
+ const permission = await requestPermission();
272
+ console.log('Camera permission:', permission);
227
273
 
228
- // Retry once after short delay if no devices found
229
- if (!devices || devices.length === 0) {
230
- await new Promise((resolve) => setTimeout(resolve, 300));
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 (!devices || devices.length === 0) {
235
- throw new Error('No camera devices available');
278
+ if (!device) {
279
+ throw new Error(`No ${currentCameraType} camera available`);
236
280
  }
237
281
 
238
- const device = getCameraDevice(devices, currentCameraType);
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 permission error:', error);
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
- const initOnMount = async () => {
264
- try {
265
- await initializeCamera();
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
- initializeCamera();
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
- await initializeCamera();
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 style={styles.cameraContainer}>
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
- <Camera
432
- ref={cameraRef}
433
- style={styles.camera}
434
- device={cameraDevice}
435
- isActive={cameraInitialized && showCamera && !isLoading}
436
- photo={true}
437
- format={cameraDevice ? format : undefined}
438
- codeScanner={showCodeScanner && cameraInitialized ? codeScanner : undefined}
439
- enableZoomGesture={false}
440
- lowLightBoost={cameraDevice?.supportsLowLightBoost}
441
- frameProcessor={
442
- !showCodeScanner && cameraInitialized ? frameProcessor : undefined
443
- }
444
- frameProcessorFps={frameProcessorFps}
445
- onInitialized={() => {
446
- setCameraInitialized(true);
447
- }}
448
- onError={(error) => {
449
- console.error('Camera error:', error);
450
- }}
451
- exposure={0}
452
- pixelFormat="yuv"
453
- preset="photo"
454
- orientation="portrait"
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: {Math.round(antiSpoofConfidence * 100)}%
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: `${antiSpoofConfidence * 100}%`,
556
- backgroundColor: antiSpoofConfidence * 100 > 40
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, useMemo, useEffect, useRef } from 'react';
2
- import { Worklets } from 'react-native-worklets-core';
3
- import { useFrameProcessor } from 'react-native-vision-camera';
4
- import { useFaceDetector } from 'react-native-vision-camera-face-detector';
5
- import {
6
- faceAntiSpoofFrameProcessor,
7
- initializeFaceAntiSpoof,
8
- isFaceAntiSpoofAvailable,
9
- } from 'react-native-vision-camera-spoof-detector';
10
-
11
- // Optimized constants - tuned for performance
12
- const FACE_STABILITY_THRESHOLD = 3;
13
- const FACE_MOVEMENT_THRESHOLD = 15;
14
- const FRAME_PROCESSOR_MIN_INTERVAL_MS = 500;
15
- const MIN_FACE_SIZE = 0.2;
16
-
17
- // Blink detection
18
- const BLINK_THRESHOLD = 0.3;
19
- const REQUIRED_BLINKS = 3;
20
-
21
- // Anti-spoofing
22
- const REQUIRED_CONSECUTIVE_LIVE_FRAMES = 3;
23
-
24
- // Face centering
25
- const FACE_CENTER_THRESHOLD_X = 0.2;
26
- const FACE_CENTER_THRESHOLD_Y = 0.15;
27
- const MIN_FACE_CENTERED_FRAMES = 2;
28
-
29
- // Performance optimization constants
30
- const MAX_FRAME_PROCESSING_TIME_MS = 500;
31
- const BATCH_UPDATE_THRESHOLD = 3;
32
- const REAL_LAPLACIAN_THRESHOLD = 3500;
33
-
34
- export const useFaceDetectionFrameProcessor = ({
35
- onStableFaceDetected = () => { },
36
- onFacesUpdate = () => { },
37
- onLivenessUpdate = () => { },
38
- onAntiSpoofUpdate = () => { },
39
- showCodeScanner = false,
40
- isLoading = false,
41
- isActive = true,
42
- livenessLevel,
43
- antispooflevel = 0.35,
44
- }) => {
45
- const { detectFaces } = useFaceDetector({
46
- performanceMode: 'fast',
47
- landmarkMode: 'none',
48
- contourMode: 'none',
49
- classificationMode: livenessLevel === 1 ? 'all' : 'none',
50
- minFaceSize: MIN_FACE_SIZE,
51
- });
52
-
53
- const isMounted = useRef(true);
54
- const antiSpoofInitialized = useRef(false);
55
- const frameProcessingStartTime = useRef(0);
56
-
57
- // Initialize anti-spoofing with memoization
58
- const initializeAntiSpoof = useCallback(async () => {
59
- if (antiSpoofInitialized.current) return true;
60
-
61
- try {
62
- const available = isFaceAntiSpoofAvailable?.();
63
- if (!available) return false;
64
-
65
- const res = await initializeFaceAntiSpoof();
66
- antiSpoofInitialized.current = true;
67
- return true;
68
- } catch (err) {
69
- console.error('[useFaceDetection] Error initializing anti-spoof:', err);
70
- return false;
71
- }
72
- }, []);
73
-
74
- useEffect(() => {
75
- if (!antiSpoofInitialized.current) {
76
- initializeAntiSpoof();
77
- }
78
- }, [initializeAntiSpoof]);
79
-
80
- // Pre-computed shared state with optimized structure
81
- const sharedState = useMemo(
82
- () =>
83
- Worklets.createSharedValue({
84
- // Core timing
85
- lastProcessedTime: 0,
86
-
87
- // Face tracking - packed for memory efficiency
88
- faceTracking: { lastX: 0, lastY: 0, lastW: 0, lastH: 0, stableCount: 0 },
89
-
90
- // State flags - packed together
91
- flags: {
92
- captured: false,
93
- showCodeScanner: showCodeScanner,
94
- isActive: isActive,
95
- hasSingleFace: false,
96
- isFaceCentered: false,
97
- eyeClosed: false,
98
- },
99
-
100
- // Liveness state
101
- liveness: {
102
- level: livenessLevel,
103
- step: 0,
104
- blinkCount: 0,
105
- },
106
-
107
- // Anti-spoof state
108
- antiSpoof: {
109
- consecutiveLiveFrames: 0,
110
- lastResult: null,
111
- isLive: false,
112
- confidence: 0,
113
- },
114
-
115
- // Face centering
116
- centering: {
117
- centeredFrames: 0,
118
- frameWidth: 0,
119
- frameHeight: 0,
120
- },
121
-
122
- // Performance tracking
123
- performance: {
124
- batchCounter: 0,
125
- lastBatchUpdate: 0,
126
- }
127
- }),
128
- []
129
- );
130
-
131
- // Batched state updates
132
- useEffect(() => {
133
- if (!isMounted.current) return;
134
-
135
- const state = sharedState.value;
136
- state.flags.showCodeScanner = !!showCodeScanner;
137
- state.flags.isActive = !!isActive;
138
- state.liveness.level = livenessLevel;
139
-
140
- if (isActive && state.flags.captured) {
141
- // Batch reset all states
142
- state.faceTracking.stableCount = 0;
143
- state.liveness.step = 0;
144
- state.liveness.blinkCount = 0;
145
- state.flags.eyeClosed = false;
146
- state.flags.captured = false;
147
- state.antiSpoof.consecutiveLiveFrames = 0;
148
- state.antiSpoof.lastResult = null;
149
- state.antiSpoof.isLive = false;
150
- state.antiSpoof.confidence = 0;
151
- state.flags.hasSingleFace = false;
152
- state.centering.centeredFrames = 0;
153
- state.flags.isFaceCentered = false;
154
- }
155
- }, [showCodeScanner, isActive, livenessLevel, sharedState]);
156
-
157
- // Optimized JS callbacks with batching
158
- const callbacksRef = useRef({
159
- lastFacesEventTime: 0,
160
- lastLivenessEventTime: 0,
161
- lastAntiSpoofEventTime: 0,
162
- pendingFacesUpdate: null,
163
- pendingLivenessUpdate: null,
164
- pendingAntiSpoofUpdate: null,
165
- });
166
-
167
- const FACES_EVENT_INTERVAL_MS = 800;
168
- const LIVENESS_EVENT_INTERVAL_MS = 700;
169
- const ANTI_SPOOF_EVENT_INTERVAL_MS = 500;
170
-
171
- // Memoized callbacks with batching
172
- const runOnStable = useMemo(
173
- () =>
174
- Worklets.createRunOnJS((faceRect, antiSpoofResult) => {
175
- onStableFaceDetected?.(faceRect, antiSpoofResult);
176
- }),
177
- [onStableFaceDetected]
178
- );
179
-
180
- const runOnFaces = useMemo(
181
- () =>
182
- Worklets.createRunOnJS((count, progress, step, isCentered, antiSpoofState) => {
183
- const now = Date.now();
184
- const callbacks = callbacksRef.current;
185
-
186
- if (now - callbacks.lastFacesEventTime > FACES_EVENT_INTERVAL_MS) {
187
- callbacks.lastFacesEventTime = now;
188
- onFacesUpdate?.({ count, progress, step, isCentered, antiSpoofState });
189
- }
190
- }),
191
- [onFacesUpdate]
192
- );
193
-
194
- const runOnLiveness = useMemo(
195
- () =>
196
- Worklets.createRunOnJS((step, extra) => {
197
- const now = Date.now();
198
- const callbacks = callbacksRef.current;
199
-
200
- if (now - callbacks.lastLivenessEventTime > LIVENESS_EVENT_INTERVAL_MS) {
201
- callbacks.lastLivenessEventTime = now;
202
- onLivenessUpdate?.(step, extra);
203
- }
204
- }),
205
- [onLivenessUpdate]
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
+ };