3

I am creating a webRTC video chat that shows a caller all active members when initiating a call from firefox and the receiver is using chrome this error is displayed "Uncaught (in promise) DOMException: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Error processing ICE candidate". And when a call is initiated from firefox and receiver uses firefox I get two errors Invalidstate: cannot add ICE candidate when there is no remote SDP and ICE failed, add a STUN and see about:webrtc for details

I dont know where I am making a mistake

/ define all data here
var usersOnline,id,currentCaller,room,caller,localUser,media,memberInfo;
// All subscribed members.
var users = [];


var token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
// create random user id
var userId = Math.random().toString(36).substring(2, 15);
// create random username
var username = token;
// authonticating user
var currentUser = {
    username: token,
    userId: userId
}
// stringify user data 
currentUser = JSON.stringify(currentUser);
var pusher = new Pusher('KEY', {
    authEndpoint: '../auth.php',
    auth: {
        params: JSON.parse(currentUser)
    },
    cluster: 'ap2',
    forceTLS: true
});

var state = pusher.connection.state;

var channel = pusher.subscribe('presence-conference');

channel.bind("pusher:subscription_succeeded", function (members) {
  console.log(members);
  id = channel.members.me.id;
  document.getElementById('mydetails').innerHTML = 'Online Now: '  + '   ( ' + (members.count - 1) +')';
  members.each(member => {
    if (member.id != channel.members.me.id) {
      users.push(member.id);
    }
  });
  renderOnline();
 });
 // Add user online
channel.bind("pusher:member_added", member => {
  users.push(member.id);
  renderOnline();
});
channel.bind("pusher:member_removed", member => {
// for remove member from list:
  var index = users.indexOf(member.id);
  users.splice(index, 1);
  if (member.id == room) {
    endCall();
  }
  renderOnline();
});

