2

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.stack.imgur.com/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

Salim Azeri
  • 43
  • 2
  • 6

1 Answers1

2

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.

jib
  • 40,579
  • 17
  • 100
  • 158
  • thanks for help. as you suggested i delete `TempDescriptions` variable and pass sdp to `getStream()`. Expected result is different, now when I attach four streams and refresh the page I get only one stream and three errors: `DOMException: Failed to set local answer sdp: Called in wrong state: kStable` which is generated in gotDescription() function ( i update question with gotDescription() method). I searched for information about this error earlier because it also appeared, but Google does not know much about it. – Salim Azeri Feb 10 '19 at 21:26
  • I've found that `getDescription()` gets the same `sdp` on each call instead of receiving sdp of every connection, that is why i'm getting `kstable` state but I do not know how to deal with it. – Salim Azeri Feb 11 '19 at 01:51
  • Thanks for answer and your time. I understood what you wrote but I have a problem with one thing, as you wrote earlier when 'ask' is emitting I should also attach connectionNumber ++, the problem is that 'transmit' page has no acces to the other 'transmit' pages so connectionNumber will always be the same. – Salim Azeri Feb 11 '19 at 16:26
  • @SalimAzeri It is up to you to decide how to differentiate the various "transmit" pages. You could make up a random id, for example. Since you want to re-connect with them that's probably a good idea, so they'll appear in the same order as last time. If that's not important, then forget the `id` and just assign them a `connection` as they come in. – jib Feb 11 '19 at 16:32
  • I created function for generating random id, one more thing, i copied function from socket.on('ask') and i get error that sdp is not defined, how should i catch sdp from setLocalDescription ? – Salim Azeri Feb 11 '19 at 17:03
  • @SalimAzeri Ah `offer` is an object, whose contents may change over time. Just copy the fields you need into temporaries, like you had, except into local `const` variables, or use [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) for the same effect. I've updated the example to use destructuring. – jib Feb 11 '19 at 17:16