无法设置本地应答 sdp: Called in wrong state: kStable [英] Failed to set local answer sdp: Called in wrong state: kStable

查看:85
本文介绍了无法设置本地应答 sdp: Called in wrong state: kStable的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

几天来,我一直在努力让我的 webRTC 客户端正常工作,我无法弄清楚我做错了什么.我正在尝试创建多对等 webrtc 客户端,并正在使用 Chrome 测试双方.当被叫方收到呼叫并创建应答时,我收到以下错误:

未能设置本地应答 sdp: Called in wrong state: kStable

接收方正确建立了两个视频连接并显示本地和远程流.但是呼叫者似乎没有收到被呼叫者的回答.有人可以暗示我在这里做错了什么吗?

这是我正在使用的代码(它是一个剥离版本,只显示相关部分并使其更具可读性)

class WebRTC_Client{私有 peerConns = {};私人 sendAudioByDefault = true;私人 sendVideoByDefault = true;私人报价选项 = {offerToReceiveAudio: 真,offerToReceiveVideo: 真};私有约束 = {音频":真实,视频": {帧率:5,宽度:256,身高:194}};私人服务器Cfg = {冰服务器:[{网址:[眩晕:stun.l.google.com:19302"]}]};私人信令频道;公共构造函数(SignalingChannel){this.SignalingChannel = SignalingChannel;this.bindSignalingHandlers();}/*...*/私人获得流(流){(<any>window).localStream = stream;this.videoAssets[0].srcObject = 流;}私人停止LocalTracks(){}私人开始(){var self = this;如果( !this.isReady() ){console.error('无法启动 WebRTC,因为尚未分配 WebSocket 用户 connectionId');}this.buttonStart.disabled = true;this.stopLocalTracks();navigator.mediaDevices.getUserMedia(this.getConstrains()).then((流) => {self.gotStream(stream);self.SignalingChannel.send(JSON.stringify({type: 'onReadyForTeamspeak'}));}).catch(function(error) { trace('getUserMedia error: ', error); });}公共 addPeerId(peerId){this.availablePeerIds[peerId] = peerId;this.preparePeerConnection(peerId);}私人准备PeerConnection(peerId){var self = this;if( this.peerConns[peerId] ){返回;}this.peerConns[peerId] = new RTCPeerConnection(this.serversCfg);this.peerConns[peerId].ontrack = function (evt) { self.gotRemoteStream(evt, peerId);};this.peerConns[peerId].onicecandidate = function (evt) { self.iceCallback(evt, peerId);};this.peerConns[peerId].onnegotiationneeded = function (evt) { if( self.isCallingTo(peerId) ) { self.createOffer(peerId);} };this.addLocalTracks(peerId);}私人 addLocalTracks(peerId){var self = this;var localTracksCount = 0;(<any>window).localStream.getTracks().forEach(功能(轨道){self.peerConns[peerId].addTrack(追踪,(<any>window).localStream);localTracksCount++;});trace('Added ' + localTracksCount + ' 本地轨道到远程节点 #' + peerId);}私人电话(){var self = this;trace('如果有可用的,开始调用所有可用的新对等点');//仅当有人要呼叫时才呼叫if( !Object.keys(this.availablePeerIds).length ){trace('我知道没有可用的可调用对等点');返回;}for( 让 peerId 在 this.availablePeerIds 中){如果(!this.availablePeerIds.hasOwnProperty(peerId)){继续;}this.preparePeerConnection(peerId);}}私人创建优惠(peerId){var self = this;this.peerConns[peerId].createOffer( this.offerOptions ).then( function (offer) { return self.peerConns[peerId].setLocalDescription(offer); } ).then( 函数 () {trace('发送报价给对等#' + peerId);self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));}).catch(function(error) { self.onCreateSessionDescriptionError(error); });}私人接听电话(peerId){var self = this;trace('应答来自对等方的呼叫#' + peerId);this.peerConns[peerId].createAnswer().then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } ).then( 函数 () {trace('发送答复给对等#' + peerId);self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));}).catch(function(error) { self.onCreateSessionDescriptionError(error); });}私人 onCreateSessionDescriptionError(error) {console.warn('创建会话描述失败:' + error.toString());}私有 gotRemoteStream(e, peerId) {如果 (this.audioAssets[peerId].srcObject !== e.streams[0]) {this.videoAssets[peerId].srcObject = e.streams[0];trace('添加远程peer#的流源#' + peerId + ' to DOM');}}私人iceCallback(事件,peerId){this.SignalingChannel.send(JSON.stringify({ "candidate": event.candidate, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));}私人句柄候选人(候选人,peerId){this.peerConns[peerId].addIceCandidate(候选人).然后(this.onAddIceCandidateSuccess,this.onAddIceCandidateError);trace('Peer #' + peerId + ': 新的 ICE 候选:' + (candidate ?Candidate.candidate : '(null)'));}私人 onAddIceCandidateSuccess() {trace('AddIceCandidate 成功.');}私人 onAddIceCandidateError(error) {console.warn('未能添加 ICE 候选:' + error.toString());}私人挂断(){}私人 bindSignalingHandlers(){this.SignalingChannel.registerHandler('onWebRTCPeerConn', (signal) => this.handleSignals(signal));}私人句柄信号(信号){var self = this,peerId = signal.connectionId;如果(信号.sdp){trace('从peer#接收到sdp' + peerId);this.peerConns[peerId].setRemoteDescription(new RTCSessionDescription(signal.sdp)).then( 函数 () {if( self.peerConns[peerId].remoteDescription.type === 'answer' ){trace('从peer#收到sdp应答'+ peerId);} else if( self.peerConns[peerId].remoteDescription.type === 'offer' ){trace('收到来自对等方的 sdp 报价' + peerId);self.answerCall(peerId);} 别的 {trace('Received sdp' + self.peerConns[peerId].remoteDescription.type + ' from peer #' + peerId);}}).catch(function(error) { trace('Unable to set remote description for peer #' + peerId + ': ' + error); });} else if(信号.候选人){this.handleCandidate(new RTCIceCandidate(signal.candidate), peerId);} else if( signal.closeConn ){trace('从对等方#接收到关闭信号' + peerId);this.endCall(peerId,true);}}}

解决方案

我一直在使用类似的构造来建立发送方和接收方之间的 WebRTC 连接,方法是调用 RTCPeerConnection.addTrack 两次(一次用于音轨,一次用于视频轨).>

我使用了与 第 2 阶段 示例中所示相同的结构rel="noreferrer">WebRTC 1.0 的演变:

let pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection(), stream, videoTrack, videoSender;(异步() => {尝试 {流 = 等待 navigator.mediaDevices.getUserMedia({video: true, audio: true});videoTrack = stream.getVideoTracks()[0];pc1.addTrack(stream.getAudioTracks()[0], 流);}赶上(e){控制台日志(e);}})();checkbox.onclick = () =>{如果(checkbox.checked){videoSender = pc1.addTrack(videoTrack, 流);} 别的 {pc1.removeTrack(videoSender);}}pc2.ontrack = e =>{video.srcObject = e.streams[0];e.track.onended = e =>video.srcObject = video.srcObject;//Chrome/Firefox 错误}pc1.onicecandidate = e =>pc2.addIceCandidate(e.candidate);pc2.onicecandidate = e =>pc1.addIceCandidate(e.candidate);pc1.onnegotiationneeded = 异步 e =>{尝试 {等待 pc1.setLocalDescription(await pc1.createOffer());等待 pc2.setRemoteDescription(pc1.localDescription);等待 pc2.setLocalDescription(await pc2.createAnswer());等待 pc1.setRemoteDescription(pc2.localDescription);}赶上(e){控制台日志(e);}}

在这里测试:https://jsfiddle.net/q8Lw39fd/

你会注意到,在这个例子中,createOffer 方法从未被直接调用;相反,它通过 addTrack 间接调用,触发 RTCPeerConnection.onnegotiationneeded 事件.

但是,就像您的情况一样,Chrome 会触发此事件两次,每个曲目一次,这会导致您提到的错误消息:

<块引用>

DOMException: Failed to set local answer sdp: Called in wrong state: kStable

顺便说一下,这不会在 Firefox 中发生:它只触发一次事件.

此问题的解决方案是为 Chrome 行为编写一个解决方法:防止嵌套调用(重新)协商机制的保护.

固定示例的相关部分是这样的:

pc1.oniceccandidate = e =>pc2.addIceCandidate(e.candidate);pc2.onicecandidate = e =>pc1.addIceCandidate(e.candidate);var isNegotiating = false;//Chrome 的解决方法:跳过嵌套协商pc1.onnegotiationneeded = 异步 e =>{如果(正在谈判){console.log("跳过嵌套协商");返回;}isNegotiation = true;尝试 {等待 pc1.setLocalDescription(await pc1.createOffer());等待 pc2.setRemoteDescription(pc1.localDescription);等待 pc2.setLocalDescription(await pc2.createAnswer());等待 pc1.setRemoteDescription(pc2.localDescription);}赶上(e){控制台日志(e);}}pc1.onsignalingstatechange = (e) =>{//Chrome 的解决方法:跳过嵌套协商isNegotiating = (pc1.signalingState != "stable");}

在这里测试:https://jsfiddle.net/q8Lw39fd/8/

您应该能够轻松地将这种保护机制实现到您自己的代码中.

for a couple of days I'm now stuck with trying to get my webRTC client to work and I can't figure out what I'm doing wrong. I'm trying to create multi peer webrtc client and am testing both sides with Chrome. When the callee receives the call and create the Answer, I get the following error:

Failed to set local answer sdp: Called in wrong state: kStable

The receiving side correctly establishes both video connnections and is showing the local and remote streams. But the caller seems not to receive the callees answer. Can someone hint me what I am doing wrong here?

Here is the code I am using (it's a stripped version to just show the relevant parts and make it better readable)

class WebRTC_Client
{
    private peerConns = {};
    private sendAudioByDefault = true;
    private sendVideoByDefault = true;
    private offerOptions = {
        offerToReceiveAudio: true,
        offerToReceiveVideo: true
    };
    private constraints = {
        "audio": true,
        "video": {
            frameRate: 5,
            width: 256,
            height: 194
        }
    };
    private serversCfg = {
        iceServers: [{
            urls: ["stun:stun.l.google.com:19302"]
        }]
    };
    private SignalingChannel;

    public constructor(SignalingChannel){
        this.SignalingChannel = SignalingChannel;
        this.bindSignalingHandlers();
    }

    /*...*/

    private gotStream(stream) {
        (<any>window).localStream = stream;
        this.videoAssets[0].srcObject = stream;
     }

    private stopLocalTracks(){}

    private start() {
        var self = this;

        if( !this.isReady() ){
            console.error('Could not start WebRTC because no WebSocket user connectionId had been assigned yet');
        }

        this.buttonStart.disabled = true;

        this.stopLocalTracks();

        navigator.mediaDevices.getUserMedia(this.getConstrains())
            .then((stream) => {
                self.gotStream(stream);
                self.SignalingChannel.send(JSON.stringify({type: 'onReadyForTeamspeak'}));
            })
            .catch(function(error) { trace('getUserMedia error: ', error); });
    }

    public addPeerId(peerId){
        this.availablePeerIds[peerId] = peerId;
        this.preparePeerConnection(peerId);
    }

    private preparePeerConnection(peerId){
        var self = this;

        if( this.peerConns[peerId] ){
            return;
        }

        this.peerConns[peerId] = new RTCPeerConnection(this.serversCfg);
        this.peerConns[peerId].ontrack = function (evt) { self.gotRemoteStream(evt, peerId); };
        this.peerConns[peerId].onicecandidate = function (evt) { self.iceCallback(evt, peerId); };
        this.peerConns[peerId].onnegotiationneeded = function (evt) { if( self.isCallingTo(peerId) ) { self.createOffer(peerId); } };

        this.addLocalTracks(peerId);
    }

    private addLocalTracks(peerId){
        var self = this;

        var localTracksCount = 0;
        (<any>window).localStream.getTracks().forEach(
            function (track) {
                self.peerConns[peerId].addTrack(
                    track,
                    (<any>window).localStream
            );
                localTracksCount++;
            }
        );
        trace('Added ' + localTracksCount + ' local tracks to remote peer #' + peerId);
    }

    private call() {
        var self = this;

        trace('Start calling all available new peers if any available');

        // only call if there is anyone to call
        if( !Object.keys(this.availablePeerIds).length ){
            trace('There are no callable peers available that I know of');
            return;
        }

        for( let peerId in this.availablePeerIds ){
            if( !this.availablePeerIds.hasOwnProperty(peerId) ){
                continue;
            }
            this.preparePeerConnection(peerId);
        }
    }

    private createOffer(peerId){
        var self = this;

        this.peerConns[peerId].createOffer( this.offerOptions )
            .then( function (offer) { return self.peerConns[peerId].setLocalDescription(offer); } )
            .then( function () {
                trace('Send offer to peer #' + peerId);
                self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
            })
            .catch(function(error) { self.onCreateSessionDescriptionError(error); });
    }

    private answerCall(peerId){
        var self = this;

        trace('Answering call from peer #' + peerId);

        this.peerConns[peerId].createAnswer()
            .then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )
            .then( function () {
                trace('Send answer to peer #' + peerId);
                self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
            })
            .catch(function(error) { self.onCreateSessionDescriptionError(error); });
    }

    private onCreateSessionDescriptionError(error) {
        console.warn('Failed to create session description: ' + error.toString());
    }

    private gotRemoteStream(e, peerId) {
        if (this.audioAssets[peerId].srcObject !== e.streams[0]) {
            this.videoAssets[peerId].srcObject = e.streams[0];
            trace('Added stream source of remote peer #' + peerId + ' to DOM');
        }
    }

    private iceCallback(event, peerId) {
        this.SignalingChannel.send(JSON.stringify({ "candidate": event.candidate, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
    }

    private handleCandidate(candidate, peerId) {
        this.peerConns[peerId].addIceCandidate(candidate)
            .then(
                this.onAddIceCandidateSuccess,
                this.onAddIceCandidateError
            );
        trace('Peer #' + peerId + ': New ICE candidate: ' + (candidate ? candidate.candidate : '(null)'));
    }

    private onAddIceCandidateSuccess() {
        trace('AddIceCandidate success.');
    }

    private onAddIceCandidateError(error) {
        console.warn('Failed to add ICE candidate: ' + error.toString());
    }

    private hangup() {}

    private bindSignalingHandlers(){
        this.SignalingChannel.registerHandler('onWebRTCPeerConn', (signal) => this.handleSignals(signal));
    }

    private handleSignals(signal){
        var self = this,
            peerId = signal.connectionId;

        if( signal.sdp ) {
            trace('Received sdp from peer #' + peerId);

            this.peerConns[peerId].setRemoteDescription(new RTCSessionDescription(signal.sdp))
                .then( function () {
                    if( self.peerConns[peerId].remoteDescription.type === 'answer' ){
                        trace('Received sdp answer from peer #' + peerId);
                    } else if( self.peerConns[peerId].remoteDescription.type === 'offer' ){
                        trace('Received sdp offer from peer #' + peerId);
                        self.answerCall(peerId);
                    } else {
                        trace('Received sdp ' + self.peerConns[peerId].remoteDescription.type + ' from peer #' + peerId);
                    }
                })
                .catch(function(error) { trace('Unable to set remote description for peer #' + peerId + ': ' + error); });
        } else if( signal.candidate ){
            this.handleCandidate(new RTCIceCandidate(signal.candidate), peerId);
        } else if( signal.closeConn ){
            trace('Closing signal received from peer #' + peerId);
            this.endCall(peerId,true);
        }
    }
}

解决方案

I have been using a similar construct to build up the WebRTC connections between sender and receiver peers, by calling the method RTCPeerConnection.addTrack twice (one for the audio track, and one for the video track).

I used the same structure as shown in the Stage 2 example shown in The evolution of WebRTC 1.0:

let pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection(), stream, videoTrack, videoSender;

(async () => {
  try {
    stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
    videoTrack = stream.getVideoTracks()[0];
    pc1.addTrack(stream.getAudioTracks()[0], stream);
  } catch (e) {
    console.log(e);  
  }
})();

checkbox.onclick = () => {
  if (checkbox.checked) {
    videoSender = pc1.addTrack(videoTrack, stream);
  } else {
    pc1.removeTrack(videoSender);
  }
}

pc2.ontrack = e => {
  video.srcObject = e.streams[0];
  e.track.onended = e => video.srcObject = video.srcObject; // Chrome/Firefox bug
}

pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
pc1.onnegotiationneeded = async e => {
  try {
    await pc1.setLocalDescription(await pc1.createOffer());
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription(await pc2.createAnswer());
    await pc1.setRemoteDescription(pc2.localDescription);
  } catch (e) {
    console.log(e);  
  }
}

Test it here: https://jsfiddle.net/q8Lw39fd/

As you'll notice, in this example the method createOffer is never called directly; instead, it is indirectly called via addTrack triggering an RTCPeerConnection.onnegotiationneeded event.

However, just as in your case, Chrome triggers this event twice, once for each track, and this causes the error message you mentioned:

DOMException: Failed to set local answer sdp: Called in wrong state: kStable

This doesn't happen in Firefox, by the way: it triggers the event only once.

The solution to this issue is to write a workaround for the Chrome behavior: a guard that prevents nested calls to the (re)negotiation mechanism.

The relevant part of the fixed example would be like this:

pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);

var isNegotiating = false;  // Workaround for Chrome: skip nested negotiations
pc1.onnegotiationneeded = async e => {
  if (isNegotiating) {
    console.log("SKIP nested negotiations");
    return;
  }
  isNegotiating = true;
  try {
    await pc1.setLocalDescription(await pc1.createOffer());
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription(await pc2.createAnswer());
    await pc1.setRemoteDescription(pc2.localDescription);
  } catch (e) {
    console.log(e);  
  }
}

pc1.onsignalingstatechange = (e) => {  // Workaround for Chrome: skip nested negotiations
  isNegotiating = (pc1.signalingState != "stable");
}

Test it here: https://jsfiddle.net/q8Lw39fd/8/

You should be able to easily implement this guard mechanism into your own code.

这篇关于无法设置本地应答 sdp: Called in wrong state: kStable的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