React+WebRTC视频通话应用ontrack事件未触发,无法传输视频轨道
React + WebRTC + Socket.io 视频通话:ontrack事件未触发问题排查与修复
核心问题分析与修复步骤
1. peer变量未用React状态管理,实例引用丢失
直接用var peer声明在组件内部,React组件重渲染时该变量会被重置,后续事件监听或操作的可能不是同一个RTCPeerConnection实例。
修复:将peer改为React状态:
const [peer, setPeer] = useState(null);
创建RTCPeerConnection时更新状态:
const newPeer = new RTCPeerConnection(configuration); setPeer(newPeer);
2. ontrack事件绑定时机过晚
你在connection-complete回调里才绑定track事件,但此时对方可能已经发送了媒体轨道,事件已触发过,自然监听不到。
修复:创建RTCPeerConnection后立即绑定track事件,发起方和应答方都要执行:
const newPeer = new RTCPeerConnection(configuration); // 立刻绑定track事件 newPeer.addEventListener('track', (event) => { const [remoteStream] = event.streams; setremotestream(remoteStream); }); setPeer(newPeer);
3. 缺失ICE候选交换逻辑
代码仅打印ICE候选,未通过Socket.io发送给对方,对方无法获取你的网络信息,P2P连接无法建立,媒体轨道无法传输。
修复:
- 发起方和应答方创建peer后,监听
icecandidate事件并发送候选:
newPeer.addEventListener('icecandidate', (event) => { if (event.candidate) { socket.emit('send-ice-candidate', event.candidate); } });
- 监听对方发送的ICE候选并添加到本地peer:
// 在useEffect中添加 socket.on('receive-ice-candidate', (candidate) => { if (peer) { peer.addIceCandidate(new RTCIceCandidate(candidate)); } });
4. 发起方addTrack时机错误
发起方先创建Offer再调用addTrack,导致Offer的SDP不包含媒体轨道描述,对方不知道要接收媒体,不会触发ontrack。
修复:发起方先添加轨道,再创建Offer:
// opencamera函数内调整顺序 stream.getTracks().forEach(track => { newPeer.addTrack(track, stream); }); const offer = await newPeer.createOffer(); await newPeer.setLocalDescription(new RTCSessionDescription(offer)); socket.emit('sending-offer', offer);
修复后的完整代码示例
import { useState, useEffect } from "react"; import io from 'socket.io-client' import ReactPlayer from "react-player"; function App() { const [peer, setPeer] = useState(null); const socket = io('http://localhost:4080/') const [email, setEmail] = useState(""); const [room, setRoom] = useState(""); const [recivingcall, setrecivingcall] = useState(false) const [makingcall, setmakingcall] = useState(false) const [mystream, setmystream] = useState('') const [connected, setconnected] = useState(false) const [remotestream, setremotestream] = useState('') const makecall = () => { console.log("email ", email, " room ", room) socket.emit('join-room', { email, room }) } const joincall = () => { } const opencamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); setmystream(stream); const configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] } const newPeer = new RTCPeerConnection(configuration); // 绑定track事件 newPeer.addEventListener('track', (event) => { const [remoteStream] = event.streams; setremotestream(remoteStream); }); // 监听ICE候选并发送 newPeer.addEventListener('icecandidate', (event) => { if (event.candidate) { socket.emit('send-ice-candidate', event.candidate); } }); setPeer(newPeer); // 先添加轨道再创建Offer stream.getTracks().forEach(track => { newPeer.addTrack(track, stream); }); const offer = await newPeer.createOffer(); await newPeer.setLocalDescription(new RTCSessionDescription(offer)); socket.emit('sending-offer', offer); } catch (err) { console.error('获取媒体流失败:', err); } } const creatanswer = async (offer) => { if (!offer) return; try { const configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] } const newPeer = new RTCPeerConnection(configuration); // 绑定track事件 newPeer.addEventListener('track', (event) => { const [remoteStream] = event.streams; setremotestream(remoteStream); }); // 监听ICE候选并发送 newPeer.addEventListener('icecandidate', (event) => { if (event.candidate) { socket.emit('send-ice-candidate', event.candidate); } }); setPeer(newPeer); await newPeer.setRemoteDescription(new RTCSessionDescription(offer)); const ans = await newPeer.createAnswer(); await newPeer.setLocalDescription(new RTCSessionDescription(ans)); socket.emit('sending-answer', ans); const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); setmystream(stream); stream.getTracks().forEach(track => { newPeer.addTrack(track, stream); }); } catch (err) { console.error('创建应答失败:', err); } } const hendelanswer_recived = async (ans) => { if (!ans) return; console.log(ans) const remotedes = new RTCSessionDescription(ans) await peer.setRemoteDescription(remotedes) console.log('peer', peer) socket.emit('connection-complete', 'ok') } useEffect(() => { socket.on('room-created', (ev) => { setconnected(true) opencamera() }) socket.on('offer-recived', (offer) => { console.log(offer) creatanswer(offer) }) socket.on('answer-recived', hendelanswer_recived) // 接收对方的ICE候选 socket.on('receive-ice-candidate', (candidate) => { if (peer) { peer.addIceCandidate(new RTCIceCandidate(candidate)); } }) socket.on('connection-complete', () => { console.log('连接完成'); }) // 组件卸载时清理资源 return () => { socket.disconnect(); if (peer) { peer.close(); } if (mystream) { mystream.getTracks().forEach(track => track.stop()); } } }, [peer, mystream]) return ( <> <input id="email" onChange={(e) => setEmail(e.target.value)}></input> <input id="room" onChange={(e) => setRoom(e.target.value)}></input> {connected ? <h1>Connecting...</h1> : <> </>} {mystream && ( <ReactPlayer playing muted height="100px" width="200px" url={mystream} /> )} <button onClick={makecall}>makecall</button> {remotestream && ( <> <h1>Remote Stream</h1> <ReactPlayer playing muted height="100px" width="200px" url={remotestream} /> </> )} {recivingcall ? <button id="joincall" onClick={joincall}>joincall</button> : <> </>} </> ) } export default App;
额外注意事项
- 确保Socket.io服务器端实现ICE候选转发逻辑,收到
send-ice-candidate后转发给房间内其他用户。 - 增加错误捕获逻辑,避免媒体流获取、PeerConnection操作等环节静默失败。
- 组件卸载时关闭PeerConnection并停止媒体轨道,防止资源泄漏。
内容的提问来源于stack exchange,提问作者mm m
相关产品推荐
相关产品推荐

