Monday, 8 July 2013

Implementing WebRTC DataChannels in Chrome Canary (July 2013)

Hey Guys,
Shortly after my last blog post on WebRTC - WebRTC - The HTML5 late arrival - I realised Chrome and Firefox had both updated their implementations to include DataChannels, the part of WebRTC that allows peer-to-peer connections to be used for sending data packets.

Fantastic news, right? The Mozilla lot are all over it and I found plenty of resources on implementing it on Firefox however the information for Chrome is a bit all over the place and lacking one concise explanation, so having puzzled it out, I'm putting my solution here, going right from the first steps through to exchanging your first packet, accounting for Chrome's implementation.

Warning: This is a pretty wordy one, so if you aren't much of a skim reader or are totally new to the subject, you probably wanna go get yourself a cup of tea first... go on, I'm not going anywhere!


Summary

Maybe you already know roughly how DataChannels work and are just looking for some help with Chrome's little gotchas, so I'll do a quick tl;dr here.

Firstly, if you're just looking for the differences in Chrome, they are simply that you need to repeat the Offer/Answer process after creating the DataChannel and you must create it with the { reliable : false } constraint (Yes, this forces you to use UDP, unfortunately that's all Chrome supports at this point) and lastly the peerConnection must be created with { optional:[ { RtpDataChannels: true } ]} .

Ok now for an actual summary, DataChannels don't allow you to just manually set a remote end point (IP:port) to connect to, that's rarely useful in P2P connections anyway apart from hacking them. So you need to set up a server just to get the 2 peers to acknowledge eachothers existence and it won't be used after that. This is actually all handled outside of the WebRTC API though, it gives you an object and you just have to get it to the other peer however you choose so in theory this could be done using a HTTP Server/WebSockets infact anything of your choosing really.

However that's not the last server you'll need in this peer-to-peer network (strange, I know) but for NAT/Firewall traversal and actually making sure the connection goes ahead you'll need to specify a STUN or TURN server. Fortunately, these are fairly standard affairs and you won't have to set  one up yourself. In fact Google maintains one you can use at: stun.l.google.com:19302

Ok with all that horrible set-up stuff done, all you'll need to do is create a PeerConnection object on either end of your P2P connection - have one (say PC1) create an OFFER, send this to your User Discovery server to reach the other(say PC2). Set the offer from PC1 as the remote description of PC2 then create an ANSWER on PC2 and send it back via the user discovery server to PC1. Once these steps are complete you should have a stable PeerConnection.

One of your peers (PC1 again) then creates a DataChannel and sends another OFFER and sends it to PC2, which as before, sets it as the remote description and sends back an ANSWER and finally you have an open DataConnection. It sounds complicated but it's really not that bad. Promise!

Step 1: Set up a User Discovery Server

So, like I said above a User Discovery Server is needed to introduce the 2 peers to each other in the first place but this can be a very simple piece of technology and can be built in virtually anything you like. However we're not lazy so lets go for a tasty Node.js WebSocket server, eh? If you don't have Node.js then you'll need to install it first and it is available here. As this was the first time I'd touched Node.js, I have to thank Silvia Pfeiffer of Ginger Technologies for posting the complete source code for her WebRTC server on her blog here. The whole blog is an interesting read and you should really check out the whole thing if you're interested in the technology but you can just grab the code here if you're rushed.

After installing you'll want to open up a command line and run the following command:
npm install websocket
I'm going to assume npm stands for Node.js Package Manager but essentially this installs the WebSocket module allowing you to not bother with all of the nasty implementation side - and take my word for it, it ain't all that pretty - of WebSocket servers. After that simply navigate to the directory that contains the code you borrowed from the link above and call:
node <filename>.js
Hey presto, one fully running and rather simple WebSocket server, which will simply negotiate connections with anyone that attempts to connect to it then broadcast anything one client says to all other clients. That's absolutely all we need to get this working.

Step 2: Create yourself a PeerConnection or two... (PC1 & PC2)

I'm not gonna cover ICE/STUN/TURN and all that sort of thing as it's simply not necessary when hacking something together, if you're interested then a good place to start is the HTML5Rocks website which has a brilliant section on setting up PeerConnections which covers all of the servers here.

So the next step is simply to jump in and get a PeerConnection going, for the purposes of the rest of this blog I'll refer to the two peers that are being connected as PC1 and PC2 just to try and avoid making things confusing. Both PC1 and PC2 will need to create a PeerConnection, this is the component that allows for Audio/Video to be streamed but is essentially just the socket connection PC1 and PC2 and just as essential for a DataChannel.

The code for creating a PeerConnection however is very straight foward:
var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
var peerConnection= new webkitRTCPeerConnection(pc_config, { optional:[ { RtpDataChannels: true } ]});
As you can see we're creating 2 objects, firstly a generic object for the configuration of our peerConnection, this is simply referencing the URL of our ice server, again see the link above for more information and what's actually going on here but know that you can leave it as it is, just fine. Next we create the actual RTCPeerConnection (using the webkit prefix), for the parameters here we're just passing in the configuration object we just made and the Chrome required flag requesting RtpDataChannels, this is all just fine as it is again.

Step 3: Make an offer (PC1)

