react-native-biometric-verifier 0.0.28 → 0.0.30

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.
@@ -2,114 +2,189 @@ import { useCallback, useMemo, useEffect, useRef } from 'react';
2
2
  import { Worklets } from 'react-native-worklets-core';
3
3
  import { useFrameProcessor } from 'react-native-vision-camera';
4
4
  import { useFaceDetector } from 'react-native-vision-camera-face-detector';
5
+ import {
6
+ faceAntiSpoofFrameProcessor,
7
+ initializeFaceAntiSpoof,
8
+ isFaceAntiSpoofAvailable,
9
+ } from 'react-native-vision-camera-face-antispoof-detector';
5
10
 
6
- // Tuned constants for liveness detection / stability
11
+ // Optimized constants - tuned for performance
7
12
  const FACE_STABILITY_THRESHOLD = 3;
8
13
  const FACE_MOVEMENT_THRESHOLD = 15;
9
14
  const FRAME_PROCESSOR_MIN_INTERVAL_MS = 500;
10
15
  const MIN_FACE_SIZE = 0.2;
11
16
 
12
- // Liveness detection constants
13
- const YAW_LEFT_THRESHOLD = -10;
14
- const YAW_RIGHT_THRESHOLD = 10;
15
- const YAW_CENTER_THRESHOLD = 5;
16
-
17
- // Blink detection constants
18
- const BLINK_THRESHOLD = 0.3; // Eye closed if below this
17
+ // Blink detection
18
+ const BLINK_THRESHOLD = 0.3;
19
19
  const REQUIRED_BLINKS = 3;
20
20
 
