WebRTC using Meteor & PeerJS: A Basic Tutorial

WebRTC means 'Web Realtime Communications' and it comes built into certain browsers like Chrome and Firefox. The WebRTC API covers all the steps to stream live video, audio & data, but it becomes complicated when you fulfill all the steps on the way to calling your friends. PeerJS abstracts away much of the WebRTC API and leaves us with a concise set of methods and callbacks to connect our calls.

Follow this tutorial and build a Meteor app with 2-person video and audio chat. First, we setup PeerJS and see how it works. Later, we add in user management so that, when our friend is online, we can click his email and have a video chat.

If you have never worked with Meteor.js, you should give it a shot. Learn to install Meteor here

Note: We're using Chrome. You could choose Firefox or maybe Opera. But I've not tested IE on Windows and Safari does not support webrtc (at this time).

See the code here

Try a live demo here

Let's get started.

Create a New Meteor App

Generate a new meteor app using meteor's command line tool.

$ meteor create webrtc-peerjs

This generates a boilerplate starter app that we will mostly delete. We will hire a top-notch designer, so you can ignore (or remove) the CSS for now. Your JS and HTML files should look like this:

if (Meteor.isClient) {  
}

if (Meteor.isServer) {  
}

// webrtc-peerjs.js
<head>  
  <title>webrtc-peerjs</title>
</head>

<body>  
  <h1>Welcome to Meteor!</h1>

  {{> hello}}
</body>

<template name="hello">  
</template>

<!-- webrtc-peerjs.html-->  

We will fit all our code into these 2 files, after we paste in one additional file: PeerJS

Add Peerjs

We are going to find the latest PeerJS and paste it into our project. No packages right now.

Copy the peer.js code from here: https://raw.githubusercontent.com/peers/peerjs/master/dist/peer.js

We only need to load this in the client, so create a 'client' directory, a new 'peer.js' file, and paste the code.

/*! peerjs build:0.3.13, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */

...

// client/peer.js

Get a PeerJS API key

PeerJS allows us access to their cloud servers, so our users can get special 'peer ids' that work like phone numbers specific to our app.

Let's get our Peer JS API keys now.

Go to http://peerjs.com/peerserver. Click the 'Developer - Free' button to get an API key.

PeerJS API key button

And you will end up seeing your key:

Seeing your PeerJS key

You will use this key later, when you create a new Peer object, like this:

var peer = new Peer({key: 'blahblah' ...});  

But before we hack our JavaScript, let's stub out a minimal HTML template.

Barebones HTML Template

Our template needs only the basics. A div each for 'myVideo' and 'theirVideo'. An input to paste 'their' peer ID. And 2 buttons: 'makeCall' & 'endCall'.

Here's the entirety of our html file:

<head>  
  <title>webrtc-peerjs</title>
</head>


<body>  
  <h1>Welcome to Meteor!</h1>

  {{> hello}}
</body>

<template name="hello">  
    <h2>Video Chat</h2>
    <div id="video-container">
      <!-- note the 'autoplay' -->
      Their video: <video id="theirVideo" class="theirVideo" autoplay></video>
      <video id="myVideo" muted="true" class="myVideo" autoplay></video>: Your video
    </div>

    <h2>Controls</h2>
    <div>
      <p>Your id: <span id="myPeerId">...</span></p>
      <p>Make a call</p><br>
      <input type="text" placeholder="Call user id..." id="remotePeerId">
      <p><a href="#" id="makeCall">Call</a></p>
      <p><a href="#" id="endCall">End call</a></p>
    </div>
</template>  

We will access our local webcam and see our own video in '#myVideo'.

We will be able to paste our friend's peer id into the input, click 'makeCall' and see our friend's video '#theirVideo'. Clicking 'endCall' will hangup.

Now we can wire up the JavaScript.

The JavaScript

We will have our 'hello' template do a couple things as soon as it loads. First, it should initiate our PeerJS connection. We can do this in the template's 'onCreated' callback:

if (Meteor.isClient) {  
  Template.hello.onCreated(function () {
      // Create a Peer instance
      window.peer = new Peer({
      key: 'applebanana',  // get a free key at http://peerjs.com/peerserver
      debug: 3,
      config: {'iceServers': [
        { url: 'stun:stun.l.google.com:19302' },
        { url: 'stun:stun1.l.google.com:19302' },
      ]}
    });

    // Handle event: upon opening our connection to the PeerJS server
    peer.on('open', function () {
      $('#myPeerId').text(peer.id);
    });

    // Handle event: remote peer receives a call
    peer.on('call', function (incomingCall) {
      window.currentCall = incomingCall;
      incomingCall.answer(window.localStream);
      incomingCall.on('stream', function (remoteStream) {
        window.remoteStream = remoteStream;
        var video = document.getElementById("theirVideo")
        video.src = URL.createObjectURL(remoteStream);
      });
    });
  });
}
// webrtc-peerjs.js

