4. WebRTC tutorial series - Screen Sharing

Screen sharing software allows people from various locations, view someone else’s computer screen in real-time. When you share your screen, your guests can see exactly what’s happening on your computer desktop at the time it is happening.

At the time this tutorial was written, both Chrome and Firefox have built-in support for screen capturing in the desktop versions. Chrome has built-in support for screen capture, however to gain permission to use this functionality, an application must use a Chrome Extension. In Firefox, the situation is simpler, as Firefox allows us to request for the screen capture directly.

You can find the latest info about WebRTC browser support on the Can I use website.

In this lesson, we will learn how to share our screen using WebRTC and Ably.

Step 1 – Create your Ably app and API key

To follow this tutorial, you will need an Ably account. Sign up for a free account if you don’t already have one.

Access to the Ably global messaging platform requires an API key for authentication. API keys exist within the context of an Ably application and each application can have multiple API keys so that you can assign different capabilities and manage access to channels and queues.

You can either create a new application for this tutorial, or use an existing one.

To create a new application and generate an API key:

  1. Log in to your Ably account dashboard
  2. Click the “Create New App” button
  3. Give it a name and click “Create app”
  4. Copy your private API key and store it somewhere. You will need it for this tutorial.

To use an existing application and API key:

  1. Select an application from “Your apps” in the dashboard
  2. In the API keys tab, choose an API key to use for this tutorial. The default “Root” API key has full access to capabilities and channels.
  3. Copy the Root API key and store it somewhere. You will need it for 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 – Designing a simple HTML layout

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

<!DOCTYPE html>
<html>
<head>
    <title>Ably WebRTC Video call Demo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
