Can't use WebRTC DataChannels with Chrome when Firefox initiates the connection
Question: I am trying to create a simple webpage using WebRTC DataChannels that sends
pings/pongs between browsers.

When **Chrome** initiates the connection and then **Chrome** connects, it
**works**.  
When **Firefox** initiates the connection and then **Firefox** connects, it
**works**.  
When **Chrome** initiates the connection and then **Firefox** connects, it
**works**.  
But when **Firefox** initiates the connection and then **Chrome** connects, it
**doesn't work**. Chrome never receives data sent by Firefox.

I'm using Firefox 26 and Chromium 32, on Archlinux.

Here is my JavaScript code:

    
    
    <!DOCTYPE html>
    <html>
    <head>
        <title>WebRTC test</title>
        <meta charset="utf-8">
    </head>
    <body>
        <button id="create" disabled>Create data channel</button>
    
        <script type="text/javascript">
            // DOM
    
            var create = document.getElementById('create');
    
            // Compatibility
    
            window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
            window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
            window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
    
            // Create a WebRTC object
    
            var rtc = new RTCPeerConnection(null);
    
            // Create a data channel
    
            var sendChannel = rtc.createDataChannel('pingtest', {reliable: false});
            var myMsg = 'ping';
    
            function setRecvChannel(recvChannel) {
                recvChannel.onmessage = function(event) {
                    if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
                        console.log('-> ' + window.btoa(event.data));
                        return; // Received channel's name, ignore
                    }
    
                    console.log('-> ' + event.data);
                    window.setTimeout(function() {
                        console.log('<- ' + myMsg);
                        sendChannel.send(myMsg);
                    }, 500);
                };
            }
    
            // Chrome and Firefox
    
            sendChannel.onopen = function(event) {
                setRecvChannel(sendChannel);
    
                if(myMsg === 'ping') {
                    console.log('<- ' + myMsg);
                    sendChannel.send(myMsg);
                }
            };
    
            // Firefox
    
            rtc.ondatachannel = function(event) {
                setRecvChannel(event.channel);
            };
    
            // ICE
    
            rtc.onicecandidate = function(event) {
                if(event.candidate) {
                    console.log('<- ' + JSON.stringify(event.candidate));
                    ws.send(JSON.stringify(event.candidate));
                }
            };
    
            // Signaling channel
    
            var ws = new WebSocket('ws://127.0.0.1:49300/');
    
            ws.onopen = function() {
                create.disabled = false;
            };
    
            ws.onmessage = function(event) {
                console.log('-> ' + event.data);
                var data = JSON.parse(event.data);
    
                if(data.sdp) {
                    rtc.setRemoteDescription(new RTCSessionDescription(data));
    
                    if(data.type === 'offer') {
                        myMsg = 'pong';
    
                        rtc.createAnswer(function(anwser) {
                            rtc.setLocalDescription(anwser, function () {
                                console.log('<- ' + JSON.stringify(anwser));
                                ws.send(JSON.stringify(anwser));
                            });
                        }, console.error);
                    }
                }
                else {
                    rtc.addIceCandidate(new RTCIceCandidate(data));
                }
            };
    
            ws.onclose = function() {
                create.disabled = true;
            };
    
            // Create an offer
    
            create.onclick = function() {
                rtc.createOffer(function(offer) {
                    rtc.setLocalDescription(offer, function () {
                        offer.sdp = offer.sdp;
                        console.log(offer.sdp);
                        console.log('<- ' + JSON.stringify(offer));
                        ws.send(JSON.stringify(offer));
                    });
                }, console.error);
            };
        </script>
    </body>
    </html>
    

