React摄像头录制上传函数重复调用及分片顺序问题求助
类实时视频录制分片上传问题排查与优化
问题背景
开发类实时视频录制功能,每隔几秒将视频片段上传至云端,使用React实现,组件挂载时启动相机和MediaRecorder,卸载时停止。当前遇到两个核心问题:
uploadToCloudinary上传函数被重复调用两次- 视频分片无法严格按顺序上传,导致后端合并时顺序混乱
同时希望获取低成本实时录制方案推荐。
现有代码
import React, { useRef, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { axiosServerInstance } from "@/utils/api/instances"; import { serverRoutes } from "@/utils/api/routes"; const cloudinaryUploadUrl = "https://api.cloudinary.com/v1_1/-----/video/upload"; const WebCam = () => { const videoRef = useRef(null); const mediaRecorderRef = useRef(null); const recordedChunksRef = useRef([]); const uploadIntervalRef = useRef(null); const [isUploading,setIsUploading]=useState(false) const isComponentMounted = useRef(true); const proctorId = useSelector((state) => state?.assessment?.assessmentTestData?.report?.proctorId); const updateProctorVideoUrl = async (chunks) => { try { await axiosServerInstance.put(serverRoutes.submitVideoUrl, { proctorId, ...chunks, }); } catch (error) { console.error("Error updating proctor:", error.response?.data || error.message); } }; const uploadToCloudinary = async (blob) => { if (!isComponentMounted.current) return; const formData = new FormData(); formData.append("file", blob); formData.append("upload_preset", "-----"); try { const response = await fetch(cloudinaryUploadUrl, { method: "POST", body: formData, }); const data = await response.json(); await updateProctorVideoUrl({ asset_id: data.asset_id, public_id: data.public_id, url: data.url, proctorId, }); } catch (error) { console.error("Error uploading video:", error); } }; const startCamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); if (videoRef.current) { videoRef.current.srcObject = stream; } return stream; } catch (error) { console.error("Error accessing camera:", error); throw error; } }; const startRecording = (stream) => { recordedChunksRef.current = []; const mediaRecorder = new MediaRecorder(stream, { mimeType: "video/mp4" }); mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { if (recordedChunksRef.current.length > 0 && !isUploading) { setIsUploading(true); const blob = new Blob(recordedChunksRef.current, { type: "video/mp4" }); await uploadToCloudinary(blob); recordedChunksRef.current = []; mediaRecorder.start(); setIsUploading(false); } }; mediaRecorder.start(); mediaRecorderRef.current = mediaRecorder; uploadIntervalRef.current = setInterval(() => { if (mediaRecorder.state === "recording" && !isUploading) { mediaRecorder.stop(); } }, 5000); }; useEffect(() => { let stream; const init = async () => { isComponentMounted.current = true; stream = await startCamera(); startRecording(stream); }; init(); return () => { isComponentMounted.current = false; if (uploadIntervalRef.current) { clearInterval(uploadIntervalRef.current); } if (stream) { stream.getTracks().forEach((track) => track.stop()); } if (videoRef.current) { videoRef.current.srcObject = null; } }; }, []); return <video ref={videoRef} autoPlay muted className="w-full h-full object-cover" />; }; export default WebCam;
问题1:上传函数重复调用的修复
原因分析
setInterval与mediaRecorder.onstop的异步时序冲突:setInterval每5秒触发停止录制,而onstop回调中立即重启录制,可能导致两次ondataavailable事件的chunk被重复收集isUploading状态更新是异步的,setIsUploading(true)后无法立即阻断setInterval的判断逻辑,导致重复触发停止录制
修复方案
改用递归setTimeout替代setInterval,确保每次上传完成后再调度下一次分片录制,避免时序重叠:
const startRecording = (stream) => { let mediaRecorder = new MediaRecorder(stream, { mimeType: "video/mp4" }); mediaRecorderRef.current = mediaRecorder; const handleRecordAndUpload = async () => { if (!isComponentMounted.current || isUploading) return; mediaRecorder.stop(); }; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { recordedChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { if (recordedChunksRef.current.length === 0 || isUploading) return; setIsUploading(true); try { const blob = new Blob(recordedChunksRef.current, { type: "video/mp4" }); await uploadToCloudinary(blob); recordedChunksRef.current = []; mediaRecorder.start(); // 上传完成后再调度下一次分片 uploadIntervalRef.current = setTimeout(handleRecordAndUpload, 5000); } catch (error) { console.error("录制分片处理失败:", error); } finally { setIsUploading(false); } }; mediaRecorder.start(); uploadIntervalRef.current = setTimeout(handleRecordAndUpload, 5000); }; // 组件卸载时清理setTimeout useEffect(() => { // ... 原有初始化逻辑 return () => { // ... 原有清理逻辑 if (uploadIntervalRef.current) { clearTimeout(uploadIntervalRef.current); } }; }, []);
问题2:确保分片严格按顺序上传
核心思路
使用上传队列维护待上传分片,只有当前分片上传完成后,才处理队列中的下一个分片,同时给每个分片添加序号,后端可根据序号确认合并顺序。
实现方案
const WebCam = () => { // ... 原有ref和state const uploadQueueRef = useRef([]); const isProcessingQueueRef = useRef(false); const chunkCountRef = useRef(0); // 分片序号计数器 // 队列处理函数 const processUploadQueue = async () => { if (isProcessingQueueRef.current || uploadQueueRef.current.length === 0) return; isProcessingQueueRef.current = true; const { blob, index } = uploadQueueRef.current.shift(); try { await uploadToCloudinary(blob, index); } catch (error) { console.error("分片上传失败,重新加入队列:", error); uploadQueueRef.current.unshift({ blob, index }); // 失败分片放回队列头部 } finally { isProcessingQueueRef.current = false; if (uploadQueueRef.current.length > 0) { processUploadQueue(); // 递归处理下一个分片 } } }; // 上传函数新增分片序号参数 const uploadToCloudinary = async (blob, chunkIndex) => { if (!isComponentMounted.current) return; const formData = new FormData(); formData.append("file", blob); formData.append("upload_preset", "-----"); const response = await fetch(cloudinaryUploadUrl, { method: "POST", body: formData, }); const data = await response.json(); await updateProctorVideoUrl({ asset_id: data.asset_id, public_id: data.public_id, url: data.url, proctorId, chunkIndex // 传递分片序号给后端 }); }; // 修改onstop逻辑,将分片加入队列 mediaRecorder.onstop = async () => { if (recordedChunksRef.current.length === 0 || isUploading) return; setIsUploading(true); try { const blob = new Blob(recordedChunksRef.current, { type: "video/mp4" }); // 加入队列时携带分片序号 uploadQueueRef.current.push({ blob, index: chunkCountRef.current++ }); recordedChunksRef.current = []; mediaRecorder.start(); processUploadQueue(); // 触发队列处理 } catch (error) { console.error("录制分片处理失败:", error); } finally { setIsUploading(false); } }; // ... 其他逻辑 };
低成本实时录制方案推荐
- WebRTC + 云对象存储:用WebRTC捕获媒体流,每隔几秒生成Blob上传至阿里云OSS、腾讯云COS等S3兼容存储,后端定时根据分片序号合并视频,成本低且兼容性好
- MediaRecorder + 云厂商视频处理服务:使用Cloudinary分段上传API、阿里云视频点播等自带分片合并能力的服务,无需自行开发后端合并逻辑,减少开发成本
- 开源WebRTC媒体服务器:如Kurento、Janus,适合需要实时转码、多人互动场景的团队,成本主要为服务器部署费用
内容的提问来源于stack exchange,提问作者Nick S
相关产品推荐
相关产品推荐

