@rick427/react-native-liveness 0.1.5 → 0.1.7
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/lib/module/LivenessCamera.js +26 -27
- package/lib/module/LivenessCamera.js.map +1 -1
- package/lib/module/livenessScoring.js +26 -9
- package/lib/module/livenessScoring.js.map +1 -1
- package/lib/module/useLivenessCamera.js +40 -8
- package/lib/module/useLivenessCamera.js.map +1 -1
- package/lib/typescript/src/LivenessCamera.d.ts.map +1 -1
- package/lib/typescript/src/livenessScoring.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useLivenessCamera.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/LivenessCamera.tsx +29 -30
- package/src/livenessScoring.ts +41 -12
- package/src/types.ts +2 -2
- package/src/useLivenessCamera.ts +42 -7
|
@@ -3,26 +3,25 @@
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Animated, StyleSheet, Text, View } from 'react-native';
|
|
5
5
|
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera';
|
|
6
|
-
import {
|
|
6
|
+
import { Circle, Path, Svg } from 'react-native-svg';
|
|
7
7
|
import { useLivenessCamera } from "./useLivenessCamera.js";
|
|
8
8
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
const FACE_RATIO = 1.35; // height-to-width ratio of the oval (~3:4 face)
|
|
9
|
+
// Circle diameter = 82 % of container width — large enough to fit any face
|
|
10
|
+
// comfortably without the user needing to fiddle with distance.
|
|
11
|
+
const CIRCLE_DIAMETER_RATIO = 0.82;
|
|
13
12
|
const STROKE_WIDTH = 3;
|
|
14
|
-
// Cubic bezier approximation constant for a smooth
|
|
13
|
+
// Cubic bezier approximation constant for a smooth circle path
|
|
15
14
|
const K = 0.5523;
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
* Returns the stroke colour for the
|
|
17
|
+
* Returns the stroke colour for the circle guide.
|
|
19
18
|
*
|
|
20
19
|
* ● White – no face / scanning (score < 0.4)
|
|
21
20
|
* ● Yellow – face detected, confidence building (0.4 ≤ score < threshold)
|
|
22
21
|
* ● Green – liveness confirmed / countdown / capture
|
|
23
22
|
* ● Red – error
|
|
24
23
|
*/
|
|
25
|
-
function
|
|
24
|
+
function getCircleColor(state, score) {
|
|
26
25
|
switch (state) {
|
|
27
26
|
case 'error':
|
|
28
27
|
return '#FF3B30';
|
|
@@ -37,14 +36,14 @@ function getOvalColor(state, score) {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
* Returns an SVG path string tracing
|
|
41
|
-
* cubic bezier curves. Used inside a compound path with
|
|
42
|
-
* to punch a transparent hole through the dark scrim.
|
|
39
|
+
* Returns an SVG path string tracing a circle at (cx, cy) with radius r
|
|
40
|
+
* using cubic bezier curves. Used inside a compound path with
|
|
41
|
+
* fillRule="evenodd" to punch a transparent hole through the dark scrim.
|
|
43
42
|
*/
|
|
44
|
-
function
|
|
45
|
-
return [`M ${cx +
|
|
43
|
+
function circlePath(cx, cy, r) {
|
|
44
|
+
return [`M ${cx + r} ${cy}`, `C ${cx + r} ${cy - r * K} ${cx + r * K} ${cy - r} ${cx} ${cy - r}`, `C ${cx - r * K} ${cy - r} ${cx - r} ${cy - r * K} ${cx - r} ${cy}`, `C ${cx - r} ${cy + r * K} ${cx - r * K} ${cy + r} ${cx} ${cy + r}`, `C ${cx + r * K} ${cy + r} ${cx + r} ${cy + r * K} ${cx + r} ${cy}`, 'Z'].join(' ');
|
|
46
45
|
}
|
|
47
|
-
function
|
|
46
|
+
function CircleOverlay({
|
|
48
47
|
width,
|
|
49
48
|
height,
|
|
50
49
|
state,
|
|
@@ -52,14 +51,14 @@ function OvalOverlay({
|
|
|
52
51
|
}) {
|
|
53
52
|
if (width === 0 || height === 0) return null;
|
|
54
53
|
const cx = width / 2;
|
|
55
|
-
//
|
|
56
|
-
const cy = height * 0.
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const color = getOvalColor(state, score);
|
|
54
|
+
// Centre the circle slightly above midpoint so the face sits naturally
|
|
55
|
+
const cy = height * 0.42;
|
|
56
|
+
const r = width * CIRCLE_DIAMETER_RATIO / 2;
|
|
57
|
+
const color = getCircleColor(state, score);
|
|
60
58
|
|
|
61
|
-
// Compound path:
|
|
62
|
-
|
|
59
|
+
// Compound path: full-screen rect + circle cutout.
|
|
60
|
+
// evenodd fill rule makes the circle area transparent.
|
|
61
|
+
const scrimD = `M0 0H${width}V${height}H0Z ${circlePath(cx, cy, r)}`;
|
|
63
62
|
return /*#__PURE__*/_jsxs(Svg, {
|
|
64
63
|
style: StyleSheet.absoluteFill,
|
|
65
64
|
width: width,
|
|
@@ -68,11 +67,10 @@ function OvalOverlay({
|
|
|
68
67
|
d: scrimD,
|
|
69
68
|
fill: "rgba(0,0,0,0.55)",
|
|
70
69
|
fillRule: "evenodd"
|
|
71
|
-
}), /*#__PURE__*/_jsx(
|
|
70
|
+
}), /*#__PURE__*/_jsx(Circle, {
|
|
72
71
|
cx: cx,
|
|
73
72
|
cy: cy,
|
|
74
|
-
|
|
75
|
-
ry: ry,
|
|
73
|
+
r: r,
|
|
76
74
|
fill: "none",
|
|
77
75
|
stroke: color,
|
|
78
76
|
strokeWidth: STROKE_WIDTH
|
|
@@ -130,7 +128,7 @@ export function LivenessCamera({
|
|
|
130
128
|
onError,
|
|
131
129
|
countdownFrom = 3,
|
|
132
130
|
livenessThreshold = 0.75,
|
|
133
|
-
confirmFrames =
|
|
131
|
+
confirmFrames = 10,
|
|
134
132
|
soundEnabled = true,
|
|
135
133
|
style
|
|
136
134
|
}) {
|
|
@@ -205,8 +203,9 @@ export function LivenessCamera({
|
|
|
205
203
|
isActive: livenessState !== 'done' && livenessState !== 'error',
|
|
206
204
|
frameProcessor: frameProcessor,
|
|
207
205
|
photo: true,
|
|
208
|
-
pixelFormat: "yuv"
|
|
209
|
-
|
|
206
|
+
pixelFormat: "yuv",
|
|
207
|
+
fps: 60
|
|
208
|
+
}), /*#__PURE__*/_jsx(CircleOverlay, {
|
|
210
209
|
width: containerSize.width,
|
|
211
210
|
height: containerSize.height,
|
|
212
211
|
state: livenessState,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useCallback","useEffect","useRef","useState","Animated","StyleSheet","Text","View","Camera","useCameraDevice","useCameraPermission","
|
|
1
|
+
{"version":3,"names":["useCallback","useEffect","useRef","useState","Animated","StyleSheet","Text","View","Camera","useCameraDevice","useCameraPermission","Circle","Path","Svg","useLivenessCamera","jsx","_jsx","jsxs","_jsxs","CIRCLE_DIAMETER_RATIO","STROKE_WIDTH","K","getCircleColor","state","score","circlePath","cx","cy","r","join","CircleOverlay","width","height","color","scrimD","style","absoluteFill","children","d","fill","fillRule","stroke","strokeWidth","CountdownBubble","value","scale","Value","current","opacity","parallel","sequence","spring","toValue","stiffness","damping","useNativeDriver","timing","duration","start","styles","countdownBubble","transform","countdownText","LivenessCamera","onCapture","onLivenessConfirmed","onError","countdownFrom","livenessThreshold","confirmFrames","soundEnabled","hasPermission","requestPermission","device","cameraRef","containerSize","setContainerSize","frameProcessor","livenessState","livenessScore","countdown","feedback","handleLayout","e","nativeEvent","layout","catch","Error","root","centered","permissionText","onLayout","ref","isActive","photo","pixelFormat","fps","feedbackContainer","feedbackText","countdownContainer","captureFlash","pointerEvents","create","flex","backgroundColor","overflow","justifyContent","alignItems","fontSize","textAlign","paddingHorizontal","position","bottom","left","right","fontWeight","textShadowColor","textShadowOffset","textShadowRadius","absoluteFillObject","borderRadius","borderWidth","borderColor","lineHeight"],"sourceRoot":"../../src","sources":["LivenessCamera.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -6,10 +6,15 @@ const WEIGHTS = {
|
|
|
6
6
|
headPose: 0.3,
|
|
7
7
|
eyesOpen: 0.3
|
|
8
8
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
9
|
+
|
|
10
|
+
// Loosened from 0.20 / 0.65 — gives more room for the user to breathe
|
|
11
|
+
const FACE_SIZE_MIN = 0.15;
|
|
12
|
+
const FACE_SIZE_MAX = 0.8;
|
|
13
|
+
const FACE_SIZE_SOFT_MARGIN = 0.05; // gradient ramp at each edge
|
|
14
|
+
|
|
15
|
+
// Loosened from 20° — ML Kit head pose is noisy, 25° still rejects clear tilts
|
|
16
|
+
const MAX_YAW_DEG = 25;
|
|
17
|
+
const MAX_PITCH_DEG = 25;
|
|
13
18
|
export function scoreFrame(face, frameWidth) {
|
|
14
19
|
if (!face.detected || frameWidth === 0) {
|
|
15
20
|
return {
|
|
@@ -20,10 +25,22 @@ export function scoreFrame(face, frameWidth) {
|
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
const faceWidthRatio = face.bounds.width / frameWidth;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
// Soft-edge face size: smooth ramp into / out of the valid range so a face
|
|
30
|
+
// sitting just outside the threshold doesn't score hard 0.
|
|
31
|
+
let faceSize;
|
|
32
|
+
if (faceWidthRatio >= FACE_SIZE_MIN && faceWidthRatio <= FACE_SIZE_MAX) {
|
|
33
|
+
faceSize = 1.0;
|
|
34
|
+
} else if (faceWidthRatio < FACE_SIZE_MIN) {
|
|
35
|
+
faceSize = Math.max(0, (faceWidthRatio - (FACE_SIZE_MIN - FACE_SIZE_SOFT_MARGIN)) / FACE_SIZE_SOFT_MARGIN);
|
|
36
|
+
} else {
|
|
37
|
+
faceSize = Math.max(0, (FACE_SIZE_MAX + FACE_SIZE_SOFT_MARGIN - faceWidthRatio) / FACE_SIZE_SOFT_MARGIN);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Soft-edge head pose: full score inside threshold, linear decay outside
|
|
41
|
+
const yawScore = Math.abs(face.yawAngle) <= MAX_YAW_DEG ? 1.0 : Math.max(0, 1 - (Math.abs(face.yawAngle) - MAX_YAW_DEG) / MAX_YAW_DEG);
|
|
42
|
+
const pitchScore = Math.abs(face.pitchAngle) <= MAX_PITCH_DEG ? 1.0 : Math.max(0, 1 - (Math.abs(face.pitchAngle) - MAX_PITCH_DEG) / MAX_PITCH_DEG);
|
|
43
|
+
const headPose = (yawScore + pitchScore) / 2;
|
|
27
44
|
|
|
28
45
|
// ML Kit returns -1 when classification is disabled or unavailable
|
|
29
46
|
const leftEye = face.leftEyeOpenProbability >= 0 ? face.leftEyeOpenProbability : 0.5;
|
|
@@ -43,7 +60,7 @@ export function rollingAverage(scores) {
|
|
|
43
60
|
}
|
|
44
61
|
export function getFeedback(face, frameWidth, livenessConfirmed) {
|
|
45
62
|
if (livenessConfirmed) return 'Liveness confirmed';
|
|
46
|
-
if (!face.detected) return 'Position your face in the
|
|
63
|
+
if (!face.detected) return 'Position your face in the circle';
|
|
47
64
|
const faceWidthRatio = face.bounds.width / frameWidth;
|
|
48
65
|
if (faceWidthRatio < FACE_SIZE_MIN) return 'Move closer';
|
|
49
66
|
if (faceWidthRatio > FACE_SIZE_MAX) return 'Move farther away';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["WEIGHTS","faceDetected","faceSize","headPose","eyesOpen","FACE_SIZE_MIN","FACE_SIZE_MAX","MAX_YAW_DEG","MAX_PITCH_DEG","scoreFrame","face","frameWidth","detected","total","faceWidthRatio","bounds","width","
|
|
1
|
+
{"version":3,"names":["WEIGHTS","faceDetected","faceSize","headPose","eyesOpen","FACE_SIZE_MIN","FACE_SIZE_MAX","FACE_SIZE_SOFT_MARGIN","MAX_YAW_DEG","MAX_PITCH_DEG","scoreFrame","face","frameWidth","detected","total","faceWidthRatio","bounds","width","Math","max","yawScore","abs","yawAngle","pitchScore","pitchAngle","leftEye","leftEyeOpenProbability","rightEye","rightEyeOpenProbability","rollingAverage","scores","length","reduce","a","b","getFeedback","livenessConfirmed","yawBad","pitchBad"],"sourceRoot":"../../src","sources":["livenessScoring.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { useFrameProcessor } from 'react-native-vision-camera';
|
|
4
|
+
import { runAtTargetFps, useFrameProcessor } from 'react-native-vision-camera';
|
|
5
5
|
import { Worklets } from 'react-native-worklets-core';
|
|
6
6
|
import { useLivenessPlugin } from "./LivenessDetector.js";
|
|
7
7
|
import { getFeedback, rollingAverage, scoreFrame } from "./livenessScoring.js";
|
|
8
8
|
const WINDOW_SIZE = 20;
|
|
9
|
+
// How many frames to decay consecutiveGood by on a bad frame.
|
|
10
|
+
// Decay (not hard reset) means one noisy frame won't erase all progress.
|
|
11
|
+
const CONSECUTIVE_DECAY = 2;
|
|
9
12
|
export function useLivenessCamera(options) {
|
|
10
13
|
const {
|
|
11
14
|
livenessThreshold,
|
|
@@ -25,11 +28,18 @@ export function useLivenessCamera(options) {
|
|
|
25
28
|
const frameWidth = useRef(0);
|
|
26
29
|
const stateRef = useRef('scanning');
|
|
27
30
|
const isCaptured = useRef(false);
|
|
31
|
+
|
|
32
|
+
// Feedback debouncing: only update the displayed text after the same message
|
|
33
|
+
// has been stable for FEEDBACK_DEBOUNCE_MS. Prevents rapid flickering when
|
|
34
|
+
// ML Kit oscillates between two states on consecutive frames.
|
|
35
|
+
const FEEDBACK_DEBOUNCE_MS = 400;
|
|
36
|
+
const feedbackTimer = useRef(null);
|
|
37
|
+
const pendingFeedback = useRef('Position your face in the circle');
|
|
28
38
|
const [state, setState] = useState({
|
|
29
39
|
livenessState: 'scanning',
|
|
30
40
|
livenessScore: 0,
|
|
31
41
|
countdown: null,
|
|
32
|
-
feedback: 'Position your face in the
|
|
42
|
+
feedback: 'Position your face in the circle'
|
|
33
43
|
});
|
|
34
44
|
const setLivenessState = useCallback(next => {
|
|
35
45
|
stateRef.current = next;
|
|
@@ -121,17 +131,32 @@ export function useLivenessCamera(options) {
|
|
|
121
131
|
frameScores.current.shift();
|
|
122
132
|
}
|
|
123
133
|
const avgScore = rollingAverage(frameScores.current);
|
|
134
|
+
|
|
135
|
+
// Decay on bad frames instead of hard reset — one noisy ML Kit result
|
|
136
|
+
// won't wipe out progress the user has already built up.
|
|
124
137
|
if (total >= livenessThreshold) {
|
|
125
138
|
consecutiveGood.current += 1;
|
|
126
139
|
} else {
|
|
127
|
-
consecutiveGood.current = 0;
|
|
140
|
+
consecutiveGood.current = Math.max(0, consecutiveGood.current - CONSECUTIVE_DECAY);
|
|
128
141
|
}
|
|
129
142
|
const isLive = consecutiveGood.current >= confirmFrames && avgScore >= livenessThreshold;
|
|
130
|
-
|
|
143
|
+
|
|
144
|
+
// Debounce the feedback text: only apply a new message after it has been
|
|
145
|
+
// stable for FEEDBACK_DEBOUNCE_MS, preventing rapid label flickering.
|
|
146
|
+
const newFeedback = getFeedback(safeFace, width, isLive);
|
|
147
|
+
if (newFeedback !== pendingFeedback.current) {
|
|
148
|
+
pendingFeedback.current = newFeedback;
|
|
149
|
+
if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
|
|
150
|
+
feedbackTimer.current = setTimeout(() => {
|
|
151
|
+
setState(prev => ({
|
|
152
|
+
...prev,
|
|
153
|
+
feedback: pendingFeedback.current
|
|
154
|
+
}));
|
|
155
|
+
}, FEEDBACK_DEBOUNCE_MS);
|
|
156
|
+
}
|
|
131
157
|
setState(prev => ({
|
|
132
158
|
...prev,
|
|
133
|
-
livenessScore: avgScore
|
|
134
|
-
feedback
|
|
159
|
+
livenessScore: avgScore
|
|
135
160
|
}));
|
|
136
161
|
if (isLive && stateRef.current === 'scanning') {
|
|
137
162
|
setLivenessState('confirmed');
|
|
@@ -146,14 +171,21 @@ export function useLivenessCamera(options) {
|
|
|
146
171
|
const frameProcessor = useFrameProcessor(frame => {
|
|
147
172
|
'worklet';
|
|
148
173
|
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
// Camera preview renders at full fps (60). ML Kit only needs ~20fps —
|
|
175
|
+
// running it on every frame would block the render thread unnecessarily.
|
|
176
|
+
runAtTargetFps(20, () => {
|
|
177
|
+
'worklet';
|
|
178
|
+
|
|
179
|
+
const face = plugin.detectLiveness(frame);
|
|
180
|
+
handleFaceDataJS(face, frame.width);
|
|
181
|
+
});
|
|
151
182
|
}, [plugin, handleFaceDataJS]);
|
|
152
183
|
useEffect(() => {
|
|
153
184
|
return () => {
|
|
154
185
|
frameScores.current = [];
|
|
155
186
|
consecutiveGood.current = 0;
|
|
156
187
|
isCaptured.current = false;
|
|
188
|
+
if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
|
|
157
189
|
};
|
|
158
190
|
}, []);
|
|
159
191
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useCallback","useEffect","useMemo","useRef","useState","useFrameProcessor","Worklets","useLivenessPlugin","getFeedback","rollingAverage","scoreFrame","WINDOW_SIZE","useLivenessCamera","options","livenessThreshold","confirmFrames","countdownFrom","soundEnabled","cameraRef","onCapture","onLivenessConfirmed","onError","plugin","frameScores","consecutiveGood","frameWidth","stateRef","isCaptured","state","setState","livenessState","livenessScore","countdown","feedback","setLivenessState","next","current","prev","capture","photo","takePhoto","flash","enableShutterSound","score","timestamp","Date","now","err","Error","String","startCountdown","tick","interval","setInterval","clearInterval","handleFaceData","face","width","safeFace","detected","bounds","x","y","height","yawAngle","pitchAngle","rollAngle","leftEyeOpenProbability","rightEyeOpenProbability","smilingProbability","total","push","length","shift","avgScore","isLive","handleFaceDataJS","createRunOnJS","frameProcessor","frame","detectLiveness"],"sourceRoot":"../../src","sources":["useLivenessCamera.ts"],"mappings":";;AAAA,SAASA,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAEzE,SAASC,iBAAiB,QAAQ,4BAA4B;
|
|
1
|
+
{"version":3,"names":["useCallback","useEffect","useMemo","useRef","useState","runAtTargetFps","useFrameProcessor","Worklets","useLivenessPlugin","getFeedback","rollingAverage","scoreFrame","WINDOW_SIZE","CONSECUTIVE_DECAY","useLivenessCamera","options","livenessThreshold","confirmFrames","countdownFrom","soundEnabled","cameraRef","onCapture","onLivenessConfirmed","onError","plugin","frameScores","consecutiveGood","frameWidth","stateRef","isCaptured","FEEDBACK_DEBOUNCE_MS","feedbackTimer","pendingFeedback","state","setState","livenessState","livenessScore","countdown","feedback","setLivenessState","next","current","prev","capture","photo","takePhoto","flash","enableShutterSound","score","timestamp","Date","now","err","Error","String","startCountdown","tick","interval","setInterval","clearInterval","handleFaceData","face","width","safeFace","detected","bounds","x","y","height","yawAngle","pitchAngle","rollAngle","leftEyeOpenProbability","rightEyeOpenProbability","smilingProbability","total","push","length","shift","avgScore","Math","max","isLive","newFeedback","clearTimeout","setTimeout","handleFaceDataJS","createRunOnJS","frameProcessor","frame","detectLiveness"],"sourceRoot":"../../src","sources":["useLivenessCamera.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LivenessCamera.d.ts","sourceRoot":"","sources":["../../../src/LivenessCamera.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"LivenessCamera.d.ts","sourceRoot":"","sources":["../../../src/LivenessCamera.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,SAAS,CAAC;AAoIlE,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,mBAAmB,EACnB,OAAO,EACP,aAAiB,EACjB,iBAAwB,EACxB,aAAkB,EAClB,YAAmB,EACnB,KAAK,GACN,EAAE,mBAAmB,2CAmFrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"livenessScoring.d.ts","sourceRoot":"","sources":["../../../src/livenessScoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"livenessScoring.d.ts","sourceRoot":"","sources":["../../../src/livenessScoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAkBzD,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,CAsDzE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAGvD;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,OAAO,GACzB,eAAe,CAmBjB"}
|
|
@@ -16,7 +16,7 @@ export type FaceData = {
|
|
|
16
16
|
smilingProbability: number;
|
|
17
17
|
};
|
|
18
18
|
export type LivenessState = 'idle' | 'scanning' | 'confirmed' | 'countdown' | 'capturing' | 'done' | 'error';
|
|
19
|
-
export type FeedbackMessage = 'Position your face in the
|
|
19
|
+
export type FeedbackMessage = 'Position your face in the circle' | 'Move closer' | 'Move farther away' | 'Look straight ahead' | 'Hold still...' | 'Stay still' | 'Open your eyes' | 'Liveness confirmed' | '';
|
|
20
20
|
export type CaptureResult = {
|
|
21
21
|
photo: PhotoFile;
|
|
22
22
|
livenessScore: number;
|
|
@@ -45,7 +45,7 @@ export type LivenessCameraProps = {
|
|
|
45
45
|
livenessThreshold?: number;
|
|
46
46
|
/**
|
|
47
47
|
* Number of consecutive high-score frames required before liveness is
|
|
48
|
-
* confirmed. Defaults to
|
|
48
|
+
* confirmed. Defaults to 10 (~500ms at 20fps).
|
|
49
49
|
*/
|
|
50
50
|
confirmFrames?: number;
|
|
51
51
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAE5D,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE;QACN,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;IAChC,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,UAAU,GACV,WAAW,GACX,WAAW,GACX,WAAW,GACX,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,eAAe,GACvB,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAE5D,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE;QACN,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;IAChC,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,UAAU,GACV,WAAW,GACX,WAAW,GACX,WAAW,GACX,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,eAAe,GACvB,kCAAkC,GAClC,aAAa,GACb,mBAAmB,GACnB,qBAAqB,GACrB,eAAe,GACf,YAAY,GACZ,gBAAgB,GAChB,oBAAoB,GACpB,EAAE,CAAC;AAEP,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,SAAS,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC;;OAEG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAE3C;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEjC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLivenessCamera.d.ts","sourceRoot":"","sources":["../../../src/useLivenessCamera.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAS,MAAM,4BAA4B,CAAC;AAKhE,OAAO,KAAK,EACV,aAAa,EAEb,eAAe,EACf,aAAa,EACd,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"useLivenessCamera.d.ts","sourceRoot":"","sources":["../../../src/useLivenessCamera.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAS,MAAM,4BAA4B,CAAC;AAKhE,OAAO,KAAK,EACV,aAAa,EAEb,eAAe,EACf,aAAa,EACd,MAAM,SAAS,CAAC;AAOjB,KAAK,OAAO,GAAG;IACb,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC3C,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AASF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO;;;;;;EA4MjD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rick427/react-native-liveness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Liveness detection library for React Native using Vision Camera v4 and ML Kit",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/LivenessCamera.tsx
CHANGED
|
@@ -5,27 +5,26 @@ import {
|
|
|
5
5
|
useCameraDevice,
|
|
6
6
|
useCameraPermission,
|
|
7
7
|
} from 'react-native-vision-camera';
|
|
8
|
-
import {
|
|
8
|
+
import { Circle, Path, Svg } from 'react-native-svg';
|
|
9
9
|
import { useLivenessCamera } from './useLivenessCamera';
|
|
10
10
|
import type { LivenessCameraProps, LivenessState } from './types';
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
const FACE_RATIO = 1.35; // height-to-width ratio of the oval (~3:4 face)
|
|
12
|
+
// Circle diameter = 82 % of container width — large enough to fit any face
|
|
13
|
+
// comfortably without the user needing to fiddle with distance.
|
|
14
|
+
const CIRCLE_DIAMETER_RATIO = 0.82;
|
|
16
15
|
const STROKE_WIDTH = 3;
|
|
17
|
-
// Cubic bezier approximation constant for a smooth
|
|
16
|
+
// Cubic bezier approximation constant for a smooth circle path
|
|
18
17
|
const K = 0.5523;
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
|
-
* Returns the stroke colour for the
|
|
20
|
+
* Returns the stroke colour for the circle guide.
|
|
22
21
|
*
|
|
23
22
|
* ● White – no face / scanning (score < 0.4)
|
|
24
23
|
* ● Yellow – face detected, confidence building (0.4 ≤ score < threshold)
|
|
25
24
|
* ● Green – liveness confirmed / countdown / capture
|
|
26
25
|
* ● Red – error
|
|
27
26
|
*/
|
|
28
|
-
function
|
|
27
|
+
function getCircleColor(state: LivenessState, score: number): string {
|
|
29
28
|
switch (state) {
|
|
30
29
|
case 'error':
|
|
31
30
|
return '#FF3B30';
|
|
@@ -40,22 +39,22 @@ function getOvalColor(state: LivenessState, score: number): string {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
|
-
* Returns an SVG path string tracing
|
|
44
|
-
* cubic bezier curves. Used inside a compound path with
|
|
45
|
-
* to punch a transparent hole through the dark scrim.
|
|
42
|
+
* Returns an SVG path string tracing a circle at (cx, cy) with radius r
|
|
43
|
+
* using cubic bezier curves. Used inside a compound path with
|
|
44
|
+
* fillRule="evenodd" to punch a transparent hole through the dark scrim.
|
|
46
45
|
*/
|
|
47
|
-
function
|
|
46
|
+
function circlePath(cx: number, cy: number, r: number): string {
|
|
48
47
|
return [
|
|
49
|
-
`M ${cx +
|
|
50
|
-
`C ${cx +
|
|
51
|
-
`C ${cx -
|
|
52
|
-
`C ${cx -
|
|
53
|
-
`C ${cx +
|
|
48
|
+
`M ${cx + r} ${cy}`,
|
|
49
|
+
`C ${cx + r} ${cy - r * K} ${cx + r * K} ${cy - r} ${cx} ${cy - r}`,
|
|
50
|
+
`C ${cx - r * K} ${cy - r} ${cx - r} ${cy - r * K} ${cx - r} ${cy}`,
|
|
51
|
+
`C ${cx - r} ${cy + r * K} ${cx - r * K} ${cy + r} ${cx} ${cy + r}`,
|
|
52
|
+
`C ${cx + r * K} ${cy + r} ${cx + r} ${cy + r * K} ${cx + r} ${cy}`,
|
|
54
53
|
'Z',
|
|
55
54
|
].join(' ');
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
function
|
|
57
|
+
function CircleOverlay({
|
|
59
58
|
width,
|
|
60
59
|
height,
|
|
61
60
|
state,
|
|
@@ -69,23 +68,22 @@ function OvalOverlay({
|
|
|
69
68
|
if (width === 0 || height === 0) return null;
|
|
70
69
|
|
|
71
70
|
const cx = width / 2;
|
|
72
|
-
//
|
|
73
|
-
const cy = height * 0.
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const color = getOvalColor(state, score);
|
|
71
|
+
// Centre the circle slightly above midpoint so the face sits naturally
|
|
72
|
+
const cy = height * 0.42;
|
|
73
|
+
const r = (width * CIRCLE_DIAMETER_RATIO) / 2;
|
|
74
|
+
const color = getCircleColor(state, score);
|
|
77
75
|
|
|
78
|
-
// Compound path:
|
|
79
|
-
|
|
76
|
+
// Compound path: full-screen rect + circle cutout.
|
|
77
|
+
// evenodd fill rule makes the circle area transparent.
|
|
78
|
+
const scrimD = `M0 0H${width}V${height}H0Z ${circlePath(cx, cy, r)}`;
|
|
80
79
|
|
|
81
80
|
return (
|
|
82
81
|
<Svg style={StyleSheet.absoluteFill} width={width} height={height}>
|
|
83
82
|
<Path d={scrimD} fill="rgba(0,0,0,0.55)" fillRule="evenodd" />
|
|
84
|
-
<
|
|
83
|
+
<Circle
|
|
85
84
|
cx={cx}
|
|
86
85
|
cy={cy}
|
|
87
|
-
|
|
88
|
-
ry={ry}
|
|
86
|
+
r={r}
|
|
89
87
|
fill="none"
|
|
90
88
|
stroke={color}
|
|
91
89
|
strokeWidth={STROKE_WIDTH}
|
|
@@ -147,7 +145,7 @@ export function LivenessCamera({
|
|
|
147
145
|
onError,
|
|
148
146
|
countdownFrom = 3,
|
|
149
147
|
livenessThreshold = 0.75,
|
|
150
|
-
confirmFrames =
|
|
148
|
+
confirmFrames = 10,
|
|
151
149
|
soundEnabled = true,
|
|
152
150
|
style,
|
|
153
151
|
}: LivenessCameraProps) {
|
|
@@ -210,8 +208,9 @@ export function LivenessCamera({
|
|
|
210
208
|
frameProcessor={frameProcessor}
|
|
211
209
|
photo
|
|
212
210
|
pixelFormat="yuv"
|
|
211
|
+
fps={60}
|
|
213
212
|
/>
|
|
214
|
-
<
|
|
213
|
+
<CircleOverlay
|
|
215
214
|
width={containerSize.width}
|
|
216
215
|
height={containerSize.height}
|
|
217
216
|
state={livenessState}
|
package/src/livenessScoring.ts
CHANGED
|
@@ -7,10 +7,14 @@ const WEIGHTS = {
|
|
|
7
7
|
eyesOpen: 0.3,
|
|
8
8
|
} as const;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
10
|
+
// Loosened from 0.20 / 0.65 — gives more room for the user to breathe
|
|
11
|
+
const FACE_SIZE_MIN = 0.15;
|
|
12
|
+
const FACE_SIZE_MAX = 0.8;
|
|
13
|
+
const FACE_SIZE_SOFT_MARGIN = 0.05; // gradient ramp at each edge
|
|
14
|
+
|
|
15
|
+
// Loosened from 20° — ML Kit head pose is noisy, 25° still rejects clear tilts
|
|
16
|
+
const MAX_YAW_DEG = 25;
|
|
17
|
+
const MAX_PITCH_DEG = 25;
|
|
14
18
|
|
|
15
19
|
export type FrameScore = {
|
|
16
20
|
total: number;
|
|
@@ -25,14 +29,39 @@ export function scoreFrame(face: FaceData, frameWidth: number): FrameScore {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
const faceWidthRatio = face.bounds.width / frameWidth;
|
|
28
|
-
const faceSize =
|
|
29
|
-
faceWidthRatio >= FACE_SIZE_MIN && faceWidthRatio <= FACE_SIZE_MAX
|
|
30
|
-
? 1.0
|
|
31
|
-
: 0.0;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Soft-edge face size: smooth ramp into / out of the valid range so a face
|
|
34
|
+
// sitting just outside the threshold doesn't score hard 0.
|
|
35
|
+
let faceSize: number;
|
|
36
|
+
if (faceWidthRatio >= FACE_SIZE_MIN && faceWidthRatio <= FACE_SIZE_MAX) {
|
|
37
|
+
faceSize = 1.0;
|
|
38
|
+
} else if (faceWidthRatio < FACE_SIZE_MIN) {
|
|
39
|
+
faceSize = Math.max(
|
|
40
|
+
0,
|
|
41
|
+
(faceWidthRatio - (FACE_SIZE_MIN - FACE_SIZE_SOFT_MARGIN)) /
|
|
42
|
+
FACE_SIZE_SOFT_MARGIN
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
faceSize = Math.max(
|
|
46
|
+
0,
|
|
47
|
+
(FACE_SIZE_MAX + FACE_SIZE_SOFT_MARGIN - faceWidthRatio) /
|
|
48
|
+
FACE_SIZE_SOFT_MARGIN
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Soft-edge head pose: full score inside threshold, linear decay outside
|
|
53
|
+
const yawScore =
|
|
54
|
+
Math.abs(face.yawAngle) <= MAX_YAW_DEG
|
|
55
|
+
? 1.0
|
|
56
|
+
: Math.max(0, 1 - (Math.abs(face.yawAngle) - MAX_YAW_DEG) / MAX_YAW_DEG);
|
|
57
|
+
const pitchScore =
|
|
58
|
+
Math.abs(face.pitchAngle) <= MAX_PITCH_DEG
|
|
59
|
+
? 1.0
|
|
60
|
+
: Math.max(
|
|
61
|
+
0,
|
|
62
|
+
1 - (Math.abs(face.pitchAngle) - MAX_PITCH_DEG) / MAX_PITCH_DEG
|
|
63
|
+
);
|
|
64
|
+
const headPose = (yawScore + pitchScore) / 2;
|
|
36
65
|
|
|
37
66
|
// ML Kit returns -1 when classification is disabled or unavailable
|
|
38
67
|
const leftEye =
|
|
@@ -61,7 +90,7 @@ export function getFeedback(
|
|
|
61
90
|
livenessConfirmed: boolean
|
|
62
91
|
): FeedbackMessage {
|
|
63
92
|
if (livenessConfirmed) return 'Liveness confirmed';
|
|
64
|
-
if (!face.detected) return 'Position your face in the
|
|
93
|
+
if (!face.detected) return 'Position your face in the circle';
|
|
65
94
|
|
|
66
95
|
const faceWidthRatio = face.bounds.width / frameWidth;
|
|
67
96
|
if (faceWidthRatio < FACE_SIZE_MIN) return 'Move closer';
|
package/src/types.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type LivenessState =
|
|
|
27
27
|
| 'error';
|
|
28
28
|
|
|
29
29
|
export type FeedbackMessage =
|
|
30
|
-
| 'Position your face in the
|
|
30
|
+
| 'Position your face in the circle'
|
|
31
31
|
| 'Move closer'
|
|
32
32
|
| 'Move farther away'
|
|
33
33
|
| 'Look straight ahead'
|
|
@@ -71,7 +71,7 @@ export type LivenessCameraProps = {
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Number of consecutive high-score frames required before liveness is
|
|
74
|
-
* confirmed. Defaults to
|
|
74
|
+
* confirmed. Defaults to 10 (~500ms at 20fps).
|
|
75
75
|
*/
|
|
76
76
|
confirmFrames?: number;
|
|
77
77
|
|
package/src/useLivenessCamera.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { Camera, Frame } from 'react-native-vision-camera';
|
|
3
|
-
import { useFrameProcessor } from 'react-native-vision-camera';
|
|
3
|
+
import { runAtTargetFps, useFrameProcessor } from 'react-native-vision-camera';
|
|
4
4
|
import { Worklets } from 'react-native-worklets-core';
|
|
5
5
|
import { useLivenessPlugin } from './LivenessDetector';
|
|
6
6
|
import { getFeedback, rollingAverage, scoreFrame } from './livenessScoring';
|
|
@@ -12,6 +12,9 @@ import type {
|
|
|
12
12
|
} from './types';
|
|
13
13
|
|
|
14
14
|
const WINDOW_SIZE = 20;
|
|
15
|
+
// How many frames to decay consecutiveGood by on a bad frame.
|
|
16
|
+
// Decay (not hard reset) means one noisy frame won't erase all progress.
|
|
17
|
+
const CONSECUTIVE_DECAY = 2;
|
|
15
18
|
|
|
16
19
|
type Options = {
|
|
17
20
|
livenessThreshold: number;
|
|
@@ -52,11 +55,20 @@ export function useLivenessCamera(options: Options) {
|
|
|
52
55
|
const stateRef = useRef<LivenessState>('scanning');
|
|
53
56
|
const isCaptured = useRef(false);
|
|
54
57
|
|
|
58
|
+
// Feedback debouncing: only update the displayed text after the same message
|
|
59
|
+
// has been stable for FEEDBACK_DEBOUNCE_MS. Prevents rapid flickering when
|
|
60
|
+
// ML Kit oscillates between two states on consecutive frames.
|
|
61
|
+
const FEEDBACK_DEBOUNCE_MS = 400;
|
|
62
|
+
const feedbackTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
63
|
+
const pendingFeedback = useRef<FeedbackMessage>(
|
|
64
|
+
'Position your face in the circle'
|
|
65
|
+
);
|
|
66
|
+
|
|
55
67
|
const [state, setState] = useState<LivenessCameraState>({
|
|
56
68
|
livenessState: 'scanning',
|
|
57
69
|
livenessScore: 0,
|
|
58
70
|
countdown: null,
|
|
59
|
-
feedback: 'Position your face in the
|
|
71
|
+
feedback: 'Position your face in the circle',
|
|
60
72
|
});
|
|
61
73
|
|
|
62
74
|
const setLivenessState = useCallback((next: LivenessState) => {
|
|
@@ -143,19 +155,36 @@ export function useLivenessCamera(options: Options) {
|
|
|
143
155
|
|
|
144
156
|
const avgScore = rollingAverage(frameScores.current);
|
|
145
157
|
|
|
158
|
+
// Decay on bad frames instead of hard reset — one noisy ML Kit result
|
|
159
|
+
// won't wipe out progress the user has already built up.
|
|
146
160
|
if (total >= livenessThreshold) {
|
|
147
161
|
consecutiveGood.current += 1;
|
|
148
162
|
} else {
|
|
149
|
-
consecutiveGood.current =
|
|
163
|
+
consecutiveGood.current = Math.max(
|
|
164
|
+
0,
|
|
165
|
+
consecutiveGood.current - CONSECUTIVE_DECAY
|
|
166
|
+
);
|
|
150
167
|
}
|
|
151
168
|
|
|
152
169
|
const isLive =
|
|
153
170
|
consecutiveGood.current >= confirmFrames &&
|
|
154
171
|
avgScore >= livenessThreshold;
|
|
155
172
|
|
|
156
|
-
|
|
173
|
+
// Debounce the feedback text: only apply a new message after it has been
|
|
174
|
+
// stable for FEEDBACK_DEBOUNCE_MS, preventing rapid label flickering.
|
|
175
|
+
const newFeedback = getFeedback(safeFace, width, isLive);
|
|
176
|
+
if (newFeedback !== pendingFeedback.current) {
|
|
177
|
+
pendingFeedback.current = newFeedback;
|
|
178
|
+
if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
|
|
179
|
+
feedbackTimer.current = setTimeout(() => {
|
|
180
|
+
setState((prev) => ({
|
|
181
|
+
...prev,
|
|
182
|
+
feedback: pendingFeedback.current,
|
|
183
|
+
}));
|
|
184
|
+
}, FEEDBACK_DEBOUNCE_MS);
|
|
185
|
+
}
|
|
157
186
|
|
|
158
|
-
setState((prev) => ({ ...prev, livenessScore: avgScore
|
|
187
|
+
setState((prev) => ({ ...prev, livenessScore: avgScore }));
|
|
159
188
|
|
|
160
189
|
if (isLive && stateRef.current === 'scanning') {
|
|
161
190
|
setLivenessState('confirmed');
|
|
@@ -182,8 +211,13 @@ export function useLivenessCamera(options: Options) {
|
|
|
182
211
|
const frameProcessor = useFrameProcessor(
|
|
183
212
|
(frame: Frame) => {
|
|
184
213
|
'worklet';
|
|
185
|
-
|
|
186
|
-
|
|
214
|
+
// Camera preview renders at full fps (60). ML Kit only needs ~20fps —
|
|
215
|
+
// running it on every frame would block the render thread unnecessarily.
|
|
216
|
+
runAtTargetFps(20, () => {
|
|
217
|
+
'worklet';
|
|
218
|
+
const face = plugin.detectLiveness(frame);
|
|
219
|
+
handleFaceDataJS(face, frame.width);
|
|
220
|
+
});
|
|
187
221
|
},
|
|
188
222
|
[plugin, handleFaceDataJS]
|
|
189
223
|
);
|
|
@@ -193,6 +227,7 @@ export function useLivenessCamera(options: Options) {
|
|
|
193
227
|
frameScores.current = [];
|
|
194
228
|
consecutiveGood.current = 0;
|
|
195
229
|
isCaptured.current = false;
|
|
230
|
+
if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
|
|
196
231
|
};
|
|
197
232
|
}, []);
|
|
198
233
|
|