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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 17:24:51