WebRTC问题:发起端无法接收音视频流
WebRTC完美协商模式发起端无法接收远程流问题排查与修复
我尝试基于WebSocket信令实现MDN文档中的WebRTC完美协商模式,建立双向视频连接。目前遇到的问题是:发起端无法接收远程音视频流,但接收端可以正常显示本地和远程流。已搭建CoTurn STUN/TURN服务器,问题仍未解决。相关代码如下:
var peerConnectionConfig = { 'iceServers': [ {'urls': 'turn:my.turn.server:3478', "username": "foo", "credential": "bar" } ] }; let localPeerConnection, remotePeerConnection; let localStream; let hangupSent = false; let isCaller = false; const localVideo = document.getElementById('localVideo'); const remoteVideo = document.getElementById('remoteVideo'); const startButton = document.getElementById('startButton'); const hangupButton = document.getElementById('hangupButton'); // Signaling WebSocket connection const signalingSocket = new WebSocket('wss://' + window.location.hostname); signalingSocket.addEventListener('message', handleSignalingData); // Set up user media and create peer connections navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: true }).then(gotLocalMediaStream).catch(handleError); startButton.addEventListener('click', startCall); hangupButton.addEventListener('click', hangUp); function gotLocalMediaStream(mediaStream) { localStream = mediaStream; localVideo.srcObject = mediaStream; localVideo.setAttribute("playsinline", true); remoteVideo.setAttribute("playsinline", true); // Create local peer connection localPeerConnection = new RTCPeerConnection(peerConnectionConfig); localPeerConnection.addEventListener('icecandidate', handleConnection); localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange); // Create remote peer connection remotePeerConnection = new RTCPeerConnection(peerConnectionConfig); remotePeerConnection.addEventListener('icecandidate', handleConnection); remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange); remotePeerConnection.addEventListener('track', gotRemoteMediaStream); // Add tracks to the local peer connection mediaStream.getTracks().forEach(track => { localPeerConnection.addTrack(track, mediaStream); }); } function startCall() { // Create offer and set local description isCaller = true; localPeerConnection.createOffer({ offerToReceiveAudio: 1, offerToReceiveVideo: 1 }) .then(offer => { return localPeerConnection.setLocalDescription(offer) }) .then(() => { // Send offer to the remote peer signalingSocket.send(JSON.stringify({ 'offer': localPeerConnection.localDescription })); }) .catch(handleError); } function hangUp() { // Send hangup signal if (!hangupSent) { // Send hangup signal only if it hasn't been sent before signalingSocket.send(JSON.stringify({ 'hangup': true })); hangupSent = true; // Set the flag to true after sending the hangup signal } localPeerConnection.close(); remotePeerConnection.close(); localPeerConnection = null; remotePeerConnection = null; } function handleConnection(event) { const peerConnection = event.target; const iceCandidate = event.candidate; if (iceCandidate) { const newIceCandidate = new RTCIceCandidate(iceCandidate); const otherPeer = getOtherPeer(peerConnection); // Send ICE candidate to the remote peer signalingSocket.send(JSON.stringify({ 'iceCandidate': newIceCandidate })); } } function gotRemoteMediaStream(event) { console.log("gotRemoteMediaStream") const mediaStream = event.streams[0]; remoteVideo.srcObject = mediaStream; // remoteVideo.setAttribute("playsinline", true); // localVideo.style.display = 'none'; } function handleConnectionChange(event) { const peerConnection = event.target; console.log(`ICE state change event: ${peerConnection.iceConnectionState}`); } function handleSignalingData(event) { const data = JSON.parse(event.data); if (data.offer) { // this is for the side receiving the call if (isCaller) { return } isCaller = false console.log("got offer") remotePeerConnection.setRemoteDescription(new RTCSessionDescription(data.offer)) .then(() => remotePeerConnection.createAnswer()) .then(answer => remotePeerConnection.setLocalDescription(answer)) .then(() => { signalingSocket.send(JSON.stringify({ 'answer': remotePeerConnection.localDescription })); }) .catch(e => { console.error("on receiving side") console.error(e.toString()) }); } else if (data.answer) { // this is for the side initiating the call console.log("got answer") if (!isCaller) { return } // Handle answer from the remote peer console.log(data.answer) localPeerConnection.setRemoteDescription(new RTCSessionDescription(data.answer)) .catch(e => { console.error("on initiating side") console.error(e.toString()) }); } else if (data.iceCandidate) { // Handle ICE candidate from the remote peer localPeerConnection.addIceCandidate(new RTCIceCandidate(data.iceCandidate)) .then(() => { }) .catch(e => { console.error("on ice candidate") console.error(isCaller) console.error(e.toString()) }); } else if (data.hangup) { // Handle hangup signal hangUp(); } } function getOtherPeer(peerConnection) { return (peerConnection === localPeerConnection) ? remotePeerConnection : localPeerConnection } function handleError(error) { console.error(`Error: ${error}`) }
核心问题分析
PeerConnection实例冗余与命名混乱
每个客户端仅需创建一个RTCPeerConnection实例,而非同时初始化localPeerConnection和remotePeerConnection。当前代码中两端都创建了两个连接实例,导致流的接收逻辑完全错位。发起端未监听
track事件
接收端的remotePeerConnection绑定了track事件来处理远程流,但发起端的localPeerConnection未添加该事件监听,因此无法触发gotRemoteMediaStream获取远程流。ICE候选处理逻辑错误
收到远程ICE候选时,代码统一往localPeerConnection中添加,未区分当前客户端是发起端还是接收端,导致接收端的ICE候选无法正确关联到对应的连接实例。
修复后的关键代码调整
1. 简化PeerConnection初始化
每个客户端仅保留一个peerConnection实例:
let peerConnection; // 移除localPeerConnection和remotePeerConnection的定义
2. 为所有端的PeerConnection添加track监听
在获取本地流后创建PeerConnection时,统一添加track事件:
function gotLocalMediaStream(mediaStream) { localStream = mediaStream; localVideo.srcObject = mediaStream; localVideo.setAttribute("playsinline", true); remoteVideo.setAttribute("playsinline", true); // 仅创建一个PeerConnection实例 peerConnection = new RTCPeerConnection(peerConnectionConfig); peerConnection.addEventListener('icecandidate', handleConnection); peerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange); // 所有端都监听track事件以接收远程流 peerConnection.addEventListener('track', gotRemoteMediaStream); // 添加本地轨道到PeerConnection mediaStream.getTracks().forEach(track => { peerConnection.addTrack(track, mediaStream); }); }
3. 修正ICE候选处理逻辑
收到远程ICE候选时,直接添加到当前客户端的PeerConnection实例:
function handleSignalingData(event) { const data = JSON.parse(event.data); if (data.offer) { if (isCaller) { return } isCaller = false; console.log("收到Offer"); peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer)) .then(() => peerConnection.createAnswer()) .then(answer => peerConnection.setLocalDescription(answer)) .then(() => { signalingSocket.send(JSON.stringify({ 'answer': peerConnection.localDescription })); }) .catch(e => { console.error("接收端处理错误"); console.error(e.toString()); }); } else if (data.answer) { if (!isCaller) { return } console.log("收到Answer"); peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer)) .catch(e => { console.error("发起端处理错误"); console.error(e.toString()); }); } else if (data.iceCandidate) { // 将ICE候选添加到当前客户端的PeerConnection peerConnection.addIceCandidate(new RTCIceCandidate(data.iceCandidate)) .catch(e => { console.error("ICE候选处理错误"); console.error(isCaller); console.error(e.toString()); }); } else if (data.hangup) { hangUp(); } }
4. 修正startCall和hangUp逻辑
function startCall() { isCaller = true; peerConnection.createOffer({ offerToReceiveAudio: 1, offerToReceiveVideo: 1 }) .then(offer => peerConnection.setLocalDescription(offer)) .then(() => { signalingSocket.send(JSON.stringify({ 'offer': peerConnection.localDescription })); }) .catch(handleError); } function hangUp() { if (!hangupSent) { signalingSocket.send(JSON.stringify({ 'hangup': true })); hangupSent = true; } peerConnection.close(); peerConnection = null; }
5. 移除冗余的getOtherPeer函数
由于不再需要区分两个PeerConnection实例,该函数可以直接删除。
内容的提问来源于stack exchange,提问作者simone
相关产品推荐
相关产品推荐

