FrameFind
@framefind/react

useBlinkDetector

Detect eye blinks in a live webcam feed using MediaPipe face landmarks.

useBlinkDetector combines EAR smoothing, baseline drift, and MediaPipe blendshapes to detect blinks in real time. It calibrates a personal baseline during the first ~500ms of face presence and fires onBlink on each detected closure.

Basic usage

"use client";

import { useBlinkDetector } from "@framefind/react";
import { useState } from "react";

export function BlinkCounter() {
  const [count, setCount] = useState(0);

  const { videoRef, result, loading } = useBlinkDetector({
    onBlink: (ear) => setCount((n) => n + 1),
  });

  return (
    <div>
      <video ref={videoRef} autoPlay playsInline muted />
      {loading && <p>Loading model…</p>}
      {!result.faceDetected && !loading && <p>No face detected</p>}
      <p>Blinks: {count}</p>
    </div>
  );
}

onBlink fires once per detected closure, not once per frame. It receives the minimum EAR across both eyes at the moment of detection — lower values indicate a tighter blink.

The result object

type BlinkStateResult = {
  faceDetected:   boolean;       // face visible in current frame
  isBlinking:     boolean;       // eyes are currently closed
  blinkDurationMs: number;       // ms eyes have been continuously closed (0 when open)
  ear:            number | null; // smoothed avg EAR (both eyes)
  baselineEar:    number | null; // learned open-eye baseline (null during warmup)
  smoothedEar:    number | null; // same as ear — exponentially filtered
  landmarks:      Point2D[] | null; // raw MediaPipe 478-point face mesh
};

baselineEar is null for the first ~500ms. Until it's established the detector won't fire blinks.

blinkDurationMs counts up while isBlinking is true. Use it to distinguish a quick blink (~100ms) from a deliberate held closure (>500ms).

Options

useBlinkDetector({
  // Callbacks — all optional
  onBlink:    (ear: number) => void,       // fires once per detected closure
  onFaceLost: ()            => void,       // fires when face absent for ~165ms
  onEARChange:(ear: number) => void,       // fires every frame with smoothed EAR
  onLandmarks:(lm: Point2D[] | null) => void,

  // Whether to run detection at all. Default: true
  enabled: true,

  // MediaPipe model and WASM — both default to the FrameFind CDN
  faceLandmarkerModelUrl: "https://cdn.framefind.moraxh.dev/...",
  mediapipeWasmPath:      "https://cdn.framefind.moraxh.dev/...",

  // MediaPipe face landmarker settings
  minFaceDetectionConfidence: 0.5,
  minFacePresenceConfidence:  0.5,
  minTrackingConfidence:      0.5,

  // Prefer GPU inference. Default: true, falls back to CPU automatically
  preferGpu: true,

  // Throttle React state updates. Default: 0 (update every frame)
  uiUpdateIntervalMs: 0,
});

Detecting held closures

blinkDurationMs lets you distinguish intent without any extra state:

const { result } = useBlinkDetector({
  onBlink: (ear) => {
    // fires on the leading edge of each closure
  },
});

// In your render or a useEffect:
if (result.isBlinking && result.blinkDurationMs > 500) {
  // eyes held shut for > 500ms
}

Sharing a video element

To run multiple detectors on the same <video>, create one ref and pass it to each hook:

import { useRef } from "react";
import { useBlinkDetector, useHeadPoseDetector } from "@framefind/react";

export function MultiDetector() {
  const videoRef = useRef<HTMLVideoElement>(null);

  const { result: blinkResult } = useBlinkDetector({ videoRef });
  const { result: poseResult } = useHeadPoseDetector({ videoRef });

  return <video ref={videoRef} autoPlay playsInline muted />;
}

Pausing and resetting

const { videoRef, isPaused, pause, resume, reset } = useBlinkDetector({
  onBlink: (ear) => console.log("blink", ear),
});

// Freeze detection without stopping the camera
pause();
resume();

// Clear EAR history and re-calibrate from scratch (~500ms warmup again)
reset();

On this page