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

WebRTC调用setRemoteDescription报错:Called in wrong state: stable

WebRTC调用setRemoteDescription报错:Called in wrong state: stable

我看了你遇到的问题,这个错误通常是因为你的RTCPeerConnection已经处于stable状态(也就是连接已经成功建立),但你又尝试调用setRemoteDescription去设置一个新的远程描述,这在WebRTC的状态机里是不允许的。结合你的场景(同一个窗口开两个标签页点击Start Call都报错)和代码来看,大概率是你的信号交互逻辑出现了重复触发的问题。

你遇到的错误信息

test/:1 Uncaught (in promise) InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable

你的HTML代码

<!DOCTYPE html>
<html>

<head>
  <style>
    button {
      border: 0;
      background-color: orange;
      padding: 10px;
      color: white;
      border-radius: 7px;
    }

    video {
      border-radius: 15px;
    }

    .videoContainer {
      display: flex;
      margin: 20px;
      width: 640px;
    }

    .videoContainer h2 {
      color: white;
      position: relative;
      bottom: -380px;
      left: -350px;
      width: max-content;
    }

    #meet {
      display: flex;
    }

    #recordButton.recording {
      background-color: red;
    }

    #downloadButton {
      background-color: #4caf50;
    }

    button:disabled {
      background-color: #cccccc;
      cursor: not-allowed;
    }
  </style>
  <title>A Free Bird Video Call</title>
  <script src="https://meet.jit.si/external_api.js"></script>
</head>
{% load static %}
<body>
  <div id="meet">
    <button id="videoOff" onclick="toggleVideoOff()" disabled>Video Off</button>
    <button id="audioMute" onclick="toggleAudioMute()" disabled>Mute Audio</button>
    <div id="remote-videos">
      <div id="videoContainer">
        <video id="localVideo" autoplay playsinline></video>
        <h2>{{ request.user.full_name }}</h2>
      </div>
    </div>
    <!-- <div class="videoContainer">
      <video id="remoteVideo" autoplay playsinline></video>
      <h2></h2>
    </div> -->
  </div>

  <div>
    <button onclick="startCall()">Start Call</button>
    <button id="recordButton" onclick="toggleRecording()" disabled>
      Start Recording
    </button>
    <button id="downloadButton" onclick="downloadRecording()" disabled>
      Download Recording
    </button>
    <button id="shareScreenButton" onclick="shareScreen()">Share Screen</button>
  </div>
  <div id="chat">
    <h3>Chat</h3>
    <div id="messages">
      <ul id="messageList"></ul>
      <input type="text" id="messageInput" placeholder="Type your message here...">
      <button id="sendMessageButton">Send</button>
    </div>
  </div>
  <input type="hidden" id="user_id" value="{{ request.user.id }}">
<script src="{% static 'js/video_call.js' %}" type="text/javascript" data-room-name="{{ room_name }}"></script>
</body>

</html>

你的JavaScript代码

var mapPeers = {}

var userId = document.getElementById('user_id').value;
console.log('User ID:', userId);
var ws;
function webSocketOnMessage(e) {
    var parsedData = JSON.parse(e.data);

    var peerUsername = parsedData['peer'];
    var action = parsedData['action'];
    
    var receiver_channel_name = parsedData['message']['receiver_channel_name'];

    if (action == 'new-peer') {
        createOfferer(peerUsername, receiver_channel_name);
        return;
    }

    if (action == 'new-offer') {
        var offer = parsedData['message']['sdp'];
        createAnswerer(offer, peerUsername, receiver_channel_name);
        return;
    }

    if (action == 'new-answer') {
        var answer = parsedData['message']['sdp'];
        
        var peer = mapPeers[peerUsername][0];
        peer.setRemoteDescription(answer);
        return;
    }
}

function startCall() {
    var loc = window.location;
    var wsStart = 'ws://';
    if (loc.protocol === 'https:') {
        wsStart = 'wss://';
    }
    var roomName = document.querySelector('script[src*="video_call.js"]').dataset.roomName;
    var endpoint = wsStart + loc.host + '/ws/webrtc/' + roomName + '/';
    ws = new WebSocket(endpoint);

    ws.addEventListener('open', (e) => {
        console.log('Connected to WebSocket server.');
        sendSignal('new-peer', {});
    });
    ws.addEventListener('message', webSocketOnMessage);

    ws.addEventListener('error', (e) => {
        console.log('WebSocket error:', e);
    });

    ws.addEventListener('close', (e) => {
        console.log('WebSocket connection closed:', e);
    });
}

var localStream = new MediaStream();

const constraints = {
    'video': true,
    'audio': true
}

const localVideo = document.getElementById('localVideo');

var videoOffButton = document.getElementById('videoOff');
var audioMuteButton = document.getElementById('audioMute');

var userMedia = navigator.mediaDevices.getUserMedia(constraints)
    .then((mediaStream) => {
        localStream = mediaStream;
        localVideo.srcObject = localStream;
        localVideo.muted = true;

        var audioTracks = localStream.getAudioTracks();
        var videoTracks = localStream.getVideoTracks();

        audioTracks[0].enabled = true;  
        videoTracks[0].enabled = true;

        videoOffButton.addEventListener('click', () => {
            videoTracks[0].enabled = !videoTracks[0].enabled;
            videoOffButton.textContent = videoTracks[0].enabled ? 'Video Off' : 'Video On';
        });

        audioMuteButton.addEventListener('click', () => {
            audioTracks[0].enabled = !audioTracks[0].enabled;
            audioMuteButton.textContent = audioTracks[0].enabled ? 'Mute Audio' : 'Unmute Audio';
        });
    })
    .catch((err) => {
        console.error("Error accessing media devices:", err);
    });

