Promises + WebRTC = <3

If you don't care for promises, their minimal impact is:

   -  navigator.getUserMedia(constraints, function(stream) {
   +  mediaDevices.getUserMedia(constraints).then(function(stream) {
        video.srcObject = stream;
        video.play();
   -  }, function (err) {
   +  }).catch(function (err) {
        console.log(err.message);
      });

But you'd be missing out by stopping there, as I believe they have a lot 
more to offer WebRTC.

To illustrate, I've rewritten our local-loop replaceTrack demo-script to 
use a promises polyfill that works in Firefox Nightly [1]. Here's a 
cleaned-up excerpt:

   function call(pc, signal) {
     return mediaDevices.getUserMedia(myrequest)
       .then(video => {
         localvideo1.srcObject = video;
         localvideo1.play();
         video1.getTracks().forEach(track => pc.addTrack(track, video));
         return pc.createOffer(offer_options);
       })
       .then(offer => pc.setLocalDescription(offer))
       .then(() => signal.then(answer => pc.setRemoteDescription(answer)));
   }

   function pickup(pc, signal) {
     return (oneway.checked? new Promise(resolve => resolve()) :
mediaDevices.getUserMedia(myrequest_reverse)
           .then(video => {
             localvideo2.srcObject = video;
             localvideo2.play();
             video.getTracks().forEach(track => pc.addTrack(track, video));
           }))
       .then(() => signal.then(offer => pc.setRemoteDescription(offer)))
       .then(() => pc.createAnswer(answer_options))
       .then(answer => pc.setLocalDescription(answer));
   }

   Promise.all([call(pc1, pc2.stable), pickup(pc2, pc1.haveLocalOffer)])
     .then(() => log("HIP HIP HOORAY"))
     .catch(failed); // 1-line error handling

This is a complete local-loop call. The two functions, call() and 
pickup(), may be a bit contrived, but illustrate that unlike most 
local-loop tests you've probably seen, each peer is set up 
asynchronously and in parallel, with only a signal sent across, much 
like in a remote call.

Several things are going on here - which I will explain - and promises 
are used in three interesting ways:

First, there's the promise-chains: then().then().then(). We start two 
chains in parallel and wait for both to complete with Promise.all. This 
is pretty straightforward promise-stuff, but powerful.

Then, there's three new promises that I've added as read-only-attributes 
(yes!) to the peerConnection polyfill that trigger on signalingstate:

    pc.hasLocalOffer
    pc.hasRemoteOffer
    pc.stable

Turns out promises can be used to track simple states. E.g. to make 
something happen when signalingstate changes to "has-local-offer", you 
just do: pc.hasLocalOffer.then(function(offer) { ... }); To make it more 
useful, I've made the fulfillment value be the offer (or answer 
depending), which means I can use them as the carrier 'signal' argument 
that gives the call() and pickup() functions the other peer's sdp when 
it becomes available.


Lastly, my final use of promises was to fix an unfortunate race in 
local-loop calls. With the two peers less tightly coupled, I started 
seeing ICE failures. Turns out candidates were arriving before the other 
peer could receive them, and addIceCandidate was failing, complaining 
that it could not be called beforesetRemoteDescription. Is that by spec?

To solve this, I relied again on the signalingstate promises:

     pc1.onicecandidate = obj => {
       pc2.haveRemoteOffer.then(() => {
         if (obj.candidate) {
           pc2.addIceCandidate(obj.candidate).catch(failed);
         }
       });
     };

     pc2.onicecandidate = obj => {
       pc1.stable.then(() => {
         if (obj.candidate) {
           pc1.addIceCandidate(obj.candidate).catch(failed);
         }
       });
     };

Turns out promises can be used to queue function-calls. Calling then() 
multiple times on the same promise queues functions up to be executed in 
order later (or right away if the promise has already been resolved). 
This made it easy to subjugate addIceCandidate's timing-needs.

So there you have it. Three uses of promises in the same API. I also 
enjoy that there is one line of error-handling in the whole thing.

Feel free to have a look at the full test or take it for a spin:

Comments welcome.

[1] https://bug1033885.bugzilla.mozilla.org/attachment.cgi?id=8492552

.: Jan-Ivar :.

Received on Saturday, 20 September 2014 05:55:08 UTC