Не удалось установить локальный ответ sdp: вызывается в неправильном состоянии: kStable

на пару дней я застрял в попытках заставить мой клиент webRTC работать, и я не могу понять, что делаю не так. Я пытаюсь создать многопользовательский клиент webrtc и тестирую обе стороны с помощью Chrome. Когда вызываемый абонент получает вызов и создает ответ, я получаю следующую ошибку:

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

Принимающая сторона правильно устанавливает оба видео соединения и показывает локальный и удаленный потоки. Но кажется, что вызывающий абонент не получает ответа. Может кто-нибудь намекнет мне, что я здесь делаю не так?

Вот код, который я использую (это урезанная версия, чтобы просто показать соответствующие части и сделать ее более читаемой)

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);
        }
    }
}

person Eric Xyz    schedule 24.02.2018    source источник
comment
Как прокомментировал Филипп Ханке, эта проблема возникла совсем недавно. исправлено в Chrome. Он имеет метку M-66, но я не уверен, что исправление было включено в Chrome 66. Если мой ответ помог, отметьте его как решение, чтобы закрыть этот вопрос;)   -  person j1elo    schedule 24.04.2018


Ответы (2)


Я использовал аналогичную конструкцию для создания соединений WebRTC между одноранговыми узлами отправителя и получателя, вызывая метод RTCPeerConnection.addTrack дважды (один для звуковой дорожки и один для видеодорожки).

Я использовал ту же структуру, что и в примере Stage 2, показанном в Развитие 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);  
  }
}

Проверьте это здесь: https://jsfiddle.net/q8Lw39fd/

Как вы заметите, в этом примере метод createOffer никогда не вызывается напрямую; вместо этого он вызывается косвенно через addTrack, запускающий RTCPeerConnection.onnegotiationneeded событие.

Однако, как и в вашем случае, Chrome запускает это событие дважды, по одному для каждой дорожки, и это вызывает указанное вами сообщение об ошибке:

DOMException: не удалось установить локальный ответ sdp: вызывается в неправильном состоянии: kStable

Между прочим, в Firefox этого не происходит: событие запускается только один раз.

Решением этой проблемы является создание обходного пути для поведения Chrome: защита, предотвращающая вложенные вызовы механизма (повторного) согласования.

Соответствующая часть фиксированного примера будет такой:

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");
}

Проверьте это здесь: https://jsfiddle.net/q8Lw39fd/8/

Вы должны иметь возможность легко реализовать этот механизм защиты в своем собственном коде.

person j1elo    schedule 01.03.2018
comment
gotiationneeded не работает в Chrome. Это было исправлено только недавно в bugs.chromium.org/p/chromium/ issues / detail? id = 740501 - person Philipp Hancke; 02.03.2018
comment
@ j1elo, что на самом деле имеет большой смысл. Спасибо за объяснение, это хорошее решение. - person Eric Xyz; 02.03.2018
comment
это все еще не работает с хром 66.0.3359.139. Мне пришлось использовать эту работу. - person manit; 27.04.2018
comment
Все еще сломан в 68. Позор Google. - person Oleg; 24.08.2018
comment
Это все еще проблема? Я испытываю нечто подобное в Chrome 84 - person Xersus; 30.07.2020
comment
Обратите внимание, что на сегодняшний день эта ошибка исправлена ​​в Chrome (была решена в M75), но я все же нашел ее в Ionic, используя iosrtc, который отстает примерно на 20 версий с точки зрения обновления библиотеки WebRTC. Так что будьте осторожны, этот обходной путь все еще может понадобиться! - person j1elo; 01.10.2020

Вы отправляете ответ сюда:

.then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )

Посмотри на мой:

     var callback = function (answer) {
         createdDescription(answer, fromId);
     };
     peerConnection[fromId].createAnswer().then(callback).catch(errorHandler);


    function createdDescription(description, fromId) {
        console.log('Got description');

        peerConnection[fromId].setLocalDescription(description).then(function() {
            console.log("Sending SDP:", fromId, description);
            serverConnection.emit('signal', fromId, {'sdp': description});
        }).catch(errorHandler);
    }
person Keyne Viana    schedule 27.02.2018
comment
Что ж, в моей версии я связываю обещания с .then (), которые, как я считаю, также должны работать и заставлять сигнал ждать завершения setLocalDescription перед отправкой сигнала . Но я понял, что в моей версии createOffer вызывается несколько раз событием onnegotiationneeded. Но я не знаю, почему браузер отправляет его несколько раз. Его следует отправлять только один раз. Я изменил свой код так, чтобы createOffer вызывался в функции call () вместо onnegotiationneeded. Теперь предупреждение kStable исчезло. - person Eric Xyz; 28.02.2018
comment
Но теперь я все еще не вижу удаленное видео на стороне звонящего. Почему-то я думаю, что теперь событие ontrack не вызывается после ответа sdp. Я не знаю почему - person Eric Xyz; 28.02.2018
comment
@EricXyz Вы правы, я скучаю по этому фрагменту кода ... Я не понимаю, почему вы ставите createOffer на переговоры, поскольку предложение, которое фактически инициирует переговоры ... у вас должна быть кнопка вызова, которая запускает createOffer только один раз. - person Keyne Viana; 28.02.2018
comment
Я сделал это, потому что использую этот клиент webRTC в контексте, когда соединение создается автоматически, и пользователю не нужно нажимать кнопку вызова. Я не знал, что лучше, и я попытался также автоматически реализовать повторное согласование при изменении настроек потока (например, включение или выключение видео или изменение качества потоковой передачи) - я подумал, что должен использовать onnegotiationneeded для такого сценария - person Eric Xyz; 28.02.2018
comment
@EricXyz Автоматически или нет, он не должен вызываться с использованием событий однорангового соединения, потому что это происходит после того, как предложение было создано. Вы упомянули повторное согласование, это еще одна тема после того, как соединение уже установлено, и вы хотите обновить де-поток (который имеет некоторые ошибки в зависимости от браузера). - person Keyne Viana; 28.02.2018
comment
@KeyneViana на самом деле, это должно работать нормально в соответствии с документами MDN: addTrack запускает onnegotiationneeded, и в разделе «Пример» этот обработчик используется для вызова createOffer(). Тот же метод косвенного вызова createOffer() используется в примере Stage 2, показанном в Эволюция WebRTC. Но это правда, что Chrome показывает и мне некоторые предупреждения. - person j1elo; 01.03.2018
comment
@ j1elo, ты прав, я вообще-то читал, что переговоры закончились, лол. Но это нужно. - person Keyne Viana; 01.03.2018
comment
@KeyneViana то же самое здесь - person Amirreza; 17.09.2018
comment
Я получил это из вашего кода. Спасибо! Но зачем нужен обратный вызов? Если у нас есть объект однорангового соединения, то почему мы не можем использовать его только там? Выдает ошибку относительно SDP. mismatch Я не понимаю, почему это сработало! - person ishan shah; 17.12.2020
comment
@ishanshah Обратный вызов используется для установки SDP. Я давно не касаюсь кода WebRTC, поэтому не уверен в вашем вопросе. - person Keyne Viana; 18.12.2020