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();