Re: Promises + WebRTC = <3

On 9/22/14 4:21 AM, Harald Alvestrand wrote:
> promises for getUserMedia (promiseUserMedia?) is a Media Capture TF matter.
> Can you repost to that list?

This is a promise-wrapper for peerConnection, not just gUM, so this 
seems like the better place. With Promises being the way forward, and 
because they work better the more API-points use them, their benefit in 
mediaDevices alone may not be obvious to everyone. So I thought it might 
be helpful for people to see how things might look in the future.

> (I'm also interested in seeing your polyfill, if it's just a polyfill,
> and figure out why it doesn't work in Chrome (if it doesn't)).....

Then click the link [1] and view page source. It's all there! :-)

I shouldn't have said "polyfill". I meant: This proof-of-concept 
test-script for Firefox has a promise-wrapper embedded in it. It is not 
meant to be a production polyfill, and doesn't work on Chrome because I 
didn't make that a priority (it calls mozPeerConnection for one).

> On 09/19/2014 10:54 PM, Jan-Ivar Bruaroey wrote:
>> 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 :.

.: Jan-Ivar :.

Received on Monday, 22 September 2014 15:49:56 UTC