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

如何修复WebRTC JavaScript中'setLocalDescription执行失败'错误?

WebRTC视频通话APP中setLocalDescription报错:Called in wrong state: stable

我正在开发基于WebRTC的视频通话APP,用JavaScript实现时遇到如下错误:

main.js:104 Uncaught (in promise) DOMException: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to set local answer sdp: Called in wrong state: stable

后端使用Flask SocketIO,Socket通信正常,问题出在peerConnection.setRemoteDescription(offer)环节,原始代码如下:

let localStream;
let remoteStream;
let peerConnection;

// const servers = {
//     iceServers: [
//         {
//             urls: ["stun:stun.l.google.com:19302"]
//         }
//     ]
// }

var socket = io.connect("http://localhost:5000");

const urlParams = new URLSearchParams(window.location.search);

roomId = urlParams.get("room");
name = urlParams.get("name");
socket.emit("join-channel", { room: roomId });
socket.on("room-message", function (message) {
    handleMessage(message, roomId);
});

let init = async () => {

    socket.on("MemberJoined", (data) => {
        handleUserJoined(roomId);
    });

    localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
    document.getElementById("vid-1").srcObject = localStream;
}

let handleUserJoined = async (roomId) => {
    console.log('A new user joined the room: ', roomId);
    createOffer(roomId);
}

let handleMessage = async (message, roomId) => {
    message = JSON.parse(JSON.parse(message).text);

    if (message.type === "offer"){
        createAnswer(roomId, message.offer);
    }

    if (message.type === "answer"){
        addAnswer(message.answer);
    }

    if (message.type === "candidate"){
        if (peerConnection){
            peerConnection.addIceCandidate(message.candidate);
        }
    }
}

