Tutorials

WebRTC 1. Data channels the right way using Ably

Introduction

A WebRTC data channel lets you send text or binary data to a peer, over an active connection.
The DataChannel API provides a generic transport service allowing web browsers to exchange generic data in a bi-directional peer-to-peer fashion.

In this tutorial, we will build a WebRTC chat using data channels, allowing us to implement chat for multiple people communicating privately.


Demo image

Step 1 – Set up a free account with Ably

In order to run these tutorials locally, you will need an Ably API key. If you are not already signed up, you should sign up now for a free Ably account. Once you have an Ably account:

  1. Log into your app dashboard
  2. Under “Your apps”, click on “Manage app” for any app you wish to use for this tutorial, or create a new one with the “Create New App” button
  3. Click on the “API Keys” tab
  4. Copy the secret “API Key” value from your Root key and store it so that you can use it later in this tutorial

    Copy API Key screenshot

Step 2 – Choosing a WebRTC library

Dealing with WebRTC directly might be a bit tedious as it generally involves a lot of lines of code. However, there are many WebRTC libraries available, that provide high-level abstractions and expose only a few methods to users, while handling major uplifting themselves. WebRTC has been an emerging standard and is still somewhat in flux, so it’s crucial to make sure that whichever library you choose is up to date and well maintained.

In all the chapters of this tutorial, we will be using simple-peer – a simple WebRTC library for video/voice and data channels.

Step 3 – STUN, TURN and WebRTC

WebRTC enables peer to peer communication, however, WebRTC still needs servers for clients to exchange metadata to coordinate communication, which is called signalling. These servers are also required to cope with network address translators (NATs) and firewalls. This is where we’ll make use of STUN and TURN servers.

Interactive Connectivity Establishment (ICE) tries to find the best path to connect to peers. It tries all possibilities simultaneously and chooses the most efficient option that works.

It is ICE’s job to find the best path to connect peers. It may be able to do that when a direct connection exists between the clients, as well as when a direct connection is not possible (i.e. behind NATs).

ICE first tries to make a connection using the host address obtained from a device’s operating system and network card; if that fails (which it will for devices behind NATs) ICE obtains an external address using a STUN server, and if that fails as well, traffic is routed via a TURN relay server.

STUN (Session Traversal Utilities for NAT) is used to implement the ICE protocol, which tries to find a working network path between the two clients. ICE will also use TURN (Traversal Using Relay NAT) relay servers (if configured in the RTCPeerConnection) for cases where the two clients (due to NAT/Firewall restrictions) can’t make a direct peer-to-peer connection.

You will have to tell your WebRTC application where to find the STUN and TURN servers. Luckily for us, there are some publicly available servers provided by Google and Twillo, which can be used to build a quick demo. In production, you might want to roll out your own STUN/TURN servers.

Since we are using the simple-peer library to manage WebRTC connections, they have pre-defined the free available servers as the default servers to connect to. If you take a look at the simple-peer source file, on line 154, you would notice this, as shown below:

Peer.config = {
  iceServers: [
    {
      urls: 'stun:stun.l.google.com:19302'
    },
    {
      urls: 'stun:global.stun.twilio.com:3478?transport=udp'
    }
  ]
}

Step 4 – Designing a simple HTML layout

Create a new file called index.html and add the following code:

<!DOCTYPE html>
<html>
<head>
    <title>Ably WebRTC DataChannel Demo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
</head>
<body>
    <script src="https://cdn.ably.io/lib/ably.min-1.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.1.2/simplepeer.min.js"></script>
    <script src="ably-datachannel.js"></script>
    <script src="connection-helper.js"></script>
    <div class="container-fluid" style="margin-top: 5em;">
        <div class="container" id="join">
            <h4 id="online">Users online (0) </h4>
            <ul id="memberList"></ul>
        </div>
        <div class="container" id="chat" style="display:none;">
            <input name="message" id="message" class="form-control" placeholder="Connecting to the peers, please wait"></input>
            <button class="demo-chat-send btn btn-success" id="chatButton" onClick="sendMessage()">Send</button>
            <ul id='list'>
            </ul>
        </div>
    </div>
</body>
<style>
    small {
        border-bottom: 2px solid black;
    }

    li {
        list-style: none;
    }
