Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebRTC connects on local connection, but fails over internet

I have some test code running that I'm using to try to learn the basics of WebRTC. This test code works on a LAN, but not over the internet, even if I use a TURN server (one side shows a status "checking" and the other "failed"). I can see that there are ice candidates in the SDPs, so I don't need to send them explicitly (right?).

This writes a lot of debug info to the console, so I can tell my signalling server is working. I'm stuck - what do I need to do differently in my code to enable it to work over the internet?

BTW, I have run other WebRTC demo scripts between my test computers, and they do work (like opentokrtc.ocom)

<html>
<head>
<title>test</title>
<script type="text/javascript">
var curInvite = null;
//create an invitation to connect and post to signalling server
function CreateInvite(){
    //function to run upon receiving a response
    var postRespFunc = function(txt){
        console.log("Posted offer and received " + txt);
        var invite = txt;
        curInvite = invite;
        document.getElementById("inviteId").innerHTML = invite;
        //then poll for answer...
        var pollFunc = function(){
            GetRequest("answered?"+invite,function(txt){
                if(txt){
                    //assume it's the answer
                    handleAnswer(txt);
                }else{
                    //poll more
                    setTimeout(pollFunc,1000);
                }
            });
        };
        //start polling for answer
        setTimeout(pollFunc,1000);
    };
    //function to run after creating the WebRTC offer
    var postFunc = function(offer){
        PostRequest('offer','offer='+encodeURIComponent(offer), postRespFunc);
    }
    //create the offer
    createLocalOffer(postFunc);
}
function AnswerInvite(){
    var invite = document.getElementById("invitation").value;
    //can we create our local description BEFORE we get the remote desc?
    //reduce to one ajax call?
    GetRequest("accept?"+invite,function(txt){
        var answerPostedCallback = function(txt){
            console.log("answerPostedCallback",txt);
        }
        var answerCallback = function(answer){
            PostRequest("answer?"+invite,'answer='+encodeURIComponent(answer), answerPostedCallback);
        }
        handleOffer(txt, answerCallback);
        //then we're waiting for a data channel to be open...
    });
}

function PostRequest(postUrl, reqStr, callback){
    var req=new XMLHttpRequest();
    req.onload = function(){
        var strResp = req.responseText;
        if(callback) callback(strResp);
    }
    //var namevalue=encodeURIComponent(document.getElementById("test").value);
    //var parameters="name="+namevalue;
    req.open("POST", postUrl, true);
    req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    req.send(reqStr);
}
function GetRequest(getUrl, callback){
    var req=new XMLHttpRequest();
    req.onload = function(){
        var strResp = req.responseText;
        if(callback) callback(strResp);
    }
    //var namevalue=encodeURIComponent(document.getElementById("test").value);
    //var parameters="name="+namevalue;
    req.open("GET", getUrl, true);
    req.send();
}

/************ WebRTC stuff ****************/
var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || 
                       window.webkitRTCPeerConnection || window.msRTCPeerConnection;
var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription ||
                       window.webkitRTCSessionDescription || window.msRTCSessionDescription;

navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia ||
                       navigator.webkitGetUserMedia || navigator.msGetUserMedia;
//SEE http://olegh.ftp.sh/public-stun.txt                       
var cfg = {"iceServers":[
{url:'stun:stun.12connect.com:3478'},
{url:'stun:stun.12voip.com:3478'}
]};

cfg.iceServers = [{
url: 'turn:numb.viagenie.ca',
    credential: 'muazkh',
    username: '[email protected]'
}]

var con = { 'optional': [{'DtlsSrtpKeyAgreement': true}] };

var peerConnection = new RTCPeerConnection(cfg,con);
var dataChannel = null;

function initDataChannel(){
    dataChannel.onerror = function (error) {
            console.log("Data Channel Error:", error);
        };

        dataChannel.onmessage = function (event) {
            console.log("Got Data Channel Message:", event.data);
            var data = JSON.parse(event.data);
            document.getElementById("chat").innerHTML+= "RECD: " + data + "<br />";
        };

        dataChannel.onopen = function () {
            console.log('data channel open');
            alert("data channel open, ready to connect!");
        };

        dataChannel.onclose = function () {
            console.log("The Data Channel is Closed");
            peerConnection.close();
            alert("Disconnected!");
        };
}

