From b0782885d535cd44460e70f2895a39722fcb0d13 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 31 Aug 2016 16:29:14 +0100 Subject: [PATCH] kill unhandled exceptions where loads and plays race by queuing them as promises --- lib/webrtc/call.js | 109 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/lib/webrtc/call.js b/lib/webrtc/call.js index 6a00c5f6a..abb4ab33f 100644 --- a/lib/webrtc/call.js +++ b/lib/webrtc/call.js @@ -74,6 +74,8 @@ MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed"; */ MatrixCall.ERR_NO_USER_MEDIA = "no_user_media"; +MatrixCall.mediaPromises = {}; + utils.inherits(MatrixCall, EventEmitter); /** @@ -144,6 +146,64 @@ MatrixCall.prototype.placeScreenSharingCall = _tryPlayRemoteStream(this); }; +/** + * Play the given HTMLMediaElement, serialising the operation into a chain + * of promises to avoid racing access to the element + * @param {Element} HTMLMediaElement to play + * @param {string} Arbitrary ID to track the chain of promises to be used + */ +MatrixCall.prototype.playElement = function(element, queueId) { + if (MatrixCall.mediaPromises[queueId]) { + MatrixCall.mediaPromises[queueId] = + MatrixCall.mediaPromises[queueId].then(function() { + return element.play(); + }); + } + else { + MatrixCall.mediaPromises[queueId] = element.play(); + } +}; + +/** + * Pause the given HTMLMediaElement, serialising the operation into a chain + * of promises to avoid racing access to the element + * @param {Element} HTMLMediaElement to pause + * @param {string} Arbitrary ID to track the chain of promises to be used + */ +MatrixCall.prototype.pauseElement = function(element, queueId) { + if (MatrixCall.mediaPromises[queueId]) { + MatrixCall.mediaPromises[queueId] = + MatrixCall.mediaPromises[queueId].then(function() { + return element.pause(); + }); + } + else { + // pause doesn't actually return a promise, but do this for symmetry + // and just in case it does in future. + MatrixCall.mediaPromises[queueId] = element.pause(); + } +}; + +/** + * Assign the given HTMLMediaElement by setting the .src attribute on it, + * serialising the operation into a chain of promises to avoid racing access + * to the element + * @param {Element} HTMLMediaElement to pause + * @param {string} the src attribute value to assign to the element + * @param {string} Arbitrary ID to track the chain of promises to be used + */ +MatrixCall.prototype.assignElement = function(element, src, queueId) { + if (MatrixCall.mediaPromises[queueId]) { + MatrixCall.mediaPromises[queueId] = + MatrixCall.mediaPromises[queueId].then(function() { + element.src = src; + }); + } + else { + element.src = src; + } +}; + /** * Retrieve the local <video> DOM element. * @return {Element} The dom element @@ -180,13 +240,15 @@ MatrixCall.prototype.setLocalVideoElement = function(element) { if (element && this.localAVStream && this.type === 'video') { element.autoplay = true; - element.src = this.URL.createObjectURL(this.localAVStream); + this.assignElement(element, + this.URL.createObjectURL(this.localAVStream), + "localVideo"); element.muted = true; var self = this; setTimeout(function() { var vel = self.getLocalVideoElement(); if (vel.play) { - vel.play(); + self.playElement(vel, "localVideo"); } }, 0); } @@ -414,16 +476,20 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) { videoEl.autoplay = true; if (this.screenSharingStream) { debuglog("Setting screen sharing stream to the local video element"); - videoEl.src = this.URL.createObjectURL(this.screenSharingStream); + this.assignElement(videoEl, + this.URL.createObjectURL(this.screenSharingStream), + "localVideo"); } else { - videoEl.src = this.URL.createObjectURL(stream); + this.assignElement(videoEl, + this.URL.createObjectURL(stream), + "localVideo"); } videoEl.muted = true; setTimeout(function() { var vel = self.getLocalVideoElement(); if (vel.play) { - vel.play(); + self.playElement(vel, "localVideo"); } }, 0); } @@ -460,12 +526,14 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) { if (localVidEl && self.type == 'video') { localVidEl.autoplay = true; - localVidEl.src = self.URL.createObjectURL(stream); + this.assignElement(localVidEl, + this.URL.createObjectURL(stream), + "localVideo"); localVidEl.muted = true; setTimeout(function() { var vel = self.getLocalVideoElement(); if (vel.play) { - vel.play(); + self.playElement(vel, "localVideo"); } }, 0); } @@ -712,7 +780,7 @@ MatrixCall.prototype._onAddStream = function(event) { t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted); }); - event.stream.onended = hookCallback(self, self._onRemoteStreamEnded); + event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded); // not currently implemented in chrome event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted); @@ -824,21 +892,21 @@ var sendCandidate = function(self, content) { var terminate = function(self, hangupParty, hangupReason, shouldEmit) { if (self.getRemoteVideoElement()) { if (self.getRemoteVideoElement().pause) { - self.getRemoteVideoElement().pause(); + self.pauseElement(self.getRemoteVideoElement(), "remoteVideo"); } - self.getRemoteVideoElement().src = ""; + self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo"); } if (self.getRemoteAudioElement()) { if (self.getRemoteAudioElement().pause) { - self.getRemoteAudioElement().pause(); + self.pauseElement(self.getRemoteAudioElement(), "remoteAudio"); } - self.getRemoteAudioElement().src = ""; + self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio"); } if (self.getLocalVideoElement()) { if (self.getLocalVideoElement().pause) { - self.getLocalVideoElement().pause(); + self.pauseElement(self.getLocalVideoElement(), "localVideo"); } - self.getLocalVideoElement().src = ""; + self.assignElement(self.getLocalVideoElement(), "", "localVideo"); } self.hangupParty = hangupParty; self.hangupReason = hangupReason; @@ -896,11 +964,13 @@ var _tryPlayRemoteStream = function(self) { if (self.getRemoteVideoElement() && self.remoteAVStream) { var player = self.getRemoteVideoElement(); player.autoplay = true; - player.src = self.URL.createObjectURL(self.remoteAVStream); + self.assignElement(player, + self.URL.createObjectURL(self.remoteAVStream), + "remoteVideo"); setTimeout(function() { var vel = self.getRemoteVideoElement(); if (vel.play) { - vel.play(); + self.playElement(vel, "remoteVideo"); } // OpenWebRTC does not support oniceconnectionstatechange yet if (self.webRtc.isOpenWebRTC()) { @@ -914,11 +984,13 @@ var _tryPlayRemoteAudioStream = function(self) { if (self.getRemoteAudioElement() && self.remoteAStream) { var player = self.getRemoteAudioElement(); player.autoplay = true; - player.src = self.URL.createObjectURL(self.remoteAStream); + self.assignElement(player, + self.URL.createObjectURL(self.remoteAStream), + "remoteAudio"); setTimeout(function() { var ael = self.getRemoteAudioElement(); if (ael.play) { - ael.play(); + self.playElement(vel, "remoteAudio"); } // OpenWebRTC does not support oniceconnectionstatechange yet if (self.webRtc.isOpenWebRTC()) { @@ -1102,6 +1174,7 @@ var forAllTracksOnStream = function(s, f) { /** The MatrixCall class. */ module.exports.MatrixCall = MatrixCall; + /** * Create a new Matrix call for the browser. * @param {MatrixClient} client The client instance to use.