faceDetection.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. 'use client'
  2. import { useEffect, useRef, useState, useCallback } from 'react';
  3. import { api_face_detection } from '@/lib/api/server';
  4. export interface FaceDetectionResult {
  5. hasFace: boolean;
  6. faceCount: number;
  7. }
  8. // 检测间隔(毫秒)- 每 200ms 检测一次,平衡性能和实时性
  9. const DETECTION_INTERVAL = 200;
  10. export function useFaceDetection(
  11. onFaceDetected?: (result: FaceDetectionResult) => void,
  12. enabled: boolean = true
  13. ) {
  14. const [isDetecting, setIsDetecting] = useState(false);
  15. const [faceDetected, setFaceDetected] = useState(false);
  16. const [faceCount, setFaceCount] = useState(0);
  17. const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
  18. const videoRef = useRef<HTMLVideoElement | null>(null);
  19. const displayVideoRef = useRef<HTMLVideoElement | null>(null);
  20. const streamRef = useRef<MediaStream | null>(null);
  21. const detectionIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  22. const isDetectingRef = useRef(false);
  23. const canvasRef = useRef<HTMLCanvasElement | null>(null);
  24. // 使用 ref 存储回调函数,确保总是使用最新的回调
  25. const onFaceDetectedRef = useRef(onFaceDetected);
  26. // 同步回调函数到 ref
  27. useEffect(() => {
  28. onFaceDetectedRef.current = onFaceDetected;
  29. }, [onFaceDetected]);
  30. // 同步 isDetecting 状态到 ref
  31. useEffect(() => {
  32. isDetectingRef.current = isDetecting;
  33. }, [isDetecting]);
  34. const handleResults = useCallback((result: { hasFace: boolean; faceCount: number }) => {
  35. const detected = result.hasFace;
  36. const count = result.faceCount;
  37. // 立即更新状态,确保状态实时反映检测结果
  38. setFaceDetected(detected);
  39. setFaceCount(count);
  40. // 使用 ref 调用回调,确保总是使用最新的回调函数
  41. if (onFaceDetectedRef.current) {
  42. onFaceDetectedRef.current({
  43. hasFace: detected,
  44. faceCount: count
  45. });
  46. }
  47. }, []);
  48. // 从视频元素捕获帧并发送到后端进行检测
  49. const detectFaces = useCallback(async () => {
  50. if (!isDetectingRef.current || !videoRef.current) {
  51. return;
  52. }
  53. const video = videoRef.current;
  54. // 检查视频是否准备好
  55. if (video.readyState < video.HAVE_CURRENT_DATA) {
  56. return;
  57. }
  58. // 检查视频尺寸
  59. if (video.videoWidth === 0 || video.videoHeight === 0) {
  60. return;
  61. }
  62. try {
  63. // 创建 canvas 用于捕获视频帧
  64. if (!canvasRef.current) {
  65. const canvas = document.createElement('canvas');
  66. canvas.width = video.videoWidth;
  67. canvas.height = video.videoHeight;
  68. canvasRef.current = canvas;
  69. }
  70. const canvas = canvasRef.current;
  71. const ctx = canvas.getContext('2d');
  72. if (!ctx) {
  73. console.error('无法获取 canvas 上下文');
  74. return;
  75. }
  76. // 绘制当前视频帧到 canvas
  77. ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  78. // 将 canvas 转换为 Blob
  79. canvas.toBlob(async (blob) => {
  80. if (!blob || !isDetectingRef.current) {
  81. return;
  82. }
  83. try {
  84. // 调用后端 API 进行人脸检测
  85. const result = await api_face_detection(blob);
  86. // 处理检测结果
  87. handleResults({
  88. hasFace: result.hasFace,
  89. faceCount: result.faceCount
  90. });
  91. // 调试信息(减少日志输出)
  92. if (result.hasFace) {
  93. const frameCount = (detectFaces as any).__frameCount || 0;
  94. (detectFaces as any).__frameCount = frameCount + 1;
  95. // 每30帧输出一次(约每6秒)
  96. if (frameCount % 30 === 0) {
  97. console.log(`✓ 检测到 ${result.faceCount} 张人脸`);
  98. }
  99. }
  100. } catch (error) {
  101. console.error('人脸检测 API 调用失败:', error);
  102. // 检测失败时,假设没有人脸
  103. handleResults({
  104. hasFace: false,
  105. faceCount: 0
  106. });
  107. }
  108. }, 'image/jpeg', 0.8); // 使用 JPEG 格式,质量 0.8 以平衡质量和大小
  109. } catch (error) {
  110. console.error('人脸检测错误:', error);
  111. }
  112. }, [handleResults]);
  113. const startDetection = useCallback(async () => {
  114. if (!enabled || isDetecting) return;
  115. try {
  116. // 请求摄像头权限(如果失败,不影响手动启动功能)
  117. const stream = await navigator.mediaDevices.getUserMedia({
  118. video: {
  119. width: { ideal: 640 },
  120. height: { ideal: 480 },
  121. facingMode: 'user'
  122. }
  123. }).catch((error): MediaStream | null => {
  124. // 摄像头不可用或用户拒绝权限时,静默失败,不影响手动启动
  125. console.warn('摄像头不可用,将使用手动启动模式:', error.message);
  126. return null;
  127. });
  128. if (!stream) {
  129. // 摄像头不可用,不启动检测,但也不报错
  130. setIsDetecting(false);
  131. return;
  132. }
  133. streamRef.current = stream;
  134. setVideoStream(stream);
  135. // 创建隐藏的 video 元素用于处理视频流(用于人脸检测)
  136. if (!videoRef.current) {
  137. const video = document.createElement('video');
  138. video.style.position = 'fixed';
  139. video.style.top = '-9999px';
  140. // 设置合适的尺寸以确保检测正常工作(不能太小)
  141. video.style.width = '640px';
  142. video.style.height = '480px';
  143. video.setAttribute('playsinline', 'true');
  144. video.setAttribute('autoplay', 'true');
  145. video.setAttribute('muted', 'true');
  146. video.setAttribute('webkit-playsinline', 'true');
  147. document.body.appendChild(video);
  148. videoRef.current = video;
  149. }
  150. const video = videoRef.current;
  151. video.srcObject = stream;
  152. // 等待视频元数据加载
  153. await new Promise<void>((resolve) => {
  154. const onLoadedMetadata = () => {
  155. video.removeEventListener('loadedmetadata', onLoadedMetadata);
  156. console.log(`视频尺寸: ${video.videoWidth}x${video.videoHeight}`);
  157. resolve();
  158. };
  159. video.addEventListener('loadedmetadata', onLoadedMetadata);
  160. video.play().catch(console.error);
  161. });
  162. // 如果存在显示用的video元素,也设置流
  163. if (displayVideoRef.current) {
  164. displayVideoRef.current.srcObject = stream;
  165. await displayVideoRef.current.play();
  166. }
  167. // 设置检测状态
  168. setIsDetecting(true);
  169. // 等待一帧确保状态更新
  170. await new Promise(resolve => requestAnimationFrame(resolve));
  171. // 开始定期检测
  172. console.log('开始人脸检测循环...');
  173. detectionIntervalRef.current = setInterval(() => {
  174. if (isDetectingRef.current) {
  175. detectFaces();
  176. }
  177. }, DETECTION_INTERVAL);
  178. console.log('人脸检测启动成功(使用 UniFace 后端)');
  179. } catch (error) {
  180. // 摄像头不可用或检测失败时,静默失败,不影响手动启动功能
  181. console.warn('启动人脸检测失败(将使用手动启动模式):', error);
  182. setIsDetecting(false);
  183. // 清理资源
  184. if (streamRef.current) {
  185. streamRef.current.getTracks().forEach(track => track.stop());
  186. streamRef.current = null;
  187. }
  188. setVideoStream(null);
  189. }
  190. }, [enabled, isDetecting, detectFaces]);
  191. // 停止检测循环(但不释放资源)
  192. const stopDetection = useCallback(() => {
  193. // 停止检测间隔
  194. if (detectionIntervalRef.current) {
  195. clearInterval(detectionIntervalRef.current);
  196. detectionIntervalRef.current = null;
  197. }
  198. // 重置状态
  199. setIsDetecting(false);
  200. setFaceDetected(false);
  201. setFaceCount(0);
  202. }, []);
  203. // 完全停止检测并释放所有资源
  204. const disposeDetection = useCallback(() => {
  205. // 停止检测间隔
  206. if (detectionIntervalRef.current) {
  207. clearInterval(detectionIntervalRef.current);
  208. detectionIntervalRef.current = null;
  209. }
  210. if (streamRef.current) {
  211. streamRef.current.getTracks().forEach(track => track.stop());
  212. streamRef.current = null;
  213. }
  214. if (videoRef.current) {
  215. videoRef.current.srcObject = null;
  216. if (videoRef.current.parentNode) {
  217. videoRef.current.parentNode.removeChild(videoRef.current);
  218. }
  219. videoRef.current = null;
  220. }
  221. if (displayVideoRef.current) {
  222. displayVideoRef.current.srcObject = null;
  223. }
  224. if (canvasRef.current) {
  225. canvasRef.current = null;
  226. }
  227. setVideoStream(null);
  228. setIsDetecting(false);
  229. setFaceDetected(false);
  230. setFaceCount(0);
  231. }, []);
  232. useEffect(() => {
  233. if (enabled && !isDetecting) {
  234. startDetection();
  235. } else if (!enabled && isDetecting) {
  236. // 如果禁用检测,完全释放资源
  237. disposeDetection();
  238. }
  239. return () => {
  240. // 组件卸载时完全释放资源
  241. disposeDetection();
  242. };
  243. // eslint-disable-next-line react-hooks/exhaustive-deps
  244. }, [enabled]);
  245. // 设置显示用的video元素引用
  246. const setDisplayVideoRef = useCallback((ref: HTMLVideoElement | null) => {
  247. displayVideoRef.current = ref;
  248. if (ref && streamRef.current) {
  249. ref.srcObject = streamRef.current;
  250. ref.play().catch(console.error);
  251. }
  252. }, []);
  253. return {
  254. isDetecting,
  255. faceDetected,
  256. faceCount,
  257. videoStream,
  258. displayVideoRef: displayVideoRef.current,
  259. setDisplayVideoRef,
  260. startDetection,
  261. stopDetection,
  262. disposeDetection
  263. };
  264. }