let createPeerConnection = async (roomId) => {
    peerConnection = new RTCPeerConnection({'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]});

    remoteStream = new MediaStream();
    document.getElementById("vid-2").srcObject = remoteStream;

    if (!localStream) {
        localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
        document.getElementById("vid-1").srcObject = localStream;
    }
    
    localStream.getTracks().forEach((track) => {
        peerConnection.addTrack(track, localStream);
    })

    peerConnection.ontrack = (event) => {
        event.streams[0].getTracks().forEach((track) => {
            remoteStream.addTrack(track);
        })
    }

    peerConnection.onicecandidate = async (event) => {
        if (event.candidate) {
            socket.emit('room-message', { room: roomId, message: { text: JSON.stringify({ "type": 'candidate', "candidate": event.candidate }) } })
        }
    }
}

let createOffer = async (roomId) => {

    await createPeerConnection(roomId);

    let offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer)

    socket.emit('room-message', { room: roomId, message: { text: JSON.stringify({ "type": 'offer', "offer": offer }) } })
}

let createAnswer = async (roomId, offer) => {
    await createPeerConnection(roomId);

    await peerConnection.setRemoteDescription(offer); // Set remote description first

    let answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    socket.emit('room-message', { room: roomId, message: { text: JSON.stringify({ "type": "answer", "answer": answer }) } });
};


let addAnswer = async (answer) => {
    if (!peerConnection.currentRemoteDescription){
        await peerConnection.setRemoteDescription(answer);
    }
}

init();

错误原因分析

  1. 重复初始化PeerConnection:全局peerConnection变量被多次覆盖,导致连接状态混乱。当新用户加入或收到offer时,都会重新创建PeerConnection,旧连接的状态未被清理,触发状态冲突。
  2. 冗余的JSON解析:message = JSON.parse(JSON.parse(message).text);执行了两次JSON解析,可能导致数据格式异常,影响后续SDP处理。
  3. 无限制的Offer创建:只要有用户加入就触发createOffer,会导致同一房间内多个Offer发送,引发连接状态冲突。
  4. 缺少连接状态校验:在创建Offer/Answer前未检查是否已有活跃连接,重复操作导致PeerConnection处于stable状态时被强制修改描述。

修改后的代码

let localStream;
let remoteStream;
let peerConnection;
let isConnectionEstablished = false; // 新增连接状态标记

var socket = io.connect("http://localhost:5000");

const urlParams = new URLSearchParams(window.location.search);
const roomId = urlParams.get("room");
const name = urlParams.get("name");

socket.emit("join-channel", { room: roomId });
socket.on("room-message", handleMessage);

let init = async () => {
    socket.on("MemberJoined", handleUserJoined);

    localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
    document.getElementById("vid-1").srcObject = localStream;
}

let handleUserJoined = async () => {
    console.log('新用户加入房间');
    // 仅当未建立连接时才创建Offer
    if (!isConnectionEstablished && !peerConnection) {
        await createOffer();
    }
}

let handleMessage = async (message) => {
    // 修正JSON解析:仅解析一次(根据后端实际返回格式调整)
    const parsedMessage = JSON.parse(message.text);

    switch(parsedMessage.type) {
        case "offer":
            if (!isConnectionEstablished && !peerConnection) {
                await createAnswer(parsedMessage.offer);
            }
            break;
        case "answer":
            if (peerConnection && !peerConnection.currentRemoteDescription) {
                await peerConnection.setRemoteDescription(parsedMessage.answer);
            }
            break;
        case "candidate":
            if (peerConnection) {
                try {
                    await peerConnection.addIceCandidate(parsedMessage.candidate);
                } catch (e) {
                    console.warn('添加ICE候选失败:', e);
                }
            }
            break;
    }
}

let createPeerConnection = async () => {
    peerConnection = new RTCPeerConnection({
        iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
    });

    remoteStream = new MediaStream();
    document.getElementById("vid-2").srcObject = remoteStream;

    localStream.getTracks().forEach((track) => {
        peerConnection.addTrack(track, localStream);
    });

    peerConnection.ontrack = (event) => {
        event.streams[0].getTracks().forEach((track) => {
            remoteStream.addTrack(track);
        });
    };

    peerConnection.onicecandidate = async (event) => {
        if (event.candidate) {
            socket.emit('room-message', { 
                room: roomId, 
                message: { 
                    text: JSON.stringify({ 
                        type: 'candidate', 
                        candidate: event.candidate 
                    }) 
                } 
            });
        }
    };

    // 监听连接状态变化,标记连接状态
    peerConnection.onconnectionstatechange = () => {
        if (peerConnection.connectionState === 'connected') {
            isConnectionEstablished = true;
        } else if (peerConnection.connectionState === 'disconnected' || peerConnection.connectionState === 'failed') {
            isConnectionEstablished = false;
            peerConnection = null;
        }
    };
}

let createOffer = async () => {
    await createPeerConnection();

    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);

    socket.emit('room-message', { 
        room: roomId, 
        message: { 
            text: JSON.stringify({ type: 'offer', offer: offer }) 
        } 
    });
}

let createAnswer = async (offer) => {
    await createPeerConnection();

    await peerConnection.setRemoteDescription(offer);
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    socket.emit('room-message', { 
        room: roomId, 
        message: { 
            text: JSON.stringify({ type: "answer", answer: answer }) 
        } 
    });
};

init();

关键修改点说明

  • 添加连接状态标记:新增isConnectionEstablished变量,避免重复创建连接和发送Offer/Answer。
  • 修正JSON解析逻辑:移除冗余的JSON.parse,确保消息解析正确。
  • PeerConnection状态管理:监听onconnectionstatechange事件,实时更新连接状态,在连接断开时重置变量。
  • 限制Offer/Answer创建时机:仅当无活跃连接时才允许创建新的Offer或Answer,避免状态冲突。
  • 错误捕获:对addIceCandidate添加异常捕获,避免单个候选失败导致整个流程中断。

内容的提问来源于stack exchange,提问作者Muhammad Imran Hossain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 00:08:08