2

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);
          }
        );
      });
    }
}
SSpoke
  • 5,656
  • 10
  • 72
  • 124

1 Answers1

7

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
SSpoke
  • 5,656
  • 10
  • 72
  • 124
  • randomly picking won't help, you need to be consistent on both sides. In a group chat that means either the person who joins creates the offer or the person that was in the room first. – Philipp Hancke Mar 12 '20 at 07:31
  • Only when I run the website on the first time the remote stream doesn't show up no errors nothing.. but on the second time I reconnect it works fine and never glitches up again.. I randomize the offerer because they both connect at the same time https://www.camspark.com/ I have another issue with my Java WebSocket and SSL its hanging up but i'll fix that hopefully by tommorrow. – SSpoke Mar 12 '20 at 10:01
  • 1
    Huge thank you @SSpoke for posting your implementation & fix, I'd been struggling with the kStable error all day. Out of curiosity did you end up fixing the fact that the remote stream doesn't show for the first user? I've built a similar implementation (using SignalR as the signaling server) and unfortunately having the same issue you've described in your comment here. As you said, 2nd/3rd/etc user can see both videos but not the first. – Corey Thompson Mar 30 '20 at 10:25
  • unfortunately I couldn't fix that and just abandoned the project as it's good as is lol, as its possible users don't have cams haha. I did add a temporary fix which does work. Possibly just a theory as I won't be testing it out but what @PhilippHancke mentioned that you should know who the offer-er is has to do with glitched streams? who knows.. but I added a chat command `/forcevid` which switches the offer-er from the current offer-er with the opposite user then it calls a `reloadCameraStream()` function which just initiates the `loadMyCameraStream()` and `loadStrangerCameraStream()` again – SSpoke Mar 31 '20 at 06:10
  • P.S.> if you look at omegle's WebRTC, here I took a screenshot https://i.imgur.com/TVsy0B7.png he uses a setTimeout() on ga() which is `setLocalDescription()` with callback to `sendPeerDescription()` which is used for `CreateAnswer()` and `CreateOffer()` I guess he combined both functions in one. I bet the setTimeout() is to fix the glitch where stream doesn't load up he had the same issue and thats how he resolved it. I have no idea how a setTimeout with 0 interval does anything as it gets executed instantly.. Source: https://www.omegle.com/static/omegle.js – SSpoke Mar 31 '20 at 06:21
  • 2
    Thanks! I ended up getting it working haha. I do have the 2nd person always being the offerer (the signaling server keeps a record of people in the room, so when a 2nd person joins the response to the caller is to make an offer to the other person) - and that seems to work :) FYI to anyone else that stumbles upon this - I had to rewrite a bit to get it to work on Safari on iOS/Mac - but it otherwise worked almost out of the box for Chrome desktop&mobile. – Corey Thompson Mar 31 '20 at 22:29