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.
- package/package.json +1 -1
- package/src/components/CaptureImageWithoutEdit.js +373 -275
- package/src/components/Card.js +6 -6
- package/src/components/CountdownTimer.js +3 -3
- package/src/components/Notification.js +6 -6
- package/src/components/StepIndicator.js +7 -7
- package/src/hooks/useCountdown.js +7 -7
- package/src/hooks/useFaceDetectionFrameProcessor.js +378 -261
- package/src/hooks/useImageProcessing.js +6 -6
- package/src/hooks/useNotifyMessage.js +4 -4
- package/src/index.js +32 -49
- package/src/utils/Global.js +48 -0
- package/src/utils/getLoaderGif.js +3 -3
- package/src/utils/constants.js +0 -73
|
@@ -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 =
|
|
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 = -
|
|
14
|
-
const YAW_RIGHT_THRESHOLD =
|
|
15
|
-
const YAW_CENTER_THRESHOLD = 5;
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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();
|