Javascript WebRTC Failed to set remote answer sdp: Called in wrong state: kHaveRemoteOffer and Called in wrong state: kStable

Viewed 5572

I can't get my WebRTC code to work properly.. I did everything right I believe and it's still not working. There is something strange why ontrack gets called so early maybe it's suppose to be like that.

The website uses javascript code, the server code I didn't post but thats where WebSockets connect is just a exchanger, what you send to server it sends the same information back to the other partner (stranger) you are connected too.

Server code looks like this little sample

    private void writeStranger(UserProfile you, String msg) {
        UserProfile stranger = you.stranger;
        if(stranger != null)
            sendMessage(stranger.getWebSocket(), msg);
    }

    public void sendMessage(WebSocket websocket, String msg) {
        try {
            websocket.send(msg);
        } catch ( WebsocketNotConnectedException e ) {
            disconnnectClient(websocket);
        }
    }

   //...

        case "ice_candidate":
            JSONObject candidatePackage = (JSONObject) packet.get(1);
            JSONObject candidate = (JSONObject) candidatePackage.get("candidate");

            obj = new JSONObject();
            list = new JSONArray();

            list.put("iceCandidate");
            obj.put("candidate", candidate);
            list.put(obj);

            System.out.println("Sent = " + list.toString());

            writeStranger(you, list.toString()); //send ice candidate to stranger

            break;
        case "send_answer":
            JSONObject sendAnswerPackage = (JSONObject) packet.get(1);
            JSONObject answer = (JSONObject) sendAnswerPackage.get("answer");

            obj = new JSONObject();
            list = new JSONArray();

            list.put("getAnswer");
            obj.put("answer", answer);
            list.put(obj);

            System.out.println("Sent = " + list.toString());

            writeStranger(you, list.toString()); //send answer to stranger

            break;
        case "send_offer":
            JSONObject offerPackage = (JSONObject) packet.get(1);
            JSONObject offer = (JSONObject) offerPackage.get("offer");

            obj = new JSONObject();
            list = new JSONArray();

            list.put("getOffer");
            obj.put("offer", offer);
            list.put(obj);

            System.out.println("Sent = " + list.toString());

            writeStranger(you, list.toString()); //send ice candidate to stranger

            break;

Here are my outputs.
RAW Text: https://pastebin.com/raw/FL8g29gG
JSON colored: https://pastebin.com/FL8g29gG

My javascript Code below

var ws;

var peerConnection, localStream;    
var rtc_server = {
  iceServers: [
                {urls: "stun:stun.l.google.com:19302"},
                {urls: "stun:stun.services.mozilla.com"},
                {urls: "stun:stun.stunprotocol.org:3478"},
                {url: "stun:stun.l.google.com:19302"},
                {url: "stun:stun.services.mozilla.com"},
                {url: "stun:stun.stunprotocol.org:3478"},
  ]
}

//offer SDP's tells other peers what you would like
var rtc_media_constraints = {
  mandatory: {
    OfferToReceiveAudio: true,
    OfferToReceiveVideo: true
  }
};

var rtc_peer_options = {
  optional: [
              {DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
  ]
}

var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

function hasSupportForVideoChat() {
   return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}

function loadMyCameraStream() {
    if (getUserMedia) {
      getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
        function(localMediaStream) {
          //Add my video
          $("div#videoBox video#you")[0].muted = true;
          $("div#videoBox video#you")[0].autoplay = true;
          $("div#videoBox video#you").attr('playsinline', '');
          $("div#videoBox video#you").attr('webkit-playsinline', '');
          $("div#videoBox video#you")[0].srcObject = localMediaStream;
          localStream = localMediaStream;
        },
        function(e) {
          addStatusMsg("Your Video has error : " + e);
        }
      );
    } else {
      addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
      return;
    }
}

