BlinkDetector
EAR + blendshape blink detection for the browser, without React.
BlinkDetector gives you blink detection without any React dependency. Pass callbacks at construction time, call processFrame on each animation frame, and the detector fires onBlink whenever a closure is detected.
Basic usage
import { BlinkDetector } from "@framefind/core";
const detector = new BlinkDetector({
onBlink: (ear) => console.log("blink", ear),
onEARChange: (ear) => console.log("ear", ear),
});
await detector.load();
const video = document.querySelector("video")!;
function loop() {
detector.processFrame(video);
requestAnimationFrame(loop);
}
video.addEventListener("play", loop, { once: true });processFrame is synchronous and internally throttles to ~30fps — calling it from a 60fps requestAnimationFrame loop is fine.
Detection algorithm
Three signals fire independently; whichever crosses its threshold first wins:
- Blendshape (primary) — MediaPipe
eyeBlinkLeft/eyeBlinkRightARKit scores. Fires when either eye score stays above0.5for ≥50ms. - Asymmetry — If one eye EAR is less than 45% of the other, treated as a wink/closure. Fires after ≥60ms.
- EAR + drop-rate — Smoothed eye aspect ratio drops below
baseline × 0.65with a minimum velocity. Fires after ≥60ms. Baseline is learned from the first ~500ms of face presence and drifts upward slowly when eyes are open.
After a blink fires, re-arming requires both a refractory period (180ms) and a relaxation delta (blendshape must drop ≥0.15 from its post-fire peak). This allows fast consecutive blinks without double-counting a single held closure.
Options
new BlinkDetector({
// 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,
// MediaPipe model and WASM — both default to the FrameFind CDN
faceLandmarkerModelUrl: "https://cdn.framefind.moraxh.dev/...",
mediapipeWasmPath: "https://cdn.framefind.moraxh.dev/...",
minFaceDetectionConfidence: 0.5,
minFacePresenceConfidence: 0.5,
minTrackingConfidence: 0.5,
// Prefer GPU via WebGL delegate. Default: true, falls back to CPU
preferGpu: true,
});Updating callbacks after construction
detector.setCallbacks({
onBlink: (ear) => console.log("blink", ear),
onFaceLost: () => console.log("face lost"),
});Only the keys you pass are updated — the rest stay unchanged.
Getters
detector.isBlinking // boolean — eyes currently closed
detector.blinkDurationMs // number — ms eyes have been continuously closed (0 when open)
detector.smoothedEarValue // number | null — exponentially smoothed avg EAR
detector.baselineEarValue // number | null — current open-eye EAR baselineResetting
detector.resetHistory();Clears EAR baseline, smoothing state, and refractory timers. The detector re-calibrates from the next frame (~500ms warmup).
Cleanup
detector.dispose();