JavaScript
Article

Building a WebRTC Video Chat Application with PeerJS

By Wern Ancheta

This article was peer reviewed by Panayiotis Velisarakos and Ravi Kiran. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

With the advent of WebRTC and the increasing capacity of browsers to handle peer-to-peer communications in real-time, it’s easier than ever to build real-time applications. In this tutorial we’ll take a look at PeerJS and how it can make our lives easier when implementing WebRTC. Throughout this tutorial we’ll be building a WebRTC video chat app with messaging features. If you need a bit of a background regarding WebRTC and peer-to-peer communication, I recommend reading The Dawn of WebRTC and Introduction to the getUserMedia API.

What Is PeerJS?

Before we move on, it’s important that we understand the main tool that we’ll be using. PeerJS is a JavaScript library that simplifies WebRTC peer-to-peer data, video, and audio calls.

What it does is act as a wrapper around the browser’s WebRTC implementation. As you might already know, browser vendors doesn’t exactly agree on a single way of implementing different features which means that for every browser there’s a different implementation for WebRTC. You as the developer would have to write different code for every browser that you plan to support. PeerJS acts as the wrapper for that code. The API that it exposes is easy to use and understand which makes it a really great candidate for implementing cross-browser WebRTC.

PeerServer

WebRTC isn’t completely peer-to-peer. The reality is that there’s always a server which bridges the connection between peers. Two or more devices can’t just magically create a connection out of thin air. There are firewalls, routers and other roadblocks that can get in the way of peer-to-peer communication, which is where a server comes in.

PeerServer is the server-side component of PeerJS and allows two or more devices to connect together. You have two choices when it comes to PeerServer. You can either implement it your own by using the PeerJS server library or use the PeerServer Cloud Service.

So which is the right choice for your project?

If you want to get started quickly then PeerServer Cloud Service is for you. Just sign up to acquire an API key, which you can then use to connect to the PeerServer Cloud. The only downside to this option is that it doesn’t really work in production. This is because you can only have 50 concurrent connections and the server isn’t in HTTPS. The video call feature requires end-to-end encryption, which means that the PeerServer that you are connecting to should be in HTTPS as well.

If you’re reading this tutorial to implement video or audio call features on your website then you will need the PeerJS server library. However, if you simply need a chat feature you can still use the PeerServer Cloud Service, provided your website doesn’t exceed the concurrent connection limit.

As we move on through this tutorial I’ll be showing you how to implement the server with both the PeerServer Cloud Service and the PeerJS server library.

Building the WebRTC Video Chat App

Now it’s time to get our hands dirty by building the app. First we’re going to work with the client-side code and then we’ll move over to the server side.

Please note that you can download the code for this tutorial from our GitHub repo. To run it, or to follow along at home, you will need to have Node and npm installed. If you’re not familiar with these, or would like some help getting them installed, check out our previous tutorials:

The Client

In this section we’ll be working primarily with the user-facing part of the app. This will have the following directory structure:

public
├── css
│   └── style.css
├── index.html
├── js
│   └── script.js
└── package.json

Dependencies

It also has the following client-side dependencies:

  • Picnic — a lightweight CSS framework.
  • jQuery — used for selecting elements on the page and event handling.
  • PeerJS — the client-side component of PeerJS.
  • Handlebars — a JavaScript templating library. We’re going to use it for generating HTML for the messages.

You can install the libraries through npm. Create a package.json file in the root of the public directory and add the following:

{
  "name": "peer-messenger-client",
  "version": "0.0.1",
  "dependencies": {
    "jquery": "~2.1.4",
    "picnic": "~4.1.2",
    "peerjs": "~0.3.14",
    "handlebars": "~4.0.5"
  }
}

Execute npm install to install all the dependencies.

Mark-Up

The index.html file is reasonably self explanatory. You can find it here. I’ve added a couple of inline comments to highlight the important points.

Take a second to notice the Handlebars template for constructing the list of messages. This loops through all the messages in the messages array. For each iteration of the loop, we display the name of the sender and the actual message. Later on we’ll take a look at how the data is being passed into this template.