Here is the WebSocket-based signaling server I've created only for test
purposes, it simply listens on port 49300 and broadcasts data received from
clients to other clients:

    
    
    #!/usr/bin/python
    #-*- encoding: Utf-8 -*-
    from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
    from string import printable
    from threading import Thread
    from base64 import b64encode
    from struct import unpack
    from hashlib import sha1
    
    PORT = 49300
    
    activeSocks = []
    
    def SignalingChannel(ip, port, sock):
        print 'Connection from %s:%s' % (ip, port)
    
        # Handling the HTTP request
    
        try:
            headers = sock.recv(8184)
    
            assert headers.upper().startswith('GET')
            assert headers.endswith('\r\n\r\n')
    
            data = headers.strip().replace('\r', '').split('\n')[1:]
    
            headers = {}
            for header in data:
                name, value = header.split(':', 1)
                headers[name.strip().lower()] = value.strip()
    
            assert headers['host']
            assert 'upgrade' in headers['connection'].lower()
            assert 'websocket' in headers['upgrade'].lower()
            assert headers['sec-websocket-version'] == '13'
            assert len(headers['sec-websocket-key']) == 24
    
            guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
            accept = b64encode(sha1(headers['sec-websocket-key'] + guid).digest())
    
            sock.send('HTTP/1.1 101 Switching Protocols\r\n' +
            'Connection: Upgrade\r\n' +
            'Upgrade: websocket\r\n' +
            'Sec-WebSocket-Accept: %s\r\n' % accept +
            '\r\n')
    
        except:
            try:
                msg = 'This is a RFC 6455 WebSocket server.\n'
    
                sock.send('HTTP/1.1 400 Bad Request\r\n' +
                'Connection: Close\r\n' +
                'Content-Length: %d\r\n' % len(msg) +
                'Content-Type: text/plain; charset=us-ascii\r\n' +
                'Sec-WebSocket-Version: 13\r\n' +
                '\r\n' +
                msg)
    
            except:
                pass
    
            sock.close()
            print 'Disconnection from %s:%s' % (ip, port)
            return
    
        activeSocks.append(sock)
    
        try:
            data = sock.recv(2)
            while len(data) == 2:
                frame = data[0] + chr(ord(data[1]) & 0b01111111)
                opcode = ord(data[0]) & 0b00001111
                mask = ord(data[1]) & 0b10000000
                paylen = ord(data[1]) & 0b01111111
    
                if paylen == 126:
                    data = sock.recv(2)
                    frame += data
                    paylen = unpack('>H', data)[0]
                elif paylen == 127:
                    data = sock.recv(8)
                    frame += data
                    paylen = unpack('>Q', data)[0]
    
                if mask:
                    mask = sock.recv(4)
    
                data = ''
                received = True
                while received and len(data) < paylen:
                    received = sock.recv(paylen - len(data))
                    data += received
    
                if mask:
                    unmasked = ''
                    for i in xrange(len(data)):
                        unmasked += chr(ord(data[i]) ^ ord(mask[i % 4]))
                else:
                    unmasked = data
    
                frame += unmasked
    
                if opcode != 8:
                    print '-- From port %d --' % port
                    if all(ord(c) < 127 and c in printable for c in unmasked):
                        print unmasked
                    else:
                        print repr(unmasked)
                    for destSock in activeSocks:
                        if destSock != sock:
                            destSock.send(frame)
                else:
                    break
    
                data = sock.recv(2)
        except:
            pass
    
        activeSocks.remove(sock)
        sock.close()
        print 'Disconnection from %s:%s' % (ip, port)
    
    listenSock = socket(AF_INET, SOCK_STREAM)
    listenSock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    
    listenSock.bind(('0.0.0.0', PORT))
    listenSock.listen(20)
    
    print 'Listening on port 49300...'
    
    while True:
        clientSock, (ip, port) = listenSock.accept()
        Thread(target=SignalingChannel, args=(ip, port, clientSock)).start()
    

To run the code, launch the signaling server, open the webpage in two browser
tabs, click the "Create data channel" button and look at the web console.

Any idea?


