Search code examples
javascriptwebrtcrtcdatachannel

WebRTC DataChannel: working in Firefox but not Chrome


I'm fairly new in WebRTC. I'm trying to establish a simple data channel between two peers, without audio nor video; just text data. At the end it's going to be a game where 2-7 peers will connect to a peer who will be game master.

After hours of googling and reading html5rocks, MDN and other stack posts, I have tried many things, but I still doesn't manage to make it work.

Everything works fine when I open the page on two different Firefox tabs. I can see that one of the tabs sends "Hello, world!" and that the other sends "It works!". The DataChannel is well established and both tabs get their respective peer's message.

However, when running it on Chrome, it doesn't work. In one of my tests, the DataChannel is mysteriously closed before I'm able to send anything, while in another, RTCPeerConnection.ondatachannel event don't seem to be called at all (more details further down). If I try to make Firefox communicate with Chrome, regardless on the order, I obtain different mysterious errors about a failure of setRemoteDescription.

OF course in none of these cases I get any error message in the web/JavaScript console; it would have been too easy.

My problem isn't in the signaling process, at least I don't think so. A plain WebSocket is used to communicate with a very simple Node.js server. I would prefer avoid using a library such as PeerJS. First of all because we better learn the thing by doing it manually, and secondly because I would like to use the signaling Node.js server for other things than just signaling. That's not a problem per se on Node side, but it is on browser side (because I'm not going to find a little raindrop in an ocean of 100+ KB minified/obfuscated source code)

The basic scenario is very simple: a list of currently connected users is automatically refreshed every 15 seconds on the page. By clicking on a user name, you get connected to him, you send "Hello, world!" while he answers "It works!" concurrently; that's eat for the moment. The simple chat text box is of course the next logical step, once I'm able to set up a basic communication.

More specifically, if I'm user A and click on user B, the following is supposed to happen :

  1. Through the signaling WebSocket, A sends a message to B indicating that he wants to call him
  2. B replies to A with a WebRTC offer
  3. A obtains the offer and replies with a WebRTC answer.
  4. The DataChannel is established
  5. When the DataChannel on B side is open, he sends "Hello, world!" to A
  6. When the DataChannel on A side is open, he sends "It works!" to B; this can happen in the opposite order

    • What should I modify in order to make it work regardless of the browsers used ? (Of course I know that it works only in Firefox and Chrome currently)
    • Bonus optional question, why do I get more than one ICE candidates, especially even after the connection has been successfully established ?

I think I should have the latest Firefox and Crhome: resp. 45 and 49, on Windows 7 64 bits.

Below is my JavaScript code; then the outputs corresponding to a few scenarios, and finally some thoughs I got so far by reading other posts and tutorials.

function log (s) {
$('#log')[0].insertAdjacentHTML('beforeEnd', s+'<br />');
}

function callUser (e) {
var uname = this.href.substring(1+this.href.indexOf('#'));
ws.send({ type: 'RTCCall', to: uname });
log('Calling ' + uname + '...');
e.preventDefault();
return false;
}

function updateUserList (o) {
var div = $('#userlist')[0];
div.innerHTML='';
div.append('p', o.userlist.length + ' connected users');
for (var i=0, n=o.userlist.length; i<n; i++) {
var uname = o.userlist[i];
var a = div.append('a', {href: '#'+uname }, uname);
div.append('br');
a.onclick = callUser;
}}