</style>
</html>

See this step in Github

What is going on in the code block above?
The code snippet above is a basic HTML declaration in which we did the following:

  1. Referenced the Bootstrap CSS library
  2. Referenced the Ably JavaScript library
  3. Referenced the simple peer JavaScript library.
  4. Referenced a JavaScript file named ably-datachannel.js (which we will create soon).
  5. Referenced a JavaScript file named connection-helper.js (which we will create soon).
  6. Declared a div with the id join which holds the list of users online.
  7. Declared a div with the id chat which holds the input text to type messages as well as the send message button.

Step 5 – Defining the connection helper class

In step 3 above, we Referenced a library called connection-helper.js. This library helps manage the simple-peer connections, in order to keep our code organized as we will be having multiple instances of the connection.

Create a new file called connection-helper.js and add the following code:

class Connection {
    constructor(remoteClient, AblyRealtime, initiator) {
        console.log(`Opening connection to ${remoteClient}`);
        this._remoteClient = remoteClient;
        this.isConnected = false;
        this._p2pConnection = new SimplePeer({
            initiator: initiator,
            trickle: false,
        });
        this._p2pConnection.on('signal', this._onSignal.bind(this));
        this._p2pConnection.on('error', this._onError.bind(this));
        this._p2pConnection.on('connect', this._onConnect.bind(this));
        this._p2pConnection.on('close', this._onClose.bind(this));
        this._p2pConnection.on('data', this._onData.bind(this));
    }

    handleSignal(signal) {
        this._p2pConnection.signal(signal);
    }

    send(msg) {
        this._p2pConnection.send(msg);
    }

    _onSignal(signal) {
        AblyRealtime.publish(`ablywebrtc-signal/${this._remoteClient}`, {
            user: clientId,
            signal: signal,
        });
    }

    _onConnect() {
        this.isConnected = true;
        console.log('connected to ' + this._remoteClient);
    }

    _onClose() {
        console.log(`connection to ${this._remoteClient} closed`);
        delete connections[this._remoteClient];
    }

    _onData(data) {
        receiveMessage(this._remoteClient, data)
    }

    _onError(error) {
        console.log(`an error occurred ${error.toString()}`);
    }
}

See this step in Github

What is going on in the code block above?

In the code block above, we have defined a connection class which makes use of the simple-peer library to manage connections.

In our constructor, we accept 3 parameters:

  1. remoteClient: this refers to the other party we want to connect to.
  2. AblyRealtime: this refers to an instance of an Ably channel
  3. initiator: this is a Boolean parameter that states whether this peer is the one initiating the connection or not.

Next, we move ahead to initiate the simple-peer library and bind some methods to events exposed by simple-peer such as signal, error, connect, close and data.

Let us now understand the methods defined in the class:

  1. handleSignal: this method is called when a signal has been sent via the realtime channel. This method passes the signal to the current peer connection.
  2. send: this method is used to send messages to the other peer. The method, in turn, calls the send method of the simple-peer instance.
  3. _onSignal: this method is called by the simple-peer library when it wants to send a signal to the other peer. In this method, we make use of the Ably realtime channel to publish the signal to the other peer.
  4. _onConnect: this method sets the class property isConnected to true. This is an indicator that the peers have been connected.
  5. _onClose: this method deletes the current connection instance from an object called connections, which we will define later on in the ably-datachannel.js file.
  6. _onData: this method is called when a new message has been sent to the other peer. Here, it calls another method called receiveMessage, which we will later define in the ably-datachannel.js file.
  7. _onError: should an error occur in our connection, this method will be called. Currently, we just log the details of the error.

Step 6 – Displaying online users

Before we can send messages via WebRTC, we need to verify that the other peer we want to connect to, is online, as WebRTC would not connect with offline peers.

Create a file named ably-datachannel.js and add:

var messageList = {}
var readStatus = {}
var membersList = []
var connections = {}
var currentChat
var apiKey = 'XXX_ABLY_API_KEY'
var clientId = 'client-' + Math.random().toString(36).substr(2, 16)
var realtime = new Ably.Realtime({ key: apiKey, clientId: clientId })
var AblyRealtime = realtime.channels.get('ChatChannel')