This code does 3 important things. First, it connects to the PeerJS cloud server, sending along an options object that includes our API key, the debug level (3 means 'tell me everything') and the STUN & TURN servers that we want to use.

Note: STUN & TURN, as well as ICE, are crucial topics. But, we don't need to understand them for this 'hello world' style tutorial. Read more when you're ready.

We added two callbacks, for two events: when our PeerJS connection opens, and when a client receives a call. First, when our client connects with the PeerJS cloud servers and receives our local peer id, is the 'open' event. That local peer id acts as a phone number. We can give it to a friend and he can call us. Similarly, when our friend loads the app on his browser, his browser also hears the 'open' event and gets his own peer id. We can use that peer id to call him.

Second, when our local peer receives an incoming call, is the 'call' event. When we call our friend, this callback fires in his browser, creates a 'call' object and sets it as window.currentCall.

Note, that last callback has its own callback, fired when our friend receives our video & audio stream. In WebRTC calls, we add a MediaStream, like a webcam feed, into the connection. If we wanted, we could only add audio, or share our screen (advanced).

When we call our friend, we send our video stream. Our friend will take our stream, set it to the var 'window.remoteStream', and show it in his browser, with the lines:

var video = document.getElementById("theirVideo")  
video.src = URL.createObjectURL(remoteStream);  

Speaking of our MediaStream, we need to access it before we make calls. Continuing in the same 'onCreated' callback:

if (Meteor.isClient) {  
  Template.hello.onCreated(function () {
    ...

        navigator.getUserMedia = ( navigator.getUserMedia ||
                            navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia ||
                            navigator.msGetUserMedia );

    // get audio/video
    navigator.getUserMedia({audio:true, video: true}, function (stream) {
        //display video
        var video = document.getElementById("myVideo");
      video.src = URL.createObjectURL(stream);
        window.localStream = stream;
      },
      function (error) { console.log(error); }
    );

  });
}
// webrtc-peerjs.js

The type of 'getUserMedia' function available depends on the browser we use, and each browser requires its own prefix. Browser makers hope to eliminate that inconsistency soon.

With this code, the template loads, connects to the PeerJS cloud server, gets a peer id for our local user, and grabs streaming audio & video from our computer.

  • Note: with 'audio: true', you will get feedback when you test locally with yourself. Setting 'audio: false' may be more pleasant.

We can add 2 events and start making calls. In the Template.hello.events:

if (Meteor.isClient) {  
  Template.hello.events({
    "click #makeCall": function () {
      var outgoingCall = peer.call($('#remotePeerId').val(), window.localStream);
      window.currentCall = outgoingCall;
      outgoingCall.on('stream', function (remoteStream) {
        window.remoteStream = remoteStream;
        var video = document.getElementById("theirVideo")
        video.src = URL.createObjectURL(remoteStream);
      });
    },

    "click #endCall": function () {
      window.currentCall.close();
    }
  });

  ...

}

// webrtc-peerjs.js

The 'makeCall' event creates a new call, passing in our friend's peer id that we typed into our HTML input and also our localStream (mic & video). We set that call to 'window.currentCall'.

We added a callback, similar to the other 'call' callback in 'Template.hello.onCreated'. When our friend answers our call and sends back his own media stream, this callback fires in our brower and adds our friend's video track to the '#theirVideo' HTML element.

Thus, we called our friend, he answered, the call, grabbed our video and sent back his own video. Voila-- two-way video chat.

The other 'click' event, 'endCall', closes the 'window.currentCall'. This is like 'hangup'. This closes the call instantly on your end. Interestingly, if you watch carefully, it takes 5+ seconds for your friends client to close on his end.

Try it out

Load up 2 tabs-- one tab must be private (Mac: Cmd+Shift+N, Windows: Ctrl+Shift+N). Copy the peer ID from one tab into the other, click 'call' and see yourself. One video is local, the other is remote.

Showing local and remote video

You can view the code thusfar on the 'basic' branch

Calling users by email

Let's take this app to the next level. We want to see a list of online users, click one and enter a video chat.

