UNPKG

react-native-biometric-verifier

Version:

A React Native module for biometric verification with face recognition and QR code scanning

617 lines (543 loc) 20.6 kB
import { useCallback, useMemo, useEffect, useRef } from 'react'; import { Worklets } from 'react-native-worklets-core'; import { useFrameProcessor } from 'react-native-vision-camera'; import { useFaceDetector } from 'react-native-vision-camera-face-detector'; import { faceAntiSpoofFrameProcessor, initializeFaceAntiSpoof, isFaceAntiSpoofAvailable, } from 'react-native-vision-camera-spoof-detector'; // Optimized constants - tuned for performance const FACE_STABILITY_THRESHOLD = 3; const FACE_MOVEMENT_THRESHOLD = 15; const FRAME_PROCESSOR_MIN_INTERVAL_MS = 500; const MIN_FACE_SIZE = 0.2; // Blink detection const BLINK_THRESHOLD = 0.3; const REQUIRED_BLINKS = 3; // Anti-spoofing const ANTI_SPOOF_CONFIDENCE_THRESHOLD = 0.7; const REQUIRED_CONSECUTIVE_LIVE_FRAMES = 3; // Face centering const FACE_CENTER_THRESHOLD_X = 0.2; const FACE_CENTER_THRESHOLD_Y = 0.15; const MIN_FACE_CENTERED_FRAMES = 2; // Performance optimization constants const MAX_FRAME_PROCESSING_TIME_MS = 500; const BATCH_UPDATE_THRESHOLD = 3; export const useFaceDetectionFrameProcessor = ({ onStableFaceDetected = () => { }, onFacesUpdate = () => { }, onLivenessUpdate = () => { }, onAntiSpoofUpdate = () => { }, showCodeScanner = false, isLoading = false, isActive = true, livenessLevel = 0, }) => { const { detectFaces } = useFaceDetector({ performanceMode: 'fast', landmarkMode: 'none', contourMode: 'none', classificationMode: livenessLevel === 1 ? 'all' : 'none', minFaceSize: MIN_FACE_SIZE, }); const isMounted = useRef(true); const antiSpoofInitialized = useRef(false); const frameProcessingStartTime = useRef(0); // Initialize anti-spoofing with memoization const initializeAntiSpoof = useCallback(async () => { if (antiSpoofInitialized.current) return true; try { const available = isFaceAntiSpoofAvailable?.(); if (!available) return false; const res = await initializeFaceAntiSpoof(); antiSpoofInitialized.current = true; return true; } catch (err) { console.error('[useFaceDetection] Error initializing anti-spoof:', err); return false; } }, []); useEffect(() => { if (!antiSpoofInitialized.current) { initializeAntiSpoof(); } }, [initializeAntiSpoof]); // Pre-computed shared state with optimized structure const sharedState = useMemo( () => Worklets.createSharedValue({ // Core timing lastProcessedTime: 0, // Face tracking - packed for memory efficiency faceTracking: { lastX: 0, lastY: 0, lastW: 0, lastH: 0, stableCount: 0 }, // State flags - packed together flags: { captured: false, showCodeScanner: showCodeScanner, isActive: isActive, hasSingleFace: false, isFaceCentered: false, eyeClosed: false, }, // Liveness state liveness: { level: livenessLevel, step: 0, blinkCount: 0, }, // Anti-spoof state antiSpoof: { consecutiveLiveFrames: 0, lastResult: null, isLive: false, confidence: 0, }, // Face centering centering: { centeredFrames: 0, frameWidth: 0, frameHeight: 0, }, // Performance tracking performance: { batchCounter: 0, lastBatchUpdate: 0, } }), [] ); // Batched state updates useEffect(() => { if (!isMounted.current) return; const state = sharedState.value; state.flags.showCodeScanner = !!showCodeScanner; state.flags.isActive = !!isActive; state.liveness.level = livenessLevel; if (isActive && state.flags.captured) { // Batch reset all states state.faceTracking.stableCount = 0; state.liveness.step = 0; state.liveness.blinkCount = 0; state.flags.eyeClosed = false; state.flags.captured = false; state.antiSpoof.consecutiveLiveFrames = 0; state.antiSpoof.lastResult = null; state.antiSpoof.isLive = false; state.antiSpoof.confidence = 0; state.flags.hasSingleFace = false; state.centering.centeredFrames = 0; state.flags.isFaceCentered = false; } }, [showCodeScanner, isActive, livenessLevel, sharedState]); // Optimized JS callbacks with batching const callbacksRef = useRef({ lastFacesEventTime: 0, lastLivenessEventTime: 0, lastAntiSpoofEventTime: 0, pendingFacesUpdate: null, pendingLivenessUpdate: null, pendingAntiSpoofUpdate: null, }); const FACES_EVENT_INTERVAL_MS = 800; const LIVENESS_EVENT_INTERVAL_MS = 700; const ANTI_SPOOF_EVENT_INTERVAL_MS = 500; // Memoized callbacks with batching const runOnStable = useMemo( () => Worklets.createRunOnJS((faceRect, antiSpoofResult) => { onStableFaceDetected?.(faceRect, antiSpoofResult); }), [onStableFaceDetected] ); const runOnFaces = useMemo( () => Worklets.createRunOnJS((count, progress, step, isCentered, antiSpoofState) => { const now = Date.now(); const callbacks = callbacksRef.current; if (now - callbacks.lastFacesEventTime > FACES_EVENT_INTERVAL_MS) { callbacks.lastFacesEventTime = now; onFacesUpdate?.({ count, progress, step, isCentered, antiSpoofState }); } }), [onFacesUpdate] ); const runOnLiveness = useMemo( () => Worklets.createRunOnJS((step, extra) => { const now = Date.now(); const callbacks = callbacksRef.current; if (now - callbacks.lastLivenessEventTime > LIVENESS_EVENT_INTERVAL_MS) { callbacks.lastLivenessEventTime = now; onLivenessUpdate?.(step, extra); } }), [onLivenessUpdate] ); const runOnAntiSpoof = useMemo( () => Worklets.createRunOnJS((result) => { const now = Date.now(); const callbacks = callbacksRef.current; if (now - callbacks.lastAntiSpoofEventTime > ANTI_SPOOF_EVENT_INTERVAL_MS) { callbacks.lastAntiSpoofEventTime = now; onAntiSpoofUpdate?.(result); } }), [onAntiSpoofUpdate] ); // Optimized face centering check - inlined for performance const isFaceCenteredInFrame = Worklets.createRunOnJS((faceBounds, frameWidth, frameHeight) => { 'worklet'; if (!faceBounds || frameWidth === 0 || frameHeight === 0) return false; const faceCenterX = faceBounds.x + faceBounds.width / 2; const faceCenterY = faceBounds.y + faceBounds.height / 2; const frameCenterX = frameWidth / 2; const frameCenterY = frameHeight / 2; return ( Math.abs(faceCenterX - frameCenterX) <= frameWidth * FACE_CENTER_THRESHOLD_X && Math.abs(faceCenterY - frameCenterY) <= frameHeight * FACE_CENTER_THRESHOLD_Y ); }); // Fast early exit conditions check const shouldProcessFrame = Worklets.createRunOnJS((state, now, isLoading) => { 'worklet'; return !( state.flags.showCodeScanner || state.flags.captured || isLoading || !state.flags.isActive || (now - state.lastProcessedTime < FRAME_PROCESSOR_MIN_INTERVAL_MS) ); }); // Optimized frame processor const frameProcessor = useFrameProcessor( (frame) => { 'worklet'; // Performance monitoring const processingStart = Date.now(); const state = sharedState.value; const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now(); // Fast early exit if (!shouldProcessFrame(state, now, isLoading)) { frame.release?.(); return; } // Performance guard - don't process if taking too long if (processingStart - frameProcessingStartTime.current < MAX_FRAME_PROCESSING_TIME_MS) { frame.release?.(); return; } frameProcessingStartTime.current = processingStart; let detected = null; let antiSpoofResult = null; try { // Initialize frame dimensions once if (state.centering.frameWidth === 0) { state.centering.frameWidth = frame.width; state.centering.frameHeight = frame.height; } // Detect faces detected = detectFaces?.(frame); // Fast path for no faces if (!detected || detected.length === 0) { state.faceTracking.stableCount = 0; state.antiSpoof.consecutiveLiveFrames = 0; state.flags.hasSingleFace = false; state.centering.centeredFrames = 0; state.flags.isFaceCentered = false; state.lastProcessedTime = now; runOnFaces(0, 0, state.liveness.step, false, { isLive: false, confidence: 0, consecutiveLiveFrames: 0, isFaceCentered: false, hasSingleFace: false, }); return; } // Process single face scenario if (detected.length === 1 && !state.flags.captured) { const face = detected[0]; if (!face?.bounds) { runOnFaces(0, 0, state.liveness.step, false, { isLive: false, confidence: 0, consecutiveLiveFrames: 0, isFaceCentered: false, hasSingleFace: false, }); return; } const bounds = face.bounds; const x = Math.max(0, bounds.x); const y = Math.max(0, bounds.y); const width = Math.max(0, bounds.width); const height = Math.max(0, bounds.height); // Local state snapshot for performance const localState = { livenessLevel: state.liveness.level, isLive: state.antiSpoof.isLive, consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames, isFaceCentered: state.flags.isFaceCentered, antiSpoofConfidence: state.antiSpoof.confidence, livenessStep: state.liveness.step, blinkCount: state.liveness.blinkCount, eyeClosed: state.flags.eyeClosed, }; // Update single face state state.flags.hasSingleFace = true; // Face centering check const centered = isFaceCenteredInFrame( bounds, state.centering.frameWidth, state.centering.frameHeight ); if (centered) { state.centering.centeredFrames = Math.min( MIN_FACE_CENTERED_FRAMES, state.centering.centeredFrames + 1 ); } else { state.centering.centeredFrames = 0; } state.flags.isFaceCentered = state.centering.centeredFrames >= MIN_FACE_CENTERED_FRAMES; // Anti-spoof detection only when face is centered and single if (state.flags.isFaceCentered) { try { antiSpoofResult = faceAntiSpoofFrameProcessor?.(frame); if (antiSpoofResult != null) { state.antiSpoof.lastResult = antiSpoofResult; const isLive = antiSpoofResult.isLive === true; const confidence = antiSpoofResult.combinedScore || antiSpoofResult.neuralNetworkScore || 0; if (isLive && confidence > ANTI_SPOOF_CONFIDENCE_THRESHOLD) { state.antiSpoof.consecutiveLiveFrames = Math.min( REQUIRED_CONSECUTIVE_LIVE_FRAMES, state.antiSpoof.consecutiveLiveFrames + 1 ); } else { state.antiSpoof.consecutiveLiveFrames = Math.max(0, state.antiSpoof.consecutiveLiveFrames - 1); } state.antiSpoof.isLive = state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES; state.antiSpoof.confidence = confidence; // Batch anti-spoof updates if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) { runOnAntiSpoof({ isLive: state.antiSpoof.isLive, confidence: state.antiSpoof.confidence, rawResult: antiSpoofResult, consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames, isFaceCentered: state.flags.isFaceCentered, }); } } } catch (antiSpoofError) { // Silent error handling } } else { // Reset anti-spoof if face not centered state.antiSpoof.consecutiveLiveFrames = 0; state.antiSpoof.isLive = false; } // Liveness logic - optimized let newLivenessStep = localState.livenessStep; let newBlinkCount = localState.blinkCount; let newEyeClosed = localState.eyeClosed; if (localState.livenessLevel === 1) { if (newLivenessStep === 0) { newLivenessStep = 1; runOnLiveness(newLivenessStep); } else if (newLivenessStep === 1) { const leftEye = face.leftEyeOpenProbability ?? 1; const rightEye = face.rightEyeOpenProbability ?? 1; const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD; if (eyesClosed && !newEyeClosed) { newBlinkCount++; newEyeClosed = true; runOnLiveness(newLivenessStep, { blinkCount: newBlinkCount }); } else if (!eyesClosed && newEyeClosed) { newEyeClosed = false; } if (newBlinkCount >= REQUIRED_BLINKS) { newLivenessStep = 2; runOnLiveness(newLivenessStep); } } } // Face stability check - optimized let newStableCount = state.faceTracking.stableCount; if (state.faceTracking.lastX === 0 && state.faceTracking.lastY === 0) { newStableCount = 1; } else { const dx = Math.abs(x - state.faceTracking.lastX); const dy = Math.abs(y - state.faceTracking.lastY); newStableCount = (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) ? state.faceTracking.stableCount + 1 : 1; } // Batch state updates state.lastProcessedTime = now; state.faceTracking.lastX = x; state.faceTracking.lastY = y; state.faceTracking.lastW = width; state.faceTracking.lastH = height; state.faceTracking.stableCount = newStableCount; state.liveness.step = newLivenessStep; state.liveness.blinkCount = newBlinkCount; state.flags.eyeClosed = newEyeClosed; state.performance.batchCounter++; const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100); // Batch face updates if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) { runOnFaces(1, progress, newLivenessStep, state.flags.isFaceCentered, { isLive: state.antiSpoof.isLive, confidence: state.antiSpoof.confidence, consecutiveLiveFrames: state.antiSpoof.consecutiveLiveFrames, isFaceCentered: state.flags.isFaceCentered, hasSingleFace: true, }); } // Capture condition - optimized const shouldCapture = !state.flags.captured && ( newStableCount >= FACE_STABILITY_THRESHOLD && state.antiSpoof.isLive && state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES && state.flags.isFaceCentered && (localState.livenessLevel === 0 || ( localState.livenessLevel === 1 && newLivenessStep === 2 && newBlinkCount >= REQUIRED_BLINKS )) ); if (shouldCapture) { state.flags.captured = true; runOnStable( { x, y, width, height }, state.antiSpoof.lastResult ); } } else { // Multiple faces - reset states state.faceTracking.stableCount = 0; state.lastProcessedTime = now; state.antiSpoof.consecutiveLiveFrames = 0; state.flags.hasSingleFace = false; state.centering.centeredFrames = 0; state.flags.isFaceCentered = false; runOnFaces(detected.length, 0, state.liveness.step, false, { isLive: false, confidence: 0, consecutiveLiveFrames: 0, isFaceCentered: false, hasSingleFace: false, }); } } catch (err) { // Error boundary - ensure frame is released } finally { frame.release?.(); } }, [detectFaces, isLoading] ); // Optimized reset functions const resetCaptureState = useCallback(() => { const state = sharedState.value; state.lastProcessedTime = 0; state.faceTracking.lastX = 0; state.faceTracking.lastY = 0; state.faceTracking.lastW = 0; state.faceTracking.lastH = 0; state.faceTracking.stableCount = 0; state.flags.captured = false; state.liveness.step = 0; state.liveness.blinkCount = 0; state.flags.eyeClosed = false; state.antiSpoof.consecutiveLiveFrames = 0; state.antiSpoof.lastResult = null; state.antiSpoof.isLive = false; state.antiSpoof.confidence = 0; state.flags.hasSingleFace = false; state.centering.centeredFrames = 0; state.flags.isFaceCentered = false; state.centering.frameWidth = 0; state.centering.frameHeight = 0; state.performance.batchCounter = 0; }, [sharedState]); const forceResetCaptureState = useCallback(() => { const current = sharedState.value; sharedState.value = { lastProcessedTime: 0, faceTracking: { lastX: 0, lastY: 0, lastW: 0, lastH: 0, stableCount: 0 }, flags: { captured: false, showCodeScanner: current.flags.showCodeScanner, isActive: current.flags.isActive, hasSingleFace: false, isFaceCentered: false, eyeClosed: false, }, liveness: { level: current.liveness.level, step: 0, blinkCount: 0, }, antiSpoof: { consecutiveLiveFrames: 0, lastResult: null, isLive: false, confidence: 0, }, centering: { centeredFrames: 0, frameWidth: 0, frameHeight: 0, }, performance: { batchCounter: 0, lastBatchUpdate: 0, } }; }, [sharedState]); const updateShowCodeScanner = useCallback( (value) => { sharedState.value.flags.showCodeScanner = !!value; }, [sharedState] ); const updateIsActive = useCallback( (active) => { sharedState.value.flags.isActive = !!active; if (!active) sharedState.value.flags.captured = false; }, [sharedState] ); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; forceResetCaptureState(); }; }, [forceResetCaptureState]); return { frameProcessor, resetCaptureState, forceResetCaptureState, updateShowCodeScanner, updateIsActive, initializeAntiSpoof, capturedSV: { value: sharedState.value.flags.captured }, antiSpoofState: { isLive: sharedState.value.antiSpoof.isLive, confidence: sharedState.value.antiSpoof.confidence, consecutiveLiveFrames: sharedState.value.antiSpoof.consecutiveLiveFrames, lastResult: sharedState.value.antiSpoof.lastResult, hasSingleFace: sharedState.value.flags.hasSingleFace, isFaceCentered: sharedState.value.flags.isFaceCentered, }, }; };