AblyRealtime.presence.subscribe('enter', function(member) {
    AblyRealtime.presence.get((err, members) => {
        membersList = members
        renderMembers()
    })
})

AblyRealtime.presence.subscribe('leave', member => {
    delete(connections[member.client_id])
    if (member.client_id === currentChat) {
        currentChat = undefined
        document.getElementById('chat').style.display = 'none'
    }
    AblyRealtime.presence.get((err, members) => {
        membersList = members
        renderMembers()
    })
})

AblyRealtime.presence.enter()

function renderMembers() {
    var list = document.getElementById('memberList')
    var online = document.getElementById('online')
    online.innerHTML = 'Users online (' + (membersList.length === 0 ? 0 : membersList.length - 1) + ')'
    var html = ''
    if (membersList.length === 1) {
        html += '<li> No member online </li>'
        list.innerHTML = html
        return
    }
    for (var index = 0; index < membersList.length; index++) {
        var element = membersList[index]
        if (element.clientId !== clientId) {
            var newMessage = readStatus[element.clientId] ? '<span class="label label-info">New</span>' : ''
            html += '<li><small>' + element.clientId + ' <button class="btn btn-xs btn-success" onclick=chat("' + element.clientId + '")>chat now</button> ' + newMessage + ' </small></li>'
        }
    }
    list.innerHTML = html
}

See this step in Github

What is going on in the code block above?

Before we move forward, please ensure that you have set the value of apikey to your private Ably API key. The other variables are explained below:

  1. messageList: this is an object which will contain keys of each client you are currently chatting with, or that has sent a message, with an array of what the messages are.
  2. readStatus: this is an object which will contain keys of each client you have a current chat with, or that has sent a message, with a Boolean value of whether you have read their last messages or not.
  3. membersList: this is an array of all currently online members you can chat with.
  4. connections: this is an object which will contain keys of each client you have a current chat with, or that has sent you a message, with their current connection object.
  5. apiKey: this is your API key for Ably as generated in step 1
  6. clientId: this is a unique identification of the current person who wants to connect to both Ably and WebRTC. In your application, you might need to get this key from a database or using some other authorised methods.
  7. realtime: an instance of Ably
  8. AblyRealtime: an instance of an Ably channel.

First, we need to make an enter subscription, so that we are notified every time a new member joins our channel. We do so by using AblyRealtime.presence. In the callback, we get a list of all current members and add it to our member’s list. After this, we call on the render members method.

Also, as we subscribed to to enter events, we subscribed to leave events, so we can keep track of members who have left the channel.

We need to call the AblyRealtime.presence.enter() method so Ably is aware that we want to enter the presence channel and keep track of the new client.

Lastly, we have our renderMembers method, which loops through the membersList, and appends them as list items to the ul tag with the id of memberList. In this method, there is also a check for the readStatus of the conversation, to check if a message was read by that user or not.


Demo image


Demo image

Step 7 – Sending and receiving messages

We have been able to identify who is online and ready to receive instant messages using WebRTC data channels and Ably, now we move to the part where we send and receive messages. Add the following to the end of your ably-datachannel.js file.

function chat(client_id) {
    if (client_id === clientId) return
        // Create a new connection
    currentChat = client_id
    if (!connections[client_id]) {
        connections[client_id] = new Connection(client_id, AblyRealtime, true)
    }
    document.getElementById('chat').style.display = 'block'
    render()
}

AblyRealtime.subscribe(`ablywebrtc-signal/${clientId}`, msg => {
    if (!connections[msg.data.user]) {
        connections[msg.data.user] = new Connection(msg.data.user, AblyRealtime, false)

    }
    connections[msg.data.user].handleSignal(msg.data.signal)
})

function sendMessage() {
    var message = document.getElementById('message')
    connections[currentChat].send(JSON.stringify({ user: clientId, message: message.value }))
    if (!messageList[currentChat]) {
        messageList[currentChat] = []
    }
    messageList[currentChat].push({ user: 'Me', message: message.value })
    message.value = ''
    render()
}

