react-native-biometric-verifier 0.0.18 → 0.0.20

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.
@@ -3,16 +3,20 @@ import { Worklets } from 'react-native-worklets-core';
3
3
  import { useFrameProcessor } from 'react-native-vision-camera';
4
4
  import { useFaceDetector } from 'react-native-vision-camera-face-detector';
5
5
 
6
- // Tuned constants for liveness detection
6
+ // Tuned constants for liveness detection / stability
7
7
  const FACE_STABILITY_THRESHOLD = 3;
8
8
  const FACE_MOVEMENT_THRESHOLD = 15;
9
- const FRAME_PROCESSOR_MIN_INTERVAL_MS = 800;
9
+ const FRAME_PROCESSOR_MIN_INTERVAL_MS = 1000; // increased to reduce load
10
10
  const MIN_FACE_SIZE = 0.2;
11
11
 
12
12
  // Liveness detection constants
13
- const YAW_LEFT_THRESHOLD = -15; // face turned left (negative yaw)
14
- const YAW_RIGHT_THRESHOLD = 15; // face turned right (positive yaw)
15
- const YAW_CENTER_THRESHOLD = 5; // face centered
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
19
+ const REQUIRED_BLINKS = 3;
16
20
 
17
21
  export const useFaceDetectionFrameProcessor = ({
18
22
  onStableFaceDetected = () => {},
@@ -21,317 +25,430 @@ export const useFaceDetectionFrameProcessor = ({
21
25
  showCodeScanner = false,
22
26
  isLoading = false,
23
27
  isActive = true,
28
+ livenessLevel = 3, // 0=no liveness, 1=blink only, 2=face turn only, 3=both
24
29
  }) => {
25
30
  const { detectFaces } = useFaceDetector({
26
31
  performanceMode: 'fast',
27
32
  landmarkMode: 'none',
28
33
  contourMode: 'none',
29
- classificationMode: 'none',
34
+ classificationMode: livenessLevel === 1 || livenessLevel === 3 ? 'all' : 'none', // Only enable classification for blink detection
30
35
  minFaceSize: MIN_FACE_SIZE,
31
36
  });
32
37
 
33
38
  const isMounted = useRef(true);
34
39
 
35
- const sharedState = useMemo(() =>
36
- Worklets.createSharedValue({
37
- lastProcessedTime: 0,
38
- lastX: 0,
39
- lastY: 0,
40
- lastW: 0,
41
- lastH: 0,
42
- stableCount: 0,
43
- captured: false,
44
- showCodeScanner: showCodeScanner,
45
- isActive: isActive,
46
- livenessStep: 0, // 0=look straight, 1=left done, 2=right done, 3=ready for capture
47
- leftTurnVerified: false,
48
- rightTurnVerified: false,
49
- currentYaw: 0,
50
- yawStableCount: 0,
51
- }), []);
52
-
53
- useEffect(() => {
54
- if (!isMounted.current) return;
55
-
56
- sharedState.value = {
57
- ...sharedState.value,
58
- showCodeScanner: showCodeScanner,
59
- isActive: isActive
60
- };
61
-
62
- // Reset when becoming active again
63
- if (isActive && sharedState.value.captured) {
64
- sharedState.value = {
65
- ...sharedState.value,
66
- captured: false,
40
+ const sharedState = useMemo(
41
+ () =>
42
+ Worklets.createSharedValue({
43
+ lastProcessedTime: 0,
44
+ lastX: 0,
45
+ lastY: 0,
46
+ lastW: 0,
47
+ lastH: 0,
67
48
  stableCount: 0,
68
- livenessStep: 0,
49
+ captured: false,
50
+ showCodeScanner: showCodeScanner,
51
+ isActive: isActive,
52
+ livenessLevel: livenessLevel,
53
+ livenessStep: 0, // 0=center, 1=left/center, 2=right/blink, 3=blink/capture, 4=capture
69
54
  leftTurnVerified: false,
70
55
  rightTurnVerified: false,
56
+ currentYaw: 0,
71
57
  yawStableCount: 0,
72
- };
58
+ blinkCount: 0,
59
+ eyeClosed: false,
60
+ }),
61
+ []
62
+ );
63
+
64
+ useEffect(() => {
65
+ 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;
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;
73
81
  }
74
- }, [showCodeScanner, isActive, sharedState]);
82
+ }, [showCodeScanner, isActive, livenessLevel, sharedState]);
75
83
 
76
- const runOnStable = useMemo(() =>
77
- Worklets.createRunOnJS((faceRect) => {
78
- try {
79
- onStableFaceDetected?.(faceRect);
80
- } catch (error) {
81
- console.error('Error in runOnStable:', error);
82
- }
83
- }), [onStableFaceDetected]
84
+ // Throttled JS callbacks to avoid flooding the JS thread
85
+ const lastFacesEventTimeRef = useRef(0);
86
+ const lastLivenessEventTimeRef = useRef(0);
87
+ const FACES_EVENT_INTERVAL_MS = 800;
88
+ const LIVENESS_EVENT_INTERVAL_MS = 700;
89
+
90
+ const runOnStable = useMemo(
91
+ () =>
92
+ Worklets.createRunOnJS((faceRect) => {
93
+ try {
94
+ onStableFaceDetected?.(faceRect);
95
+ } catch (error) {
96
+ console.error('Error in runOnStable:', error);
97
+ }
98
+ }),
99
+ [onStableFaceDetected]
84
100
  );
85
101
 
86
- const runOnFaces = useMemo(() =>
87
- Worklets.createRunOnJS((count, progress, step) => {
88
- try {
89
- onFacesUpdate?.({ count, progress, step });
90
- } catch (error) {
91
- console.error('Error in runOnFaces:', error);
92
- }
93
- }), [onFacesUpdate]
102
+ const runOnFaces = useMemo(
103
+ () =>
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);
113
+ }
114
+ }),
115
+ [onFacesUpdate]
94
116
  );
95
117
 
96
- const runOnLiveness = useMemo(() =>
97
- Worklets.createRunOnJS((step) => {
118
+ const runOnLiveness = useMemo(
119
+ () =>
120
+ 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);
129
+ }
130
+ }),
131
+ [onLivenessUpdate]
132
+ );
133
+
134
+ const frameProcessor = useFrameProcessor(
135
+ (frame) => {
136
+ 'worklet';
137
+ const state = sharedState.value;
138
+
139
+ // quick exits — do not call frame.release here; we'll release in finally
140
+ if (state.showCodeScanner || state.captured || isLoading || !state.isActive) {
141
+ frame.release?.();
142
+ return;
143
+ }
144
+
145
+ const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now();
146
+ if (now - state.lastProcessedTime < FRAME_PROCESSOR_MIN_INTERVAL_MS) {
147
+ frame.release?.();
148
+ return;
149
+ }
150
+
151
+ let detected = null;
98
152
  try {
99
- onLivenessUpdate?.(step);
100
- } catch (error) {
101
- console.error('Error in runOnLiveness:', error);
153
+ // Keep try/finally around detection to ensure release
154
+ 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;
102
160
  }
103
- }), [onLivenessUpdate]
104
- );
105
161
 