function sendSignal(action, message) {
    var jsonStr = JSON.stringify({
    'peer': userId,
    'action': action,
    'message': message,
    })
    ws.send(jsonStr);
}

function createOfferer(peerUsername, receiver_channel_name) {
    var peer = new RTCPeerConnection(null);

    addLocalTracks(peer);

    var dc = peer.createDataChannel('channel');
    dc.addEventListener('open', () => {
        console.log('Data channel is open');
    });
    dc.addEventListener('message', dcOnMessage);

    var remoteVideo = createVideo(peerUsername);
    setOnTrack(peer, remoteVideo);

    mapPeers[peerUsername] = [peer, dc];

    peer.addEventListener('iceconnectionstatechange', () => {
        var iceConnectionState = peer.iceConnectionState;
        if (iceConnectionState === 'disconnected' || iceConnectionState === 'failed' || iceConnectionState === 'closed') {
            delete mapPeers[peerUsername];
            if (iceConnectionState != 'closed') {   
                peer.close();
            }
            removeVideo(remoteVideo);
        }
    });

    peer.addEventListener('icecandidate', (event) => {
        if (event.candidate) {
            console.log("Sending ICE candidate", JSON.stringify(peer.localDescription));
            return;
        }
        sendSignal('new-offer', {
            'sdp': peer.localDescription,
            'receiver_channel_name': receiver_channel_name,
        });
    });

    peer.createOffer()
    .then((offer) => {
        peer.setLocalDescription(offer);
    })
    .then(() => {
        console.log('local description set successfully');
    });
}

function createAnswerer(offer, peerUsername, receiver_channel_name) {
    var peer = new RTCPeerConnection({
        'iceServers': [
            {'urls': 'stun:stun.l.google.com:19302'},
            {'urls': 'stun:stun1.l.google.com:19302'},
            {'urls': 'turn:turn.brii.me:5349?transport=udp', 'credential': 'brii', 'username': 'brii'},
            {'urls': 'turn:turn.anyfirewall.com:443?transport=tcp', 'credential': 'webrtc', 'username': 'webrtc'},
        ]
    });

    addLocalTracks(peer);

    var remoteVideo = createVideo(peerUsername);
    setOnTrack(peer, remoteVideo);

    peer.addEventListener('datachannel', (event) => {
        peer.dc = event.channel;
        peer.dc.addEventListener('open', () => {
            console.log('Data channel is open');
        });
        peer.dc.addEventListener('message', dcOnMessage);
        mapPeers[peerUsername] = [peer, peer.dc];
    });

    peer.addEventListener('iceconnectionstatechange', () => {
        var iceConnectionState = peer.iceConnectionState;
        if (iceConnectionState === 'disconnected' || iceConnectionState === 'failed' || iceConnectionState === 'closed') {
            delete mapPeers[peerUsername];
            if (iceConnectionState != 'closed') {   
                peer.close();
            }
            removeVideo(remoteVideo);
        }
    });

    peer.addEventListener('icecandidate', (event) => {
        if (event.candidate) {
            console.log("Sending ICE candidate", JSON.stringify(peer.localDescription));
            return;
        }
        sendSignal('new-answer', {
            'sdp': peer.localDescription,
            'receiver_channel_name': receiver_channel_name,
        });
    });

    peer.setRemoteDescription(offer)
    .then(() => {
        console.log('remote description set successfully');
        return peer.createAnswer();
    })
    .then((answer) => {
        peer.setLocalDescription(answer);
    });
}

// 注:代码中缺失addLocalTracks、createVideo等辅助函数,需确保这些函数已正确实现

具体修复建议

  1. 过滤自身信号
    在WebSocket消息处理开头,先判断信号是否是自己发送的,如果是直接跳过,避免给自己创建连接:

    function webSocketOnMessage(e) {
        var parsedData = JSON.parse(e.data);
        var peerUsername = parsedData['peer'];
        
        // 跳过自己发送的信号
        if (peerUsername === userId) {
            return;
        }
        
        // 原有逻辑...
    }
    
  2. 避免重复创建Peer连接
    在创建offer或answer之前,先检查是否已经存在对应peer的连接,存在则直接返回:

    function createOfferer(peerUsername, receiver_channel_name) {
        if (mapPeers[peerUsername]) {
            console.log('已存在与', peerUsername, '的连接,跳过重复创建');
            return;
        }
        // 原有代码...
    }
    
    function createAnswerer(offer, peerUsername, receiver_channel_name) {
        if (mapPeers[peerUsername]) {
            console.log('已存在与', peerUsername, '的连接,跳过重复创建');
            return;
        }
        // 原有代码...
    }
    
  3. 检查连接状态后再设置远程描述
    在处理new-answer时,先判断peer的状态,如果已经处于stable就跳过:

    if (action == 'new-answer') {
        var answer = parsedData['message']['sdp'];
        
        if (!mapPeers[peerUsername]) {
            console.warn('未找到与', peerUsername, '的连接');
            return;
        }
        var peer = mapPeers[peerUsername][0];
        
        // 连接已稳定,无需重复设置
        if (peer.connectionState === 'stable' || peer.signalingState === 'stable') {
            console.log('连接已处于稳定状态,跳过setRemoteDescription');
            return;
        }
    
        peer.setRemoteDescription(answer);
        return;
    }
    

这个错误的核心就是重复触发了连接建立流程,导致在连接稳定后又尝试设置远程描述,按照上面的思路调整信号逻辑,应该就能解决问题了。

备注:内容来源于stack exchange,提问作者Hoko L

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 03:20:32