//used when peerConnection is an answerer
peerConnection.ondatachannel = function (e) {
    dataChannel = e.channel || e; // Chrome sends event, FF sends raw channel
    initDataChannel();
    console.log("Received datachannel", arguments);
}
//to initiate a connection
function createLocalOffer(callback) {
        //create datachannel
        try {
        dataChannel = peerConnection.createDataChannel('test', {reliable:true});
        initDataChannel();
        console.log("Created datachannel (peerConnection)");
    } catch (e) { console.warn("No data channel (peerConnection)", e); }
        //set event handler
        peerConnection.onicecandidate = function (e) {
                console.log("ICE candidate (peerConnection)", e);
                if (e.candidate == null) {
                        console.log("ice candidate",peerConnection.localDescription);
                        callback(JSON.stringify(peerConnection.localDescription));
                }
        };
    peerConnection.createOffer(function (desc) {
        peerConnection.setLocalDescription(desc);
        console.log("created local offer", desc);
    }, function () {console.warn("Couldn't create offer");});
}

peerConnection.onconnection = function(e){
    console.log("peerConnection connected",e);
};

function onsignalingstatechange(state) {
    console.info('signaling state change:', state);
}

function oniceconnectionstatechange(state) {
    console.info('ice connection state change:', state);
    console.info('iceConnectionState: ', peerConnection.iceConnectionState);
}

function onicegatheringstatechange(state) {
    console.info('ice gathering state change:', state);
}

peerConnection.onsignalingstatechange = onsignalingstatechange;
peerConnection.oniceconnectionstatechange = oniceconnectionstatechange;
peerConnection.onicegatheringstatechange = onicegatheringstatechange;

//local handles answer from remote
function handleAnswer(answerJson) {
        var obj = JSON.parse(answerJson);
        var answerDesc = new RTCSessionDescription(obj);
    peerConnection.setRemoteDescription(answerDesc);
}

/* functions for remote side */

//handle offer from the initiator
function handleOffer(offerJson, callback) {
        var obj = JSON.parse(offerJson);
        var offerDesc = new RTCSessionDescription(obj);
    peerConnection.setRemoteDescription(offerDesc);
    //set event handler
        peerConnection.onicecandidate = function (e) {
                console.log("ICE candidate (peerConnection)", e);
                if (e.candidate == null) {
                        console.log("ice candidate",peerConnection.localDescription);
                }
        };
    peerConnection.createAnswer(function (answerDesc) {
        console.log("Created local answer: ", answerDesc);
        peerConnection.setLocalDescription(answerDesc);
        callback(JSON.stringify(answerDesc));
    }, function () { console.warn("No create answer"); });
}

function sendMessage() {
            var msg = document.getElementById("msg").value;
            document.getElementById("msg").value = null;
            document.getElementById("chat").innerHTML+= "SENT: " + msg + "<br />";
            var obj = {message: msg};
            dataChannel.send(JSON.stringify(msg));
    return false;
};

</script>
</script>
</head>
<body>
<p>test</p>
<p>
<div id="createWrapper">
<h4>create an invitiation</h4>
<button type="button" onclick="CreateInvite();">create invitation</button>
<h3 id="inviteId"></h3>
</div>
<div id="acceptWrapper">
<h4>or accept an inviation</h4>
<input id="invitation" type="text" name="invitation" />
<button type="button" onclick="AnswerInvite()">answer invitation</button>
</div>
<p>Once the data channel is open type your messages below</p>
<input type="text" id="msg" /><button type="button" onclick="sendMessage()">send</button>
<div id="chat"></div>
</body>
</html>