106
- const frameProcessor = useFrameProcessor((frame) => {
107
- 'worklet';
108
- const state = sharedState.value;
162
+ try {
163
+ if (!detected || detected.length === 0) {
164
+ state.stableCount = 0;
165
+ state.yawStableCount = 0;
166
+ state.lastProcessedTime = now;
167
+ runOnFaces(0, 0, state.livenessStep);
168
+ return;
169
+ }
109
170
 
110
- // Early return conditions
111
- if (state.showCodeScanner || state.captured || isLoading || !state.isActive) {
112
- frame.release?.();
113
- return;
114
- }
171
+ if (detected.length === 1 && !state.captured) {
172
+ const face = detected[0];
173
+ if (!face?.bounds || face.yawAngle === undefined) {
174
+ runOnFaces(0, 0, state.livenessStep);
175
+ return;
176
+ }
115
177
 
116
- const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now();
117
- if (now - state.lastProcessedTime < FRAME_PROCESSOR_MIN_INTERVAL_MS) {
118
- frame.release?.();
119
- return;
120
- }
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);
121
183
 
122
- let detected;
123
- try {
124
- detected = detectFaces?.(frame);
125
- frame.release?.();
126
- } catch (error) {
127
- frame.release?.();
128
- return;
129
- }
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;
130
194
 
131
- if (!detected || detected.length === 0) {
132
- sharedState.value = {
133
- ...state,
134
- stableCount: 0,
135
- lastProcessedTime: now,
136
- yawStableCount: 0
137
- };
138
- runOnFaces(0, 0, state.livenessStep);
139
- return;
140
- }
195
+ // ---- Liveness Logic Based on Level ----
196
+ if (livenessLevel === 0) {
197
+ // No liveness check - only stability
198
+ // Skip all liveness steps and go directly to capture when stable
199
+ livenessStep = 0;
200
+ }
201
+ else if (livenessLevel === 1) {
202
+ // Blink detection only
203
+ if (livenessStep === 0) {
204
+ // Center face first
205
+ if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
206
+ yawStableCount++;
207
+ if (yawStableCount >= 2) {
208
+ livenessStep = 1; // Move to blink step
209
+ runOnLiveness(livenessStep);
210
+ yawStableCount = 0;
211
+ }
212
+ } else yawStableCount = 0;
213
+ }
214
+ // Blink detection
215
+ else if (livenessStep === 1) {
216
+ const leftEye = face.leftEyeOpenProbability ?? 1;
217
+ const rightEye = face.rightEyeOpenProbability ?? 1;
218
+ const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD;
141
219
 
142
- if (detected.length === 1 && !state.captured) {
143
- const face = detected[0];
144
- if (!face?.bounds || face.yawAngle === undefined) {
145
- runOnFaces(0, 0, state.livenessStep);
146
- return;
147
- }
220
+ if (eyesClosed && !eyeClosed) {
221
+ blinkCount++;
222
+ eyeClosed = true;
223
+ runOnLiveness(livenessStep, { blinkCount });
224
+ } else if (!eyesClosed && eyeClosed) {
225
+ eyeClosed = false;
226
+ }
148
227
 
149
- const yaw = face.yawAngle;
150
- const x = Math.max(0, face.bounds.x);
151
- const y = Math.max(0, face.bounds.y);
152
- const width = Math.max(0, face.bounds.width);
153
- const height = Math.max(0, face.bounds.height);
154
-
155
- let newLivenessStep = state.livenessStep;
156
- let newLeftTurnVerified = state.leftTurnVerified;
157
- let newRightTurnVerified = state.rightTurnVerified;
158
- let newYawStableCount = state.yawStableCount;
159
-
160
- // Liveness detection logic
161
- if (newLivenessStep === 0) {
162
- // Step 0: Wait for face to be centered and stable
163
- if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
164
- newYawStableCount++;
165
- if (newYawStableCount >= 2) {
166
- newLivenessStep = 1; // Ready for left turn
167
- runOnLiveness(newLivenessStep);
168
- newYawStableCount = 0;
228
+ if (blinkCount >= REQUIRED_BLINKS) {
229
+ livenessStep = 2; // Ready for capture
230
+ runOnLiveness(livenessStep);
231
+ }
232
+ }
169
233
  }
170
- } else {
171
- newYawStableCount = 0;
172
- }
173
- }
174
- else if (newLivenessStep === 1 && !newLeftTurnVerified) {
175
- // Step 1: Detect left turn
176
- if (yaw < YAW_LEFT_THRESHOLD) {
177
- newYawStableCount++;
178
- if (newYawStableCount >= 2) {
179
- newLeftTurnVerified = true;
180
- newLivenessStep = 2; // Ready for right turn
181
- runOnLiveness(newLivenessStep);
182
- newYawStableCount = 0;
234
+ else if (livenessLevel === 2) {
235
+ // Face turn detection only
236
+ if (livenessStep === 0) {
237
+ if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
238
+ yawStableCount++;
239
+ if (yawStableCount >= 2) {
240
+ livenessStep = 1; // Move to left turn
241
+ runOnLiveness(livenessStep);
242
+ yawStableCount = 0;
243
+ }
244
+ } else yawStableCount = 0;
245
+ } else if (livenessStep === 1 && !leftTurnVerified) {
246
+ if (yaw < YAW_LEFT_THRESHOLD) {
247
+ yawStableCount++;
248
+ if (yawStableCount >= 2) {
249
+ leftTurnVerified = true;
250
+ livenessStep = 2; // Move to right turn
251
+ runOnLiveness(livenessStep);
252
+ yawStableCount = 0;
253
+ }
254
+ } else yawStableCount = Math.max(0, yawStableCount - 0.5);
255
+ } else if (livenessStep === 2 && leftTurnVerified && !rightTurnVerified) {
256
+ if (yaw > YAW_RIGHT_THRESHOLD) {
257
+ yawStableCount++;
258
+ if (yawStableCount >= 2) {
259
+ rightTurnVerified = true;
260
+ livenessStep = 3; // Ready for capture
261
+ runOnLiveness(livenessStep);
262
+ yawStableCount = 0;
263
+ }
264
+ } else yawStableCount = Math.max(0, yawStableCount - 0.5);
265
+ }
183
266
  }
184
- } else {
185
- newYawStableCount = Math.max(0, newYawStableCount - 0.5);
186
- }
187
- }
188
- else if (newLivenessStep === 2 && newLeftTurnVerified && !newRightTurnVerified) {
189
- // Step 2: Detect right turn
190
- if (yaw > YAW_RIGHT_THRESHOLD) {
191
- newYawStableCount++;
192
- if (newYawStableCount >= 2) {
193
- newRightTurnVerified = true;
194
- newLivenessStep = 3; // Ready for capture
195
- runOnLiveness(newLivenessStep);
196
- newYawStableCount = 0;
267
+ else if (livenessLevel === 3) {
268
+ // Both blink and face turn detection
269
+ if (livenessStep === 0) {
270
+ if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
271
+ yawStableCount++;
272
+ if (yawStableCount >= 2) {
273
+ livenessStep = 1; // Move to left turn
274
+ runOnLiveness(livenessStep);
275
+ yawStableCount = 0;
276
+ }
277
+ } else yawStableCount = 0;
278
+ } else if (livenessStep === 1 && !leftTurnVerified) {
279
+ if (yaw < YAW_LEFT_THRESHOLD) {
280
+ yawStableCount++;
281
+ if (yawStableCount >= 2) {
282
+ leftTurnVerified = true;
283
+ livenessStep = 2; // Move to right turn
284
+ runOnLiveness(livenessStep);
285
+ yawStableCount = 0;
286
+ }
287
+ } else yawStableCount = Math.max(0, yawStableCount - 0.5);
288
+ } else if (livenessStep === 2 && leftTurnVerified && !rightTurnVerified) {
289
+ if (yaw > YAW_RIGHT_THRESHOLD) {
290
+ yawStableCount++;
291
+ if (yawStableCount >= 2) {
292
+ rightTurnVerified = true;
293
+ livenessStep = 3; // Move to blink step
294
+ runOnLiveness(livenessStep);
295
+ yawStableCount = 0;
296
+ }
297
+ } else yawStableCount = Math.max(0, yawStableCount - 0.5);
298
+ }
299
+ // Blink detection
300
+ else if (livenessStep === 3 && leftTurnVerified && rightTurnVerified) {
301
+ const leftEye = face.leftEyeOpenProbability ?? 1;
302
+ const rightEye = face.rightEyeOpenProbability ?? 1;
303
+ const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD;
304
+
305
+ if (eyesClosed && !eyeClosed) {
306
+ blinkCount++;
307
+ eyeClosed = true;
308
+ runOnLiveness(livenessStep, { blinkCount });
309
+ } else if (!eyesClosed && eyeClosed) {
310
+ eyeClosed = false;
311
+ }
312
+
313
+ if (blinkCount >= REQUIRED_BLINKS) {
314
+ livenessStep = 4; // Ready for capture
315
+ runOnLiveness(livenessStep);
316
+ }
317
+ }
197
318
  }
198
- } else {
199
- newYawStableCount = Math.max(0, newYawStableCount - 0.5);
200
- }
201
- }
202
- else if (newLivenessStep === 3 && newLeftTurnVerified && newRightTurnVerified) {
203
- // Step 3: Wait for face to return to center for capture
204
- if (Math.abs(yaw) < YAW_CENTER_THRESHOLD) {
205
- newYawStableCount++;
206
- } else {
207
- newYawStableCount = 0;
208
- }
209
- }
210
319
 
211
- // Calculate face stability for capture
212
- let newStableCount = state.stableCount;
213
- if (state.lastX === 0 && state.lastY === 0) {
214
- newStableCount = 1;
215
- } else {
216
- const dx = Math.abs(x - state.lastX);
217
- const dy = Math.abs(y - state.lastY);
218
- if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
219
- newStableCount = state.stableCount + 1;
320
+ // ---- Face stability check ----
321
+ let newStableCount = state.stableCount;
322
+ if (state.lastX === 0 && state.lastY === 0) newStableCount = 1;
323
+ else {
324
+ const dx = Math.abs(x - state.lastX);
325
+ const dy = Math.abs(y - state.lastY);
326
+ if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD)
327
+ newStableCount = state.stableCount + 1;
328
+ else newStableCount = 1;
329
+ }
330
+
331
+ // Mutate sharedState fields directly (avoid object reallocation)
332
+ state.lastProcessedTime = now;
333
+ state.lastX = x;
334
+ state.lastY = y;
335
+ state.lastW = width;
336
+ state.lastH = height;
337
+ state.stableCount = newStableCount;
338
+ state.livenessStep = livenessStep;
339
+ state.leftTurnVerified = leftTurnVerified;
340
+ state.rightTurnVerified = rightTurnVerified;
341
+ state.currentYaw = yaw;
342
+ state.yawStableCount = yawStableCount;
343
+ state.blinkCount = blinkCount;
344
+ state.eyeClosed = eyeClosed;
345
+
346
+ const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100);
347
+ runOnFaces(1, progress, livenessStep);
348
+
349
+ // Capture condition based on liveness level
350
+ let shouldCapture = false;
351
+
352
+ if (livenessLevel === 0) {
353
+ // No liveness - capture when stable
354
+ shouldCapture = newStableCount >= FACE_STABILITY_THRESHOLD;
355
+ }
356
+ else if (livenessLevel === 1) {
357
+ // Blink only - capture after blinks and stable
358
+ shouldCapture = livenessStep === 2 && blinkCount >= REQUIRED_BLINKS && newStableCount >= FACE_STABILITY_THRESHOLD;
359
+ }
360
+ else if (livenessLevel === 2) {
361
+ // Face turn only - capture after turns and stable
362
+ shouldCapture = livenessStep === 3 && leftTurnVerified && rightTurnVerified && newStableCount >= FACE_STABILITY_THRESHOLD;
363
+ }
364
+ else if (livenessLevel === 3) {
365
+ // Both - capture after turns, blinks and stable
366
+ shouldCapture = livenessStep === 4 && blinkCount >= REQUIRED_BLINKS && newStableCount >= FACE_STABILITY_THRESHOLD;
367
+ }
368
+
369
+ if (shouldCapture && !state.captured) {
370
+ state.captured = true;
371
+ runOnStable({ x, y, width, height });
372
+ }
220
373
  } else {
221
- newStableCount = 1;
374
+ // multiple faces — reset stability but keep liveness step
375
+ state.stableCount = 0;
376
+ state.lastProcessedTime = now;
377
+ state.yawStableCount = 0;
378
+ runOnFaces(detected.length, 0, state.livenessStep);
222
379
  }
380
+ } catch (err) {
381
+ // protect worklet from throwing
382
+ try {
383
+ console.error('FrameProcessor worklet error:', err);
384
+ } catch (e) {}
385
+ } finally {
386
+ // Always release the frame exactly once
387
+ frame.release?.();
223
388
  }