function createRTCPeerConnection (to) {
log("Creating RTCPeerConnection...");
var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
var pc = new RTCPeerConnection(pcConfig, pcOptions);
pc.onicecandidate = e=>{ if (e&&e.candidate) { ws.send({ type: 'RTCSignal', to: to, candidate: e.candidate }); log('ICE candidate received'); }};
pc.onconnectionstatechange = e=>log("Connection state change: " +pc.connectionState);
pc.onnegotiationneeded = e=>{ console.log("Negotiation needed: ", e); log("Negotiation needed: " +e); };
pc.onicecandidateerror = e=>log("ICE candidate error: " +e);
pc.oniceconnectionstatechange = e=>log("ICE connection state change: " +pc.iceConnectionState);
pc.onicegatheringstatechange = e=>log("ICE gathering state change: " +pc.iceGatheringState);
pc.onsignalingstatechange = e=>log("Signaling state change: " +pc.signalingState);
pc.onaddstream = e=>{ console.log(e); log('Add stream'); };
pc.ondatachannel = e=>{ 
log("Received data channel " + e.channel.label);
pc.channel=e.channel;
pc.channel.onopen = e=>{ log("Data channel opened"); pc.channel.send("It works!"); };
pc.channel.onmessage = e=>log("Message from " + to + ": " + e.data);
pc.channel.onerror = e=>log("Data channel error: " +e);
pc.channel.onclose = e=>log("Data channel closed: " +e);
};
log("RTCPeerConnection created");
return pc;
}

function createDataChannel (pc, name) {
log("Creating DataChannel " + name + "...");
pc.channel=pc.createDataChannel(name, { ordered: false });
pc.channel.onopen = _=>{ pc.channel.send("Hello, world!"); log("Data channel opened"); };
pc.channel.onmessage = e=>log("Message from " + pc.from + ": " + e.data);
pc.channel.onerror = e=>log("Data channel error: " +e);
pc.channel.onclose = e=>log("Data channel closed: " +e);
log("DataChannel " + name + " created");
return pc.channel;
}

var ws = new WSClient('ws://localhost:3003/');
var pc, 
pcConfig = {iceServers:[{url:'stun:stun.l.google.com:19302'}]},
pcOptions = { optional:  [
{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] 
},
sdpOptions = {mandatory:  { OfferToReceiveAudio: true,  OfferToReceiveVideo: false } };

log('Initializing...');
ws.on('connect', _=>log('Connected to web socket'));
ws.on('disconnect', _=>log('Disconnected from web socket'));
ws.on('userlist', o=>updateUserList(o));
ws.connect() .then(_=>{ ws.send({type:'userlist'}); setInterval(_=>ws.send({ type: 'userlist' }), 15000); });

ws.on('RTCCall', o=>{
log(o.from + " is calling !");
if (!pc) pc = createRTCPeerConnection(o.from);
pc.from = o.from;
pc.channel = createDataChannel(pc, 'chat');
pc.createOffer(desc=>{ 
pc.setLocalDescription(desc, _=>log("setLocalDescription succeeded"), fail=>log("setLocalDescription failed: " + fail));
log("Sending offer to " + o.from); 
ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); }, 
fail=>log("createOffer failed: "+fail), sdpOptions);
});//RTCCall

ws.on('RTCSignal', o=>{
log("Received signal from " + o.from + ": " + (o.sdp?"sdp":"") + (o.candidate?"ICE":""));
if (!pc) pc = createRTCPeerConnection(o.from);
pc.from = o.from;
if (o.sdp)  pc.setRemoteDescription(new RTCSessionDescription(o.sdp), _=>log("setRemoteDescription succeeded"), fail=>log("setRemoteDescription failed: " +fail));
else  if (o.candidate) pc.addIceCandidate(new RTCIceCandidate(o.candidate));
if (o.answer) pc.createAnswer(desc=>{ 
pc.setLocalDescription(desc, _=>log("setLocalDescription succeeded"), fail=>log("setLocalDescription failed: " + fail));
log("Sending answer to " + o.from); 
ws.send({type: 'RTCSignal', to: o.from, sdp: desc}); 
}, 
fail=>log("createAnswer failed: "+fail), sdpOptions);
});

Here is the output when firefox connects to firefox, what works perfectly :

Caller:

Initializing...
Connected to web socket
Calling user132...
Received signal from user132: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
Signaling state change: have-remote-offer
setRemoteDescription succeeded
Received signal from user132: ICE
Received signal from user132: ICE
Received signal from user132: ICE
Sending answer to user132
Signaling state change: stable
setLocalDescription succeeded
Received signal from user132: ICE
ICE connection state change: checking
ICE connection state change: connected
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
Received data channel chat
Received signal from user132: ICE
ICE candidate received
Data channel opened
Message from user132: Hello, world!

