| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- 'use client'
- import { useEffect, useRef, useState, useCallback } from 'react';
- import { api_face_detection } from '@/lib/api/server';
- export interface FaceDetectionResult {
- hasFace: boolean;
- faceCount: number;
- }
- // 检测间隔(毫秒)- 每 200ms 检测一次,平衡性能和实时性
- const DETECTION_INTERVAL = 200;
- export function useFaceDetection(
- onFaceDetected?: (result: FaceDetectionResult) => void,
- enabled: boolean = true
- ) {
- const [isDetecting, setIsDetecting] = useState(false);
- const [faceDetected, setFaceDetected] = useState(false);
- const [faceCount, setFaceCount] = useState(0);
- const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
- const videoRef = useRef<HTMLVideoElement | null>(null);
- const displayVideoRef = useRef<HTMLVideoElement | null>(null);
- const streamRef = useRef<MediaStream | null>(null);
- const detectionIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
- const isDetectingRef = useRef(false);
- const canvasRef = useRef<HTMLCanvasElement | null>(null);
- // 使用 ref 存储回调函数,确保总是使用最新的回调
- const onFaceDetectedRef = useRef(onFaceDetected);
-
- // 同步回调函数到 ref
- useEffect(() => {
- onFaceDetectedRef.current = onFaceDetected;
- }, [onFaceDetected]);
- // 同步 isDetecting 状态到 ref
- useEffect(() => {
- isDetectingRef.current = isDetecting;
- }, [isDetecting]);
- const handleResults = useCallback((result: { hasFace: boolean; faceCount: number }) => {
- const detected = result.hasFace;
- const count = result.faceCount;
-
- // 立即更新状态,确保状态实时反映检测结果
- setFaceDetected(detected);
- setFaceCount(count);
-
- // 使用 ref 调用回调,确保总是使用最新的回调函数
- if (onFaceDetectedRef.current) {
- onFaceDetectedRef.current({
- hasFace: detected,
- faceCount: count
- });
- }
- }, []);
- // 从视频元素捕获帧并发送到后端进行检测
- const detectFaces = useCallback(async () => {
- if (!isDetectingRef.current || !videoRef.current) {
- return;
- }
- const video = videoRef.current;
-
- // 检查视频是否准备好
- if (video.readyState < video.HAVE_CURRENT_DATA) {
- return;
- }
- // 检查视频尺寸
- if (video.videoWidth === 0 || video.videoHeight === 0) {
- return;
- }
- try {
- // 创建 canvas 用于捕获视频帧
- if (!canvasRef.current) {
- const canvas = document.createElement('canvas');
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
- canvasRef.current = canvas;
- }
- const canvas = canvasRef.current;
- const ctx = canvas.getContext('2d');
-
- if (!ctx) {
- console.error('无法获取 canvas 上下文');
- return;
- }
- // 绘制当前视频帧到 canvas
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- // 将 canvas 转换为 Blob
- canvas.toBlob(async (blob) => {
- if (!blob || !isDetectingRef.current) {
- return;
- }
- try {
- // 调用后端 API 进行人脸检测
- const result = await api_face_detection(blob);
-
- // 处理检测结果
- handleResults({
- hasFace: result.hasFace,
- faceCount: result.faceCount
- });
-
- // 调试信息(减少日志输出)
- if (result.hasFace) {
- const frameCount = (detectFaces as any).__frameCount || 0;
- (detectFaces as any).__frameCount = frameCount + 1;
- // 每30帧输出一次(约每6秒)
- if (frameCount % 30 === 0) {
- console.log(`✓ 检测到 ${result.faceCount} 张人脸`);
- }
- }
- } catch (error) {
- console.error('人脸检测 API 调用失败:', error);
- // 检测失败时,假设没有人脸
- handleResults({
- hasFace: false,
- faceCount: 0
- });
- }
- }, 'image/jpeg', 0.8); // 使用 JPEG 格式,质量 0.8 以平衡质量和大小
- } catch (error) {
- console.error('人脸检测错误:', error);
- }
- }, [handleResults]);
- const startDetection = useCallback(async () => {
- if (!enabled || isDetecting) return;
- try {
- // 请求摄像头权限(如果失败,不影响手动启动功能)
- const stream = await navigator.mediaDevices.getUserMedia({
- video: {
- width: { ideal: 640 },
- height: { ideal: 480 },
- facingMode: 'user'
- }
- }).catch((error): MediaStream | null => {
- // 摄像头不可用或用户拒绝权限时,静默失败,不影响手动启动
- console.warn('摄像头不可用,将使用手动启动模式:', error.message);
- return null;
- });
-
- if (!stream) {
- // 摄像头不可用,不启动检测,但也不报错
- setIsDetecting(false);
- return;
- }
- streamRef.current = stream;
- setVideoStream(stream);
- // 创建隐藏的 video 元素用于处理视频流(用于人脸检测)
- if (!videoRef.current) {
- const video = document.createElement('video');
- video.style.position = 'fixed';
- video.style.top = '-9999px';
- // 设置合适的尺寸以确保检测正常工作(不能太小)
- video.style.width = '640px';
- video.style.height = '480px';
- video.setAttribute('playsinline', 'true');
- video.setAttribute('autoplay', 'true');
- video.setAttribute('muted', 'true');
- video.setAttribute('webkit-playsinline', 'true');
- document.body.appendChild(video);
- videoRef.current = video;
- }
- const video = videoRef.current;
- video.srcObject = stream;
-
- // 等待视频元数据加载
- await new Promise<void>((resolve) => {
- const onLoadedMetadata = () => {
- video.removeEventListener('loadedmetadata', onLoadedMetadata);
- console.log(`视频尺寸: ${video.videoWidth}x${video.videoHeight}`);
- resolve();
- };
- video.addEventListener('loadedmetadata', onLoadedMetadata);
- video.play().catch(console.error);
- });
- // 如果存在显示用的video元素,也设置流
- if (displayVideoRef.current) {
- displayVideoRef.current.srcObject = stream;
- await displayVideoRef.current.play();
- }
- // 设置检测状态
- setIsDetecting(true);
-
- // 等待一帧确保状态更新
- await new Promise(resolve => requestAnimationFrame(resolve));
-
- // 开始定期检测
- console.log('开始人脸检测循环...');
- detectionIntervalRef.current = setInterval(() => {
- if (isDetectingRef.current) {
- detectFaces();
- }
- }, DETECTION_INTERVAL);
-
- console.log('人脸检测启动成功(使用 UniFace 后端)');
- } catch (error) {
- // 摄像头不可用或检测失败时,静默失败,不影响手动启动功能
- console.warn('启动人脸检测失败(将使用手动启动模式):', error);
- setIsDetecting(false);
- // 清理资源
- if (streamRef.current) {
- streamRef.current.getTracks().forEach(track => track.stop());
- streamRef.current = null;
- }
- setVideoStream(null);
- }
- }, [enabled, isDetecting, detectFaces]);
- // 停止检测循环(但不释放资源)
- const stopDetection = useCallback(() => {
- // 停止检测间隔
- if (detectionIntervalRef.current) {
- clearInterval(detectionIntervalRef.current);
- detectionIntervalRef.current = null;
- }
- // 重置状态
- setIsDetecting(false);
- setFaceDetected(false);
- setFaceCount(0);
- }, []);
- // 完全停止检测并释放所有资源
- const disposeDetection = useCallback(() => {
- // 停止检测间隔
- if (detectionIntervalRef.current) {
- clearInterval(detectionIntervalRef.current);
- detectionIntervalRef.current = null;
- }
- if (streamRef.current) {
- streamRef.current.getTracks().forEach(track => track.stop());
- streamRef.current = null;
- }
- if (videoRef.current) {
- videoRef.current.srcObject = null;
- if (videoRef.current.parentNode) {
- videoRef.current.parentNode.removeChild(videoRef.current);
- }
- videoRef.current = null;
- }
- if (displayVideoRef.current) {
- displayVideoRef.current.srcObject = null;
- }
- if (canvasRef.current) {
- canvasRef.current = null;
- }
- setVideoStream(null);
- setIsDetecting(false);
- setFaceDetected(false);
- setFaceCount(0);
- }, []);
- useEffect(() => {
- if (enabled && !isDetecting) {
- startDetection();
- } else if (!enabled && isDetecting) {
- // 如果禁用检测,完全释放资源
- disposeDetection();
- }
- return () => {
- // 组件卸载时完全释放资源
- disposeDetection();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [enabled]);
- // 设置显示用的video元素引用
- const setDisplayVideoRef = useCallback((ref: HTMLVideoElement | null) => {
- displayVideoRef.current = ref;
- if (ref && streamRef.current) {
- ref.srcObject = streamRef.current;
- ref.play().catch(console.error);
- }
- }, []);
- return {
- isDetecting,
- faceDetected,
- faceCount,
- videoStream,
- displayVideoRef: displayVideoRef.current,
- setDisplayVideoRef,
- startDetection,
- stopDetection,
- disposeDetection
- };
- }
|