[EDIT: HERE IS WORKING CODE, IN CASE IT'S USEFUL TO ANYONE ELSE. You will still need your own signalling server and working STUN/TURN server(s), but this was helpful to me to understand the basics]

<html>
<head>
<title>test</title>
<script type="text/javascript">
var curInvite = null;
//create an invitation to connect and post to signalling server
function CreateInvite(){
  //function to run upon receiving a response
  var postRespFunc = function(txt){
    console.log("Posted offer and received " + txt);
    var invite = txt;
    curInvite = invite;
    document.getElementById("inviteId").innerHTML = invite;
    //then poll for answer...
    var pollFunc = function(){
      GetRequest("answered?"+invite,function(txt){
        if(txt){
          //assume it's the answer
          handleAnswer(txt);
        }else{
          //poll more
          setTimeout(pollFunc,1000);
        }
      });
    };
    //start polling for answer
    setTimeout(pollFunc,100);
  };
  //function to run after creating the WebRTC offer
  var postFunc = function(offer){
    PostRequest('offer','offer='+encodeURIComponent(offer), postRespFunc);
  }
  //create the offer
  createLocalOffer(postFunc);
}
function AnswerInvite(){
  var invite = document.getElementById("invitation").value;
  //can we create our local description BEFORE we get the remote desc?
  //reduce to one ajax call?
  GetRequest("accept?"+invite,function(txt){
    var answerPostedCallback = function(txt){
      console.log("answerPostedCallback",txt);
    }
    var answerCallback = function(answer){
      PostRequest("answer?"+invite,'answer='+encodeURIComponent(answer), answerPostedCallback);
    }
    handleOffer(txt, answerCallback);
    //then we're waiting for a data channel to be open...
  });
}

function PostRequest(postUrl, reqStr, callback){
  var req=new XMLHttpRequest();
  req.onload = function(){
    var strResp = req.responseText;
    if(callback) callback(strResp);
  }
  //var namevalue=encodeURIComponent(document.getElementById("test").value);
  //var parameters="name="+namevalue;
  req.open("POST", postUrl, true);
  req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  req.send(reqStr);
}
function GetRequest(getUrl, callback){
  var req=new XMLHttpRequest();
  req.onload = function(){
    var strResp = req.responseText;
    if(callback) callback(strResp);
  }
  //var namevalue=encodeURIComponent(document.getElementById("test").value);
  //var parameters="name="+namevalue;
  req.open("GET", getUrl, true);
  req.send();
}

/************ WebRTC stuff ****************/
var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || 
                       window.webkitRTCPeerConnection || window.msRTCPeerConnection;
var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription ||
                       window.webkitRTCSessionDescription || window.msRTCSessionDescription;

navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia ||
                       navigator.webkitGetUserMedia || navigator.msGetUserMedia;
//SEE http://olegh.ftp.sh/public-stun.txt                       
var cfg = {"iceServers":[
    {url: 'turn:numb.viagenie.ca',
        credential: 'muazkh',
        username: '[email protected]'
    }
]};

var con = { 'optional': [{'DtlsSrtpKeyAgreement': true}] };

var peerConnection = null;

function createPeer(){
  peerConnection = new RTCPeerConnection(cfg,con);

  //used when peerConnection is an answerer
  peerConnection.ondatachannel = function (e) {
      dataChannel = e.channel || e; // Chrome sends event, FF sends raw channel
      initDataChannel();
      console.log("Received datachannel", arguments);
  }

  peerConnection.onsignalingstatechange = onsignalingstatechange;
  peerConnection.oniceconnectionstatechange = oniceconnectionstatechange;
  peerConnection.onicegatheringstatechange = onicegatheringstatechange;

  peerConnection.onconnection = function(e){
    console.log("peerConnection connected",e);
  };
}

var dataChannel = null;

function initDataChannel(){
  dataChannel.onerror = function (error) {
      console.log("Data Channel Error:", error);
    };

    dataChannel.onmessage = function (event) {
      console.log("Got Data Channel Message:", event.data);
      var data = JSON.parse(event.data);
      document.getElementById("chat").innerHTML+= "RECD: " + data + "<br />";
    };

    dataChannel.onopen = function () {
      console.log('data channel open');
      alert("data channel open, ready to connect!");
    };

    dataChannel.onclose = function () {
      console.log("The Data Channel is Closed");
      peerConnection.close();
      alert("Disconnected!");
    };
}

//to initiate a connection
function createLocalOffer(callback) {
  createPeer();
    //create datachannel
    try {
        dataChannel = peerConnection.createDataChannel('test', {reliable:true});
        initDataChannel();
        console.log("Created datachannel (peerConnection)");
    } catch (e) { console.warn("No data channel (peerConnection)", e); }
    //set event handler
    peerConnection.onicecandidate = function (e) {
        console.log("ICE candidate (peerConnection)", e);
        if (e.candidate == null) {
            console.log("ice candidate",peerConnection.localDescription);
            callback(JSON.stringify(peerConnection.localDescription));
        }
    };
    peerConnection.createOffer(function (desc) {
        peerConnection.setLocalDescription(desc);
        console.log("created local offer", desc);
    }, function () {console.warn("Couldn't create offer");});
}


function onsignalingstatechange(state) {
    console.info('signaling state change:', state);
}

function oniceconnectionstatechange(state) {
    console.info('ice connection state change:', state);
    console.info('iceConnectionState: ', peerConnection.iceConnectionState);
}

function onicegatheringstatechange(state) {
    console.info('ice gathering state change:', state);
}

//local handles answer from remote
function handleAnswer(answerJson) {
    var obj = JSON.parse(answerJson);
    var answerDesc = new RTCSessionDescription(obj);
    peerConnection.setRemoteDescription(answerDesc);
}

/* functions for remote side */

//handle offer from the initiator
function handleOffer(offerJson, callback) {
    createPeer();
    var obj = JSON.parse(offerJson);
    var offerDesc = new RTCSessionDescription(obj);
    //set event handler
    peerConnection.onicecandidate = function (e) {
        console.log("ICE candidate (peerConnection)", e);
        if (e.candidate == null) {
          console.log("ice candidate",peerConnection.localDescription);
          callback(JSON.stringify(peerConnection.localDescription));
        }
    };
    peerConnection.setRemoteDescription(offerDesc);
    peerConnection.createAnswer(function (answerDesc) {
        console.log("Created local answer: ", answerDesc);
        peerConnection.setLocalDescription(answerDesc);
    }, function () { console.warn("No create answer"); });
}

function sendMessage() {
      var msg = document.getElementById("msg").value;
      document.getElementById("msg").value = null;
      document.getElementById("chat").innerHTML+= "SENT: " + msg + "<br />";
      var obj = {message: msg};
      dataChannel.send(JSON.stringify(msg));
    return false;
};

</script>
</script>
</head>
<body>
<p>test</p>
<p>
<div id="createWrapper">
<h4>create an invitiation</h4>
<button type="button" onclick="CreateInvite();">create invitation</button>
<h3 id="inviteId"></h3>
</div>
<div id="acceptWrapper">
<h4>or accept an inviation</h4>
<input id="invitation" type="text" name="invitation" />
<button type="button" onclick="AnswerInvite()">answer invitation</button>
</div>
<p>Once the data channel is open type your messages below</p>
<input type="text" id="msg" /><button type="button" onclick="sendMessage()">send</button>
<div id="chat"></div>
</body>
</html>
like image 308
Aerik Avatar asked Dec 11 '15 06:12

Aerik


People also ask

Why does WebRTC fail?

If the connection fails, it could be a problem of: Signaling: your browser (or iQunet Server) is not allowed to contact peer.iqunet.lu SSL port 80/443. WebRTC data traffic: your computer (or iQunet Server) are behind a very strict firewall that drops all UDP, STUN, TURN etc.

How does WebRTC bypass NAT?

VoIP (and WebRTC) need to be able to pass media between two peers that might both be behind NAT devices. It also requires external packets to be able to pass to the internal network. This requires mechanisms known as NAT Traversal to be used. In WebRTC, the selected mechanisms are STUN, TURN and ICE.

Can WebRTC work without Internet?

Although WebRTC enables peer-to-peer communication, it still needs a server for signaling: to enable the exchange of media and network metadata to bootstrap a peer connection.

Can WebRTC work without server?

Unfortunately, WebRTC can't create connections without some sort of server in the middle. We call this the signal channel or signaling service. It's any sort of channel of communication to exchange information before setting up a connection, whether by email, postcard, or a carrier pigeon.


2 Answers

The SDP will only contain the candidates that have been gathered up to that point in time, so unless you wait for the null candidate in the pc.onicecandidate callback, you wont get all candidates this way (you seem to be waiting in your createLocalOffer, but not in your handleOffer, I think is the problem here).

That said, I don't recommend this approach since it can take up to 20 seconds for the ICE agent to exhaust all paths (happens a lot on systems with VPN for instance). Instead, I highly recommend sending candidates explicitly to the other side. i.e. Trickle ICE.

like image 117
jib Avatar answered Sep 29 '22 01:09

jib


If you don't see any candidate like relay or server-reflexive then you can try first to capture a tcpdump using wireshark or so to see if there is any outgoing packet to your STUN/TURN server, if no outgoing packet by filtering as STUN then your configuration may not working. If you see out going STUN/TURN traffic then whatever the error is you might get some response from server which may have authentication error or some other error but based on the response type you can determine the issue with STUN/TRUN. If you see success response from STUN/TURN then you can see if you are including the candidates correctly in SDP or your signalling offer/answer message to advertise the candidates to other end.

like image 44
Palash Borhan Uddin Avatar answered Sep 29 '22 00:09

Palash Borhan Uddin