Task Vision
Face Detection
Configure MediaPipe face detection in a Next.js application.
Installation
npx shadcn@latest add @krave-docs/face-detectionUsing Face Detection
import HandLandMark from "@/components/HandLandMark";
export default function Home() {
return (
<HandLandMark
position="top-right" //top-left, top-right, bottom-left, bottom-right
boxColor="#55e6c1"
labelColor="#f8fafc"
size={4}
/>
);
}Source Code
"use client";
import { useEffect, useRef, useState } from "react";
import { Camera, CameraOff, LoaderCircle } from "lucide-react";
import {
DrawingUtils,
FaceDetector,
FilesetResolver,
} from "@mediapipe/tasks-vision";
type CameraPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
interface FaceDetectionProps {
position: CameraPosition;
boxColor: string;
labelColor: string;
size: number;
}
function FaceDetection({
position,
boxColor,
labelColor,
size,
}: FaceDetectionProps) {
const safePosition = position ?? "bottom-right";
const safeBoxColor = boxColor ?? "#7dd3fc";
const safeLabelColor = labelColor ?? "#0f172a";
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const detectorRef = useRef<FaceDetector | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const animationFrameRef = useRef<number | null>(null);
const webcamRunningRef = useRef(false);
const [isModelReady, setIsModelReady] = useState(false);
const [isCameraRunning, setIsCameraRunning] = useState(false);
const [isCameraStarting, setIsCameraStarting] = useState(false);
const [faceDetected, setFaceDetected] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const cameraPositionClasses: Record<CameraPosition, string> = {
"top-left": "left-4 top-4 sm:left-6 sm:top-6",
"top-right": "right-4 top-4 sm:right-6 sm:top-6",
"bottom-left": "bottom-4 left-4 sm:bottom-6 sm:left-6",
"bottom-right": "bottom-4 right-4 sm:bottom-6 sm:right-6",
};
useEffect(() => {
let cancelled = false;
async function createDetector() {
try {
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.1.0/wasm",
);
const detectorOptions = {
baseOptions: {
modelAssetPath:
"https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite",
},
runningMode: "VIDEO" as const,
minDetectionConfidence: 0.75,
minSuppressionThreshold: 0.3,
};
let detector: FaceDetector;
try {
detector = await FaceDetector.createFromOptions(vision, {
...detectorOptions,
baseOptions: { ...detectorOptions.baseOptions, delegate: "GPU" },
});
} catch (gpuError) {
console.warn("GPU face detection unavailable; retrying with CPU:", gpuError);
detector = await FaceDetector.createFromOptions(vision, {
...detectorOptions,
baseOptions: { ...detectorOptions.baseOptions, delegate: "CPU" },
});
}
if (cancelled) {
detector.close();
return;
}
detectorRef.current = detector;
setIsModelReady(true);
} catch (error) {
console.error("Failed to load the face detection model:", error);
if (!cancelled) {
setErrorMessage("Could not load the face detector. Check your connection and reload.");
}
}
}
void createDetector();
return () => {
cancelled = true;
webcamRunningRef.current = false;
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
}
streamRef.current?.getTracks().forEach((track) => track.stop());
detectorRef.current?.close();
};
}, []);
async function toggleCamera() {
if (webcamRunningRef.current) {
webcamRunningRef.current = false;
setIsCameraRunning(false);
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
if (videoRef.current) {
videoRef.current.srcObject = null;
}
const canvas = canvasRef.current;
const context = canvas?.getContext("2d");
if (canvas && context) {
context.clearRect(0, 0, canvas.width, canvas.height);
}
setFaceDetected(false);
return;
}
setIsCameraStarting(true);
setErrorMessage(null);
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "user" },
});
const video = videoRef.current;
if (!video) {
stream.getTracks().forEach((track) => track.stop());
return;
}
streamRef.current = stream;
video.srcObject = stream;
await video.play();
webcamRunningRef.current = true;
setIsCameraRunning(true);
animationFrameRef.current = window.requestAnimationFrame((timestamp) => {
void predictWebcam(timestamp);
});
} catch (error) {
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
console.error("Failed to start the camera:", error);
setErrorMessage(
error instanceof Error && error.name === "NotAllowedError"
? "Camera access was denied. Allow camera access in your browser settings and try again."
: "Could not start the camera. Check that it is connected and not being used by another app.",
);
} finally {
setIsCameraStarting(false);
}
}
async function predictWebcam(timestamp: number) {
const video = videoRef.current;
const canvas = canvasRef.current;
const detector = detectorRef.current;
if (!video || !canvas || !webcamRunningRef.current) {
return;
}
if (!detector || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA || video.videoWidth === 0) {
animationFrameRef.current = window.requestAnimationFrame((frameTime) => {
void predictWebcam(frameTime);
});
return;
}
const canvasContext = canvas.getContext("2d");
if (!canvasContext) {
setErrorMessage("Could not create the face detection canvas.");
webcamRunningRef.current = false;
setIsCameraRunning(false);
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
return;
}
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
let detections;
try {
detections = detector.detectForVideo(video, timestamp).detections;
} catch (error) {
console.error("Face detection failed:", error);
setErrorMessage("Face detection failed. Please restart the camera.");
webcamRunningRef.current = false;
setIsCameraRunning(false);
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
return;
}
const faceDetections = detections.filter(
(detection) => (detection.categories?.[0]?.score ?? 0) >= 0.75,
);
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
setFaceDetected(faceDetections.length > 0);
if (faceDetections.length > 0) {
const drawingUtils = new DrawingUtils(canvasContext);
canvasContext.font = "16px sans-serif";
for (const detection of faceDetections) {
if (!detection.boundingBox) continue;
const box = detection.boundingBox;
const mirroredBox = {
...box,
originX: canvas.width - box.originX - box.width,
};
drawingUtils.drawBoundingBox(mirroredBox, {
color: safeBoxColor,
lineWidth: size,
fillColor: "transparent",
});
const category = detection.categories?.[0];
const score = category?.score ? Math.round(category.score * 100) : 0;
const labelText = `Face - ${score}%`;
const textWidth = canvasContext.measureText(labelText).width;
canvasContext.fillStyle = safeLabelColor;
canvasContext.fillRect(mirroredBox.originX, mirroredBox.originY, textWidth + 12, 26);
canvasContext.fillStyle = "#ffffff";
canvasContext.fillText(labelText, mirroredBox.originX + 6, mirroredBox.originY + 18);
}
}
if (webcamRunningRef.current) {
animationFrameRef.current = window.requestAnimationFrame((frameTime) => {
void predictWebcam(frameTime);
});
}
}
return (
<main className="relative min-h-screen w-full overflow-hidden bg-[#090a0c]">
<div
className={`absolute z-10 aspect-video w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-2xl border border-white/[0.09] bg-[#0e1012] shadow-2xl shadow-black/50 sm:w-[min(28rem,calc(100vw-3rem))] ${cameraPositionClasses[safePosition] ?? cameraPositionClasses["bottom-right"]}`}
>
<video
ref={videoRef}
autoPlay
muted
playsInline
aria-label="Camera preview"
className={`absolute inset-0 size-full object-cover ${isCameraRunning ? "scale-x-[-1]" : "hidden"}`}
/>
<canvas
ref={canvasRef}
aria-hidden="true"
className={`absolute inset-0 size-full object-cover ${isCameraRunning ? "block" : "hidden"}`}
/>
{!isCameraRunning && (
<div className="absolute inset-0 flex flex-col items-center justify-center px-4 text-center">
<CameraOff className="mb-3 size-7 text-[#59c596]" aria-hidden="true" />
<p className="text-sm font-medium text-[#f2f3f4]">
{errorMessage ?? "Camera is off"}
</p>
<p className="mt-1 text-xs text-[#92969b]">
{isModelReady
? "Press the camera button to start"
: errorMessage
? "Face detection could not be initialized"
: "Face detection is loading"}
</p>
</div>
)}
{isCameraRunning && (
<div
className="absolute bottom-3 left-3 rounded-full border border-white/10 bg-[#101311]/90 px-2.5 py-1 text-xs text-[#f2f3f4] shadow-lg backdrop-blur"
role="status"
aria-live="polite"
>
{faceDetected ? "Face detected" : "No face detected"}
</div>
)}
<button
type="button"
onClick={() => void toggleCamera()}
disabled={!isModelReady || isCameraStarting}
aria-label={
isCameraStarting
? "Starting camera"
: isCameraRunning
? "Turn camera off"
: "Turn camera on"
}
title={
isCameraStarting
? "Starting camera"
: isCameraRunning
? "Turn camera off"
: "Turn camera on"
}
className="absolute right-3 top-3 z-20 flex size-10 items-center justify-center rounded-full border border-white/10 bg-[#101311]/90 text-white shadow-lg backdrop-blur transition hover:border-[#59c596]/40 hover:bg-[#171c19] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#59c596] disabled:cursor-wait disabled:opacity-70"
>
{isCameraStarting ? (
<LoaderCircle className="size-4 animate-spin text-[#59c596]" aria-hidden="true" />
) : isCameraRunning ? (
<CameraOff className="size-4 text-[#d1d5d8]" aria-hidden="true" />
) : (
<Camera className="size-4 text-[#59c596]" aria-hidden="true" />
)}
</button>
</div>
</main>
);
}
export default FaceDetection;