WebRTC 01: Data Channels

Set up a WebRTC connection between two clients and send simple messages

Let’s start with a simple example: Establishing a connection between two browser windows and sending text messages back and forth. For this we’ll use WebRTC data-channels - the basic connection type that lets you send text and binary data directly between two peers.

Choosing a WebRTC library

By browser standards the WebRTC API is extremly complicated and low level. When you want to establish a Websocket connection all you need to do is call new Websocket( 'ws://...' )… but with WebRTC the entire connection establishment, generating offers and answers, sending and receiving ICE candidates and other protocol steps are up to you. If you’d like to give it a try I can highly recommend Mozilla’s DataChannel tutorial, but for this guide we’ll keep things simpler by using a library.

There are many WebRTC libraries available that provide convenience methods or high level abstractions. WebRTC has been an emerging standard and is still somehwhat in flux, so its crucial to make sure that whatever library you choose is up to date and well maintained.

For our examples we’ll be using Simple Peer: a basic, very clean low level wrapper around P2P connections.

Who’s calling who?

Time to get our browser windows to call each other. But first we have to work out who’s awaiting the call and who’s making it. To keep things simple we’ll add a hash #initiator to one window’s URL. So when establishing the connection we specify

const p2pConnection = new SimplePeer({
    initiator: document.location.hash === '#initiator'
});

In the full mesh example well compare usernames localUserName > remoteUserName to achieve the same.

Signaling

As the connection is being established, both peers need to send information about themselves and how to reach them to each other - the previously mentioned Interactive Connectivity Establishment Process (or ICE for short).

Simple peer makes this easy. Whenever our local peer wants to send a signal to the remote, it emits a 'signal' event. Whenever we receive a signal we process it using our connection’s .signal() method. The signals themselves are transmitted using events, deepstreamHub’s publish/subscribe mechanism.

We establish a connection by calling

const ds = deepstream( '<your dsh url>' ).login();

To make sure we’re not receiving our own events we’ll create a random username on both sides

const userName = 'user/' + ds.getUid();

and use it to filter out our own signals

p2pConnection.on( 'signal', signal => {
    ds.event.emit( 'rtc-signal', {
        sender: userName,
        signal: signal
    });
});

ds.event.subscribe( 'rtc-signal', msg => {
    if( msg.sender !== userName ) {
        p2pConnection.signal( msg.signal );
    }
});

Once established, our connection emits a 'connect' event.

p2pConnection.on( 'connect', () => {});

from here on we can simply send messages using

p2pConnection.send( 'Hey ho' );

and receive them via

p2pConnection.on( 'data', data => {
    console.log( data.toString() );
});

Bottom line: Establishing a connection between two peers is easy enough - but once we move on to many-to-many connectivity and rooms, things get a little more tricky.