npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ngmthaq20/react-face-id-capture

v1.0.39

Published

React component for face registration with real-time face detection and multi-angle capture

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-capture

Peer Dependencies

yarn add react react-dom

Quick 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 and onComplete still 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. With showIntroScreen a 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

  1. Intro Screen (optional — showIntroScreen) — Instructions with a "Get Started" button.
  2. 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.
  3. 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.
  4. 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.
  5. 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