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

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}`) }

核心问题分析

  1. PeerConnection实例冗余与命名混乱
    每个客户端仅需创建一个RTCPeerConnection实例,而非同时初始化localPeerConnection和remotePeerConnection。当前代码中两端都创建了两个连接实例,导致流的接收逻辑完全错位。

  2. 发起端未监听track事件
    接收端的remotePeerConnection绑定了track事件来处理远程流,但发起端的localPeerConnection未添加该事件监听,因此无法触发gotRemoteMediaStream获取远程流。

  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 15:04:57