Websocket+JS语音通话报错:setRemoteDescription处于stable状态调用失败
WebRTC语音通话setRemoteDescription报错:Called in wrong state: stable
问题概述
使用WebSocket和JavaScript开发语音通话功能时,客户端Offer阶段触发错误:
Uncaught (in promise) DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable
错误位于client.js第139行,修改createAnswer函数后问题仍未解决。
错误原因分析
这个错误表示调用setRemoteDescription时,RTCPeerConnection处于stable状态——即会话协商已完成,当前没有待处理的Offer/Answer需要确认。结合代码来看,核心问题包括:
- 目标用户变量
sendTo赋值错误:初始化时取按钮的val()(无意义),后续未正确更新为通话目标,导致消息发送/接收逻辑混乱,引发重复协商请求 - 重复设置远端描述:在
client-answer分支中,仅判断pc.localDescription存在就调用setRemoteDescription,未检查当前连接状态,会话稳定后重复调用触发错误 - 事件重复绑定:
sendIceCandidate函数每次调用都会重新绑定onicecandidate和ontrack事件,导致多次触发候选发送,干扰会话状态 - Offer创建逻辑错误:
createOffer中未接收createOffer的返回值,直接使用pc.localDescription,可能导致描述未正确生成
解决方案
1. 修正sendTo变量的赋值逻辑
确保sendTo始终指向当前通话的目标用户,在触发呼叫和接收请求时正确更新:
// 呼叫按钮点击事件:从输入框获取目标用户名 $('#callBtn').on('click' , () =>{ sendTo = callToUsernameInput.value; getCam(); send('is-client-ready', null, sendTo); }) // 收到is-client-ready请求时,将sendTo设为请求发起方 case 'is-client-ready': sendTo = by; if (!pc){ await getConn(); } if (pc.iceConnectionState === "connected"){ send('client-already-oncall', null, sendTo); }else{ // 先解绑再绑定,避免多次绑定同一事件 answerBtn.off('click').on('click' , ()=>{ send('client-is-ready', null, sendTo) }) } break; // 收到client-offer时,将发送方作为目标用户传入createAnswer case 'client-offer': createAnswer(by, data); break;
2. 优化RTCPeerConnection状态检查
在设置远端描述前,检查当前连接的signalingState,避免在stable状态下执行:
// client-answer分支修正 case 'client-answer': // 仅在本地已发送Offer(have-local-offer状态)时设置远端Answer if (pc.signalingState === 'have-local-offer') { await pc.setRemoteDescription(new RTCSessionDescription(data)); } break;
3. 修正createOffer函数逻辑
正确接收createOffer的返回值,且只绑定一次ICE候选事件:
async function createOffer(sendTo){ // 仅绑定一次ICE候选和媒体轨道事件 if (!pc.onicecandidate) { pc.onicecandidate = e =>{ if (e.candidate !== null){ send('client-candidate' , e.candidate, sendTo); } } pc.ontrack = e =>{ remoteAudio.srcObject = e.streams[0]; } } try { const offer = await pc.createOffer(options); await pc.setLocalDescription(offer); send('client-offer', pc.localDescription, sendTo); } catch (error) { console.error('创建Offer失败:', error); } }
4. 修正createAnswer函数逻辑
同样添加状态检查,避免重复处理Offer:
async function createAnswer(sendTo, data) { if (!pc) { await getConn(); } if (!localStream) { await getCam(); } // 仅绑定一次ICE候选和媒体轨道事件 if (!pc.onicecandidate) { pc.onicecandidate = e =>{ if (e.candidate !== null){ send('client-candidate' , e.candidate, sendTo); } } pc.ontrack = e =>{ remoteAudio.srcObject = e.streams[0]; } } try { // 仅在stable状态下处理新的Offer if (pc.signalingState === 'stable') { await pc.setRemoteDescription(new RTCSessionDescription(data)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); send('client-answer', pc.localDescription, sendTo); } } catch (error) { console.error('创建Answer失败:', error); } }
关键注意事项
- 确保RTCPeerConnection的事件(如
onicecandidate、ontrack)仅绑定一次,避免重复触发 - 严格管理会话状态,根据
signalingState判断是否需要执行Offer/Answer操作 - 始终确保
sendTo变量指向正确的通话目标,避免消息发送错误引发的重复协商
内容的提问来源于stack exchange,提问作者Sobhan Hashemi
相关产品推荐
相关产品推荐