WebRTC PeerConnections work on an Offer/Answer basis, that means for a connection to be established one peer must make an offer and the other peer must answer that offer. These offers and answers are what goes through your User Discovery server so without further ado, we'll have the following code run on PC1 ONLY (if you run it on both then one of your PC's will end up in the wrong state and it'll all just fail)
peerConnection.createOffer(gotDescription);
How painless, eh? Just call createOffer on your peerConnection and pass it one parameter, this should be a function reference with one argument which will be the offer.
function gotDescription(desc){ 
 peerConnection.setLocalDescription(desc); 
 socket.send(JSON.stringify({"sdp": desc}));
}
There's not a great deal to do with it, set it as the Local Description (End Point) in your peerConnection and then encode it for safe travelling and send it via whatever means you choose, the example of course uses WebSockets but I've assumed that you already know or can figure how to use those.

Step 4: Answer an offer (PC2)

Great, so by some magical internet process your offer has made it's way from PC1 to PC2 and now PC2 has to answer it and finish establishing the connection.
function onSckMessage(evt) {
    var desc = JSON.parse(evt.data).sdp;
     peerConnection.setRemoteDescription(new RTCSessionDescription(desc));
     peerConnection.createAnswer(gotDescription);
}
And that's how, assuming you have encoded your RTCSessionDescription the same way I have you'll want to extract the appropriate part again, which is what I'm storing in data but this is just a generic Javascript object at that point you pass it into the constructor of an RTCSessionDescription to make it the correct type again and then set that as the RemoteDescription (end point) of PC2. (See the logic, the local end point on PC1 is the remote end point on PC2.) Once that's been set, your PeerConnection will let you make an Answer using the createAnswer function. yes, I've used the exact same callback function because it will be handled in exactly the same way. The answer becomes PC2's localDescription and then gets fired over our WebSocket back to PC1.

Step 5: Create some DataChannels! (PC1)

We have now received our Answer - that can mean only one thing, we'll have a stable PeerConnection in a jiffy but let's handle that super quick and move on to the interesting bit by creating our DataChannels too.
function onSckMessage(evt) {
    var desc = JSON.parse(evt.data).sdp; 
    peerConnection.setRemoteDescription(new RTCSessionDescription(desc));
    if(!dataChannel){
        dataChannel = peerConnection.createDataChannel("HelloWorld",   { reliable : false });
        dataChannel.onmessage = onDcMessage;
        peerConnection.createOffer(gotDescription);
    }
        
}
So just like with the offer, the answer from PC2 becomes the remoteDescription (end point) of PC1 (so the local end point of PC2 is the remote end point of PC1) and that's our connection stable. You can confirm this with peerConnection.signallingState which should == "stable"

Now, we're gonna think ahead a bit (trust me with this) and check whether we've created a DataChannel already or not. If not then we call peerConnection.createDataChannel with 2 parameters, the first one is just a label for it. Nothing particularly special about this, just keep them consistent. The 2nd parameter is your configuration and if you read through the Chrome gotchas way back in the summary you'll know that you can't tamper with this just now. They've only implemented unreliable DataChannels and that's all you can use for now.

 So, go ahead and create your dataChannel and store it in an easily accessible place. Whilst you're here you may as well bind to the onmessage event handler, so you can process data as soon as it starts coming in. But wait! What's this last bit? Chrome requires you go through the whole Offer/Answer process again so that both ends know that they're using a DataChannel, you can pretty much go back to Step 3, there is only difference from the first time you did it.

This bit occurs on PC2 only
This time when PC2 receives your new Offer it'll trigger an event "ondatachannel", you're going to want to handle this event but don't worry there's not much to do here.
peerConnection.ondatachannel = function(event){ 
dataChannel = event.channel; 
dataChannel.onmessage = dcMessage; 
}
Easy! We're just gonna take the fully functional dataChannel out of the single parameter and bind to the onmessage event again ready to collect data on this side. Now we just have to wait for our existing code to send the answer back AND you'll realise when it gets back to PC1, this time dataChannel exists so the same event handler runs fine and doesn't try to create the DataChannel again (told you to trust me).


In fact, you can go one step further and make PC1 and PC2 run off the same page with a simple if statement:
function onSckMessage(evt) {
    var desc = JSON.parse(evt.data).sdp; 
    peerConnection.setRemoteDescription(new RTCSessionDescription(signal));
    if(signal.type == "offer"){
        peerConnection.createAnswer(gotDescription, fail);
    }
    else
    {
        if(!dataChannel){
        dataChannel = peerConnection.createDataChannel("BKOM",   { reliable : false });
        dataChannel.onmessage = onDcMessage;
        peerConnection.createOffer(gotDescription, fail);
        }
        
    }
}
and Good News Everybody! We're done, as soon as PC2's answer gets back and is handled by PC1 the dataChannel should be open for business (you can check with dataChannel.readyState == "open" or catch the dataChannel.onopen event)

Now all you have to do is:
dataChannel.send("Your Message");

Conclusion

So there they are DataChannels in all their glory, a very powerful tool even at this early stage and I'm really excited to see what they get used for. I hope this article helps someone play around because I genuinely did invest a lot of time in figuring out the various bits and bobs necessary to pull all this together in Chrome. Anyway, thanks for reading - if I have helped feel free to let me know what you get up to with DataChannels (or WebRTC in general) down below, same if you have any questions.

Also if you wanna see my full implementation it's here: http://brianbea.com/cb/DataChannels.html
And check out my references, there aren't many of them and they are all fantastic resources in their own right and well worth a read.

References

(I'm not gonna bother with Harvard style or anything silly like that but my degree has taught me to back up what I say and there are some pretty interesting reads amongst all of this. All of these were accessed 07/07/2013 and some are pretty much guaranteed to change (and soon).