</head>
<body>
    <script src="https://cdn.temasys.io/adapterjs/0.15.x/adapter.min.js"></script>
    <script src="https://cdn.temasys.io/adapterjs/0.15.x/adapter.screenshare.js"></script>
    <script src="https://cdn.ably.com/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-screenshare.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="call" style="display:none;">
            <video width="320" height="240" id="local" controls></video>
            <video width="320" height="240" id="remote" controls></video>
            <button class="btn btn-xs btn-danger" onclick="handleEndCall()">End call</button>
        </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:

  1. Referenced the Bootstrap CSS library
  2. Referenced the temasys adapter.js to iron out cross platform issues for WebRTC
  3. Referenced the temasys adapter.screenshare.js which handles connection to Chrome extensions and getting screen permissions.
  4. Referenced the Ably JavaScript library
  5. Referenced the simple peer JavaScript library.
  6. Referenced a JavaScript file called ably-screenshare.js (which we will create soon).
  7. Referenced a JavaScript file called connection-helper.js (which we will create soon).
  8. Declared a div with the id of join which will hold the list of users online.
  9. Declared a div with the id of `call@ which holds the video feeds for the screen shares.

Step 4 – Defining the connection helper class

In Step 3 above, we Referenced a library called connection-helper.js. This library helps to manage the simple-peer connections, so as 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, stream) {
        console.log(`Opening connection to ${remoteClient}`)
        this._remoteClient = remoteClient
        this.isConnected = false
        this._p2pConnection = new SimplePeer({
            initiator: initiator,
            stream: stream
        })
        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('stream', this._onStream.bind(this))
    }
    handleSignal(signal) {
        this._p2pConnection.signal(signal)
    }
    send(msg) {
        this._p2pConnection.send(msg)
    }
    destroy() {
        this._p2pConnection.destroy()
    }
    _onSignal(signal) {
        AblyRealtime.publish(`rtc-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`)
        handleEndCall(this._remoteClient)
    }
    _onStream(data) {
        receiveStream(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 4 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 if this is the peer initiating the connection or not.
  4. stream: this is the video/audio stream coming directly from the user’s webcam and microphone.

Let us go ahead and understand the methods defined in the class:

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

Step 5 – Displaying online users

Before we can make calls 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 called ably-screenshare.js and add the following code:

var membersList = []
var connections = {}
var currentCall
var localStream
var constraints = { video: { mediaSource: 'screen' }, audio: false }
var apiKey = 'XXX_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 => {
    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) {
            html += '<li><small>' + element.clientId + ' <button class="btn btn-xs btn-success" onclick=call("' + element.clientId + '")>call now</button> </small></li>'
        }
    }
    list.innerHTML = html
}

See this step in Github

What is going on in the code block above?

In the code block above please note that you need to set the value of apiKey to your Ably API key, we also defined a couple of variables, these are explained below:

  1. membersList: this is an array of all currently online members you can chat with.
  2. connections: this is an object which will contain keys of each client you have a video call with, with their current connection object.
  3. currentCall: this variable holds the clientId of the user you are currently in a call with.
  4. localStream: this variable will hold a reference to the stream coming from your local webcam/microphone.
  5. constraints: an object which defines the media objects needed for the computer to generate the stream. Here, please note that the video key is passed as an object, setting the mediaSource to screen
  6. apiKey: this is your API key for Ably as generated in step 1
  7. 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.
  8. realtime: an instance of Ably
  9. 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 functions, which loops through the membersList, and appends them as list items to the `ul` tag with the id of memberList.

Step 6 – Making and receiving screenshare calls

We have been able to identify who is online and ready to receive instant messages using WebRTC data channels, now we move to the part where we send and receive screen calls.

Before jumping into the code, it would be nice to notify users when a call is coming in, and when a call is declined. Let’s go ahead and implement that.

Paste the following code at the end of your ably-screenshare.js file:

function call(client_id) {
    if (client_id === clientId) return
    alert(`attempting to call ${client_id}`)
    AblyRealtime.publish(`incoming-call/${client_id}`, {
            user: clientId
        })
}
AblyRealtime.subscribe(`incoming-call/${clientId}`, call => {
    if (currentCall != undefined) {
        // user is on another call
        AblyRealtime.publish(`call-details/${call.data.user}`, {
            user: clientId,
            msg: 'User is on another call'
        })
        return
    }
    var isAccepted = confirm(`You have a call from ${call.data.user}, do you want to accept?`)
    if (!isAccepted) {
        // user rejected the call
        AblyRealtime.publish(`call-details/${call.data.user}`, {
            user: clientId,
            msg: 'User declined the call'
        })
        return
    }
    currentCall = call.data.user
    AblyRealtime.publish(`call-details/${call.data.user}`, {
        user: clientId,
        accepted: true
    })
})
AblyRealtime.subscribe(`call-details/${clientId}`, call => {
    if (call.data.accepted) {
        initiateCall(call.data.user)
    } else {
        alert(call.data.msg)
    }
})

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 call button next to their name. This button, in turn, calls the call method, which we have just defined.

First, we send an incoming-call event coined by joining the incoming-call string with the client’s id, to notify the other peer that someone is requesting a video call.

Next, we subscribe to the incoming-call event, then do a couple of checks.

  1. check if user’s currentCall is not undefined, which means he is on another call, then tell the caller the other peer is on a call.
  2. verify that the user wants to pick the call. if he does not, tell the caller so, else, tell the user the call request has been accepted.

After the checks are done, we subscribe to the call-details event to receive information on the requested call. Once the call is accepted, fire a method called initiateCall.

Now, let’s go ahead and implement the call, receive and end call features.

Paste the following code at the end of your ably-screenshare.js file:

Note: Since Chrome requires an extension to share your screen, you will need to install the temasys chrome plugin

function initiateCall(client_id) {
    AdapterJS.WebRTCReady(function(isUsingPlugin) {
        // The WebRTC API is ready.
        navigator.mediaDevices.getUserMedia(constraints)
            .then(function(stream) {
                /* use the stream */
                localStream = stream
                var video = document.getElementById('local')
                attachMediaStream(video, stream);
                video.play()
                    // Create a new connection
                currentCall = client_id
                if (!connections[client_id]) {
                    connections[client_id] = new Connection(client_id, AblyRealtime, true, stream)
                }
                document.getElementById('call').style.display = 'block'
            })
            .catch(function(err) {
                /* handle the error */
                alert('Could not get video stream from source')
            })
    });
}

AblyRealtime.subscribe(`rtc-signal/${clientId}`, msg => {
    if (localStream === undefined) {
        AdapterJS.WebRTCReady(function(isUsingPlugin) {
            navigator.mediaDevices.getUserMedia(constraints)
                .then(function(stream) {
                    /* use the stream */
                    console.log(stream)
                    localStream = stream
                    var video = document.getElementById('local')
                    attachMediaStream(video, stream);
                    video.play()
                    connect(msg.data, stream)
                })
                .catch(function(err) {
                    alert('error occurred while trying to get stream')
                })
        })
    } else {
        connect(msg.data, localStream)
    }
})
function connect(data, stream) {
    if (!connections[data.user]) {
        connections[data.user] = new Connection(data.user, AblyRealtime, false, stream)
    }
    connections[data.user].handleSignal(data.signal)
    document.getElementById('call').style.display = 'block'
}
function receiveStream(client_id, stream) {
    var video = document.getElementById('remote')
    attachMediaStream(video, stream);
    video.play()
    renderMembers()
}

See this step in Github

Here, we have defined the initiateCall method, which waits for the adapter.js function to be ready, then gets the stream from the user via navigator.mediaDevices.getUserMedia, then passes it to the Connection class, to initiate a connection with the other peer. You will also notice a helper function attachMediaStream which is used to render the stream to the video element. This function is also exposed by adapter.js

If you remember, in Step 4, when we have defined the Connection class, we handled the _onSignal event using Ably. Here, we have defined the rtc-signal/${clientId} event for that purpose.

Since we are attempting to connect to the other peer, this event would be fired. So in this event, we check if a stream is assigned, if not, get the user stream, and pass it to the connect function. If assigned, call the connect function

In the connect function, we check if the connection already exists. If it doesn’t, create a new connection instance with the stream passed into it, else if it does exist, send the signal to the connection.

Next, you would see the receiveStream function. This function was called in step 4, under the _onStream. What it does, is to load the remote stream into the second video tag in our markup.

That’s it, we are connected to the other peer, and we can see a live screen.

How do we end calls then?

Let’s take a look at the function below:

function handleEndCall(client_id = null) {
    if (client_id && client_id != currentCall) {
        return
    }
    client_id = currentCall;
    alert('call ended')
    currentCall = undefined
    connections[client_id].destroy()
    delete connections[client_id]
    for (var track of localStream.getTracks()) {
        track.stop()
    }
    localStream = undefined
    document.getElementById('call').style.display = 'none'
}

See this step in Github

In the function above, notice that we receive an optional argument of client_id. Why is this argument optional?

If you remember, in the Connection class, the _onClose() method calls this function, passing in the client id of the other peer whose connection was closed/dropped. We use this parameter, to know which connection to delete from our connections object. This function is also called when you click the end call button, but here, no parameter is passed, as we have a variable called currentCall, which holds the value of the call you must be ending.

In this function, we destroy the peer connection, as well as delete the key from our connection objects. We then stop the Video and Audio streams coming from the webcam, then hide the video inputs.

Step 7 – 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

Note: This demo uses the getUserMedia API as illustrated throughout the tutorial. At the time of the release of this tutorial, this API has limited support on mobile browsers running on iOS. You can check the current support here.

To try this example yourself, Open this demo on a different computer to see WebRTC screenshare in action.

Please note that you are required to use a different computer as the screen-share cannot be captured with two browsers at the same time.

Conclusion

In this tutorial, we have seen how to use WebRTC in conjunction with Ably to build a real-time screen share app.