function receiveMessage(client_id, data) {
    if (!messageList[client_id]) {
        messageList[client_id] = []
    }
    data = JSON.parse(data)
    readStatus[client_id] = true
    messageList[client_id].push({ user: client_id, message: data.message })
    renderMembers()
    render()
}
function render() {
    if (!messageList[currentChat]) {
        return
    }
    var list = document.getElementById('list')
    var html = ''
    if (messageList[currentChat].length === 0) {
        html += '<li>No chat messages available</li>'
        list.innerHTML = html
        return
    }
    for (var index = 0; index < messageList[currentChat].length; index++) {
        var element = messageList[currentChat][index]
        readStatus[element.user] = false
        if (!element.user) {
            html += '<li><small>' + element.message + '</small></li>'
        } else {
            html += '<li>' + element.user + ': ' + element.message + '</li>'
        }
    }
    list.innerHTML = html
    renderMembers()
}

See this step in Github

What is going on in the code block above?

To chat with a user/member, we need to click the chat now button next to their name. This button, in turn, calls the chat method, which we have just defined.

The chat method receives a single argument, i.e, the client-id, which is the client you want to have a chat with. We then check to be sure that the user is not attempting to open a chat with his own instance. Although it isn’t possible for the user to open a chat with his own instance, we do this as an extra layer of security. At this point, if the user does not have an active connection with the other peer, it means he is the one initiating the chat, so we create a new connection with that client_id, passing the initiator status to be true. After this, we call the render method to render the chat messages.

For the two pairs to connect, the other peer has to be signaled of the incoming connection. This is where our subscription comes in. We subscribe to an event specifically for the current client’s id, which we defined the event as ablywebrtc-signal/${clientId}. Here, we check if the current user has a connection instance of the current client trying to connect, if not, we initiate a new connection for the other party, and surely the initiator flag is set to false. We then call on the handleSignal method of the connection class, to handle the signal coming in and connect the two pairs.

Now, we can go ahead to send and receive messages.

In our sendMessage method, we use the currentChat variable to figure out who it is that we are trying to send a message to, then we pass our message as a string to the send method of the controller class. We also push the message to the message list of the person we are chatting with. This is because WebRTC would not send the message back to the sender.

Remember the receiveMessage method we called in our connection-helper.js file that we declared earlier? Here it is. We receive the message here with the id of the user who sent it, then push it to the message list of that user. Then we call our render method.

Finally, we define the much loved render method, which loops through all our messages and sets the read status to false.

At this point, if we open the index.html file in two separate browsers, we should be able to send and receive messages as seen below:


Demo image

Step 8 – Adding audio notifications

In our current app, we have a new badge on the edge of the client’s id, which shows that someone has sent us a new message, however, that requires that the user must be on the page and be actively looking for the new status before he notices that a new message has been sent to him by someone who is not his current chat. We can improve this by adding audio notifications.

Let’s add the following at the beginning of our ably-datachannel.js file:

var audio = new Audio('https://notificationsounds.com/soundfiles/a86c450b76fb8c371afead6410d55534/file-sounds-1108-slow-spring-board.mp3');

Here, we have created a new Audio element with an mp3 sound. This sound can be any sound you like. I used a random link I saw off the internet here.

Next, let us update the receiveMessage method to be able to play the notification

function receiveMessage(client_id, data) {
    if (!messageList[client_id]) {
        messageList[client_id] = []
    }
    data = JSON.parse(data)
    readStatus[client_id] = true
    messageList[client_id].push({ user: client_id, message: data.message })
    audio.play()
    renderMembers()
    render()
}

See this step in Github

Testing our app

To test the messaging system we have just built, serve your index.html file as a static file via any server of your choice.

Using Node.js, install the http-server package:

npm install http-server -g

Once the http-server package is done installing, at the root of your working directory, run:

http-server

Navigate to http://127.0.0.1:8081 to view the demo.

Live demo

To try this example yourself, Open this demo in a new browser window to see data channels in action.

Step 9 – Conclusion

In this tutorial, we have seen how to use WebRTC data channels in conjunction with Ably to create realtime chats. Do you have any thoughts on making this lesson better? Let us know.

Next: Chapter 2 – Straightforward Video calls with WebRTC and Ably


Get started now with our free plan

It includes 3m messages per month, 100 peak connections, 100 peak channels, and loads of features.

Create your free account