function renderOnline(){
  var list = "";
  users.forEach(function(user) {
    list +=
      `<li>` +
      user +//this will call user
      ` <input type="button" style="float:right;"  value="Call" onclick="callUser('` +
      user +
      `')" id="makeCall" /></li>`;
  });
  document.getElementById("userDetails").innerHTML = list;
}
        //To iron over browser implementation anomalies like prefixes
    GetRTCPeerConnection();
    GetRTCSessionDescription();
    GetRTCIceCandidate();
    prepareCaller();
    function prepareCaller() {
      //Initializing a peer connection
      caller = new window.RTCPeerConnection();
      //Listen for ICE Candidates and send them to remote peers
      caller.onicecandidate = function(evt) {
        if (!evt.candidate) return;
        console.log("onicecandidate called");
        onIceCandidate(caller, evt);
      };
      //onaddstream handler to receive remote feed and show in remoteview video element
      caller.onaddstream = function(evt) {
        console.log("onaddstream called");
        if("srcObject" in document.getElementById("selfview")){
            document.getElementById("selfview").srcObject = evt.stream;
        }else{
        if (window.URL) {
          document.getElementById("remoteview").src = window.URL.createObjectURL(
            evt.stream
          );
        } else {
          document.getElementById("remoteview").src = evt.stream;
        }
      }
      };
    }
    function getCam() {
      //Get local audio/video feed and show it in selfview video element
      return navigator.mediaDevices.getUserMedia({
         audio: {
             echoCancellation: true,
             sampleSize:8
         },
         video: {
            width: 1080,
            height: 720,
            aspectRatio: { ideal: 1.777778 }
         }
      });
    }

    function GetRTCIceCandidate() {
      window.RTCIceCandidate =
        window.RTCIceCandidate ||
        window.webkitRTCIceCandidate ||
        window.mozRTCIceCandidate ||
        window.msRTCIceCandidate;

      return window.RTCIceCandidate;
    }

    function GetRTCPeerConnection() {
      window.RTCPeerConnection =
        window.RTCPeerConnection ||
        window.webkitRTCPeerConnection ||
        window.mozRTCPeerConnection ||
        window.msRTCPeerConnection;
      return window.RTCPeerConnection;
    }

    function GetRTCSessionDescription() {
      window.RTCSessionDescription =
        window.RTCSessionDescription ||
        window.webkitRTCSessionDescription ||
        window.mozRTCSessionDescription ||
        window.msRTCSessionDescription;
      return window.RTCSessionDescription;
    }

    //Create and send offer to remote peer on button click
    function callUser(user) {
      getCam()
        .then(stream => {
            if("srcObject" in document.getElementById("selfview")){
            document.getElementById("selfview").srcObject = stream;
        }else{
          if (window.URL) {
            document.getElementById("selfview").src = window.URL.createObjectURL(
              stream
            );
          } else {
            document.getElementById("selfview").src = stream;
          }
        }
          toggleEndCallButton();
          caller.addStream(stream);
          localUserMedia = stream;
          caller.createOffer().then(function(desc) {
            caller.setLocalDescription(new RTCSessionDescription(desc));
            channel.trigger("client-sdp", {
              sdp: desc,
              room: user,
              from: id
            });
            room = user;
          });
        })
        .catch(error => {
          console.log("an error occured", error);
        });
    }

    function endCall() {
      room = undefined;
      caller.close();
      for (let track of localUserMedia.getTracks()) {
        track.stop();
      }
      prepareCaller();
      toggleEndCallButton();
    }

    function endCurrentCall() {
      channel.trigger("client-endcall", {
        room: room
      });

      endCall();
    }

    //Send the ICE Candidate to the remote peer
    function onIceCandidate(peer, evt) {
      if (evt.candidate) {
        channel.trigger("client-candidate", {
          candidate: evt.candidate,
          room: room
        });
      }
    }

    function toggleEndCallButton() {
      if (document.getElementById("endCall").style.display == "block") {
        document.getElementById("endCall").style.display = "none";
      } else {
        document.getElementById("endCall").style.display = "block";
      }
    }

    //Listening for the candidate message from a peer sent from onicecandidate handler
    channel.bind("client-candidate", function(msg) {
      if (msg.room == room) {
        console.log("candidate received");
        caller.addIceCandidate(new RTCIceCandidate(msg.candidate));
      }
    });

    //Listening for Session Description Protocol message with session details from remote peer
    channel.bind("client-sdp", function(msg) {
      if (msg.room == id) {
        console.log("sdp received");
        var answer = confirm(
          "You have a call from: " + msg.from + "Would you like to answer?"
        );
        if (!answer) {
          return channel.trigger("client-reject", { room: msg.room, rejected: id });
        }
        room = msg.room;
        getCam()
          .then(stream => {
            localUserMedia = stream;
            toggleEndCallButton();
            if("srcObject" in document.getElementById("selfview")){
            document.getElementById("selfview").srcObject = stream;
        }else{
            if (window.URL) {
              document.getElementById("selfview").src = window.URL.createObjectURL(
                stream
              );
            } else {
              document.getElementById("selfview").src = stream;
            }
          }
            caller.addStream(stream);
            var sessionDesc = new RTCSessionDescription(msg.sdp);
            caller.setRemoteDescription(sessionDesc);
            caller.createAnswer().then(function(sdp) {
              caller.setLocalDescription(new RTCSessionDescription(sdp));
              channel.trigger("client-answer", {
                sdp: sdp,
                room: room
              });
            });
          })
          .catch(error => {
            console.log("an error occured", error);
          });
      }
    });

    //Listening for answer to offer sent to remote peer
    channel.bind("client-answer", function(answer) {
      if (answer.room == room) {
        console.log("answer received");
        caller.setRemoteDescription(new RTCSessionDescription(answer.sdp));
      }
    });

    channel.bind("client-reject", function(answer) {
      if (answer.room == room) {
        console.log("Call declined");
        alert("call to " + answer.rejected + "was politely declined");
        endCall();
      }
    });

    channel.bind("client-endcall", function(answer) {
      if (answer.room == room) {
        console.log("Call Ended");
        endCall();
      }
    });

