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?

Was it helpful?

Solution

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 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:

It is also worth noting that there is currently a max message buffer size of 16KB 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.

OTHER TIPS

FYI, I revised the HTML further to try and troubleshoot a problem I'm having talking to the C++ libjingle (neither Chrome, nor Firefox) ;)

This version shows on-page status. Not pretty, but useful.

  <!DOCTYPE html>
  <html>
  <head>
      <title>WebRTC test</title>
      <meta charset="utf-8">
  </head>
  <body>
      <button id="create" disabled>Create data channel</button>

  <div id="output1">output1</div>
  <div id="output2">output2</div>
  <div id="output3">output3</div>
  <div id="output4">output4</div>
  <div id="output5">output5</div>
  <div id="output6">output6</div>
  <div id="output7">output7</div>
  <div id="output8">output8</div>


  <script type="text/javascript">
     var myCounts = {
         output1: 0,
         output2: 0
     };

     var server1 = 'ws://127.0.0.1:49300/';

     // 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;
     }

     // 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) {
           myCounts.output1++;
           document.getElementById("output1").innerHTML = myCounts.output1 + ": " + event.data;

           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) {
         myCounts.output2++;
         document.getElementById("output2").innerHTML = myCounts.output2 + " channel: " + event.channel.label;
         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(server1);
     document.getElementById("output3").innerHTML="created WebSocket (not connected) " + server1;

     ws.onopen = function() {
        create.disabled = false;
        document.getElementById("output3").innerHTML="onOpen WebSocket " + server1;
     };

     ws.onmessage = function(event) {
        document.getElementById("output3").innerHTML="onMessage WebSocket " + event.data;
        console.log('-> ' + event.data);
        var data = JSON.parse(event.data);

        if (data.sdp) {
           rtc.setRemoteDescription(new RTCSessionDescription(data));

           if (data.type === 'offer') {
               document.getElementById("output4").innerHTML="received SessiionDescription offer";
               myMsg = 'pong';

              rtc.createAnswer(function(anwser) {
                rtc.setLocalDescription(anwser, function () {
                    console.log('<- ' + JSON.stringify(anwser));
                    ws.send(JSON.stringify(anwser));
                });
              }, console.error);
           }
           else if (data.type == 'answer') {
               document.getElementById("output6").innerHTML="received SessiionDescription answer";
           };
        }
        // CHANGE: Chrome with offer bug asked to initiate the offer.
        else if (data.reverseInitiate === true) {
           document.getElementById("output8").innerHTML="was asked to reverseInitiate, I doOffer";
           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 {
           document.getElementById("output7").innerHTML="sending reverseInitiate";
           ws.send(JSON.stringify( {reverseInitiate:true} ));
        };
     };
  </script>

  </body>
  </html>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top