React Hooks¶
The @moveris/react package provides hooks for building custom liveness verification UIs. Use these when you need more control than the pre-built components offer.
In plain terms
Hooks let you build your own UI while the SDK handles the logic: useLiveness for the default capture flow, useCamera for camera access, useFrameCapture for capturing frames, useFaceDetection for face detection, useSmartFrameCapture for quality-gated capture, useDetectionPipeline for gaze + eye-region gating, and useModels for dynamic model selection.
Optional parameters
Parameters followed by ? (for example, options?) are optional. You can omit them if you don't need to customize the call. For instance, useLiveness() works without passing options.
Import¶
import {
useLiveness,
useLivenessV2,
useCamera,
useFrameCapture,
useFaceDetection,
useSmartFrameCapture,
useDetectionPipeline,
useModels,
} from '@moveris/react';
useLiveness¶
The main hook that manages the full liveness verification flow: camera access, frame capture, API submission, and result handling.
const {
state,
result,
error,
errorCode,
errorMessage,
progress,
start,
stop,
reset,
restartCapture,
} = useLiveness(options?);
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model | FastCheckModel | DEFAULT_MODEL (mixed-30-v3_1_1) | Model to use |
source | FrameSource | 'live' | Frame source |
sessionId | string | auto-generated | Optional. Session ID for API calls. When provided, the same ID is used for every request in the session. |
endpoint | 'fast-check-crops' \| 'fast-check-stream' \| 'fast-check' \| 'live-check' | 'fast-check-crops' | API endpoint. Use live-check for Mixed V3.1.1 with client landmarks; fast-check-stream for streaming; fast-check for batch with full frames. |
sequentialStreaming | boolean | false | When endpoint is fast-check-stream, send one frame at a time instead of in parallel |
frameCount | number | from model | Frames to capture (30, 60, or 120) |
onResult | (result: LivenessResult) => void | -- | Result callback |
onError | (error: Error) => void | -- | Error callback |
onProgress | (received: number, total: number) => void | -- | Progress callback |
Return Value¶
| Property | Type | Description |
|---|---|---|
state | 'idle' \| 'capturing' \| 'uploading' \| 'processing' \| 'complete' \| 'error' | Current state of the verification flow |
result | LivenessResult \| null | Verification result when complete |
error | Error \| null | Error if the flow failed |
errorCode | string \| null | API error code when the error is a LivenessApiError |
errorMessage | string \| null | User-facing error message |
progress | { current: number; total: number } | Frames captured so far and frames required |
start | () => void | Start the verification flow |
stop | () => void | Stop capturing |
reset | () => void | Reset to idle state |
restartCapture | () => void | Drop the current sequence and start a new one (use with onSequenceReset) |
Example¶
function CustomLivenessUI() {
const {
state,
result,
error,
progress,
start,
reset,
} = useLiveness({
endpoint: 'fast-check-stream',
sequentialStreaming: true,
onResult: (r) => console.log('Done:', r.verdict),
});
return (
<div>
{state === 'idle' && <button onClick={start}>Begin</button>}
{state === 'capturing' && <p>{progress.current}/{progress.total}</p>}
{state === 'processing' && <p>Analyzing...</p>}
{result && <p>Verdict: {result.verdict}</p>}
{error && <p>Error: {error.message}</p>}
{(result || error) && <button onClick={reset}>Try Again</button>}
</div>
);
}
useLivenessV2¶
Self-contained hook for the upload-first HTTP pipeline (POST /api/v2/upload, then /api/v2/fast-check or /api/v2/live-check). The "V2" in the name is that API path, not Mixed V2 models. It still runs Mixed V3.1.1 (mixed-30-v3_1_1 by default). Completely separate from useLiveness.
const {
state,
result,
error,
progress,
start,
stop,
reset,
captureFrame,
restartCapture,
} = useLivenessV2(options?);
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
model | FastCheckModel | from context | Model alias (mixed-30-v3_1_1 default) |
endpoint | 'fast-check' \| 'live-check' | 'fast-check' | Final check endpoint. live-check requires landmarks on uploaded frames |
uploadChunkSize | number | 5 | Frames per upload request |
source | FrameSource | 'live' | Frame source |
fps | number | 30 | Capture FPS |
sessionId | string | auto-generated | Session ID shared across upload + check |
onResult | (result: LivenessResult) => void | — | Result callback |
onError | (error: Error) => void | — | Error callback |
onProgress | (received: number, total: number) => void | — | Upload/capture progress |
Landmarks for live-check
Pass the full CapturedFrame (including landmarks) to captureFrame() when endpoint is live-check. Pair with useSmartFrameCapture and set requireLandmarks: true so capture does not start before the FaceLandmarker is ready.
useCamera¶
Manages camera permission and the MediaStream. Attach the stream to your own <video> element, or use LivenessCamera.
const {
stream,
error,
permissionStatus,
capabilities,
validation,
checkPermission,
start,
stop,
applyConstraints,
isActive,
} = useCamera(options?);
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
onCapabilities | (capabilities, validation) => void | -- | Fires after the stream starts and capabilities are read |
Return Value¶
| Property | Type | Description |
|---|---|---|
stream | MediaStream \| null | Active media stream |
error | Error \| null | Camera initialization error |
permissionStatus | 'undetermined' \| 'granted' \| 'denied' \| 'unavailable' | Camera permission |
capabilities | CameraCapabilities \| null | Camera track capabilities when available (browser-dependent) |
validation | CameraValidationResult \| null | Validation result against default requirements |
checkPermission | () => Promise<CameraPermissionStatus> | Read permission without prompting |
start | () => Promise<void> | Request permission and start the stream |
stop | () => void | Stop and release the camera |
applyConstraints | (constraints: MediaTrackConstraints) => Promise<void> | Apply runtime track constraints when supported |
isActive | boolean | Whether the camera is currently running |
Example¶
function CameraPreview() {
const videoRef = useRef<HTMLVideoElement>(null);
const { stream, error, permissionStatus, start, isActive } = useCamera();
useEffect(() => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
}, [stream]);
if (permissionStatus !== 'granted' && !isActive) {
return <button onClick={start}>Enable Camera</button>;
}
if (error) return <p>Camera error: {error.message}</p>;
return (
<video
ref={videoRef}
autoPlay
playsInline
muted
style={{ width: '100%' }}
/>
);
}
Capability support
capabilities depends on browser support for MediaStreamTrack.getCapabilities(). Some browsers (for example, older Firefox versions) may return limited or no capability data.
useFrameCapture¶
Captures frames from a video element on an interval, or on demand.
const { captureFrame } = useFrameCapture({
videoRef,
onFrame: (frame) => {},
interval: 33,
enabled: true,
});
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
videoRef | RefObject<HTMLVideoElement \| null> | required | Video element to capture from |
onFrame | (frame: CapturedFrame) => void | required | Called for each captured frame |
interval | number | 33 | Capture interval in milliseconds (~30 FPS) |
enabled | boolean | true | Whether the interval is running |
mirror | boolean | false | Mirror captured frames. Leave false for API payloads |
Return Value¶
| Property | Type | Description |
|---|---|---|
captureFrame | () => CapturedFrame \| null | Capture one frame now. Returns null if the video is not ready |
Example¶
function ManualCapture() {
const videoRef = useRef<HTMLVideoElement>(null);
const [count, setCount] = useState(0);
const { stream, permissionStatus, start, isActive } = useCamera();
const { captureFrame } = useFrameCapture({
videoRef,
onFrame: () => setCount((n) => n + 1),
enabled: false,
});
useEffect(() => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
}, [stream]);
if (permissionStatus !== 'granted' && !isActive) {
return <button onClick={start}>Enable Camera</button>;
}
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<button onClick={() => captureFrame()}>Capture ({count}/30)</button>
</div>
);
}
useFaceDetection¶
Loads a face detector and exposes detect(video) for on-demand detection. Supports MediaPipe (web) and custom adapters.
const {
isReady,
isInitializing,
error,
initialize,
detect,
lastResult,
} = useFaceDetection(options?);
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
adapter | FaceDetectorAdapter | built-in MediaPipe | Optional custom detector |
autoInitialize | boolean | true | Initialize on mount |
Adapters¶
MediaPipe (Web)¶
import { createMediaPipeAdapter } from '@moveris/react';
const adapter = createMediaPipeAdapter({
modelPath: 'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite',
delegate: 'GPU',
});
Return Value¶
| Property | Type | Description |
|---|---|---|
isReady | boolean | Whether the detector is ready |
isInitializing | boolean | Whether the detector is still loading |
error | Error \| null | Initialization error |
initialize | () => Promise<void> | Initialize manually when autoInitialize is false |
detect | (video: HTMLVideoElement) => Promise<FaceDetectionResult> | Run detection on a video frame |
lastResult | FaceDetectionResult \| null | Last detection result |
Example¶
function FaceDetectionPreview() {
const videoRef = useRef<HTMLVideoElement>(null);
const { stream, permissionStatus, start, isActive } = useCamera();
const { isReady, detect, lastResult } = useFaceDetection();
useEffect(() => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
}, [stream]);
const handleDetect = async () => {
if (!videoRef.current) return;
await detect(videoRef.current);
};
if (permissionStatus !== 'granted' && !isActive) {
return <button onClick={start}>Enable Camera</button>;
}
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<button onClick={handleDetect} disabled={!isReady}>Detect</button>
{lastResult?.detected && <p>Face detected!</p>}
</div>
);
}
useSmartFrameCapture¶
Intelligent frame capture that only counts frames when quality and alignment checks pass. Rejects faces below 4% of frame area. Gates capture based on external detectors and supports restart() when a blocking condition occurs (e.g. hidden/shadowed eyes).
const {
state, // 'idle' | 'capturing' | 'complete' | 'error'
progress, // { captured, target, highQuality, goodQuality }
feedback, // string - User guidance message
ovalState, // 'no_face' | 'poor' | 'good' | 'perfect'
frames, // CapturedFrame[]
start, // () => void
stop, // () => void
reset, // () => void
restart, // () => void - reset all state and immediately resume capturing
} = useSmartFrameCapture({
videoRef,
targetFrames: 30,
captureMode: 'full',
onFrameCapture: (frame, index, total) => console.log('Captured:', index),
onComplete: (frames) => console.log('All frames:', frames),
onError: (error) => console.error(error),
onSequenceReset: (reason) => liveness.restartCapture(),
});
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
videoRef | RefObject<HTMLVideoElement \| null> | required | Reference to the live video element |
targetFrames | number | 10 | Pass 30, 60, or 120 to match the model. The hook default is 10, which the API rejects. |
captureIntervalMs | number | 0 | Optional tick cap in ms. Omit to run as fast as the browser allows. |
minCaptureDelay | number | 0 | Extra minimum gap between accepted frames. Leave 0 unless you need a hard spacing floor. |
blurThreshold | number | auto | Floor for adaptive blur threshold (defaults: 100 desktop, 150 mobile). The hook may raise the effective threshold from a rolling window; it will not drop below MIN_BLUR_FLOOR (20). From 3.22+, the adaptive curve is platform-aware (iOS / Android / HarmonyOS / desktop). |
brightnessThreshold | number | 60 (center) / 50 (face) | Floor for adaptive brightness threshold. The hook calibrates to recent luminance samples; it will not drop below MIN_BRIGHTNESS_FLOOR (30). |
captureMode | 'crop' \| 'full' | 'crop' | Capture mode (crop for fast-check-crops, full for fast-check / fast-check-stream) |
detectionGate | () => boolean | - | External gate: only capture when it returns true |
detectFace | (video) => Promise<FaceBoundingBox \| null> | built-in | Optional custom face detection function |
requireLandmarks | boolean | false | When true, isDetectorReady also waits for the FaceLandmarker before capture. Set for live-check so short sessions do not finish without landmarks. Leave false for fast-check / fast-check-crops. |
onFrameCapture | (frame, index, total) => void | - | Called for each captured frame |
onComplete | (frames, sdkQuality) => void | - | Called when all frames are captured |
onError | (error) => void | - | Error callback |
onQualityUpdate | (quality) => void | - | Called on each quality update (for UI feedback) |
onSequenceReset | (reason) => void | - | Fired when a partial sequence is discarded (blur, quality gate, detection gate). Wire this to restartCapture() on useLiveness / useLivenessV2. |
Adaptive thresholds (3.19+ / platform-aware 3.22+)
By default, blur and brightness gates adapt to each user's camera and lighting instead of using fixed cutoffs. From 3.22.0, tuning (window size, top percentile, fraction) follows the detected platform via detectPlatform() and PLATFORM_BLUR_PARAMS. After five consecutive blur or brightness rejections, the hook surfaces persistent advisory feedback that takes priority over generic "hold still" copy. A blur rejection resets the frame buffer so V3+ models always receive fully consecutive frames. See the Changelog entries for 2.9.0 and 2.13.0.
Return Value¶
| Property | Type | Description |
|---|---|---|
state | 'idle' \| 'capturing' \| 'complete' \| 'error' | Current capture state |
progress | { captured: number; target: number; highQuality: number; goodQuality: number } | Capture progress |
feedback | string | User guidance message |
ovalState | OvalGuideState | Guide visual state (no_face / poor / good / perfect) |
frames | CapturedFrame[] | Captured frames |
isDetectorReady | boolean | true when the primary face detector is ready. With requireLandmarks: true, also waits for the FaceLandmarker |
start | () => void | Start capture loop |
stop | () => void | Stop capture loop |
reset | () => void | Reset to initial state |
restart | () => void | Reset state and immediately resume capturing |
Example¶
function SmartCapture() {
const videoRef = useRef<HTMLVideoElement>(null);
const { stream, permissionStatus, start: startCamera, isActive } = useCamera();
const {
state,
progress,
feedback,
ovalState,
start,
reset,
restart,
} = useSmartFrameCapture({
videoRef,
targetFrames: 30,
captureMode: 'full',
onComplete: (captured) => {
console.log('All frames captured:', captured.length);
},
});
useEffect(() => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
}, [stream]);
if (permissionStatus !== 'granted' && !isActive) {
return <button onClick={startCamera}>Enable Camera</button>;
}
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
{feedback && <p>{feedback}</p>}
<p>
Frames: {progress.captured}/{progress.target}
</p>
<p>Guide: {ovalState}</p>
{state === 'idle' && <button onClick={start}>Start</button>}
{state === 'complete' && <button onClick={reset}>Try Again</button>}
{state !== 'idle' && <button onClick={() => restart()}>Restart</button>}
</div>
);
}
Capture Modes¶
| Mode | Output | Use With | Description |
|---|---|---|---|
'crop' | 224x224 PNG | fast-check-crops | Face-cropped frame (client-side detection) |
'full' | PNG downscaled to fit 1280×720 | fast-check, fast-check-stream | Full video frame (server-side detection) |
Important:
fast-checkandfast-check-streamexpect full video frames because the server performs its own face detection. Onlyfast-check-cropsexpects pre-cropped 224x224 images.
Capture pacing
captureIntervalMs and minCaptureDelay both default to 0. Pass values only when you need to cap the tick rate or enforce a minimum gap between accepted frames.
Detection Gate¶
Use detectionGate to integrate external detectors (e.g. gaze, eye-region gating):
const detectionPassedRef = useRef(true);
useSmartFrameCapture({
videoRef,
detectionGate: () => detectionPassedRef.current,
onFrameCapture: (frame) => {
console.log('Captured frame:', frame.index);
},
});
When the gate returns false, the frame is silently skipped and the internal counter does NOT increment.
Uploading stage behavior
In recent SDK releases, camera and detection loops stop once frame collection is complete (uploading stage). This keeps camera usage lower while the API request is in flight.
useDetectionPipeline¶
Encapsulates the gaze + eye-region detection pipeline and provides: - detectionGate for useSmartFrameCapture - getWarnings() to collect non-blocking warnings at submission time (e.g. glasses glare) - onEyeWarning callback for real-time eye quality feedback (e.g. "Eyes are in shadow", "Glare detected") just before onRestartNeeded fires
const restartRef = useRef<() => void>(() => {});
const onRestartNeeded = useCallback(() => restartRef.current(), []);
const { detectionGate, getWarnings } = useDetectionPipeline({
videoRef,
enabled: isSessionOpen,
onRestartNeeded,
onGazeFeedback: (message) => setGazeFeedback(message), // optional
});
const { start, stop, restart } = useSmartFrameCapture({
videoRef,
targetFrames: 30,
captureMode: 'full',
detectionGate,
onComplete: (frames) => {
const warnings = getWarnings();
// Submit frames + warnings to your API...
submitFrames(frames, warnings);
},
});
// Wire restart after the hook call.
restartRef.current = restart;
Options¶
| Option | Type | Default | Description |
|---|---|---|---|
videoRef | RefObject<HTMLVideoElement \| null> | required | Video element ref used by the detectors |
enabled | boolean | required | Activates/deactivates the detection loop |
onRestartNeeded | () => void | required | Called when hidden/shadowed eyes are detected (blocking) |
intervalMs | number | 200 | Detection interval in milliseconds |
onGazeFeedback | (message: string) => void | - | Called with each gaze feedback update (empty string = clear) |
onEyeWarning | (message: string) => void | - | Called with eye failure reason ("Eyes are in shadow", "Glare detected", etc.) just before onRestartNeeded |
Return Value¶
| Property | Type | Description |
|---|---|---|
detectionGate | () => boolean | Pass to useSmartFrameCapture.detectionGate |
getWarnings | () => string[] | Returns accumulated session warnings; call at submit time |
Detection Behaviour¶
| Condition | Behaviour |
|---|---|
| Gaze off-camera | detectionGate returns false; onGazeFeedback provides a hint |
| Gaze restored | detectionGate returns true; onGazeFeedback('') clears the hint |
| Glasses glare | Non-blocking; adds "User was wearing glasses" to warnings |
| Hidden/shadowed eyes | Calls onRestartNeeded(); you should wire this to useSmartFrameCapture.restart() |
useModels¶
Fetches the live model registry from the API for dynamic model selection. Returns active and deprecated models, with local fallback while loading or on error. Use this when you want users to choose a model from the current API configuration (e.g. a model selector dropdown).
Return Value¶
| Property | Type | Description |
|---|---|---|
models | ModelEntry[] | Full model list (active + deprecated) |
activeModels | ModelEntry[] | Non-deprecated models only |
deprecatedModels | ModelEntry[] | Deprecated models only |
loading | boolean | Whether the fetch is in progress |
error | Error \| null | Error if the fetch failed |
refetch | () => void | Manually re-fetch the model list |
Example¶
function ModelPicker() {
const { activeModels, deprecatedModels, loading } = useModels();
if (loading) return <span>Loading models…</span>;
return (
<select>
{activeModels.map((m) => (
<option key={m.id} value={m.id}>
{m.label} ({m.min_frames} frames)
</option>
))}
{deprecatedModels.length > 0 && (
<optgroup label="Deprecated">
{deprecatedModels.map((m) => (
<option key={m.id} value={m.id}>
{m.label} (deprecated)
</option>
))}
</optgroup>
)}
</select>
);
}
Recommended models
Use activeModels to show only non-deprecated models. Prefer mixed-30-v3_1_1 for most integrations. See Models overview.
Utility Classes¶
The @moveris/react package also exports utility classes for advanced use cases:
DetectionManager¶
Orchestrates multiple detectors (face, gaze, hand occlusion):
Frame Utilities¶
import {
analyzeBlur,
analyzeBlurFromVideo,
analyzeLighting,
captureVideoFrame,
captureFaceCroppedFrame,
checkFrameQuality,
FrameCollector,
VideoFrameSync,
CameraStabilizer,
} from '@moveris/react';
analyzeBlur(imageData)-- Returns a blur score for the imageanalyzeLighting(imageData)-- Evaluates lighting conditionscaptureVideoFrame(video, index, startTime, options?)-- Capture a single frame from a video element. Returns aCapturedFramewith PNG base64 pixels; honorswidth/height/mirror/blurThreshold.captureFaceCroppedFrame(video, index, startTime, faceBoundingBox, options?)-- Capture and crop a 224×224 face region from videocheckFrameQuality(frame)-- Combined quality check (blur + lighting)FrameCollector-- Collects frames at a configured intervalVideoFrameSync-- Synchronizes frame capture with video playbackCameraStabilizer-- Detects when the camera feed has stabilized. From 3.22.3, retries once by default after a failed cold-start window (StabilizerConfig.maxRetries, default1).
Capture defaults in 3.10+
captureVideoFrame always encodes to PNG base64 and pins the canvas to sRGB (willReadFrequently: true), so blur and brightness checks stay consistent on Display P3 / wide-gamut displays. The default full-frame size cap is 1280×720 to match the V3.1 training resolution path. See the Changelog for 2.7.0.