Add users

First, we add basic Meteor accounts stuff.

$ meteor add accounts-ui

...

$ meteor add accounts-password

And, in the main HTML, add the 'loginButtons' template so we can login and register new users.

<body>  
  <h1>Welcome to Meteor!</h1>
  {{> loginButtons}}
  {{> hello}}
</body>

<!-- webrtc-peerjs.html -->  

Add Presence

Next, we add a package to that gives us Presences, a special collection that monitors online users.

Read about Meteor Presence here

$ meteor add tmeasday:presence

Save the current users's Peer ID

Now, we have user accounts and will be able to see which users are online. But, in addition to a user's presence, we must also know his peer id, so that we can call him via PeerJS. We will refactor the PeerJS 'on open' callback in 'Template.hello.onCreated':

// within the Template.hello.onCreated callback

peer.on('open', function () {  
  $('#myPeerId').text(peer.id);
  // update the current user's profile
  Meteor.users.update({_id: Meteor.userId()}, {
    $set: {
      profile: { peerId: peer.id}
    }
  });
});

// webrtc-peerjs.js

Upon opening the PeerJS connection and receiving a peer id, we save the peer id to the user's profile.

Hide until current user logged in

Let's hide all the video stuff until the user is logged in. Otherwise, the template will load without any user, and then we'd have add an 'onLogin' callback to update the user's peer id after logging in. And anyways, there's no use seeing the call controls before you're logged in. Checking for the current user improves the UX:

<body>  
  <h1>Welcome to Meteor!</h1>
  {{> loginButtons}}
  {{#if currentUser}}
    {{> hello}}
  {{/if}}
</body>  

Publish users

Let's remove autopublish and properly publish & subscribe users and presences.

First, remove autopublish

$ meteor remove autopublish

Now, publish online users and presences. Note that this will be our only server-side code.

if (Meteor.isServer) {  
  Meteor.publish('presences', function() {
    return Presences.find({}, { userId: true });
  });
  Meteor.publish("users", function () {
    return Meteor.users.find({}, {fields: {"profile.peerId": true, "emails.address": true} });
  });
}

// webrtc-peerjs.js

Back on the client, in 'Template.hello.onCreated' subscribe to 'users' and 'presences':

Template.hello.onCreated(function () {  
    Meteor.subscribe("users");
    Meteor.subscribe("presences");
    // ...
}

// webrtc-peerjs.js

Add a helper to find online users:

if (Meteor.isClient) {  
  Template.hello.helpers({
    users: function () {
      var userIds = Presences.find().map(function(presence) {return presence.userId;});
      // exclude the currentUser
      return Meteor.users.find({_id: {$in: userIds, $ne: Meteor.userId()}});
    }
  });
}
// webrtc-peerjs.js

First, find the Presences and get an array of just their corresponding userIds. Then find users using that array.

Show users in the HTML:

<h2>Controls</h2>  
<div>  
  <p>Your id: <span id="myPeerId">...</span></p>
  <p>Make a call</p><br>
  {{#each users}}
    <a href="#" id="makeCall">Call {{#each emails}}{{address}}{{/each}}</a>
  {{/each}}
</div>

<!-- webrtc-peerjs.html -->  

Note: we can make better hacks to get the user's email, but this works for now.

At this point, we can see who's online. Now we can refactor the 'makeCall' event to use the user's peer id.

// Template.hello.events:
    "click #makeCall": function () {
      var user = this;
      var outgoingCall = peer.call(user.profile.peerId, window.localStream);
      window.currentCall = outgoingCall;
      outgoingCall.on('stream', function (remoteStream) {
        window.remoteStream = remoteStream;
        var video = document.getElementById("theirVideo")
        video.src = URL.createObjectURL(remoteStream);
      });
    },

// webrtc-peerjs.js

This app is all set, so try it out. Login as 2 different users in 2 different tabs. Click to call an online user.

Click to call

See the code at github

Try the demo here

Summary

This was a 'hello world' style tutorial with a big payoff: encrypted, live streaming audio & video communications between two browsers.

You might want to do more:

  • muting the audio
  • toggling video on & off
  • screensharing
  • creating 'full mesh' conference calls with 3+ people
  • streaming data for games
  • deploying across iOS and Android

There's a lot to learn and the WebRTC API seems to be rapidly evolving. Screensharing used to be more straightforward, but Chrome has made it so you must use a browser extension. You can expect other WebRTC internals to change as well.

But WebRTC is ready for production and it is awesome. Give it a try!