ugx-face-liveness
v2.7.2
Published
Client-side Angular face-liveness capture using MediaPipe Face Landmarker. The guided flow requires one face to align, complete a randomized challenge (head turns and a smile), hold still, and then captures a JPEG snapshot plus a short WebM recording.
Readme
UGX Face Liveness v2
Client-side Angular face-liveness capture using MediaPipe Face Landmarker. The guided flow requires one face to align, complete a randomized challenge (head turns and a smile), hold still, and then captures a JPEG snapshot plus a short WebM recording.
What changed in v2
- Replaced the global
face-api.jsscript and four legacy model downloads with one MediaPipe face-landmarker model. - Runs model inference in a dedicated Web Worker so slow detections do not block buttons, instructions, or camera rendering.
- Limits camera capture to a low-end-friendly 640x480 at 20–24 fps and adapts inference cadence to measured device speed.
- Records only the challenge, after initial alignment, at a controlled bitrate.
- Replaces the old always-on blink/smile thresholds with a randomized challenge sequence, so a pre-recorded clip cannot be timed against a fixed order.
- Keeps the existing output names and emitted payload values.
New in 2.4
| Option | Type | Description |
| --- | --- | --- |
| facingMode | 'user' \| 'environment' | Which camera to open. Defaults to 'user' (selfie). The preview is mirrored only for 'user'. |
| allowCameraSwitch | boolean | Show a flip button on the preview. Also available as the switchCamera() method. |
| actions | ChallengeAction[] | Which challenges to draw from. Defaults to 'turnLeft', 'turnRight', 'smile'; 'blink' is still supported and can be opted back in here. |
| shuffleActions | boolean | false pins the order you supply instead of randomizing. |
| captureLabel | string | Label for the capture button. |
See CHANGELOG.md for the full list, including the Safari / iOS model-loading fix.
This is a fully client-side presentation check. It raises the effort required for simple photo and replay attacks, but no RGB browser-only library can guarantee that every sophisticated spoof or injected camera stream will be rejected.
Compatibility
ugx-face-liveness v2 supports Angular 19–21. It is a breaking release relative to v1 because the old faceDetectionOptions input was removed.
Install
npm install ugx-face-liveness@2Copy the runtime assets
The worker, model, and MediaPipe WebAssembly files are lazy-loaded from /assets/face-liveness by default. Copy the package assets in the consuming application's angular.json build and test targets:
{
"glob": "**/*",
"input": "node_modules/ugx-face-liveness/assets",
"output": "assets/face-liveness"
}For this workspace, the demo uses the source assets instead:
{
"glob": "**/*",
"input": "projects/face-liveness/src/lib/assets",
"output": "assets/face-liveness"
}No external script tag is required. The glob copies the whole assets/ folder,
so files added in newer versions (e.g. models-v2/efficientdet_lite0.tflite,
~4.6 MB, used for phone/screen detection) are picked up on the next build — just
rebuild after upgrading. If that model is missing, device detection is silently
skipped and core liveness keeps working.
Usage
import { Component } from '@angular/core';
import { FaceLivenessComponent } from 'ugx-face-liveness';
@Component({
selector: 'app-verification',
standalone: true,
imports: [FaceLivenessComponent],
template: `
<fl-face-liveness
[options]="{ timeoutMs: 25000 }"
(faceDetectionStatusChange)="onFaceStatus($event)"
(errorOccurred)="onError($event)"
(livenessCompleted)="onComplete($event)"
/>
`,
})
export class VerificationComponent {
onFaceStatus(isAligned: boolean): void {}
onError(message: string): void {}
onComplete(result: { snapshot: Blob; video: Blob | null }): void {}
}FaceLivenessModule remains available for NgModule applications.
Options
interface FaceLivenessOptions {
assetBaseUrl?: string; // /assets/face-liveness
detectionIntervalMs?: number; // 140
maxDetectionIntervalMs?: number; // 280
minDetectionConfidence?: number; // 0.55
timeoutMs?: number; // 25000
startupTimeoutMs?: number; // 60000 per camera/face-model phase
captureWidth?: number; // 640
captureHeight?: number; // 480
videoBitsPerSecond?: number; // 600000
thresholds?: Partial<ChallengeThresholds>; // {} — override challenge tuning
deviceDetection?: { // phone/screen anti-spoof layer
enabled?: boolean; // true
required?: boolean; // false — degrade, do not fail closed
loadTimeoutMs?: number; // 15000
scoreThreshold?: number; // 0.4
minFramesToFlag?: number; // 2
minFramesToClear?: number; // 2
};
}Phone / screen detection
When deviceDetection.enabled is on (the default), the worker also runs a small
object detector (assets/models-v2/efficientdet_lite0.tflite). If a phone,
tablet or monitor is held up in front of the camera — overlapping the face — the
flow pauses and shows "Remove any phone or screen from view.", then
auto-resumes once the device is gone. A phone lying in the background does not
trip it. The detector runs in its own worker, so its load can never stall
face detection, and it is bounded twice: by loadTimeoutMs inside that worker,
and by the host, which terminate()s a worker it cannot get an answer from.
Detection degrades by default: a missing, failed or stalled detector leaves
the session running on the face model alone rather than blocking it. The
downgrade is never silent — deviceDetectionStatusChange reports it live, and
livenessCompleted carries deviceDetectionActive, which is where a host with
a strict policy rejects the result. Set required: true to fail closed inside
the component instead, or enabled: false to skip the model entirely.
Camera and face-model startup have separate startupTimeoutMs deadlines, so an
error identifies the stalled side. Camera playback and first-frame delivery are
also bounded, and their temporary media listeners are removed during cleanup.
Limitations — this raises the bar, it is not KYC-grade on its own:
- It catches a device only when its body/bezel is visible. A screen cropped to fill the entire frame (no edges) looks like a bare face and can slip through.
- Detection is client-side, so a determined attacker can still bypass it by injecting a fake video stream (a virtual camera). For high-stakes identity checks, verify liveness server-side as well.
The detector automatically increases its interval when inference is slow. Lowering detectionIntervalMs can make the flow less reliable on low-end devices.
thresholds lets you override any of the challenge tuning values (face-height gate, yaw/roll tolerances, blink scores) without touching CSS — e.g. { thresholds: { minFaceHeight: 0.4 } }. It is merged over the built-in defaults and also drives the size of the on-screen alignment oval, so the guide always matches the gate.
Outputs preserved from v1
faceDetectionStatusChange: booleanerrorOccurred: stringlivenessCompleted: { snapshot: Blob; video: Blob | null; deviceDetectionActive: boolean }
Additional security-status output:
deviceDetectionStatusChange: { status: 'loading' | 'ready' | 'disabled' | 'unavailable'; message?: string }
The snapshot is JPEG. The video is WebM VP9 or VP8 where MediaRecorder is available; otherwise video is null.
Local development
npm start
npm run build:lib
npm run test:lib -- --watch=falsenpm start and npm run build:lib automatically rebuild the generated worker bundle. Use Node.js 20 or 22 LTS.
Model provenance
The included face_landmarker.task is Google's MediaPipe Face Landmarker float16 model bundle. MediaPipe is Apache-2.0 licensed. The model provides 478 three-dimensional facial landmarks; v2 uses those landmarks for face bounds, roll, yaw, ordered movement, and stillness checks.
