You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ReactJS中Webcam直播时浏览器冻结问题排查求助

人脸识别应用浏览器无报错冻结问题排查

我正在开发一个简单的人脸识别应用,通过Webcam实时捕获帧并进行人脸检测,但运行一段时间后浏览器无报错冻结。请问该问题是否与Webcam流相关,或是存在其他问题?

相关源码

import './App.css';

import React, { useEffect, useRef,useState } from 'react';
import * as faceapi from 'face-api.js';
import axios from 'axios';

function App() {
  
  const videoRef = useRef();
  let isDetectionInProgress = false;

  const loadModels = async () =>{
    try{
      await faceapi.nets.tinyFaceDetector.loadFromUri('/model')
    }catch(error){
      console.log(error)
    }
  }
  useEffect(() => {
    
    loadModels();

    const init = async () => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true });
        videoRef.current.srcObject = stream;
      } catch (error) {
        console.error('Error accessing webcam:', error);
      }
    };
  
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      init();
    } else {
      console.error('Error accessing webcam:');
    }
    
    const intervalId = setInterval(() => {
      if (!isDetectionInProgress) {
        emitFrame();
      }
    }, 500);

    const hardRefreshIntervalId = setInterval(() => {
      // This code will be executed every hour (3600000 milliseconds)
      window.location.reload(true);
    }, 3600000);
  

    return () => {

      if (videoRef.current.srcObject) {
        const tracks = videoRef.current.srcObject.getTracks();
        tracks.forEach((track) => track.stop());
        videoRef.current.srcObject = null;
      }

      clearInterval(intervalId);
      clearInterval(hardRefreshIntervalId)
      console.log("App unmounted")
    };
  }, []);

  const emitFrame = async () => {
    isDetectionInProgress = true
    if (videoRef.current?.srcObject?.getVideoTracks()[0]) {
      
      let imageCapture = new ImageCapture(videoRef.current.srcObject.getVideoTracks()[0]);
      
      if (faceapi.nets.tinyFaceDetector.isLoaded) {
        let blob = null;
        try {
          
          blob = await imageCapture.takePhoto();
    
          if ("size" in blob) {
            
            
            let imageUrl = null;
            let image = null;
            try{
              imageUrl = URL.createObjectURL(blob);
              image = new Image();
              image.src = imageUrl;
              const faces =  await faceapi.detectAllFaces(image, new faceapi.TinyFaceDetectorOptions({}));
              if(faces && faces.length>0 && image){
              
                const formData = new FormData();
                formData.append('blob', blob, 'frame.png'); 
                
                const now = new Date();
                const isoFormattedTime = now.toISOString(); 
                formData.append('isoFormattedTime', isoFormattedTime);
        
                axios
                .post('http://127.0.0.1:5000/backend/detect_face', formData,{
                  headers: {
                    'Content-Type': 'multipart/form-data', 
                  },
                })
                .then((response) => {
                  if (response.status === 200) {
     
                    const speechUtterance = new SpeechSynthesisUtterance(response.data?.message);
                    window.speechSynthesis.speak(speechUtterance);
                    console.log(response.data?.message);
                    console.log(response.data?.request_time,response.data?.response_time);
                  } else {
                    console.log('No face Detected', response.data?.message);
                  }
                })
                .catch((error) => {
                  console.error('Error during api response :', error);
                });
                
              }
            }catch(error){
              console.log(error)
            }
            finally{
              URL.revokeObjectURL(imageUrl);
              image = null
            }
            
          }
        } catch (error) {
          console.log("imageCapture",imageCapture)
          console.error('Error during face detection:', error);
        } finally{
          blob = null
        }
      }
      imageCapture = null
    }
    isDetectionInProgress = false
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
    <h1 style={{ color: 'red', fontSize: '24px' }}>Ixora Solution Limited AI Front Desk Assistance</h1>
    <video ref={videoRef} autoPlay></video>
  </div>
  );
}

export default App;

问题根源分析

冻结问题和Webcam流本身无关,核心是内存泄漏和异步任务管控失效:

  • 任务并发失控:用普通变量isDetectionInProgress控制并发,React组件重渲染时变量会被重置,导致多个emitFrame任务同时执行,异步操作堆积占满主线程。
  • 内存泄漏风险:axios请求无取消机制,组件卸载或请求堆积时会残留未完成请求;ImageCapture、blob等对象在异常场景下可能未被正确回收;URL.createObjectURL存在遗漏回收的可能。
  • 定时器与异步任务冲突:500ms定时器固定触发任务,若某次检测或API请求耗时超过500ms,会导致任务持续堆积,最终拖垮浏览器。

修复方案

1. 用React状态管控并发

将普通变量改为useState,确保状态在组件生命周期内稳定:

const [isDetectionInProgress, setIsDetectionInProgress] = useState(false);

const emitFrame = async () => {
  if (isDetectionInProgress) return;
  setIsDetectionInProgress(true);
  // 原有检测逻辑
  setIsDetectionInProgress(false);
};

2. 优化资源回收与请求管控

  • 给axios请求添加取消令牌,避免组件卸载后请求继续执行:
// 在useEffect中创建取消令牌
const source = axios.CancelToken.source();

// 请求时配置令牌
axios.post('http://127.0.0.1:5000/backend/detect_face', formData,{
  headers: { 'Content-Type': 'multipart/form-data' },
  cancelToken: source.token
})

// 组件卸载时取消请求
return () => {
  source.cancel('组件卸载,取消未完成请求');
  // 原有清理逻辑
};
  • 确保图片加载完成后再执行检测:
image.onload = async () => {
  const faces = await faceapi.detectAllFaces(image, new faceapi.TinyFaceDetectorOptions({}));
  // 后续人脸处理逻辑
};

3. 改用requestAnimationFrame替代定时器

贴合浏览器渲染周期,避免任务堆积:

useEffect(() => {
  let animationId;
  const detectLoop = async () => {
    if (!isDetectionInProgress) {
      await emitFrame();
    }
    animationId = requestAnimationFrame(detectLoop);
  };
  detectLoop();
  return () => {
    cancelAnimationFrame(animationId);
    // 原有清理逻辑
  };
}, [isDetectionInProgress]);

4. 简化帧捕获逻辑

用canvas绘制video帧替代ImageCapture,兼容性更好:

const canvas = document.createElement('canvas');
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));

内容的提问来源于stack exchange,提问作者sultan rayhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 20:43:24