21
+ // Anti-spoofing
22
+ const ANTI_SPOOF_CONFIDENCE_THRESHOLD = 0.7;
23
+ const REQUIRED_CONSECUTIVE_LIVE_FRAMES = 3;
24
+
25
+ // Face centering
26
+ const FACE_CENTER_THRESHOLD_X = 0.2;
27
+ const FACE_CENTER_THRESHOLD_Y = 0.15;
28
+ const MIN_FACE_CENTERED_FRAMES = 2;
29
+
30
+ // Performance optimization constants
31
+ const MAX_FRAME_PROCESSING_TIME_MS = 500;
32
+ const BATCH_UPDATE_THRESHOLD = 3;
33
+
21
34
  export const useFaceDetectionFrameProcessor = ({
22
35
  onStableFaceDetected = () => { },
23
36
  onFacesUpdate = () => { },
24
37
  onLivenessUpdate = () => { },
38
+ onAntiSpoofUpdate = () => { },
25
39
  showCodeScanner = false,
26
40
  isLoading = false,
27
41
  isActive = true,
28
- livenessLevel,
42
+ livenessLevel = 0,
29
43
  }) => {
30
44
  const { detectFaces } = useFaceDetector({
31
45
  performanceMode: 'fast',
32
46
  landmarkMode: 'none',
33
47
  contourMode: 'none',
34
- classificationMode: (livenessLevel === 1 || livenessLevel === 3) ? 'all' : 'none', // Only enable classification for blink detection
48
+ classificationMode: livenessLevel === 1 ? 'all' : 'none',
35
49
  minFaceSize: MIN_FACE_SIZE,
36
50
  });
37
51
 
38
52
  const isMounted = useRef(true);
53
+ const antiSpoofInitialized = useRef(false);
54
+ const frameProcessingStartTime = useRef(0);
55
+
56
+ // Initialize anti-spoofing with memoization
57
+ const initializeAntiSpoof = useCallback(async () => {
58
+ if (antiSpoofInitialized.current) return true;
59
+
60
+ try {
61
+ const available = isFaceAntiSpoofAvailable?.();
62
+ if (!available) return false;
63
+
64
+ const res = await initializeFaceAntiSpoof();
65
+ antiSpoofInitialized.current = true;
66
+ return true;
67
+ } catch (err) {
68
+ console.error('[useFaceDetection] Error initializing anti-spoof:', err);
69
+ return false;
70
+ }
71
+ }, []);
39
72
 
73
+ useEffect(() => {
74
+ if (!antiSpoofInitialized.current) {
75
+ initializeAntiSpoof();
76
+ }
77
+ }, [initializeAntiSpoof]);
78
+
79
+ // Pre-computed shared state with optimized structure
40
80
  const sharedState = useMemo(
41
81
  () =>
42
82
  Worklets.createSharedValue({
83
+ // Core timing
43
84
  lastProcessedTime: 0,
44
- lastX: 0,
45
- lastY: 0,
46
- lastW: 0,
47
- lastH: 0,
48
- stableCount: 0,
49
- captured: false,
50
- showCodeScanner: showCodeScanner,
51
- isActive: isActive,
52
- livenessLevel: livenessLevel || 0, // Ensure default value
53
- livenessStep: 0, // 0=center, 1=left/center, 2=right/blink, 3=blink/capture, 4=capture
54
- leftTurnVerified: false,
55
- rightTurnVerified: false,
56
- currentYaw: 0,
57
- yawStableCount: 0,
58
- blinkCount: 0,
59
- eyeClosed: false,
85
+
86
+ // Face tracking - packed for memory efficiency
87
+ faceTracking: { lastX: 0, lastY: 0, lastW: 0, lastH: 0, stableCount: 0 },
88
+
89
+ // State flags - packed together
90
+ flags: {
91
+ captured: false,
92
+ showCodeScanner: showCodeScanner,
93
+ isActive: isActive,
94
+ hasSingleFace: false,
95
+ isFaceCentered: false,
96
+ eyeClosed: false,
97
+ },
98
+
99
+ // Liveness state
100
+ liveness: {
101
+ level: livenessLevel,
102
+ step: 0,
103
+ blinkCount: 0,
104
+ },
105
+
106
+ // Anti-spoof state
107
+ antiSpoof: {
108
+ consecutiveLiveFrames: 0,
109
+ lastResult: null,
110
+ isLive: false,
111
+ confidence: 0,
112
+ },
113
+
114
+ // Face centering
115
+ centering: {
116
+ centeredFrames: 0,
117
+ frameWidth: 0,
118
+ frameHeight: 0,
119
+ },
120
+
121
+ // Performance tracking
122
+ performance: {
123
+ batchCounter: 0,
124
+ lastBatchUpdate: 0,
125
+ }
60
126
  }),
61
127
  []
62
128
  );
63
129
 
130
+ // Batched state updates
64
131
  useEffect(() => {
65
132
  if (!isMounted.current) return;
66
- // update flags without recreating object
67
- sharedState.value.showCodeScanner = !!showCodeScanner;
68
- sharedState.value.isActive = !!isActive;
69
- sharedState.value.livenessLevel = livenessLevel || 0;
70
-
71
- if (isActive && sharedState.value.captured) {
72
- // reset minimal fields
73
- sharedState.value.captured = false;
74
- sharedState.value.stableCount = 0;
75
- sharedState.value.livenessStep = 0;
76
- sharedState.value.leftTurnVerified = false;
77
- sharedState.value.rightTurnVerified = false;
78
- sharedState.value.yawStableCount = 0;
79
- sharedState.value.blinkCount = 0;
80
- sharedState.value.eyeClosed = false;
133
+
134
+ const state = sharedState.value;
135
+ state.flags.showCodeScanner = !!showCodeScanner;
136
+ state.flags.isActive = !!isActive;
137
+ state.liveness.level = livenessLevel;
138
+
139
+ if (isActive && state.flags.captured) {
140
+ // Batch reset all states
141
+ state.faceTracking.stableCount = 0;
142
+ state.liveness.step = 0;
143
+ state.liveness.blinkCount = 0;
144
+ state.flags.eyeClosed = false;
145
+ state.flags.captured = false;
146
+ state.antiSpoof.consecutiveLiveFrames = 0;
147
+ state.antiSpoof.lastResult = null;
148
+ state.antiSpoof.isLive = false;
149
+ state.antiSpoof.confidence = 0;
150
+ state.flags.hasSingleFace = false;
151
+ state.centering.centeredFrames = 0;
152
+ state.flags.isFaceCentered = false;
81
153
  }
82
154
  }, [showCodeScanner, isActive, livenessLevel, sharedState]);
83
155
 
84
- // Throttled JS callbacks to avoid flooding the JS thread
85
- const lastFacesEventTimeRef = useRef(0);
86
- const lastLivenessEventTimeRef = useRef(0);
156
+ // Optimized JS callbacks with batching
157
+ const callbacksRef = useRef({
158
+ lastFacesEventTime: 0,
159
+ lastLivenessEventTime: 0,
160
+ lastAntiSpoofEventTime: 0,
161
+ pendingFacesUpdate: null,
162
+ pendingLivenessUpdate: null,
163
+ pendingAntiSpoofUpdate: null,
164
+ });
165
+
87
166
  const FACES_EVENT_INTERVAL_MS = 800;
88
167
  const LIVENESS_EVENT_INTERVAL_MS = 700;
168
+ const ANTI_SPOOF_EVENT_INTERVAL_MS = 500;
89
169
 
170
+ // Memoized callbacks with batching
90
171
  const runOnStable = useMemo(
91
172
  () =>
92
- Worklets.createRunOnJS((faceRect) => {
93
- try {
94
- onStableFaceDetected?.(faceRect);
95
- } catch (error) {
96
- console.error('Error in runOnStable:', error);
97
- }
173
+ Worklets.createRunOnJS((faceRect, antiSpoofResult) => {
174
+ onStableFaceDetected?.(faceRect, antiSpoofResult);
98
175
  }),
99
176
  [onStableFaceDetected]
100
177
  );
101
178
 
102
179
  const runOnFaces = useMemo(
103
180
  () =>
104
- Worklets.createRunOnJS((count, progress, step) => {
105
- try {
106
- const now = Date.now();
107
- if (now - lastFacesEventTimeRef.current > FACES_EVENT_INTERVAL_MS) {
108
- lastFacesEventTimeRef.current = now;
109
- onFacesUpdate?.({ count, progress, step });
110
- }
111
- } catch (error) {
112
- console.error('Error in runOnFaces:', error);
181
+ Worklets.createRunOnJS((count, progress, step, isCentered, antiSpoofState) => {
182
+ const now = Date.now();
183
+ const callbacks = callbacksRef.current;
184
+
185
+ if (now - callbacks.lastFacesEventTime > FACES_EVENT_INTERVAL_MS) {
186
+ callbacks.lastFacesEventTime = now;
187
+ onFacesUpdate?.({ count, progress, step, isCentered, antiSpoofState });
113
188
  }
114
189
  }),
115
190
  [onFacesUpdate]
@@ -118,333 +193,399 @@ export const useFaceDetectionFrameProcessor = ({
118
193
  const runOnLiveness = useMemo(
119
194
  () =>
120
195
  Worklets.createRunOnJS((step, extra) => {
121
- try {
122
- const now = Date.now();
123
- if (now - lastLivenessEventTimeRef.current > LIVENESS_EVENT_INTERVAL_MS) {
124
- lastLivenessEventTimeRef.current = now;
125
- onLivenessUpdate?.(step, extra);
126
- }
127
- } catch (error) {
128
- console.error('Error in runOnLiveness:', error);
196
+ const now = Date.now();
197
+ const callbacks = callbacksRef.current;
198
+
199
+ if (now - callbacks.lastLivenessEventTime > LIVENESS_EVENT_INTERVAL_MS) {
200
+ callbacks.lastLivenessEventTime = now;
201
+ onLivenessUpdate?.(step, extra);
129
202
  }
130
203
  }),
131
204
  [onLivenessUpdate]
132
205
  );
133
206
 
207
+ const runOnAntiSpoof = useMemo(
208
+ () =>
209
+ Worklets.createRunOnJS((result) => {
210
+ const now = Date.now();
211
+ const callbacks = callbacksRef.current;
212
+
213
+ if (now - callbacks.lastAntiSpoofEventTime > ANTI_SPOOF_EVENT_INTERVAL_MS) {
214
+ callbacks.lastAntiSpoofEventTime = now;
215
+ onAntiSpoofUpdate?.(result);
216
+ }
217
+ }),
218
+ [onAntiSpoofUpdate]
219
+ );
220
+
221
+ // Optimized face centering check - inlined for performance
222
+ const isFaceCenteredInFrame = Worklets.createRunOnJS((faceBounds, frameWidth, frameHeight) => {
223
+ 'worklet';
224
+
225
+ if (!faceBounds || frameWidth === 0 || frameHeight === 0) return false;
226
+
227
+ const faceCenterX = faceBounds.x + faceBounds.width / 2;
228
+ const faceCenterY = faceBounds.y + faceBounds.height / 2;
229
+ const frameCenterX = frameWidth / 2;
230
+ const frameCenterY = frameHeight / 2;
231
+
232
+ return (
233
+ Math.abs(faceCenterX - frameCenterX) <= frameWidth * FACE_CENTER_THRESHOLD_X &&
234
+ Math.abs(faceCenterY - frameCenterY) <= frameHeight * FACE_CENTER_THRESHOLD_Y
235
+ );
236
+ });
237
+
238
+ // Fast early exit conditions check
239
+ const shouldProcessFrame = Worklets.createRunOnJS((state, now, isLoading) => {
240
+ 'worklet';
241
+ return !(
242
+ state.flags.showCodeScanner ||
243
+ state.flags.captured ||
244
+ isLoading ||
245
+ !state.flags.isActive ||
246
+ (now - state.lastProcessedTime < FRAME_PROCESSOR_MIN_INTERVAL_MS)
247
+ );
248
+ });
249
+
250
+ // Optimized frame processor
134
251
  const frameProcessor = useFrameProcessor(
135
252
  (frame) => {
136
253
  'worklet';
137
- const state = sharedState.value;
138
254
 
139
- // quick exits — do not call frame.release here; we'll release in finally
140
- if (state.showCodeScanner || state.captured || isLoading || !state.isActive) {
255
+ // Performance monitoring
256
+ const processingStart = Date.now();
257
+
258
+ const state = sharedState.value;
259
+ const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now();
260
+
261
+ // Fast early exit
262
+ if (!shouldProcessFrame(state, now, isLoading)) {
141
263
  frame.release?.();
142
264
  return;
143
265
  }
144
266
 
145
- const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now();
146
- if (now - state.lastProcessedTime < FRAME_PROCESSOR_MIN_INTERVAL_MS) {
267
+ // Performance guard - don't process if taking too long
268
+ if (processingStart - frameProcessingStartTime.current < MAX_FRAME_PROCESSING_TIME_MS) {
147
269
  frame.release?.();
148
270
  return;
149
271
  }
272
+
273
+ frameProcessingStartTime.current = processingStart;
150
274
 
151
275
  let detected = null;
276
+ let antiSpoofResult = null;
277
+
152
278
  try {
153
- // Keep try/finally around detection to ensure release
279
+ // Initialize frame dimensions once
280
+ if (state.centering.frameWidth === 0) {
281
+ state.centering.frameWidth = frame.width;
282
+ state.centering.frameHeight = frame.height;
283
+ }
284
+
285
+ // Detect faces
154
286
  detected = detectFaces?.(frame);
155
- } catch (err) {
156
- // detection failed; update timestamp to avoid spin and return
157
- state.lastProcessedTime = now;
158
- frame.release?.();
159
- return;
160
- }
161
287
 
162
- try {
288
+ // Fast path for no faces
163
289
  if (!detected || detected.length === 0) {
164
- state.stableCount = 0;
165
- state.yawStableCount = 0;
290
+ state.faceTracking.stableCount = 0;
291
+ state.antiSpoof.consecutiveLiveFrames = 0;
292
+ state.flags.hasSingleFace = false;
293
+ state.centering.centeredFrames = 0;
294
+ state.flags.isFaceCentered = false;
166
295
  state.lastProcessedTime = now;
167
- runOnFaces(0, 0, state.livenessStep);
296
+
297
+ runOnFaces(0, 0, state.liveness.step, false, {
298
+ isLive: false,
299
+ confidence: 0,
300
+ consecutiveLiveFrames: 0,
301
+ isFaceCentered: false,
302
+ hasSingleFace: false,
303
+ });
168
304
  return;
169
305
  }
170
306
 
171
- if (detected.length === 1 && !state.captured) {
307
+ // Process single face scenario
308
+ if (detected.length === 1 && !state.flags.captured) {
172
309
  const face = detected[0];
173
- if (!face?.bounds || face.yawAngle === undefined) {
174
- runOnFaces(0, 0, state.livenessStep);
310
+ if (!face?.bounds) {
311
+ runOnFaces(0, 0, state.liveness.step, false, {
312
+ isLive: false,
313
+ confidence: 0,
314
+ consecutiveLiveFrames: 0,
315
+ isFaceCentered: false,
316
+ hasSingleFace: false,
317
+ });
175
318
  return;
176
319
  }
177
320
 
178
- const yaw = face.yawAngle;
179
- const x = Math.max(0, face.bounds.x);
180
- const y = Math.max(0, face.bounds.y);
181
- const width = Math.max(0, face.bounds.width);
182
- const height = Math.max(0, face.bounds.height);
183
-
184
- // read values locally and then write back to shared state fields
185
- let {
186
- livenessStep,
187
- leftTurnVerified,
188
- rightTurnVerified,
189
- yawStableCount,
190
- blinkCount,
191
- eyeClosed,
192
- livenessLevel,
193
- } = state;
194
-
195
- // ---- Liveness Logic Based on Level ----
196
- // Level 0: Basic face detection with stability check only
197
- if (livenessLevel === 0) {
198
- // No liveness check - only stability
199
- // Skip all liveness steps and go directly to capture when stable
200
- livenessStep = 0;
201
- }
202
- // Level 1: Face detection with blink verification
203
- else if (livenessLevel === 1) {
204
- // Blink detection only
205
- if (livenessStep === 0) {
206
- // Center face first
207
- if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
208
- yawStableCount++;
209
- if (yawStableCount >= 2) {
210
- livenessStep = 1; // Move to blink step
211
- runOnLiveness(livenessStep);
212
- yawStableCount = 0;
213
- }
214
- } else yawStableCount = 0;
215
- }
216
- // Blink detection
217
- else if (livenessStep === 1) {
218
- const leftEye = face.leftEyeOpenProbability ?? 1;
219
- const rightEye = face.rightEyeOpenProbability ?? 1;
220
- const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD;
221
-
222
- if (eyesClosed && !eyeClosed) {
223
- blinkCount++;
224
- eyeClosed = true;
225
- runOnLiveness(livenessStep, { blinkCount });
226
- } else if (!eyesClosed && eyeClosed) {
227
- eyeClosed = false;
228
- }
229
-
230
- if (blinkCount >= REQUIRED_BLINKS) {
231
- livenessStep = 2; // Ready for capture
232
- runOnLiveness(livenessStep);
233
- }
234
- }
321
+ const bounds = face.bounds;
322
+ const x = Math.max(0, bounds.x);
323
+ const y = Math.max(0, bounds.y);
324
+ const width = Math.max(0, bounds.width);
325
+ const height = Math.max(0, bounds.height);
326
+
327
+ // Local state snapshot for performance
328
+ const localState = {
329
+ livenessLevel: state.liveness.level,
330
+ isLive: state.antiSpoof.isLive,
331
+ consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
332
+ isFaceCentered: state.flags.isFaceCentered,
333
+ antiSpoofConfidence: state.antiSpoof.confidence,
334
+ livenessStep: state.liveness.step,
335
+ blinkCount: state.liveness.blinkCount,
336
+ eyeClosed: state.flags.eyeClosed,
337
+ };
338
+
339
+ // Update single face state
340
+ state.flags.hasSingleFace = true;
341
+
342
+ // Face centering check
343
+ const centered = isFaceCenteredInFrame(
344
+ bounds,
345
+ state.centering.frameWidth,
346
+ state.centering.frameHeight
347
+ );
348
+
349
+ if (centered) {
350
+ state.centering.centeredFrames = Math.min(
351
+ MIN_FACE_CENTERED_FRAMES,
352
+ state.centering.centeredFrames + 1
353
+ );
354
+ } else {
355
+ state.centering.centeredFrames = 0;
235
356
  }
236
- else if (livenessLevel === 2) {
237
- // Face turn detection only
238
- if (livenessStep === 0) {
239
- if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
240
- yawStableCount++;
241
- if (yawStableCount >= 2) {
242
- livenessStep = 1; // Move to left turn
243
- runOnLiveness(livenessStep);
244
- yawStableCount = 0;
357
+ state.flags.isFaceCentered = state.centering.centeredFrames >= MIN_FACE_CENTERED_FRAMES;
358
+
359
+ // Anti-spoof detection only when face is centered and single
360
+ if (state.flags.isFaceCentered) {
361
+ try {
362
+ antiSpoofResult = faceAntiSpoofFrameProcessor?.(frame);
363
+
364
+ if (antiSpoofResult != null) {
365
+ state.antiSpoof.lastResult = antiSpoofResult;
366
+
367
+ const isLive = antiSpoofResult.isLive === true;
368
+ const confidence = antiSpoofResult.combinedScore || antiSpoofResult.neuralNetworkScore || 0;
369
+
370
+ if (isLive && confidence > ANTI_SPOOF_CONFIDENCE_THRESHOLD) {
371
+ state.antiSpoof.consecutiveLiveFrames = Math.min(
372
+ REQUIRED_CONSECUTIVE_LIVE_FRAMES,
373
+ state.antiSpoof.consecutiveLiveFrames + 1
374
+ );
375
+ } else {
376
+ state.antiSpoof.consecutiveLiveFrames = Math.max(0, state.antiSpoof.consecutiveLiveFrames - 1);
245
377
  }
246
- } else yawStableCount = 0;
247
- } else if (livenessStep === 1 && !leftTurnVerified) {
248
- if (yaw < YAW_LEFT_THRESHOLD) {
249
- yawStableCount++;
250
- if (yawStableCount >= 2) {
251
- leftTurnVerified = true;
252
- livenessStep = 2; // Move to right turn
253
- runOnLiveness(livenessStep);
254
- yawStableCount = 0;
378
+ state.antiSpoof.isLive = state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES;
379
+ state.antiSpoof.confidence = confidence;
380
+
381
+ // Batch anti-spoof updates
382
+ if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) {
383
+ runOnAntiSpoof({
384
+ isLive: state.antiSpoof.isLive,
385
+ confidence: state.antiSpoof.confidence,
386
+ rawResult: antiSpoofResult,
387
+ consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
388
+ isFaceCentered: state.flags.isFaceCentered,
389
+ });
255
390
  }
256
- } else yawStableCount = Math.max(0, yawStableCount - 0.5);
257
- } else if (livenessStep === 2 && leftTurnVerified && !rightTurnVerified) {
258
- if (yaw > YAW_RIGHT_THRESHOLD) {
259
- yawStableCount++;
260
- if (yawStableCount >= 2) {
261
- rightTurnVerified = true;
262
- livenessStep = 3; // Ready for capture
263
- runOnLiveness(livenessStep);
264
- yawStableCount = 0;
265
- }
266
- } else yawStableCount = Math.max(0, yawStableCount - 0.5);
391
+ }
392
+ } catch (antiSpoofError) {
393
+ // Silent error handling
267
394
  }
395
+ } else {
396
+ // Reset anti-spoof if face not centered
397
+ state.antiSpoof.consecutiveLiveFrames = 0;
398
+ state.antiSpoof.isLive = false;
268
399
  }
269
- else if (livenessLevel === 3) {
270
- // Both blink and face turn detection
271
- if (livenessStep === 0) {
272
- if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
273
- yawStableCount++;
274
- if (yawStableCount >= 2) {
275
- livenessStep = 1; // Move to left turn
276
- runOnLiveness(livenessStep);
277
- yawStableCount = 0;
278
- }
279
- } else yawStableCount = 0;
280
- } else if (livenessStep === 1 && !leftTurnVerified) {
281
- if (yaw < YAW_LEFT_THRESHOLD) {
282
- yawStableCount++;
283
- if (yawStableCount >= 2) {
284
- leftTurnVerified = true;
285
- livenessStep = 2; // Move to right turn
286
- runOnLiveness(livenessStep);
287
- yawStableCount = 0;
288
- }
289
- } else yawStableCount = Math.max(0, yawStableCount - 0.5);
290
- } else if (livenessStep === 2 && leftTurnVerified && !rightTurnVerified) {
291
- if (yaw > YAW_RIGHT_THRESHOLD) {
292
- yawStableCount++;
293
- if (yawStableCount >= 2) {
294
- rightTurnVerified = true;
295
- livenessStep = 3; // Move to blink step
296
- runOnLiveness(livenessStep);
297
- yawStableCount = 0;
298
- }
299
- } else yawStableCount = Math.max(0, yawStableCount - 0.5);
400
+
401
+ // Liveness logic - optimized
402
+ let newLivenessStep = localState.livenessStep;
403
+ let newBlinkCount = localState.blinkCount;
404
+ let newEyeClosed = localState.eyeClosed;
405
+
406
+ if (localState.livenessLevel === 1) {
407
+ if (newLivenessStep === 0) {
408
+ newLivenessStep = 1;
409
+ runOnLiveness(newLivenessStep);
300
410
  }
301
- // Blink detection
302
- else if (livenessStep === 3 && leftTurnVerified && rightTurnVerified) {
411
+ else if (newLivenessStep === 1) {
303
412
  const leftEye = face.leftEyeOpenProbability ?? 1;
304
413
  const rightEye = face.rightEyeOpenProbability ?? 1;
305
414
  const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD;
306
415
 
307
- if (eyesClosed && !eyeClosed) {
308
- blinkCount++;
309
- eyeClosed = true;
310
- runOnLiveness(livenessStep, { blinkCount });
311
- } else if (!eyesClosed && eyeClosed) {
312
- eyeClosed = false;
416
+ if (eyesClosed && !newEyeClosed) {
417
+ newBlinkCount++;
418
+ newEyeClosed = true;
419
+ runOnLiveness(newLivenessStep, { blinkCount: newBlinkCount });
420
+ } else if (!eyesClosed && newEyeClosed) {
421
+ newEyeClosed = false;
313
422
  }
314
423
 
315
- if (blinkCount >= REQUIRED_BLINKS) {
316
- livenessStep = 4; // Ready for capture
317
- runOnLiveness(livenessStep);
424
+ if (newBlinkCount >= REQUIRED_BLINKS) {
425
+ newLivenessStep = 2;
426
+ runOnLiveness(newLivenessStep);
318
427
  }
319
428
  }
320
429
  }
321
430
 
322
- // ---- Face stability check ----
323
- let newStableCount = state.stableCount;
324
- if (state.lastX === 0 && state.lastY === 0) newStableCount = 1;
325
- else {
326
- const dx = Math.abs(x - state.lastX);
327
- const dy = Math.abs(y - state.lastY);
328
- if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD)
329
- newStableCount = state.stableCount + 1;
330
- else newStableCount = 1;
431
+ // Face stability check - optimized
432
+ let newStableCount = state.faceTracking.stableCount;
433
+ if (state.faceTracking.lastX === 0 && state.faceTracking.lastY === 0) {
434
+ newStableCount = 1;
435
+ } else {
436
+ const dx = Math.abs(x - state.faceTracking.lastX);
437
+ const dy = Math.abs(y - state.faceTracking.lastY);
438
+ newStableCount = (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD)
439
+ ? state.faceTracking.stableCount + 1
440
+ : 1;
331
441
  }
332
442
 
333
- // Mutate sharedState fields directly (avoid object reallocation)
443
+ // Batch state updates
334
444
  state.lastProcessedTime = now;
335
- state.lastX = x;
336
- state.lastY = y;
337
- state.lastW = width;
338
- state.lastH = height;
339
- state.stableCount = newStableCount;
340
- state.livenessStep = livenessStep;
341
- state.leftTurnVerified = leftTurnVerified;
342
- state.rightTurnVerified = rightTurnVerified;
343
- state.currentYaw = yaw;
344
- state.yawStableCount = yawStableCount;
345
- state.blinkCount = blinkCount;
346
- state.eyeClosed = eyeClosed;
445
+ state.faceTracking.lastX = x;
446
+ state.faceTracking.lastY = y;
447
+ state.faceTracking.lastW = width;
448
+ state.faceTracking.lastH = height;
449
+ state.faceTracking.stableCount = newStableCount;
450
+ state.liveness.step = newLivenessStep;
451
+ state.liveness.blinkCount = newBlinkCount;
452
+ state.flags.eyeClosed = newEyeClosed;
453
+ state.performance.batchCounter++;
347
454
 
348
455
  const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100);
349
- runOnFaces(1, progress, livenessStep);
350
-
351
- // Capture condition based on liveness level
352
- let shouldCapture = false;
353
-
354
- // Level 0: Basic face detection - capture when stable
355
- if (livenessLevel === 0) {
356
- shouldCapture = newStableCount >= FACE_STABILITY_THRESHOLD;
357
- }
358
- // Level 1: Blink verification - capture after blinks and stable
359
- else if (livenessLevel === 1) {
360
- shouldCapture = livenessStep === 2 && blinkCount >= REQUIRED_BLINKS && newStableCount >= FACE_STABILITY_THRESHOLD;
361
- }
362
- else if (livenessLevel === 2) {
363
- // Face turn only - capture after turns and stable
364
- shouldCapture = livenessStep === 3 && leftTurnVerified && rightTurnVerified && newStableCount >= FACE_STABILITY_THRESHOLD;
365
- }
366
- else if (livenessLevel === 3) {
367
- // Both - capture after turns, blinks and stable
368
- shouldCapture = livenessStep === 4 && blinkCount >= REQUIRED_BLINKS && newStableCount >= FACE_STABILITY_THRESHOLD;
456
+
457
+ // Batch face updates
458
+ if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) {
459
+ runOnFaces(1, progress, newLivenessStep, state.flags.isFaceCentered, {
460
+ isLive: state.antiSpoof.isLive,
461
+ confidence: state.antiSpoof.confidence,
462
+ consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames,
463
+ isFaceCentered: state.flags.isFaceCentered,
464
+ hasSingleFace: true,
465
+ });
369
466
  }
370
467
 
371
- if (shouldCapture && !state.captured) {
372
- state.captured = true;
373
- runOnStable({ x, y, width, height });
468
+ // Capture condition - optimized
469
+ const shouldCapture = !state.flags.captured && (
470
+ newStableCount >= FACE_STABILITY_THRESHOLD &&
471
+ state.antiSpoof.isLive &&
472
+ state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES &&
473
+ state.flags.isFaceCentered &&
474
+ (localState.livenessLevel === 0 || (
475
+ localState.livenessLevel === 1 &&
476
+ newLivenessStep === 2 &&
477
+ newBlinkCount >= REQUIRED_BLINKS
478
+ ))
479
+ );
480
+
481
+ if (shouldCapture) {
482
+ state.flags.captured = true;
483
+ runOnStable(
484
+ { x, y, width, height },
485
+ state.antiSpoof.lastResult
486
+ );
374
487
  }
375
488
  } else {
376
- // multiple faces reset stability but keep liveness step
377
- state.stableCount = 0;
489
+ // Multiple faces - reset states
490
+ state.faceTracking.stableCount = 0;
378
491
  state.lastProcessedTime = now;
379
- state.yawStableCount = 0;
380
- runOnFaces(detected.length, 0, state.livenessStep);
492
+ state.antiSpoof.consecutiveLiveFrames = 0;
493
+ state.flags.hasSingleFace = false;
494
+ state.centering.centeredFrames = 0;
495
+ state.flags.isFaceCentered = false;
496
+
497
+ runOnFaces(detected.length, 0, state.liveness.step, false, {
498
+ isLive: false,
499
+ confidence: 0,
500
+ consecutiveLiveFrames: 0,
501
+ isFaceCentered: false,
502
+ hasSingleFace: false,
503
+ });
381
504
  }
382
505
  } catch (err) {
383
- // protect worklet from throwing
384
- try {
385
- console.error('FrameProcessor worklet error:', err);
386
- } catch (e) { }
506
+ // Error boundary - ensure frame is released
387
507
  } finally {
388
- // Always release the frame exactly once
389
508
  frame.release?.();
390
509
  }
391
510
  },
392
511
  [detectFaces, isLoading]
393
512
  );
394
513
 
395
- // Reset and cleanup logic
514
+ // Optimized reset functions
396
515
  const resetCaptureState = useCallback(() => {
397
516
  const state = sharedState.value;
398
517
  state.lastProcessedTime = 0;
399
- state.lastX = 0;
400
- state.lastY = 0;
401
- state.lastW = 0;
402
- state.lastH = 0;
403
- state.stableCount = 0;
404
- state.captured = false;
405
- state.livenessStep = 0;
406
- state.leftTurnVerified = false;
407
- state.rightTurnVerified = false;
408
- state.currentYaw = 0;
409
- state.yawStableCount = 0;
410
- state.blinkCount = 0;
411
- state.eyeClosed = false;
518
+ state.faceTracking.lastX = 0;
519
+ state.faceTracking.lastY = 0;
520
+ state.faceTracking.lastW = 0;
521
+ state.faceTracking.lastH = 0;
522
+ state.faceTracking.stableCount = 0;
523
+ state.flags.captured = false;
524
+ state.liveness.step = 0;
525
+ state.liveness.blinkCount = 0;
526
+ state.flags.eyeClosed = false;
527
+ state.antiSpoof.consecutiveLiveFrames = 0;
528
+ state.antiSpoof.lastResult = null;
529
+ state.antiSpoof.isLive = false;
530
+ state.antiSpoof.confidence = 0;
531
+ state.flags.hasSingleFace = false;
532
+ state.centering.centeredFrames = 0;
533
+ state.flags.isFaceCentered = false;
534
+ state.centering.frameWidth = 0;
535
+ state.centering.frameHeight = 0;
536
+ state.performance.batchCounter = 0;
412
537
  }, [sharedState]);
413
538
 
414
539
  const forceResetCaptureState = useCallback(() => {
415
- const show = sharedState.value.showCodeScanner;
416
- const active = sharedState.value.isActive;
417
- const level = sharedState.value.livenessLevel;
418
- sharedState.value.lastProcessedTime = 0;
419
- sharedState.value.lastX = 0;
420
- sharedState.value.lastY = 0;
421
- sharedState.value.lastW = 0;
422
- sharedState.value.lastH = 0;
423
- sharedState.value.stableCount = 0;
424
- sharedState.value.captured = false;
425
- sharedState.value.showCodeScanner = show;
426
- sharedState.value.isActive = active;
427
- sharedState.value.livenessLevel = level;
428
- sharedState.value.livenessStep = 0;
429
- sharedState.value.leftTurnVerified = false;
430
- sharedState.value.rightTurnVerified = false;
431
- sharedState.value.currentYaw = 0;
432
- sharedState.value.yawStableCount = 0;
433
- sharedState.value.blinkCount = 0;
434
- sharedState.value.eyeClosed = false;
540
+ const current = sharedState.value;
541
+
542
+ sharedState.value = {
543
+ lastProcessedTime: 0,
544
+ faceTracking: {
545
+ lastX: 0, lastY: 0, lastW: 0, lastH: 0, stableCount: 0
546
+ },
547
+ flags: {
548
+ captured: false,
549
+ showCodeScanner: current.flags.showCodeScanner,
550
+ isActive: current.flags.isActive,
551
+ hasSingleFace: false,
552
+ isFaceCentered: false,
553
+ eyeClosed: false,
554
+ },
555
+ liveness: {
556
+ level: current.liveness.level,
557
+ step: 0,
558
+ blinkCount: 0,
559
+ },
560
+ antiSpoof: {
561
+ consecutiveLiveFrames: 0,
562
+ lastResult: null,
563
+ isLive: false,
564
+ confidence: 0,
565
+ },
566
+ centering: {
567
+ centeredFrames: 0,
568
+ frameWidth: 0,
569
+ frameHeight: 0,
570
+ },
571
+ performance: {
572
+ batchCounter: 0,
573
+ lastBatchUpdate: 0,
574
+ }
575
+ };
435
576
  }, [sharedState]);
436
577
 
437
578
  const updateShowCodeScanner = useCallback(
438
579
  (value) => {
439
- sharedState.value.showCodeScanner = !!value;
580
+ sharedState.value.flags.showCodeScanner = !!value;
440
581
  },
441
582
  [sharedState]
442
583
  );
443
584
 
444
585
  const updateIsActive = useCallback(
445
586
  (active) => {
446
- sharedState.value.isActive = !!active;
447
- if (!active) sharedState.value.captured = false;
587
+ sharedState.value.flags.isActive = !!active;
588
+ if (!active) sharedState.value.flags.captured = false;
448
589
  },
449
590
  [sharedState]
450
591
  );
@@ -463,6 +604,15 @@ export const useFaceDetectionFrameProcessor = ({
463
604
  forceResetCaptureState,
464
605
  updateShowCodeScanner,
465
606
  updateIsActive,
466
- capturedSV: { value: sharedState.value.captured },
607
+ initializeAntiSpoof,
608
+ capturedSV: { value: sharedState.value.flags.captured },
609
+ antiSpoofState: {
610
+ isLive: sharedState.value.antiSpoof.isLive,
611
+ confidence: sharedState.value.antiSpoof.confidence,
612
+ consecutiveLiveFrames: sharedState.value.antiSpoof.consecutiveLiveFrames,
613
+ lastResult: sharedState.value.antiSpoof.lastResult,
614
+ hasSingleFace: sharedState.value.flags.hasSingleFace,
615
+ isFaceCentered: sharedState.value.flags.isFaceCentered,
616
+ },
467
617
  };
468
618
  };