389
+ },
390
+ [detectFaces, isLoading]
391
+ );
224
392
 
225
- // Update shared state
226
- sharedState.value = {
227
- ...state,
228
- lastProcessedTime: now,
229
- lastX: x,
230
- lastY: y,
231
- lastW: width,
232
- lastH: height,
233
- stableCount: newStableCount,
234
- livenessStep: newLivenessStep,
235
- leftTurnVerified: newLeftTurnVerified,
236
- rightTurnVerified: newRightTurnVerified,
237
- currentYaw: yaw,
238
- yawStableCount: newYawStableCount,
239
- };
240
-
241
- // Calculate progress for UI
242
- const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100);
243
- runOnFaces(1, progress, newLivenessStep);
244
-
245
- // Capture conditions: liveness complete + face stable + face centered
246
- if (newLivenessStep === 3 &&
247
- newLeftTurnVerified &&
248
- newRightTurnVerified &&
249
- newStableCount >= FACE_STABILITY_THRESHOLD &&
250
- Math.abs(yaw) < YAW_CENTER_THRESHOLD &&
251
- newYawStableCount >= 2) {
252
-
253
- sharedState.value = {
254
- ...sharedState.value,
255
- captured: true
256
- };
257
- runOnStable({ x, y, width, height });
258
- }
259
-
260
- } else {
261
- // Multiple faces or other conditions
262
- sharedState.value = {
263
- ...state,
264
- stableCount: 0,
265
- lastProcessedTime: now,
266
- yawStableCount: 0
267
- };
268
- runOnFaces(detected.length, 0, state.livenessStep);
269
- }
270
- }, [detectFaces, isLoading]);
271
-
393
+ // Reset and cleanup logic
272
394
  const resetCaptureState = useCallback(() => {
273
- sharedState.value = {
274
- ...sharedState.value,
275
- lastProcessedTime: 0,
276
- lastX: 0,
277
- lastY: 0,
278
- lastW: 0,
279
- lastH: 0,
280
- stableCount: 0,
281
- captured: false,
282
- livenessStep: 0,
283
- leftTurnVerified: false,
284
- rightTurnVerified: false,
285
- currentYaw: 0,
286
- yawStableCount: 0,
287
- };
395
+ const state = sharedState.value;
396
+ state.lastProcessedTime = 0;
397
+ state.lastX = 0;
398
+ state.lastY = 0;
399
+ state.lastW = 0;
400
+ state.lastH = 0;
401
+ state.stableCount = 0;
402
+ state.captured = false;
403
+ state.livenessStep = 0;
404
+ state.leftTurnVerified = false;
405
+ state.rightTurnVerified = false;
406
+ state.currentYaw = 0;
407
+ state.yawStableCount = 0;
408
+ state.blinkCount = 0;
409
+ state.eyeClosed = false;
288
410
  }, [sharedState]);
