react-native-biometric-verifier 0.0.65 → 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.
@@ -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
+ };