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等辅助函数,需确保这些函数已正确实现
具体修复建议
过滤自身信号
在WebSocket消息处理开头,先判断信号是否是自己发送的,如果是直接跳过,避免给自己创建连接:function webSocketOnMessage(e) { var parsedData = JSON.parse(e.data); var peerUsername = parsedData['peer']; // 跳过自己发送的信号 if (peerUsername === userId) { return; } // 原有逻辑... }避免重复创建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; } // 原有代码... }检查连接状态后再设置远程描述
在处理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
相关产品推荐
相关产品推荐

