Modern WebRTC Javascript with Promises

tl;dr: with adapter.js you can write WebRTC code that is spec-compliant and works in all supported browsers. That is the web we want.

We have come a long way since WebRTC was first enabled by default in Nightly back in February 2013 after interoperability had been achieved earlier that month. Since then a lot has happened.

One of the bigger updates to the specifications was the addition of a Promise-based version of the API instead of callback-based API in December 2014. Firefox has supported the Promise-based versions of the getUserMedia and the RTCPeerConnection APIs for quite a while now. Yay!

That means you can write code like this:

  navigator.mediaDevices.enumerateDevices()
  .then(function (devices) {
     // look for audio and video devices, call getUserMedia
    return navigator.mediaDevices.getUserMedia({audio: true, video: true})
  })
  .then(function (stream) {
    // attach to video element
    // attach to peerconnection and establish a connection to the peer
  })
  .catch(function (error) {
    // handle any error that occurred in the chain
  });

Compare that to the callback-way of doing it:

  navigator.mediaDevices.enumerateDevices(function (devices) {
    // look for audio and video devices, call getUserMedia
   navigator.mediaDevices.getUserMedia({audio: true, video: true},
     function (stream) {
        // attach to video element
        // attach to peerconnection and establish a connection to the peer
     },
     function (error) {
        // handle errors from getUserMedia
     }
  }, function (error) {
    // handle errors from enumerateDevices, often forgotten
  });

The promise-based version is easier to read and, more importantly, has a single error handler. With ES6 and arrow functions it gets a lot shorter even.

Now, if you want to write something that is more than a demo, you will quickly notice that there are other browsers out there. Browsers that have not yet caught up with the changes in the specification...

This is not really a new issue. The RTCPeerConnection and getUserMedia APIs have been prefixed in both Firefox and Chrome since they got introduced in late 2012. One of the ways to work around that has been the adapter.js shim written by Google’s WebRTC team (oh, and us). It provided a way to detect what browser was used and allows developers to develop against a uniform API.

Over time, it has grown from a simple script consisting of not much more than

    var RTCPeerConnection = mozRTCPeerConnection || webkitRTCPeerConnection;
    var getUserMedia = mozGetUserMedia || webkitGetUserMedia;

to a rather complex piece of software with unit tests and continuous integration in multiple browsers.

Recently, instead of just making things work, adapter.js now aims to make things work in the way the specifications describe them, without waiting for every browser to catch up. Promise-support was one of the first things we got to work and it turned out to be easier than we thought. One just has to overload the native methods as shown here. The trick is to look at the number of arguments to the function call and the type of the first argument to determine whether the Promise-based version

     pc.createOffer(constraints)
    .then(successCallback)
    .catch(errorCallback)

or the legacy variant

    pc.createOffer(successCallback, errorCallback, constraints)

is intended. Pretty amazing what you can do with a little JavaScript. The tests for the legacy version and the Promise-based version show the difference quite nicely. The test for the legacy API is 53 lines long and indented 12 levels. The promise-based test is 19 lines and indented only one level, making it much easier to read.

The list of differences hidden by adapter goes on...

This is stuff you can do with javascript now, instead of waiting for the browsers to catch up.

One of the last big remaining issues was the lack of support for setting the srcObject property of video tags so that you can attach a MediaStream to them. In the past, adapter.js has used two helper functions, attachMediaStream and reattachMediaStream, to hide the different ways which have to be used in Chrome and Firefox. Mozilla has supported mozSrcObject in a prefixed version for a very long time now and Firefox 42 will unprefix it. Chrome does not support this and still requires the src property to be set to an object URL.

Now how do you control the behavior of HTML elements? The answer was given by Chrome developer Paul Kinlan recently: you just define setters and getters on the HTMLMediaElement’s prototype as shown here. Which means you can just write the following:

   navigator.mediaDevices.getUserMedia({video: true})
   .then(function (stream) {
     document.getElementById(‘localVideo’).srcObject = stream;
   })

This way, the Microsoft Edge demos for getUserMedia require no changes to work in other browsers like Firefox and Chrome. Plus there is no need to fall back to helper functions just because some browsers are lagging behind the standard.

All the webrtc.org samples, one of the most useful developer resources out there, have been updated to use this instead of the old attachMediaStream helper. Because there is nothing worse than teaching developers the wrong way to do things. Writing standards-compliant code that just works is the Web we want.

If you find this as helpful as I do, please give thanks to Harald Alvestrand, Jan-Ivar Bruaroey and Christoffer Jansson for working together on this.

You might also enjoy reading:

Blog Archives: