Search code examples
javascriptnode.jssocket.iowebrtcrtcpeerconnection

Reconnecting serveral peerConnections after page reload


I'm creating a web application for monitoring by smartphones using WebRTC, and for the signalling server I'm using socket.io.

When I'm sending a stream, I create an RTCPeerConnection object on the "watch" page that receives this stream. Streams I send on separate pages. The user can attach up to four streams from a smartphone, so on the "watch" page there are up to four RTCPeerConnection objects.

Streams are received automatically as soon as the offer from the "transmit" page appears, then the RTCPeerConnection object is created on the "watch" page and connected to the standard WebRTC schema.

"transmit" page:

  function onCreateOfferSuccess(sdp){
  //console.log(sdp);
  pc.setLocalDescription(new RTCSessionDescription(sdp));
  console.log('ask');
  socket.emit('ask', {"sdp":JSON.stringify(pc.localDescription),
                      "user": loggedUserID,
                      "fromSocket": ownSocket});
}

"watch" page:

socket.on('ask', function(offer){
   if (offer.user === loggedUserID){
      TempDescriptions = JSON.parse(offer.sdp);
      console.log(TempDescriptions)
      currTransmiterSocket = offer.fromSocket;
      console.log(currTransmiterSocket);
      getStream();
}

function getStream(){
   try {
       setTimeout(function(){
           console.log(time, 'getStream()');
           connection = getPeerConnection();

           connection.setRemoteDescription(
           new RTCSessionDescription(TempDescriptions),
           function() {
               connection.createAnswer(gotDescription, function(error){
           console.log(error)
           });
           }, function(error){
               console.log(error)
           });
       }, getStreamDelay*3000)
       getStreamDelay++
   }

   catch(err){
       console.log(err);
   }
};

My web application requires functionality in which when we exit from the "watch" page and return to it again, all previously included streams must be displayed.

To implement this functionality, I use the oniceconnectionstatechange method. If the stream is disconnected, the iceRestart function is executed which creates the offer with the option {iceRestart: true}

"transmit" page:

var options_with_restart = {offerToReceiveAudio: false,
                            offerToReceiveVideo: true,
                            iceRestart: true};

function iceRestart(event){  
  try{
      setTimeout(function(){
       pc.createOffer(options_with_restart).then(onCreateOfferSuccess, onCreateOfferError);
      },1000);
  } catch(error) {
      console.log(error);

The problem is that when I restart the "watch" page, all pages "transmit" send to ask at once. Only one object is connected, although four RTCPeerConnection objects are created at once (let's assume that the user sends four streams).

I have been struggling with this problem for several days. I tried to set an increasing time delay on subsequent calls to the getStream() function as seen in the above code, I tried to check the signallingState connections before executing the getStream() function, I tried several other methods but none of them worked.

If you need some part of my code to help, please write.

edit:

gotDescription() method in "watch" page.

function gotDescription(sdp) {
   try{
       connection.setLocalDescription(sdp,
       function() {
       registerIceCandidate();
       socket.emit('response', {"sdp": sdp,
                                "user": loggedUserID,
                                "fromSocket": ownSocket,
                                "toSocket": currTransmiterSocket});
        }, function(error){
               console.log(error)
           });
   } catch(err){
       console.log(err);
   }
}

I add console.log with RTCPeerConnection object

console output: https://i.sstatic.net/dQXkE.png1

log shows that the signalingState of connection is "stable" but when I develop the object, signalingState is equal to "have-remote-offer"

like here


Solution

  • Remove the TempDescriptions global variable, and pass the sdp to getStream(offer.sdp) directly.

    Otherwise, you've socket.on('ask', function(offer){ called 4 times, overwriting TempDescriptions. Then 3+ seconds later your 4 setTimeouts roll around, all accessing the final value of TempDescriptions only.

    That's probably why only one RTCPeerConnection re-connects.

    In general, using time delay to separate connections seems like a bad idea, as it slows down re-connection. Instead, why not send an id? E.g.

    socket.emit('ask', {id: connectionNumber++,
                        sdp: JSON.stringify(pc.localDescription),
                        user: loggedUserID,
                        fromSocket: ownSocket});
    

    Update: Stop adding global variables to window

    Whenever you assign to an undeclared variable like this:

    connection = getPeerConnection();
    

    ...it creates a global on window, e.g. window.connection, and you have the same problem. You have 4 connection, but you're storing them in one variable.

    Type "use strict"; at the head of your source file to catch this:

    ReferenceError: assignment to undeclared variable connection
    

    Scoping: The general problem

    You're dealing with 4 connections here, but you lack an approach for scoping each instance.

    Most other languages would tell you to create a class and make object instances, and put everything including connection on this. That's one good approach. In JS you can use closures instead. But at minimum you still need 4 variables holding the 4 connections, or an array of connections. Then you look up—e.g. from the id I mentioned—which connection to deal with.

    Also, your try/catches aren't going to catch asynchronous errors. Instead of defining all these callbacks, I strongly recommend using promises, or even async/await when dealing with the highly asynchronous WebRTC API. This makes scoping trivial. E.g.

    const connections = [];
    
    socket.on('ask', async ({user, id, sdp, fromSocket}) => {
      try {
        if (user != loggedUserID) return;
        if (!connections[id]) {
          connections[id] = getPeerConnection();
          registerIceCandidate(connections[id]);
        }
        const connection = connections[id];
        await connection.setRemoteDescription(JSON.parse(sdp));
        await connection.setLocalDescription(await connection.createAnswer());
        socket.emit('response', {sdp,
                                 user: loggedUserID,
                                 fromSocket: ownSocket,
                                 toSocket: fromSocket});
      } catch (err) {
        console.log(err);
      }
    };
    

    This way the error handling is solid.