title: WebRTC DataChannel: working in Firefox but not Chrome
body: <p>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.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>OF course in none of these cases I get any error message in the web/JavaScript console; it would have been too easy.</p>
  
  <p>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)</p>
  
  <p>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.</p>
  
  <p>More specifically, if I'm user A and click on user B, the following is supposed to happen :</p>
  
  <ol>
  <li>Through the signaling WebSocket, A sends a message to B indicating that he wants to call him</li>
  <li>B replies to A with a WebRTC offer</li>
  <li>A obtains the offer and replies with a WebRTC answer.</li>
  <li>The DataChannel is established</li>
  <li>When the DataChannel on B side is open, he sends "Hello, world!" to A</li>
  <li><p>When the DataChannel on A side is open, he sends "It works!" to B; this can happen in the opposite order</p>
  
  <ul>
  <li>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)</li>
  <li>Bonus optional question, why do I get more than one ICE candidates, especially even after the connection has been successfully established ?</li>
  </ul></li>
  </ol>
  
  <p>I think I should have the latest Firefox and Crhome: resp. 45 and 49, on Windows 7 64 bits.</p>
  
  <p>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.</p>
  
  <pre><code>function log (s) {
  $('#log')[0].insertAdjacentHTML('beforeEnd', s+'&lt;br /&gt;');
  }
  
  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&lt;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=&gt;{ if (e&amp;&amp;e.candidate) { ws.send({ type: 'RTCSignal', to: to, candidate: e.candidate }); log('ICE candidate received'); }};
  pc.onconnectionstatechange = e=&gt;log("Connection state change: " +pc.connectionState);
  pc.onnegotiationneeded = e=&gt;{ console.log("Negotiation needed: ", e); log("Negotiation needed: " +e); };
  pc.onicecandidateerror = e=&gt;log("ICE candidate error: " +e);
  pc.oniceconnectionstatechange = e=&gt;log("ICE connection state change: " +pc.iceConnectionState);
  pc.onicegatheringstatechange = e=&gt;log("ICE gathering state change: " +pc.iceGatheringState);
  pc.onsignalingstatechange = e=&gt;log("Signaling state change: " +pc.signalingState);
  pc.onaddstream = e=&gt;{ console.log(e); log('Add stream'); };
  pc.ondatachannel = e=&gt;{ 
  log("Received data channel " + e.channel.label);
  pc.channel=e.channel;
  pc.channel.onopen = e=&gt;{ log("Data channel opened"); pc.channel.send("It works!"); };
  pc.channel.onmessage = e=&gt;log("Message from " + to + ": " + e.data);
  pc.channel.onerror = e=&gt;log("Data channel error: " +e);
  pc.channel.onclose = e=&gt;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 = _=&gt;{ pc.channel.send("Hello, world!"); log("Data channel opened"); };
  pc.channel.onmessage = e=&gt;log("Message from " + pc.from + ": " + e.data);
  pc.channel.onerror = e=&gt;log("Data channel error: " +e);
  pc.channel.onclose = e=&gt;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', _=&gt;log('Connected to web socket'));
  ws.on('disconnect', _=&gt;log('Disconnected from web socket'));
  ws.on('userlist', o=&gt;updateUserList(o));
  ws.connect() .then(_=&gt;{ ws.send({type:'userlist'}); setInterval(_=&gt;ws.send({ type: 'userlist' }), 15000); });
  
  ws.on('RTCCall', o=&gt;{
  log(o.from + " is calling !");
  if (!pc) pc = createRTCPeerConnection(o.from);
  pc.from = o.from;
  pc.channel = createDataChannel(pc, 'chat');
  pc.createOffer(desc=&gt;{ 
  pc.setLocalDescription(desc, _=&gt;log("setLocalDescription succeeded"), fail=&gt;log("setLocalDescription failed: " + fail));
  log("Sending offer to " + o.from); 
  ws.send({type: 'RTCSignal', to: o.from, answer: true, sdp: desc}); }, 
  fail=&gt;log("createOffer failed: "+fail), sdpOptions);
  });//RTCCall
  
  ws.on('RTCSignal', o=&gt;{
  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), _=&gt;log("setRemoteDescription succeeded"), fail=&gt;log("setRemoteDescription failed: " +fail));
  else  if (o.candidate) pc.addIceCandidate(new RTCIceCandidate(o.candidate));
  if (o.answer) pc.createAnswer(desc=&gt;{ 
  pc.setLocalDescription(desc, _=&gt;log("setLocalDescription succeeded"), fail=&gt;log("setLocalDescription failed: " + fail));
  log("Sending answer to " + o.from); 
  ws.send({type: 'RTCSignal', to: o.from, sdp: desc}); 
  }, 
  fail=&gt;log("createAnswer failed: "+fail), sdpOptions);
  });
  </code></pre>
  
  <p>Here is the output when firefox connects to firefox, what works perfectly :</p>
  
  <p>Caller:</p>
  
  <pre><code>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!
  </code></pre>
  
  <p>called:</p>
  
  <pre><code>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!
  </code></pre>
  
  <p>Here is the output when Chrome connects to Chrome, what fails :</p>
  
  <p>Caller:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Called:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Here is the output when Firefox connects to Chrome, what fails:</p>
  
  <p>Fiefox caller:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Chrome called:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Here is the output when Firefox connects to Chrome the other way round, what also fails:
  Chrome caller:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Firefox called:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Now, a few thoughs :</p>
  
  <ol>
  <li><p>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 :</p>
  
  <p>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');</p></li>
  </ol>
  
  <p>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:</p>
  
  <p>Caller:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>Called:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p>And this one: <code>{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }]</code>
  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.</p>
  
  <p>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.</p>
  
  <p>Thank you very much for your help.</p>
  
  <p>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.</p>