<script id="messages-template" type="text/x-handlebars-template">
  {{#each messages}}
  <li>
    <span class="from">{{from}}:</span> {{text}}
  </li>
  {{/each}}
</script>

Styling

The app only has minimal styling since most of the prettifying needs are already handled by Picnic. Our additional styles (in css/style.css) include some rules for positioning the elements, hiding things, and overriding the default one’s provided by the browser.

Main App Script

The main JavaScript file (in js/script.js) is where all the logic of the app is contained. This includes such things as the login, initiating the call and capturing the users video. Let’s take a look at the code.

We start off by declaring a couple of variables—the messages array, which will be used to store the messages that are sent by each of the peers, peer_id, which stores the ID of the remote peer, name, which stores the name of the current user and conn to store the current peer connection. We also compile the messages template using Handlebars and store the result in messages_template:

var messages = [];
var peer_id, name, conn;
var messages_template = Handlebars.compile($('#messages-template').html());

Next, we create a new PeerJS instance which accepts an object containing configuration options. Here, we’re specifying the options host, port and the path, as this demo will use the PeerJS server library (I’ll go into the configuration for the PeerServer Cloud Service later on). The debug option allows us to set the verbosity of the debug mode (3 being the highest) and the config option allows us to specify the ICE (Interactive Connectivity Establishment) servers. This can either be a STUN or TURN server. You don’t really need to worry about the different terminologies for now. All you need to know is that these servers are used to make sure that the peer connection is successfully established. If you want to dive into to the specifics of WebRTC, I recommend you to read this HTML5Rocks article on WebRTC Basics and/or this Mozilla Hacks article on WebRTC acronyms. If you want to use another STUN or TURN server, you can look at this list of freely available STUN and TURN servers.

var peer = new Peer({
  host: 'localhost',
  port: 9000,
  path: '/peerjs',
  debug: 3,
  config: {'iceServers': [
  { url: 'stun:stun1.l.google.com:19302' },
  { url: 'turn:numb.viagenie.ca',
    credential: 'muazkh', username: 'webrtc@live.com' }
  ]}
});

When the connection has been successfully initialized on the server, the open event is fired on the peer object. At this point the ID of the current user becomes available and we insert it into the page. This user can then give this ID to another user so that they can connect.

peer.on('open', function(){
  $('#id').text(peer.id);
});

The next line adds a getUserMedia property to the navigator object, the value of which will be the flavor of getUserMedia in the browser. For WebKit browsers such as Chrome or Safari, this will be webkitGetUserMedia. For Firefox, this will be mozGetUserMedia. For other browsers which implement the UserMedia API, this will simply be getUserMedia. Note that this isn’t a fool-proof solution. You can use a shim such as getUserMedia.js if you want a more unified way to use the getUserMedia API.

navigator.getUserMedia = navigator.getUserMedia ||
                         navigator.webkitGetUserMedia ||
                         navigator.mozGetUserMedia;

Then comes a function for getting the current user’s video feed. This uses the navigator.getUserMedia function which accepts three arguments:

  1. An object containing configuration options. In this case we set the audio and video to true so both the camera and microphone in the device will work to provide a video stream.
  2. A success callback function which we have specified as the argument for the getVideo function. Note that a stream will be passed as an argument into this anonymous callback function.
  3. An error callback function which simply alerts the user that an error has occurred.
function getVideo(callback){
  navigator.getUserMedia(
    {audio: true, video: true},
    callback,
    function(error){
      console.log(error);
      alert('An error occurred. Please try again');
    }
  );
}

getVideo(function(stream){
  window.localStream = stream;
  onReceiveStream(stream, 'my-camera');
});

The onReceiveStream function is responsible for initializing the video stream;

function onReceiveStream(stream, element_id){
  var video = $('#' + element_id + ' video')[0];
  video.src = window.URL.createObjectURL(stream);
  window.peer_stream = stream;
}

Once the user clicks Log In, we use PeerJS to connect them with the user with the ID they have specified. The metadata object is used to pass in custom data to the peer. In this case we’re passing in the username so that we can display it on the peer’s side. Once the connection is established, the data event is triggered every time a message is sent to the current user. For convenience I’m going to refer to the current user (the one who initiates the connection) as User A, and the peer (the one who is being connected to) as User B.

$('#login').click(function(){
  name = $('#name').val();
  peer_id = $('#peer_id').val();
  if(peer_id){
    conn = peer.connect(peer_id, {metadata: {
      'username': name
    }});
    conn.on('data', handleMessage);
  }

  $('#chat').removeClass('hidden');
  $('#connect').addClass('hidden');
});

When User A tries to connect to User B, the connection event is triggered on User B’s peer object. This allows us to set the connection for User B and get the ID of User A (connection.peer). From that connection we can listen for the data event which is triggered whenever User A sends a message to User B. After that, we hide the peer ID text field, update its value to use the ID of the connecting peer and show the username of User A.

peer.on('connection', function(connection){
  conn = connection;
  peer_id = connection.peer;
  conn.on('data', handleMessage);

  $('#peer_id').addClass('hidden').val(peer_id);
  $('#connected_peer_container').removeClass('hidden');
  $('#connected_peer').text(connection.metadata.username);
});

The handleMessage function accepts the data that was sent by the remote peer. This data is used to generate the HTML for the list of messages.

function handleMessage(data){
  var header_plus_footer_height = 285;
  var base_height = $(document).height() - header_plus_footer_height;
  var messages_container_height = $('#messages-container').height();
  messages.push(data);

  var html = messages_template({'messages' : messages});
  $('#messages').html(html);

  if(messages_container_height >= base_height){
    $('html, body').animate({ scrollTop: $(document).height() }, 500);
  }
}

As you might imagine, the sendMessage function is used to send messages to a remote peer.

function sendMessage(){
  var text = $('#message').val();
  var data = {'from': name, 'text': text};

  conn.send(data);
  handleMessage(data);
  $('#message').val('');
}

When a user initiates a call, we use the call method provided by the peer object to call the remote peer. The result of this function call returns an object in which we can listen for the stream event. This event is fired when the video feed of the remote peer becomes available to the user who initiated the call.

$('#call').click(function(){
  var call = peer.call(peer_id, window.localStream);
  call.on('stream', function(stream){
    window.peer_stream = stream;
    onReceiveStream(stream, 'peer-camera');
  });
});

When the current user makes a call to a remote peer, the call event is triggered. From here we execute the onReceiveCall function to accept the call. If you want to add additional functionality for accepting a call, this is a good place to add it.

peer.on('call', function(call){
  onReceiveCall(call);
});

The onReceiveCall function accepts the current call object as its argument. The the call object’s answer method is invoked to proceed with the call. This accepts the video feed of the current user as its argument. When the video feed of the remote peer becomes available, the stream event is triggered. From there we can call the onReceiveStream function to setup the remote peer’s video.

function onReceiveCall(call){
  call.answer(window.localStream);
  call.on('stream', function(stream){
    window.peer_stream = stream;
    onReceiveStream(stream, 'peer-camera');
  });
}

The Server

Now we’re ready to proceed with the server. First, I’ll look at using the PeerServer Cloud Service, after which I’ll examine how to run your own PeerServer using peerjs-server.

Acquiring an API Key from the PeerServer Cloud Service

Go to peerjs.com and click on the Developer – Free button. This will open a modal window that will ask for your credentials. Fill out the form and click on the Complete Registration button. Once that’s done, you’ll be redirected to the dashboard page where you can see your API key.

Copy that and update the js/script.jsfile with your API key. You can remove the host, port and path properties from the configuration options in favour of a key property.

var peer = new Peer( {
  key: 'YOUR PEERSERVER CLOUD SERVICE API KEY',
  debug: 3,
  config: {'iceServers': [
    { url: 'stun:stun1.l.google.com:19302' },
    { url: 'turn:numb.viagenie.ca',
      credential: 'muazkh', username: 'webrtc@live.com' }
  ]}
});

And that’s it. You can now jump to the demo section at the end of the article to see what the finished product looks like. Please bear in mind the limitations of this method (namely that you can only have 50 concurrent connections and that video is not possible).

Running Your Own PeerServer

In this section I’ll walk you through how to run your own peer server.

This will have the following directory structure:

server
├── peer-server.js
└── package.json

The server component is going to need the peerjs-server. You can install it by creating a package.json file in the root of the server directory and add the following:

{
  "name": "peer-messenger-peerserver",
  "version": "0.0.1",
  "dependencies": {
    "peer": "^0.2.8"
  }
}

Execute npm install to install the dependencies.

As we’re working locally, the server folder can go alongside your public folder in your project’s root directory (just as we have in our GitHub repo). If you’re not working locally, then the server folder will be on a remote server somewhere.

Creating the PeerServer

Inside your working directory for the server component, create a peer-server.js file and add the following code:

var PeerServer = require('peer').PeerServer;
var server = PeerServer({port: 9000, path: '/peerjs'});

What this does is create a PeerJS server that runs on port 9000. You can run the server by executing node peer-server.js from the terminal. To check if the server is running, access http://localhost:9000/peerjs from the browser and you should see something similar to the following:

{"name":"PeerJS Server","description":"A server side element to broker connections between PeerJS clients.","website":"http://peerjs.com/"}

Deploying to a Remote Server

If you’re planning to deploy to a remote server later on, it has to be via HTTPS. This is because browsers only allows access to a device’s camera and microphone if the connection is secure. Here’s the code for the PeerServer to make it run on HTTPS:

var fs = require('fs');
var PeerServer = require('peer').PeerServer;

var server = PeerServer({
  port: 9000,
  ssl: {
    key: fs.readFileSync('/path/to/your/ssl/key/here.key'),
    cert: fs.readFileSync('/path/to/your/ssl/certificate/here.crt')
  }
});

This requires you to supply the key and certificate file that you acquired from your SSL certificate provider.

Running the App

To run the WebRTC video chat app, you’ll need to have a server that will serve the files that we’ve created earlier on the client-side section. This is because the getUserMedia API won’t work by simply opening the index.html file on the browser. There are a whole bunch of ways to do this. Here are some of them:

Using PHP

If you have PHP installed on your machine, you can execute the following command while inside the public directory to serve the files.

php -S localhost:3000

You can then access the app by going to: http://localhost:3000.

Using Apache

If you have Apache installed, you can copy the peer-messenger directory over to the root of your web folder and access the app using the following URL: http://localhost/public.

Using Node

If you want to use Node, you can install Express and have it serve the static files. You can install Express with the following command:

npm install express

Next, create an app-server.js file and add the following code:

var express = require('express');
var app = express();

app.listen(3000);
app.use(express.static('public'));

You can then run the app by executing node app-server.js. And you can access it by going to: http://localhost:3000.

Demo

Whichever method you choose, here’s what the app should look like:

The finished WebRTC video chat app

From there the user can enter their username and the ID of the peer they’re trying to connect to. For convenience let’s call this user the “initiator” because he is the one initiating the connection:

Login screen

Once the initiator has logged in, peerJS sends the connection data to the peer. For convenience let’s call this user the “receiver”. Once the data is received on the receiver’s side, the screen is updated to show the username of the initiator.

Show initiator username

The receiver can then enter their username and click on the Login button to begin talking to the initiator:

Frst message from receiver

The initiator replies and clicks on the Call button:

Initiator replies and initiates the call

Once the call goes through, the initiator and the receiver both see the video of the peer they’re connected to:

Remote video available

Conclusion

In this tutorial you’ve learned about PeerJS and how you can use it to create real-time apps. Specifically we’ve created a messaging application that allows the user to send text and make a video call to a remote peer. PeerJS is a really great cross-browser way to implement WebRTC in web applications.

Don’t forget, the code used in this tutorial is available on GitHub. Clone it, make something cool and have fun!

Have you made something cool using PeerJS and/or WebRTC? Let me know in the comments below.

Meet the author
Wern is a web developer from the Philippines. He loves building things for the web and sharing the things he has learned by writing in his blog. When he's not coding or learning something new, he enjoys watching anime and playing video games.
  • http://exexzian.com exex zian

    @Wern Really a nice tutorial and addition of creating our own peerServer is plus.
    by the way how can we achieve broadcasting an host’s video to multiple subscribers using peerjs?

    • Wern_Ancheta

      hi @exexzian:disqus , you can apply the same idea as what we have in the tutorial. Only this time, you have to store the connections in an array. And the peers that will subscribe to the broadcast shouldn’t send their own mediaStream.

  • Shafayat

    Very nice article.
    working fine in Chrome , but showing this error in Firefox
    “TypeError: b is undefined
    http://localhost:3000/node_modules/peerjs/dist/peer.min.js
    Line 1″

    any idea

  • http://poketech.org/ Poketech

    Thank you for your article. It works great. Only one thing, do you have a problem with the sound ? There is a big noise from my laptop when i do video chat without headphone…

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the lastest in JavaScript, once a week, for free.