web-dev-qa-db-fra.com

Impossible de définir la réponse locale sdp: Appelé dans un état incorrect: kStable

depuis quelques jours, je suis maintenant obligé d'essayer de faire fonctionner mon client webRTC et je ne peux pas comprendre ce que je fais de mal… .. J'essaie de créer un client Webrtc multi-pairs et je teste les deux side with Chrome . Lorsque l'appelé reçoit l'appel et crée la réponse, le message d'erreur suivant s'affiche:

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

Le côté récepteur établit correctement les deux connexions vidéo et affiche les flux locaux et distants. Mais l'appelant ne semble pas recevoir la réponse aux appels. Est-ce que quelqu'un peut me dire ce que je fais mal ici?

Voici le code que j'utilise (c'est une version allégée qui montre simplement les parties pertinentes et qui le rend plus lisible)

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);
        }
    }
}
6
Eric Xyz

J'utilise une construction similaire pour établir les connexions WebRTC entre homologues émetteurs et destinataires, en appelant la méthode RTCPeerConnection.addTrack deux fois (une pour la piste audio et une pour la piste vidéo).

J'ai utilisé la même structure que celle illustrée dans l'exemple Stage 2 présenté dans L'évolution de 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);  
  }
}

Testez-le ici: https://jsfiddle.net/q8Lw39fd/

Comme vous le constaterez, dans cet exemple, la méthode createOffer n'est jamais appelée directement; Au lieu de cela, il est appelé indirectement via addTrack, ce qui déclenche un événement RTCPeerConnection.onnegotiationneeded .

Toutefois, comme dans votre cas, Chrome déclenche cet événement deux fois, une fois pour chaque piste, ce qui provoque le message d'erreur que vous avez mentionné:

DOMException: Impossible de définir la réponse locale sdp: Appelé dans un état incorrect: kStable

En passant, cela ne se produit pas dans Firefox: cela déclenche l'événement une seule fois.

La solution à ce problème consiste à écrire une solution de contournement pour le comportement de Chrome: une protection qui empêche les appels imbriqués au mécanisme de (re) négociation.

La partie pertinente de l'exemple fixe serait comme ceci:

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

Testez-le ici: https://jsfiddle.net/q8Lw39fd/8/

Vous devriez pouvoir facilement implémenter ce mécanisme de garde dans votre propre code.

7
j1elo

Vous envoyez la réponse ici:

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

Regarde le mien:

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