@daboss2003/liveness-web
v1.0.8
Published
Web liveness detection using MediaPipe Face Landmarker (CDN)
Maintainers
Readme
@daboss2003/liveness-web
Lightweight web liveness detection using MediaPipe Face Landmarker (CDN). The SDK provides the full UI: oval face frame, camera, and step progress. You only supply callbacks.
Install
npm install @daboss2003/liveness-webUsage
import { startLiveness } from "@daboss2003/liveness-web";
startLiveness({
container: document.getElementById("root"), // optional; defaults to document.body
callbacks: {
onSuccess(imageBase64) {
console.log("Verified", imageBase64.length);
},
onFailure(reason) {
console.error(reason);
},
onChallengeChanged(stepIndex, stepLabel) {
console.log(`Step ${stepIndex + 1}: ${stepLabel}`);
},
},
});- Auto-start: Verification starts as soon as
startLivenessis called (no Start button). - Runs until complete: The flow continues until the user passes all steps or a hard error occurs. Wrong poses do not stop the session; the user can keep trying until they get each step right.
- No timeouts: Steps are not timed out.
- Progress: The oval border fills by segment as each step is completed (5 steps total).
To cancel and release the camera:
import { stop } from "@daboss2003/liveness-web";
stop();Errors (CDN / connectivity)
When CDN or connectivity fails, onFailure is called with a string you can compare to exported constants:
LIVENESS_ERROR_CDN_NOT_AVAILABLE("cdnNotAvailable") — CDN/assets unavailable after retries; internet was confirmed. Your app can fall back to a normal camera flow (e.g. capture without liveness).LIVENESS_ERROR_OFFLINE("offline") — No internet connection (e.g. user is offline).
Example:
import { startLiveness, LIVENESS_ERROR_CDN_NOT_AVAILABLE, isCdnNotAvailableError } from "@daboss2003/liveness-web";
startLiveness({
callbacks: {
onFailure(reason) {
if (isCdnNotAvailableError(reason)) {
// Fall back to normal camera flow
return;
}
console.error(reason);
},
},
});Embedding in your own UI
By default, startLiveness takes over the viewport and renders its own oval frame, step dots, and instructions. If you want to drop just the camera + face frame into a slot in your own designed page (custom button, branding, copy), pass:
startLiveness({
container: mySlotEl, // your sized div (give it explicit width & height)
embed: true, // render inside container instead of full-screen
shape: "circle", // or "oval" (default)
showInstructions: false, // hide built-in text/dots — your UI shows them
minSize: 240, // floor on shape diameter (px) so small screens stay usable
theme: {
progressColor: "#1a0f4d",
progressErrorColor: "#ff3b3b",
progressWidth: 4,
progressLineCap: "round",
overlayColor: "#5b34d6", // fill outside the cutout — match your bg
overlayErrorColor: "rgba(180,0,0,0.6)",
},
callbacks: {
onChallengeChanged: (i, label) => myStatus.textContent = label,
onFaceInOval: (inside, reason) => { if (!inside) myStatus.textContent = reason ?? ""; },
onSuccess, onFailure,
},
});Sounds still play in embed mode and with showInstructions: false — only the visuals are hidden.
A complete working example (yellow page, purple circle slot, host-owned Start button) lives at demo-apps/web/embed.html + demo-apps/web/src/embed.ts. Run it with:
cd demo-apps/web && npm install && npm run dev
# open http://localhost:5173/embed.htmlAll new options are optional — omitting them gives you the original full-screen oval behaviour.
Picking which challenges run
By default, all 5 challenges run in a random order. Pass steps to restrict to a subset (e.g. for shorter flows or A/B testing). The dot progress indicator and the progress ring both adjust to the active subset count.
import { startLiveness, type LivenessStepKey } from "@daboss2003/liveness-web";
startLiveness({
steps: ["nod", "blink", "mouth"], // run only these, in this set
shuffleSteps: true, // default true — randomise within the subset
callbacks: { /* … */ },
});Valid keys are "left", "blink", "right", "nod", "mouth". Unknown keys and duplicates are dropped; an empty array falls back to all 5.
shuffleSteps: false runs the steps in the order you pass.
Tuning individual steps
Each challenge has its own thresholds — bump them up to be stricter (harder to pass, fewer false positives), or down to be more lenient.
Blink
| Field | Default | Effect |
|---|---|---|
| blinkClosedThreshold | 0.50 | Min blendshape score that counts as "eyes closed". Raise to reject squinting / glare / hooded eyelids; lower if real blinks don't register. Below ~0.35 you start catching passive eye states. |
| blinkOpenThreshold | 0.25 | Max score that counts as "eyes open". Keep a clear gap from blinkClosedThreshold. |
| blinkMinClosedMs | 60 | Eyes must stay closed at least this long. Raise to require deliberate (slower) blinks. |
| blinkMaxDurationMs | 4000 | Abandon and reset if eyes stay closed longer than this. |
Head turn — yawTurnDelta (degrees needed), yawWrongDirDelta (wrong-direction abort), headTurnHoldMs (sustain time).
Nod — nodDownDelta (degrees chin must drop), nodReturnFraction, nodReturnMaxDelta.
Mouth — mouthOpenThreshold, mouthOpenMarThreshold, mouthHoldMs.
Capture (post-success) — captureMaxYaw, captureMaxPitch, captureMaxMouthScore, captureMaxBlinkScore (all bound how "neutral" the final frame must be).
Pass any subset as part of config:
startLiveness({
config: {
blinkClosedThreshold: 0.55, // tighter blink
blinkMinClosedMs: 100, // require ~6 frames of closure
yawTurnDelta: 12, // looser head turn
},
callbacks: { /* … */ },
});See engine.ts for the full config object and every default value.
Notes
- Uses MediaPipe Tasks Vision Web from CDN.
- Camera permission is required in the browser.
