Failed to set remote answer sdp: Called in wrong state: stable

Tags: , , , ,



I am trying to write a WebRTC application using socket.io.

The signalling server is written in python and looks like this.

import socketio
import uvicorn
from starlette.applications import Starlette

ROOM = 'room'


sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
star_app = Starlette(debug=True)
app = socketio.ASGIApp(sio, star_app)


@sio.event
async def connect(sid, environ):
    await sio.emit('ready', room=ROOM, skip_sid=sid)
    sio.enter_room(sid, ROOM)


@sio.event
async def data(sid, data):
    await sio.emit('data', data, room=ROOM, skip_sid=sid)


@sio.event
async def disconnect(sid):
    sio.leave_room(sid, ROOM)


if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8003)

The client side looks like this

<script>
    const SIGNALING_SERVER_URL = 'http://127.0.0.1:8003?session_id=1';
    // WebRTC config: you don't have to change this for the example to work
    // If you are testing on localhost, you can just use PC_CONFIG = {}
    const PC_CONFIG = {};

    // Signaling methods
    let socket = io(SIGNALING_SERVER_URL, {autoConnect: false});

    socket.on('data', (data) => {
        console.log('Data received: ', data);
        handleSignalingData(data);
    });

    socket.on('ready', () => {
        console.log('Ready');
        // Connection with signaling server is ready, and so is local stream
        createPeerConnection();
        sendOffer();
    });

    let sendData = (data) => {
        socket.emit('data', data);
    };

    // WebRTC methods
    let pc;
    let localStream;
    let remoteStreamElement = document.querySelector('#remoteStream');

    let getLocalStream = () => {
        navigator.mediaDevices.getUserMedia({audio: true, video: true})
            .then((stream) => {
                console.log('Stream found');
                localStream = stream;
                // Connect after making sure that local stream is availble
                socket.connect();
            })
            .catch(error => {
                console.error('Stream not found: ', error);
            });
    }

    let createPeerConnection = () => {
        try {
            pc = new RTCPeerConnection(PC_CONFIG);
            pc.onicecandidate = onIceCandidate;
            pc.onaddstream = onAddStream;
            pc.addStream(localStream);
            console.log('PeerConnection created');
        } catch (error) {
            console.error('PeerConnection failed: ', error);
        }
    };

    let sendOffer = () => {
        console.log('Send offer');
        pc.createOffer().then(
            setAndSendLocalDescription,
            (error) => {
                console.error('Send offer failed: ', error);
            }
        );
    };

    let sendAnswer = () => {
        console.log('Send answer');
        pc.createAnswer().then(
            setAndSendLocalDescription,
            (error) => {
                console.error('Send answer failed: ', error);
            }
        );
    };

    let setAndSendLocalDescription = (sessionDescription) => {
        pc.setLocalDescription(sessionDescription);
        console.log('Local description set');
        sendData(sessionDescription);
    };

    let onIceCandidate = (event) => {
        if (event.candidate) {
            console.log('ICE candidate');
            sendData({
                type: 'candidate',
                candidate: event.candidate
            });
        }
    };

    let onAddStream = (event) => {
        console.log('Add stream');
        remoteStreamElement.srcObject = event.stream;
    };

    let handleSignalingData = (data) => {
        // let msg = JSON.parse(data);
        switch (data.type) {
            case 'offer':
                createPeerConnection();
                pc.setRemoteDescription(new RTCSessionDescription(data));
                sendAnswer();
                break;
            case 'answer':
                pc.setRemoteDescription(new RTCSessionDescription(data));
                break;
            case 'candidate':
                pc.addIceCandidate(new RTCIceCandidate(data.candidate));
                break;
        }
    };

    // Start connection
    getLocalStream();
</script>

Also i use this code for client as socket.io

https://github.com/socketio/socket.io/blob/master/client-dist/socket.io.js

When two people are in the connection, everything works great. But as soon as a third user tries to connect to them, the streaming stops with an error

Uncaught (in promise) DOMException: Failed to execute ‘setRemoteDescription’ on ‘RTCPeerConnection’: Failed to set remote answer sdp: Called in wrong state: stable

I don’t have much knowledge of javascript, so I need your help. Thanks.

P.S. I see this error in all browsers.

Answer

The reason why you are getting this error message is because when a third user joins, it sends an offer to the 2 previously connected users, and therefore, it receives 2 answers. Since one RTCPeerConnection can only establish one peer-to-peer connection, it will complain when it tries to setRemoteDescription on the answer that arrived later, because it already has a stable connection with the peer whose SDP answer arrived first. To handle multiple users, you will need to instantiate a new RTCPeerConnection for every remote peer.

That said, you can manage multiple RTCPeerConnections using some sort of dictionary or list structure. Through your signalling server, whenever a user connects you can emit a unique user id (could be the socket id). When receiving this id, you just instantiate a new RTCPeerConnection and map the received id to the newly created peer connection and then when you will have to setRemoteDescription on all entries of your data structure.

This would also remove the memory leaks in your code every time a new user joins when you overwrite the peer connection variable ‘pc’ when it is still in use.

Notice though, that this solution is not scalable at all, since you are going to be creating new peer connections exponentially, with ~6 the quality of your call will already be terrible. If your intention is to have a conference room, you should really look into using an SFU, but be aware that usually, it is quite cumbersome to set it up.

Checkout Janus videoroom plugin for an open-source SFU implementation.



Source: stackoverflow