Peer.js+React视频聊天远程流黑屏问题排查求助
Peer.js + React 视频聊天远程流黑屏问题解决方案
问题描述
使用Peer.js和React开发简易视频聊天应用,呼叫接通后两端接收到的远程视频流均黑屏,尽管已将视频元素的srcObject设置为该流。排查细节:
- 打印远程流及其轨道,得到包含音频和视频
MediaStreamTrack的MediaStream对象,但视频轨道的capabilities仅含设备ID和空的facingMode数组,无宽高比或流尺寸信息。 - 监听
srcObject设置事件时,仅触发loadstart,loadedmetadata、loadeddata和canplay均未触发,readyState始终为0。
问题代码
const vidSenderRef = useRef(null); const vidReceiverRef = useRef(null); const containerRef = useRef(null); const [user, setUser] = useContext(userContext); const [me, setMe] = useState(null); const [stream, setStream] = useState(null); const [isCalling, setIsCalling] = useState(false); async function askUserMedia() { let stream = null; try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", }, audio: false }) } catch (err) { if (err.message === "Device in use") { const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(device => device.label.includes("OBS")); const video = videoDevices[0].deviceId; stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: video }, facingMode: "user", }, audio: false }) } else { toastError("Error" + err.message) } } setStream(stream); vidSenderRef.current.srcObject = stream; } function assignPeer() { const peer = new Peer({ host: "localhost", port: 9000, path: "/", secure: true, // debug: 3, config: { 'iceServers': [ { url: 'stun:stun01.sipphone.com' }, { url: 'turn:192.158.29.39:3478?transport=tcp', credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', username: '28224511:1379330808' }, { url: 'turn:turn.bistri.com:80', credential: 'homeo', username: 'homeo' }, ] }, }) return peer; } async function callUser(ID) { const call = me.call(ID, stream); call.on("stream", (userVideoStream) => { vidReceiverRef.current.srcObject = userVideoStream; console.log(userVideoStream.getVideoTracks()[0].getCapabilities()); vidReceiverRef.current.play() }); setIsCalling(true); } async function answerCall() { me.on("call", (call) => { call.answer(stream); call.on("stream", (userVideoStream) => { vidReceiverRef.current.srcObject = userVideoStream; // vidReceiverRef.current.play(); }); }); setIsCalling(true); } useEffect(() => { Cookies.set("currentRoom", "00ewRc"); Socket.emit("join", { sender: user.ID, receiver: user.ID === 1 ? 2 : 1, }); askUserMedia(); setMe(assignPeer()); }, []); useEffect(() => { // caller side if (!me || !stream) return; if (props.caller === user.ID) { Socket.emit("handshake", { caller: user.ID, callee: props.callee, isCallerHandshake: true, room: Cookies.get("currentRoom") }); } // callee side if (props.callee === user.ID && !isCalling) { answerCall(); } Socket.on("handshake", (data) => { if (data.isCallerHandshake && data.callee === user.ID) { Socket.emit("handshake", { caller: data.caller, callee: user.ID, isCalleeHandshake: true, ID: me.id, room: Cookies.get("currentRoom") }) } else if (data.isCalleeHandshake && data.caller === user.ID && !isCalling) { callUser(data.ID); } }) }, [me, stream]); useEffect(() => { vidReceiverRef.current.play(); }, [])
修复步骤
1. 等待Peer实例完全初始化
Peer初始化是异步操作,需等待其获取到服务器分配的ID后再使用,否则会导致呼叫逻辑异常:
function assignPeer() { return new Promise((resolve) => { const peer = new Peer({ host: "localhost", port: 9000, path: "/", secure: window.location.protocol === "https:", // 随当前协议自动切换 config: { 'iceServers': [ { urls: 'stun:stun.l.google.com:19302' }, // 替换为稳定的公共STUN服务器 // 替换为有效的TURN服务器(若需要) { urls: 'turn:your-turn-server.com:3478', credential: 'your-credential', username: 'your-username' } ] }, }); peer.on('open', (id) => resolve(peer)); peer.on('error', (err) => { toastError("Peer初始化失败: " + err.message); resolve(null); }); }); } // 修改初始化useEffect useEffect(() => { Cookies.set("currentRoom", "00ewRc"); Socket.emit("join", { sender: user.ID, receiver: user.ID === 1 ? 2 : 1, }); askUserMedia(); // 异步初始化Peer assignPeer().then(peerInstance => { if(peerInstance) setMe(peerInstance); }); }, []);
2. 修复重复绑定的呼叫监听
原answerCall每次调用都会新增call事件监听器,导致重复处理。改为单次绑定并在组件卸载时清理:
useEffect(() => { if (!me || !stream) return; const handleIncomingCall = (call) => { call.answer(stream); call.on("stream", (userVideoStream) => { if (!vidReceiverRef.current) return; vidReceiverRef.current.srcObject = userVideoStream; // 等待元数据加载完成后再播放 vidReceiverRef.current.onloadedmetadata = () => { vidReceiverRef.current.play().catch(err => { console.error("自动播放失败,请手动触发:", err); }); }; }); }; me.on("call", handleIncomingCall); // 组件卸载时清理监听器 return () => me.off("call", handleIncomingCall); }, [me, stream]);
3. 安全操作视频元素
操作视频元素前先判断是否存在,避免空指针错误,并处理播放异常:
// 修改callUser中的流处理逻辑 async function callUser(ID) { const call = me.call(ID, stream); call.on("stream", (userVideoStream) => { if (!vidReceiverRef.current) return; vidReceiverRef.current.srcObject = userVideoStream; vidReceiverRef.current.onloadedmetadata = () => { vidReceiverRef.current.play().catch(err => { console.error("播放失败:", err); }); }; }); setIsCalling(true); }
4. 删除无效的播放钩子
最后一个直接调用vidReceiverRef.current.play()的useEffect无意义,此时srcObject尚未设置,直接删除即可。
验证修复
完成修改后重新测试:
- 确认Peer实例成功获取ID后再发起呼叫
- 检查远程流的
readyState是否变为1(HAVE_METADATA)或更高 - 确认视频元素的
loadedmetadata事件正常触发
内容的提问来源于stack exchange,提问作者Hải Đỗ
相关产品推荐
相关产品推荐