I EXPECTED that the video call will work don't want to use any API, help me see where I went wrong.

1 Answers1

9

Call setRemoteDescription(offer) before requesting the camera.

This puts the RTCPeerConnection in the right signaling state ("have-remote-offer") to receive and process remote ICE candidates correctly.

There's no time to request the camera first when an offer comes in. Incoming offers are typically followed closely by trickled ICE candidates on your signaling channel. addIceCandidate won't know what to do with those if it hasn't seen an offer.

Move the setRemoteDescription call ahead of the getMedia call in the promise chain to fix it. You have more time then before returning an answer.

Though that's still not great, since this approach often ends up blocking initial WebRTC negotiation on a user permission prompt for the camera. This is called tight coupling. Sadly, the current state of WebRTC encourages it, since getting the best IP mode is gated on getUserMedia in most browsers.

Lastly, there's a lot of old API usage here. See my other answer for newer APIs to use.

jib
  • 40,579
  • 17
  • 100
  • 158
  • I have upvoted you sir I dont know who has down voted you, I cant downvote you and the answer is working but its not connecting to turn server giving me an error ```` – Nevil Saul Blemuss Jul 29 '19 at 21:16
  • ICE failed, add a TURN server and see about:webrtc for more details this is the error – Nevil Saul Blemuss Jul 29 '19 at 21:17
  • From this information I changed my strategy to the following: Instead of calling `addIceCandidate` as soon as the signaling information arrives, I add the candidates to a queue. When it's time to call `setRemoteDescription`, I execute it, and right after I go through my candidates list and call `addIceCandidate` on each one, worked like a charm! – Maximiliano Guerra Jun 11 '20 at 15:00
  • @MaximilianoGuerra Bad idea. It defeats the purpose of trickle ICE and slows down initial connection, because it delays the ICE agent starting on that end. Didn't calling `setRemoteDescription` first and `getUserMedia` second work for you? Queuing ICE candidates is an anti-pattern. Might as well not use them at all then. – jib Jun 11 '20 at 21:11
  • @jib The way my application works is that both peers have to be logged in the system and with all media setup ready (camera and mic). Only when both are ready is that one of them have the permission to call the other (one can only call, other can only answer). So I couldn't find a way to `setRemoteDescription` ahead of `getMedia`, as my flow would be turned upside down. It's like this: 1. getMedia -> emit enter room, 2. on enter room -> enable call button. 3. Click call button -> emit send offer, 4. on offer, enable answer button, 5. click button, emit answer, 6. on answer setRemoteDescription – Maximiliano Guerra Jun 15 '20 at 13:56
  • @jib I studied the ways of webRTC while trying to develop this for around two weeks, and finally I've make it work this way. There are lots of things I don't understand yet, like this "trickle ICE" and it would be a considerable work to change everything and make it work. But I appreciate if you have any suggestions for me if I can make it right without changing most of the flow. PS: There is no problem slowing down the initial connection, as my application works by one user waiting the other to call, and answering it (like you would in a phone call). – Maximiliano Guerra Jun 15 '20 at 14:07
  • @MaximilianoGuerra just call `setRemoteDescription(offer)` before enabling the answer button. It's [timing-sensitive](https://stackoverflow.com/a/36851163/918910). – jib Jun 16 '20 at 00:07
  • @jib Thanks! That worked, removed all the "queue" code. The component is now cleaner and without anti-pattern, much better. I was under the impression that I could only call `setRemoteDescription` when the user answered the call, because that would trigger the `ontrack` and the camera being rendered, just after your comment and some reading that I could understand the `ontrack` was only triggered after the `createAnswer()` was sent and the peer received it. – Maximiliano Guerra Jun 17 '20 at 23:15