Answer: Looking through the Chrome/Firefox bug trackers it looks like this issue has
been identified and resolved, but only in [Chrome Canary
33.0.1715.0](https://code.google.com/p/webrtc/issues/detail?id=2279#c17) or
higher.

If you're unwilling to require the Chrome Canary build mentioned above, you
can detect the bad peer combination, and have your 'offer' button signal the
other client to make an offer.

Pseudocode:

    
    
    socket.onMessage(msg) {
       if(msg == "request-offer"){
          doOffer();
       }
       ...
    }
    
    createDataChannelButton.onClick() {
       if(!canCreateChannelBasedOnBrowser){
          socket.send("request-offer");
       }
       else {
          doOffer();
       }
    }
    

With your example code:

    
    
    <!DOCTYPE html>
    <html>
    <head>
       <title>WebRTC test</title>
       <meta charset="utf-8">
    </head>
    <body>
    <button id="create" disabled>Create data channel</button>
    
    <script type="text/javascript">
       // DOM
    
       // CHANGE: Add basic browser detection based on google's adapter.js file.
       var rtcBrowserVersion = 0;
       var rtcCanInitiateDataOffer = false;
       if (navigator.mozGetUserMedia) {
          rtcBrowserVersion = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
          rtcCanInitiateDataOffer = true;
       } else if (navigator.webkitGetUserMedia) {
          // Chrome Canary reports major version 35 for me.  Can't find a reliable resource to confirm
          // canary versions.
          rtcBrowserVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
          rtcCanInitiateDataOffer = rtcBrowserVersion >= 35;
       }
    
    
       var create = document.getElementById('create');
    
       // Compatibility
    
       window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
       window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
       window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
    
       // Create a WebRTC object
    
       var rtc = new RTCPeerConnection(null);
    
       // Create a data channel
    
       var sendChannel = rtc.createDataChannel('pingtest', {reliable: false});
       var myMsg = 'ping';
    
       function setRecvChannel(recvChannel) {
          recvChannel.onmessage = function(event) {
             if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
                console.log('-> ' + window.btoa(event.data));
                return; // Received channel's name, ignore
            }
    
            console.log('-> ' + event.data);
            window.setTimeout(function() {
               console.log('<- ' + myMsg);
               sendChannel.send(myMsg);
            }, 500);
         };
      }
    
      // Chrome and Firefox
    
      sendChannel.onopen = function(event) {
         setRecvChannel(sendChannel);
    
         if(myMsg === 'ping') {
            console.log('<- ' + myMsg);
            sendChannel.send(myMsg);
         }
      };
    
      // Firefox
    
      rtc.ondatachannel = function(event) {
         setRecvChannel(event.channel);
      };
    
      // ICE
    
      rtc.onicecandidate = function(event) {
         if(event.candidate) {
            console.log('<- ' + JSON.stringify(event.candidate));
            ws.send(JSON.stringify(event.candidate));
         }
      };
    
      // Signaling channel
    
      var ws = new WebSocket('ws://127.0.0.1:49300/');
    
      ws.onopen = function() {
         create.disabled = false;
      };
    
      ws.onmessage = function(event) {
         console.log('-> ' + event.data);
         var data = JSON.parse(event.data);
    
        if(data.sdp) {
           rtc.setRemoteDescription(new RTCSessionDescription(data));
    
           if(data.type === 'offer') {
              myMsg = 'pong';
    
              rtc.createAnswer(function(anwser) {
                 rtc.setLocalDescription(anwser, function () {
                    console.log('<- ' + JSON.stringify(anwser));
                    ws.send(JSON.stringify(anwser));
                 });
              }, console.error);
           }
        }
        // CHANGE: Chrome with offer bug asked to initiate the offer.
        else if(data.initiate === true){
           doOffer();
        }
        else {
           rtc.addIceCandidate(new RTCIceCandidate(data));
        }
     };
    
     ws.onclose = function() {
       create.disabled = true;
    };
    
    // Create an offer
    
    // CHANGE: Create function for offer, so that it may be called from ws.onmessage
    function doOffer(){
       rtc.createOffer(function(offer) {
          rtc.setLocalDescription(offer, function () {
             offer.sdp = offer.sdp;
             console.log(offer.sdp);
             console.log('<- ' + JSON.stringify(offer));
             ws.send(JSON.stringify(offer));
          });
       }, console.error);
    }
    
    create.onclick = function() {
       // CHANGE: If this client is not able to negotiate a data channel, send a
       // message to the peer asking them to offer the channel.
       if(rtcCanInitiateDataOffer){
          doOffer();
       }
       else {
          ws.send(JSON.stringify({initiate:true}));
       }
    };
    

Related Issues:

  * WebRTC - [Test SCTP data channel interop with Firefox](https://code.google.com/p/webrtc/issues/detail?id=2279) _Closed as Fixed 11/25/13_
  * Chromium - [SCTP Data Channel fails to connect if Chrome offers and FF answers](https://code.google.com/p/webrtc/issues/detail?id=2540#c3) \- _Closed as Fixed 11/4/13_

_It is also worth noting that there is currently a[max message buffer size of
16KB](https://code.google.com/p/webrtc/issues/detail?id=2279#c18) when sending
data to a Chrome peer. If you are sending large data messages, you will need
to break them into 16KB chunks before transmitting them over the data
channel._