289
411
 
290
412
  const forceResetCaptureState = useCallback(() => {
291
- sharedState.value = {
292
- lastProcessedTime: 0,
293
- lastX: 0,
294
- lastY: 0,
295
- lastW: 0,
296
- lastH: 0,
297
- stableCount: 0,
298
- captured: false,
299
- showCodeScanner: sharedState.value.showCodeScanner,
300
- isActive: sharedState.value.isActive,
301
- livenessStep: 0,
302
- leftTurnVerified: false,
303
- rightTurnVerified: false,
304
- currentYaw: 0,
305
- yawStableCount: 0,
306
- };
413
+ const show = sharedState.value.showCodeScanner;
414
+ const active = sharedState.value.isActive;
415
+ const level = sharedState.value.livenessLevel;
416
+ sharedState.value.lastProcessedTime = 0;
417
+ sharedState.value.lastX = 0;
418
+ sharedState.value.lastY = 0;
419
+ sharedState.value.lastW = 0;
420
+ sharedState.value.lastH = 0;
421
+ sharedState.value.stableCount = 0;
422
+ sharedState.value.captured = false;
423
+ sharedState.value.showCodeScanner = show;
424
+ sharedState.value.isActive = active;
425
+ sharedState.value.livenessLevel = level;
426
+ sharedState.value.livenessStep = 0;
427
+ sharedState.value.leftTurnVerified = false;
428
+ sharedState.value.rightTurnVerified = false;
429
+ sharedState.value.currentYaw = 0;
430
+ sharedState.value.yawStableCount = 0;
431
+ sharedState.value.blinkCount = 0;
432
+ sharedState.value.eyeClosed = false;
307
433
  }, [sharedState]);
308
434
 
309
435
  const updateShowCodeScanner = useCallback(
310
436
  (value) => {
311
- sharedState.value = {
312
- ...sharedState.value,
313
- showCodeScanner: !!value
314
- };
315
- },
437
+ sharedState.value.showCodeScanner = !!value;
438
+ },
316
439
  [sharedState]
317
440
  );
318
441
 
319
442
  const updateIsActive = useCallback(
320
443
  (active) => {
321
- sharedState.value = {
322
- ...sharedState.value,
323
- isActive: active,
324
- // Reset capture state when deactivating
325
- captured: active ? sharedState.value.captured : false
326
- };
327
- },
444
+ sharedState.value.isActive = !!active;
445
+ if (!active) sharedState.value.captured = false;
446
+ },
328
447
  [sharedState]
329
448
  );
330
449
 
331
- // Cleanup on unmount
332
450
  useEffect(() => {
333
451
  isMounted.current = true;
334
-
335
452
  return () => {
336
453
  isMounted.current = false;
337
454
  forceResetCaptureState();