called:

Initializing...
Connected to web socket
user133 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
Negotiation needed: [object Event]
DataChannel chat created
Sending offer to user133
Signaling state change: have-local-offer
setLocalDescription succeeded
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
Received signal from user133: sdp
Signaling state change: stable
setRemoteDescription succeeded
ICE connection state change: checking
ICE connection state change: connected
ICE candidate received
Data channel opened
Received signal from user133: ICE
Received signal from user133: ICE
Received signal from user133: ICE
Received signal from user133: ICE
Received signal from user133: ICE
Message from user133: It works!

Here is the output when Chrome connects to Chrome, what fails :

Caller:

Initializing...
Connected to web socket
Calling user134...
Received signal from user134: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
setRemoteDescription succeeded
Signaling state change: have-remote-offer
Sending answer to user134
setLocalDescription succeeded
Signaling state change: stable
Received signal from user134: ICE
ICE connection state change: checking
ICE candidate received
Received signal from user134: ICE
ICE candidate received
ICE connection state change: connected

Called:

Initializing...
Connected to web socket
user135 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
DataChannel chat created
Negotiation needed: [object Event]
Sending offer to user135
Signaling state change: have-local-offer
setLocalDescription succeeded
Received signal from user135: sdp
Data channel closed: [object Event]
setRemoteDescription succeeded
Signaling state change: stable
ICE connection state change: checking
ICE candidate received
Received signal from user135: ICE
ICE candidate received
Received signal from user135: ICE
ICE connection state change: connected
ICE connection state change: completed

Here is the output when Firefox connects to Chrome, what fails:

Fiefox caller:

Initializing...
Connected to web socket
Calling user136...
Received signal from user136: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
Signaling state change: have-remote-offer
setRemoteDescription succeeded
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE
Sending answer to user136
Signaling state change: stable
setLocalDescription succeeded
ICE connection state change: failed
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE
Received signal from user136: ICE

Chrome called:

Initializing...
Connected to web socket
user137 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
DataChannel chat created
Negotiation needed: [object Event]
Sending offer to user137
setLocalDescription succeeded
Signaling state change: have-local-offer
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
Received signal from user137: sdp
setRemoteDescription failed: OperationError: Failed to parse SessionDescription. 
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received

Here is the output when Firefox connects to Chrome the other way round, what also fails: Chrome caller:

Initializing...
Connected to web socket
Calling user138...
Received signal from user138: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
setRemoteDescription failed: OperationError: Failed to set remote offer sdp: Session error code: ERROR_CONTENT. Session error description: Failed to set
remote data description send parameters..
Signaling state change: have-remote-offer
Sending answer to user138
setLocalDescription failed: OperationError: Failed to set local sdp: Session error code: ERROR_CONTENT. Session error description: Failed to set remote
data description send parameters..
Received signal from user138: ICE
Received signal from user138: ICE
Received signal from user138: ICE
Received signal from user138: ICE

Firefox called:

Initializing...
Connected to web socket
user139 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
Negotiation needed: [object Event]
DataChannel chat created
Sending offer to user139
Signaling state change: have-local-offer
setLocalDescription succeeded
Received signal from user139: sdp
Signaling state change: stable
setRemoteDescription succeeded
ICE candidate received
ICE candidate received
ICE candidate received
ICE candidate received
ICE connection state change: failed

Now, a few thoughs :

  1. I have read multiple times that the DataChannel should be created before the offer is sent. Thus I tried to modify my code as follows to make sure it's the case :

    pc.createOffer(desc=>{ pc.setLocalDescription(desc, _=>say("setLocalDescription succeeded"), fail=>say("setLocalDescription failed: " + fail)); say("Sending offer to " + o.from); ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); }, fail=>say("createOffer failed: "+fail), sdpOptions); pc.channel = createDataChannel(pc, 'chat');

