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
|
-
//
|
|
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
|
-
//
|
|
13
|
-
const
|
|
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:
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
lastW: 0,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
67
|
-
sharedState.value
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
//
|
|
140
|
-
|
|
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
|
-
|
|
146
|
-
if (
|
|
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
|
-
//
|
|
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
|
-
|
|
288
|
+
// Fast path for no faces
|
|
163
289
|
if (!detected || detected.length === 0) {
|
|
164
|
-
state.stableCount = 0;
|
|
165
|
-
state.
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
// Process single face scenario
|
|
308
|
+
if (detected.length === 1 && !state.flags.captured) {
|
|
172
309
|
const face = detected[0];
|
|
173
|
-
if (!face?.bounds
|
|
174
|
-
runOnFaces(0, 0, state.
|
|
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
|
|
179
|
-
const x = Math.max(0,
|
|
180
|
-
const y = Math.max(0,
|
|
181
|
-
const width = Math.max(0,
|
|
182
|
-
const height = Math.max(0,
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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 && !
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
runOnLiveness(
|
|
311
|
-
} else if (!eyesClosed &&
|
|
312
|
-
|
|
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 (
|
|
316
|
-
|
|
317
|
-
runOnLiveness(
|
|
424
|
+
if (newBlinkCount >= REQUIRED_BLINKS) {
|
|
425
|
+
newLivenessStep = 2;
|
|
426
|
+
runOnLiveness(newLivenessStep);
|
|
318
427
|
}
|
|
319
428
|
}
|
|
320
429
|
}
|
|
321
430
|
|
|
322
|
-
//
|
|
323
|
-
let newStableCount = state.stableCount;
|
|
324
|
-
if (state.lastX === 0 && state.lastY === 0)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
//
|
|
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.
|
|
341
|
-
state.
|
|
342
|
-
state.
|
|
343
|
-
state.
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
//
|
|
377
|
-
state.stableCount = 0;
|
|
489
|
+
// Multiple faces - reset states
|
|
490
|
+
state.faceTracking.stableCount = 0;
|
|
378
491
|
state.lastProcessedTime = now;
|
|
379
|
-
state.
|
|
380
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
406
|
-
state.
|
|
407
|
-
state.
|
|
408
|
-
state.
|
|
409
|
-
state.
|
|
410
|
-
state.
|
|
411
|
-
state.
|
|
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
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
};
|