function loadStrangerCameraStream() {
    if(!hasSupportForVideoChat())
      return;

    peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
    if (peerConnection.addTrack !== undefined)
      localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
    else
      peerConnection.addStream(localStream);

    peerConnection.onicecandidate = function(e) {
      if (!e || !e.candidate)
        return;
      ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
    };

    if (peerConnection.addTrack !== undefined) {
      //newer technology
      peerConnection.ontrack = function(e) {
        //e.streams.forEach(stream => doAddStream(stream));
        addStatusMsg("ontrack called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.streams[0];
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    } else {
      //older technology
      peerConnection.onaddstream = function(e) {
        addStatusMsg("onaddstream called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.stream;
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    }

    peerConnection.createOffer(
      function(offer) {
        peerConnection.setLocalDescription(offer, function () {
          //both offer and peerConnection.localDescription are the same.
          addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
          //addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
          ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
        },
        function(e) {
          addStatusMsg('createOffer, set description error' + e);
        });
      },
      function(e) {
        addStatusMsg("createOffer error: " + e);
      },
      rtc_media_constraints
    );
}

function closeStrangerCameraStream() {
    $('div#videoBox video#stranger')[0].srcObject = null
    if(peerConnection)
      peerConnection.close();
}     

function iceCandidate(candidate) {
  //ICE = Interactive Connectivity Establishment
  if(peerConnection)
    peerConnection.addIceCandidate(new IceCandidate(candidate));
  else
    addStatusMsg("peerConnection not created error");
  addStatusMsg("Peer Ice Candidate = " + JSON.stringify(candidate));
}

function getAnswer(answer) {    
    if(!hasSupportForVideoChat())
      return;

    if(peerConnection) {
  peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
    console.log("get answer ok");
    addStatusMsg("peerConnection, SessionDescription answer is ok");
  },
  function(e) {
    addStatusMsg("peerConnection, SessionDescription fail error: " + e);
  });
    }
}

function getOffer(offer) {
    if(!hasSupportForVideoChat())
      return;
    addStatusMsg("peerConnection, setRemoteDescription offer: " + JSON.stringify(offer));
    if(peerConnection) {
      peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
        peerConnection.createAnswer(
          function(answer) {
            peerConnection.setLocalDescription(answer);
            addStatusMsg("create answer sent: " + JSON.stringify(answer));
            ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
          },
          function(e) {
            addStatusMsg("peerConnection, setRemoteDescription create answer fail: " + e);
          }
        );
      });
    }
}
1 Answers

My website where I use it: https://www.camspark.com/
Fixed myself I figured out I had 2 problems with this code.

First problem was then createOffer() must only be sent by 1 person not both people.. you have to randomly pick which person which does the createOffer().

Second problem is the ICE Candidate's you have to create a queue/array for both sides, which holds all the incoming ice_candidates. Only do the peerConnection.addIceCandidate(new IceCandidate(candidate)); when the response to createOffer() is received and the setRemoteDescription from createOffer() response is set up.

Both getAnswer() and getOffer() use exactly same code, but one is received for 1 client while the other is received for the other client. Both need to flush the IceCandidates array when either of them is triggered.. Maybe if anyone wants you could combine both functions into 1 function as the code is the same.

Final working code looks like this

var ws;

var peerConnection, localStream;  
//STUN = (Session Traversal Utilities for NAT)  
var rtc_server = {
  iceServers: [
                {urls: "stun:stun.l.google.com:19302"},
                {urls: "stun:stun.services.mozilla.com"},
                {urls: "stun:stun.stunprotocol.org:3478"},
                {url: "stun:stun.l.google.com:19302"},
                {url: "stun:stun.services.mozilla.com"},
                {url: "stun:stun.stunprotocol.org:3478"},
  ]
}

//offer SDP = [Session Description Protocol] tells other peers what you would like
var rtc_media_constraints = {
  mandatory: {
    OfferToReceiveAudio: true,
    OfferToReceiveVideo: true
  }
};

var rtc_peer_options = {
  optional: [
              {DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
  ]
}
var finishSDPVideoOffer = false;
var isOfferer = false;
var iceCandidates = [];
var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

function hasSupportForVideoChat() {
   return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}

function loadMyCameraStream() {
    if (getUserMedia) {
      getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
        function(localMediaStream) {
          //Add my video
          $("div#videoBox video#you")[0].muted = true;
          $("div#videoBox video#you")[0].autoplay = true;
          $("div#videoBox video#you").attr('playsinline', '');
          $("div#videoBox video#you").attr('webkit-playsinline', '');
          $("div#videoBox video#you")[0].srcObject = localMediaStream;
          localStream = localMediaStream;
        },
        function(e) {
          addStatusMsg("Your Video has error : " + e);
        }
      );
    } else {
      addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
      return;
    }
}