This modification doesn't change anything for firefox. It continues working as well as before. For Chrome, it still doesn't work; but the output is different. Previously, it semmed that the DataChannel is mysteriously closed before I'm able to send anything, just before calling setRemoteDescription. IN this case however, I don't get any news, the DataChannel stays in connecting state. Here is the output:

Caller:

Initializing...
Connected to web socket
Calling user142...
Received signal from user142: sdp
Creating RTCPeerConnection...
RTCPeerConnection created
setRemoteDescription succeeded
Signaling state change: have-remote-offer
Sending answer to user142
Signaling state change: stable
setLocalDescription succeeded
ICE candidate received
Received signal from user142: ICE
ICE connection state change: checking
ICE candidate received
Received signal from user142: ICE
ICE connection state change: connected

Called:

Initializing...
Connected to web socket
user143 is calling !
Creating RTCPeerConnection...
RTCPeerConnection created
Creating DataChannel chat...
DataChannel chat created
Negotiation needed: [object Event]
Sending offer to user143
setLocalDescription succeeded
Signaling state change: have-local-offer
Received signal from user143: sdp
Signaling state change: stable
setRemoteDescription succeeded
ICE connection state change: checking
Received signal from user143: ICE
ICE candidate received
ICE candidate received
Received signal from user143: ICE
ICE connection state change: connected
ICE connection state change: completed

Anyway it seems that in none of the two cases, the event RTCPeerConnection.ondatachannel is never called. I have the feeling that I can't really know well if my handler is just never called, or if the connection hasn't well been established.

I have also tried to create the DataChannel at another moment without success. For example, after setRemoteDescription has been called on both side. IN that case, Firefox refuses to create an offer, because I'm neither requesting audio/video, nor a track (I don't know what it is) and nor a DataChannel (it hasn't been created yet). So my conclusion so far is that creating the channel before sending the offer is the correct way; at least the only one that will work with Firefox.

I have also read many times that, given that I'm not requesting audio/video, I'm not obliged to send an offer and an answer. But if I squeeze that out from my code, nothing seems to happen. No ICE server exchange and so on... Somewhere else, I read that no ICE server stuff starts before setLocalDescription is called. So I must call setLocalDescription, and therefore I must create an offer. From there it seems logical that I'm obliged to send it to the other peer via the signaling channel, that I'm obliged to call setRemoteDescription and then required to answer.

I'm using sdpOptions = {mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: false } };`` in my code, although I don't plan to send audio/video streams. I have already googled a lot before noticing that if I set them both to false, then Chrome never starts its ICE server thing, and thus there couldn't be any P2P connection.

And this one: {DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] I copied it from a tutorial without really knowing what it does. Anyway, removing it alltogether, or setting one or the other to false doesn't change anything to my results.

Thank you for reading a so long post. I hope you have an idea how I could solve the problem. Please telle me what I should do or at least give me clues on what it could be.

Thank you very much for your help.

EDit: OMG! It seems that all my lines of code have been collapsed together in a single big line. I'm very sorry, it wasn't expected. Telle me how to fix this for the next time in a little comment. Thank you.


Solution

  • Remove this:

    pcOptions = { optional:  [
    {DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] 
    },
    

    It's old non-standard chrome stuff that does nothing in Firefox, but causes bats to fly out in Chrome. Data channels don't run over rtp, nor rely on srtp, in the spec.

    While you're at it, remove this as well:

    sdpOptions = {mandatory: {OfferToReceiveAudio: true,  OfferToReceiveVideo: false}};
    

    The format has changed to (note the lower-case 'o's):

    sdpOptions = { offerToReceiveAudio: true,  offerToReceiveVideo: false};
    

    But it's unnecessary for just data channels. If it still doesn't work, let me know.

    I also highly recommend adapter.js, the official WebRTC polyfill, which lets you use the latest spec with promises etc. like this. A shim more than a library, it aims to eventually disappear.