@ngmthaq20/react-face-id-capture
v1.0.39
Published
React component for face registration with real-time face detection and multi-angle capture
Maintainers
Readme
@ngmthaq20/react-face-id-capture
React component for face registration with real-time face detection and multi-angle capture. The user slowly rolls their head in a circle while the camera records; the motion is post-processed to automatically select the best frame for each of 6 face angles (center, top, top-left, top-right, left, right) using AI-powered face detection.
Installation
yarn add @ngmthaq20/react-face-id-capturePeer Dependencies
yarn add react react-domQuick Start
import { FaceRegister } from "@ngmthaq20/react-face-id-capture";
function App() {
return (
<FaceRegister
locale="en"
onComplete={(captures) => {
// captures: exactly 6 face images, or [] when no face was detected
captures.forEach((c) => {
console.log(c.step); // "center" | "top" | "topLeft" | "topRight" | "left" | "right"
console.log(c.labelKey); // translation key for the step label
console.log(c.data); // base64 JPEG data URL
});
}}
onExit={() => {
// Called when the user backs out of the intro/result screen
console.log("User exited");
}}
/>
);
}By default the component drops the user straight into the capture screen and fires onComplete as soon as processing finishes — no intro or result screen. Opt into those screens with showIntroScreen and showResultScreen:
<FaceRegister
locale="en"
showIntroScreen
showResultScreen
onComplete={handleComplete}
onExit={handleExit}
/>Props
| Prop | Type | Default | Description |
| ------------------ | ----------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------ |
| onComplete | (captures: Capture[]) => void | — | Called with exactly 6 face images, or an empty array when no usable face was found |
| onError | (error: FaceRegisterError) => void | — | Called for every recoverable and unrecoverable failure. Non-terminal — see Error Handling |
| onExit | () => void | — | Called when the user backs out of the intro screen or "Discard & Exit" on the result |
| onDiagnostics | (event: FaceRegisterDiagnostic) => void | — | Called at every stage of the capture pipeline. See Diagnostics |
| debug | boolean | false | Also log every diagnostic event to the console, including in a production build |
| locale | string | — | Required. Active language ("en", "ja", or custom — falls back to "en" if unknown) |
| translations | FaceRegisterTranslations | — | Override or extend translation strings |
| showIntroScreen | boolean | false | Show the instructional intro screen before the camera starts |
| showResultScreen | boolean | false | Show the captured-images review screen before completing |
Error Handling
onError reports every failure the component detects. It is non-terminal: the component notifies the host, recovers where it can, and never renders an error screen — the host app owns user-facing error UX.
- After recording (
RECORDING_FAILED,RECORDING_UNREADABLE,POST_PROCESS_FAILED,NO_FACE_DETECTED) the flow continues normally andonCompletestill fires, with an empty array. - During startup (
MODEL_LOAD_FAILED,CAMERA_DENIED,CAMERA_UNAVAILABLE) the camera is released and the component stops rendering entirely, so your own error UI is fully visible. WithshowIntroScreena camera failure returns to the intro screen instead, where the user can start another attempt.
interface FaceRegisterError {
code: FaceRegisterErrorCode;
message: string;
cause?: unknown;
}| Code | When it fires |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| MODEL_LOAD_FAILED | The face-api models could not be fetched from the CDN |
| CAMERA_DENIED | getUserMedia rejected with NotAllowedError / SecurityError (permission refused or blocked) |
| CAMERA_UNAVAILABLE | No camera device, the device is busy or over-constrained, or it produced no decoded frame in time |
| RECORDING_FAILED | MediaRecorder could not be started, or the finished recording was empty |
| RECORDING_UNREADABLE | The recording was not empty but the browser could not decode a single frame of it |
| POST_PROCESS_FAILED | Frame extraction or scoring threw, or faces were found but no frame could be encoded into a capture |
| NO_FACE_DETECTED | Post-processing finished but no recorded frame yielded a usable face pose |
<FaceRegister
locale="en"
onComplete={handleComplete}
onError={(error) => {
if (error.code === "CAMERA_DENIED") showPermissionHelp();
else reportToSentry(error);
}}
/>Diagnostics
onError says that a run failed; onDiagnostics says where. It reports each stage of the pipeline as it happens — camera acquisition, recording, ring coverage, and every phase of post-processing — so a capture that produces no images can be traced without a dev build. It fires on healthy runs too, and is purely observational: nothing about the capture changes based on whether you pass it.
Set debug to also log every event to the console. Unlike the library's internal logs, this survives a production build, which makes it the quickest way to have a user reproduce a problem on a deployed site.
<FaceRegister locale="en" debug onComplete={handleComplete} />Each event is discriminated by stage:
| Stage | Fires when | Key fields |
| ------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| models:ready | Detection models are loaded and GPU-warmed | backend (should be "webgl"), loadMs (weight download/deserialize), warmupMs (one-time GPU warmup), cached, warmupFailed |
| camera:started | getUserMedia resolved | trackSettings (what the device actually gave), requestedWidth / requestedHeight, viewport size, portrait |
| recording:started | MediaRecorder started | mimeType — the container/codec the browser settled on |
| coverage:complete | The live sweep finished | reason ("covered" or "safetyCap"), ticksFilled / requiredTicks, elapsedMs |
| recording:stopped | The recording Blob was assembled | blobSize, blobType, chunkCount, viaTimeout |
| postprocess:source | The recorded Blob was opened for sampling | durationSec, durationSource ("metadata", "probe", "seekable", "fallback", "none"), sampleCount, frameWidth, frameHeight |
| postprocess:extract | The frame-sampling strategy for this recording was chosen | mode ("seek" or "playback"), primed, readyState, seekableRanges, seekableEnd |
| postprocess:orientation | The upright orientation of the recording was probed | candidates, chosenTurns (90° steps), conclusive, probedFrames, blankFrames, missedFrames |
| postprocess:scan | A full scan pass over the sampled frames ended | pass, turns, sampledFrames, missedFrames, and per-reason counts: blankFrames, noDetectionFrames, maskedFrames, nonFiniteFrames, detectedFrames, framedFrames |
| postprocess:result | Frame selection ended | captureCount, source ("framed", "all", "none"), passesRun |
type FaceRegisterDiagnostic =
| {
stage: "models:ready";
backend: string;
loadMs: number;
warmupMs: number;
cached: boolean;
warmupFailed: boolean;
}
| { stage: "camera:started" /* … */ }
| { stage: "recording:started"; mimeType: string }
| { stage: "coverage:complete" /* … */ }
| { stage: "recording:stopped" /* … */ }
| { stage: "postprocess:source" /* … */ }
| { stage: "postprocess:extract" /* … */ }
| { stage: "postprocess:orientation" /* … */ }
| { stage: "postprocess:scan" /* … */ }
| { stage: "postprocess:result" /* … */ };Reporting a capture problem
When onComplete returns an empty array, collect the events alongside the error and send both — the error names the symptom, the event trail names the cause.
function Register() {
const events = useRef<FaceRegisterDiagnostic[]>([]);
return (
<FaceRegister
locale="en"
onDiagnostics={(event) => events.current.push(event)}
onError={(error) => {
reportToSupport({ code: error.code, message: error.message, events: events.current });
}}
onComplete={handleComplete}
/>
);
}Read the trail in order. models:ready fires first and is where a one-time cost is expected: on the first scan of a page load, the detection models' shader programs have to compile before any inference can run, and that compilation now happens here — during the loading spinner — rather than at the camera preview, which is what used to cause a ~30s freeze right after the preview appeared. A large warmupMs on an uncached models:ready is normal for exactly one event per page load; every later models:ready (from a subsequent mount or a "Register Again") should report cached: true. If backend is ever anything other than "webgl", warmup cannot fix the underlying cost — every inference, not just the first, is paying it. recording:stopped with blobSize: 0 means nothing was recorded. A healthy blobSize followed by postprocess:source with durationSource: "none" means the browser could not read the length of its own recording by any route, so sampleCount is 0 and nothing downstream ever ran — that is what RECORDING_UNREADABLE reports. Any durationSource other than "metadata" means the container carried no duration and a fallback supplied it; the run is still healthy. A postprocess:scan whose sampledFrames is high but detectedFrames is 0 means the frames were recovered but no face was found in them — compare blankFrames (frames that decoded empty) against noDetectionFrames (frames that decoded fine but held no detectable face), and check chosenTurns on postprocess:orientation, since a recording scanned at the wrong rotation detects nothing.
Frames missed entirely vs. frames decoded blank. postprocess:scan and postprocess:orientation both report blankFrames and missedFrames as separate counters, because they name different failures. blankFrames is a frame that arrived and decoded to nothing — the visitor saw a canvas, and it read as empty. missedFrames is a sample instant the pass actually reached and still could not produce a frame for — a stalled seek, or (in playback mode) a decoded frame with no picture to draw — so the visitor never ran for it. It does not count instants a pass stopped short of (an early exit once orientation is conclusive, or running out of wall-clock time): that is the pass choosing to stop, not a failure to read anything. sampledFrames: 0 together with a nonzero missedFrames is a browser that could not read the recording back, which is exactly what routes a run to RECORDING_UNREADABLE instead of NO_FACE_DETECTED. Cross-check postprocess:extract: mode: "seek" with missedFrames climbing toward sampleCount means seeking silently failed on this browser (the iOS Safari defect this pipeline works around); primed: false means even priming playback never reached a decoded frame, so neither extraction mode could have worked, and low seekableRanges/seekableEnd values confirm the browser never indexed the blob for random access.
Capture Object
interface Capture {
step: "center" | "top" | "topLeft" | "topRight" | "left" | "right";
labelKey: string; // translation key for the step label (e.g. "faceRegister.labelCenter")
data: string; // base64 JPEG data URL
}How It Works
- Intro Screen (optional —
showIntroScreen) — Instructions with a "Get Started" button. - Preparing — A loading overlay stays up while the detection models load, the camera is acquired, the first frame is decoded, and a short alignment grace elapses. Model loading also pays a one-time GPU warmup (shader compilation) on the first scan of a page load, so it takes longer than a plain weight download; every scan after the first is fast because that cost is already paid. The capture screen is only mounted once recording and live detection are already running, so it is never shown in an inert state.
- Capture Screen — Camera feed with a circular overlay and a progress ring. The user slowly rolls their head in a circle. The component detects the face in real-time and fills a window of ring ticks around the current head direction on every sample — not just the single nearest tick — so the ring visibly lights up in a consistent band as the head turns, regardless of how fast the roll is. Coverage completes once 80% of the ring is filled (roughly 250° of head travel) rather than a full lap, at which point the ring locks green and recording stops automatically.
- Processing Screen — The recorded motion is sampled into frames and scored against each target pose to pick the best image per angle. Every frame with a usable pose is scored against all six angles, so the result is always exactly 6 captures — the same frame may back more than one angle when the pool is small.
- Result Screen (optional —
showResultScreen) — Displays the captured images with "Save & Continue", "Register Again", and "Discard & Exit" options.
onComplete receives either exactly 6 captures or an empty array — never a partial set. It is empty only when no recorded frame yielded a usable face pose, or when recording/post-processing failed; in every such case onError fires first with the specific code. No image is ever fabricated from an undetected frame.
When showResultScreen is false, onComplete fires automatically once processing finishes.
Translations
Built-in languages: English (en) and Japanese (ja). The library ships its own lightweight translation layer — no i18next or other i18n peer dependency is required.
Change language
<FaceRegister locale="ja" onComplete={handleComplete} />Override specific strings
<FaceRegister
translations={{
introTitle: "Verify Your Identity",
getStarted: "Begin Scan",
save: "Confirm & Continue",
}}
onComplete={handleComplete}
/>Add a custom language
<FaceRegister
locale="ko"
translations={{
introTitle: "얼굴 등록",
introSub: "본인 확인을 위해 얼굴 프로필을 설정합니다",
getStarted: "시작하기",
// ... other keys
}}
onComplete={handleComplete}
/>All Translation Keys
| Key | Default (EN) |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| introTitle | Face Registration |
| introSub | Set up your face profile for identification |
| introStep1 | Position your face within the circle |
| introStep2 | Slowly roll your head in a circle |
| introStep3 | Hold steady while we capture each angle |
| getStarted | Get Started |
| back | Back |
| hudTitle | Face Registration |
| hudProgress | {{current}} / {{total}} |
| recordingInstruction | Slowly roll your head in a circle |
| stepCenter | Look straight ahead |
| stepTop | Tilt your face up |
| stepTopLeft | Tilt your face up and left |
| stepTopRight | Tilt your face up and right |
| stepLeft | Turn your face left |
| stepRight | Turn your face right |
| outsideOval | Move your face into the oval |
| maskWarning | We need to see your full face |
| maskWarningDetail | Make sure nothing is covering your face |
| labelCenter | Center |
| labelTop | Top |
| labelTopLeft | Top Left |
| labelTopRight | Top Right |
| labelLeft | Left |
| labelRight | Right |
| processingTitle | Analyzing |
| processingSub | Selecting the best images from your motion... |
| retryTitle | Let's try that again |
| retrySub | We couldn't capture every angle. Roll your head a little slower so we can see each position. |
| retryButton | Try Again |
| resultTitle | Registration Complete |
| resultSub | {{count}} face images captured successfully |
| registerAgain | Register Again |
| save | Save & Continue |
| discard | Discard & Exit |
| loadingModels | Loading face detection models... |
| preparingCamera | Starting camera... |
Exported Types
import type {
Capture,
FaceRegisterDiagnostic,
FaceRegisterError,
FaceRegisterErrorCode,
FaceRegisterProps,
FaceRegisterTranslations,
RotationCandidateReport,
StepName,
} from "@ngmthaq20/react-face-id-capture";Requirements
- HTTPS required for camera access (except
localhost) - Face detection models are loaded from CDN (
@vladmandic/face-api) - Works on desktop and mobile browsers
License
MIT