function loadStrangerCameraStream(isOfferer_) {
    if(!hasSupportForVideoChat())
      return;

    //Only add pending ICE Candidates when getOffer() is finished.
    finishSDPVideoOfferOrAnswer = false;
    iceCandidates = []; //clear ICE Candidates array.
    isOfferer = isOfferer_;

    peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
    if (peerConnection.addTrack !== undefined)
      localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
    else
      peerConnection.addStream(localStream);

    peerConnection.onicecandidate = function(e) {
      if (!e || !e.candidate)
        return;    
      ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
    };

    if (peerConnection.addTrack !== undefined) {
      //newer technology
      peerConnection.ontrack = function(e) {
        //e.streams.forEach(stream => doAddStream(stream));
        addStatusMsg("ontrack called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.streams[0];
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    } else {
      //older technology
      peerConnection.onaddstream = function(e) {
        addStatusMsg("onaddstream called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.stream;
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    }

    if(isOfferer) {
      peerConnection.createOffer(
        function(offer) {
          peerConnection.setLocalDescription(offer, function () {
            //both offer and peerConnection.localDescription are the same.
            addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
            //addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
            ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
          },
          function(e) {
            addStatusMsg('createOffer, set description error' + e);
          });
        },
        function(e) {
          addStatusMsg("createOffer error: " + e);
        },
        rtc_media_constraints
      );
    }
}

function closeStrangerCameraStream() {
    $('div#videoBox video#stranger')[0].srcObject = null
    if(peerConnection)
      peerConnection.close();
}     

function iceCandidate(candidate) {
  //ICE = Interactive Connectivity Establishment
  if(!finishSDPVideoOfferOrAnswer) {
    iceCandidates.push(candidate);
    addStatusMsg("Queued iceCandidate");
    return;
  }

  if(!peerConnection) {
    addStatusMsg("iceCandidate peerConnection not created error.");
    return;
  }

  peerConnection.addIceCandidate(new IceCandidate(candidate));
  addStatusMsg("Added on time, Peer Ice Candidate = " + JSON.stringify(candidate));
}

function getAnswer(answer) {    
    if(!hasSupportForVideoChat())
      return;

    if(!peerConnection) {
      addStatusMsg("getAnswer peerConnection not created error.");
      return;
    }

    peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
      addStatusMsg("getAnswer SessionDescription answer is ok");
      finishSDPVideoOfferOrAnswer = true;
      while (iceCandidates.length) {
        var candidate = iceCandidates.shift();
        try {
          peerConnection.addIceCandidate(new IceCandidate(candidate));
          addStatusMsg("Adding queued ICE Candidates");
        } catch(e) {
          addStatusMsg("Error adding queued ICE Candidates error:" + e);
        }
      }
      iceCandidates = [];
    },
    function(e) {
      addStatusMsg("getAnswer SessionDescription fail error: " + e);
    });
}

function getOffer(offer) {
    if(!hasSupportForVideoChat())
      return;

    if(!peerConnection) {
      addStatusMsg("getOffer peerConnection not created error.");
      return;
    }

    addStatusMsg("getOffer setRemoteDescription offer: " + JSON.stringify(offer));
    peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
      finishSDPVideoOfferOrAnswer = true;
      while (iceCandidates.length) {
        var candidate = iceCandidates.shift();
        try {
          peerConnection.addIceCandidate(new IceCandidate(candidate));
          addStatusMsg("Adding queued ICE Candidates");
        } catch(e) {
          addStatusMsg("Error adding queued ICE Candidates error:" + e);
        }
      }
      iceCandidates = [];
      if(!isOfferer) {
        peerConnection.createAnswer(
          function(answer) {
            peerConnection.setLocalDescription(answer);
            addStatusMsg("getOffer create answer sent: " + JSON.stringify(answer));
            ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
          },
          function(e) {
            addStatusMsg("getOffer setRemoteDescription create answer fail: " + e);
          }
        );
      }
    });
}

Here is the patch I did on server-side WebSocket (Java) server.

//JSON
 //["connected", {videoChatOfferer: true}]
 //["connected", {videoChatOfferer: false}]
 JSONObject obj = new JSONObject();
 JSONArray list = new JSONArray();
 list.put("loadStrangerCameraStream");
 obj.put("videoChatOfferer", true); //first guy offerer for WebRTC.
 list.put(obj);
 server.sendMessage(websocket, list.toString()); //connected to chat partner
 obj.put("videoChatOfferer", false); //second guy isn't offerer.
 list.put(obj);
 server.sendMessage(stranger.getWebSocket(), list.toString()); //connected to chat partner
Related