michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const TIMEUPDATE_TIMEOUT_LENGTH = 10000; michael@0: const ENDED_TIMEOUT_LENGTH = 10000; michael@0: michael@0: /* Time we wait for the canplaythrough event to fire michael@0: * Note: this needs to be at least 30s because the michael@0: * B2G emulator in VMs is really slow. */ michael@0: const CANPLAYTHROUGH_TIMEOUT_LENGTH = 60000; michael@0: michael@0: /** michael@0: * This class manages playback of a HTMLMediaElement with a MediaStream. michael@0: * When constructed by a caller, an object instance is created with michael@0: * a media element and a media stream object. michael@0: * michael@0: * @param {HTMLMediaElement} mediaElement the media element for playback michael@0: * @param {MediaStream} mediaStream the media stream used in michael@0: * the mediaElement for playback michael@0: */ michael@0: function MediaStreamPlayback(mediaElement, mediaStream) { michael@0: this.mediaElement = mediaElement; michael@0: this.mediaStream = mediaStream; michael@0: } michael@0: michael@0: MediaStreamPlayback.prototype = { michael@0: michael@0: /** michael@0: * Starts media with a media stream, runs it until a canplaythrough and michael@0: * timeupdate event fires, and stops the media. michael@0: * michael@0: * @param {Boolean} isResume specifies if this media element is being resumed michael@0: * from a previous run michael@0: * @param {Function} onSuccess the success callback if the media playback michael@0: * start and stop cycle completes successfully michael@0: * @param {Function} onError the error callback if the media playback michael@0: * start and stop cycle fails michael@0: */ michael@0: playMedia : function MSP_playMedia(isResume, onSuccess, onError) { michael@0: var self = this; michael@0: michael@0: this.startMedia(isResume, function() { michael@0: self.stopMediaElement(); michael@0: onSuccess(); michael@0: }, onError); michael@0: }, michael@0: michael@0: /** michael@0: * Starts the media with the associated stream. michael@0: * michael@0: * @param {Boolean} isResume specifies if the media element playback michael@0: * is being resumed from a previous run michael@0: * @param {Function} onSuccess the success function call back michael@0: * if media starts correctly michael@0: * @param {Function} onError the error function call back michael@0: * if media fails to start michael@0: */ michael@0: startMedia : function MSP_startMedia(isResume, onSuccess, onError) { michael@0: var self = this; michael@0: var canPlayThroughFired = false; michael@0: michael@0: // If we're initially running this media, check that the time is zero michael@0: if (!isResume) { michael@0: is(this.mediaStream.currentTime, 0, michael@0: "Before starting the media element, currentTime = 0"); michael@0: } michael@0: michael@0: /** michael@0: * Callback fired when the canplaythrough event is fired. We only michael@0: * run the logic of this function once, as this event can fire michael@0: * multiple times while a HTMLMediaStream is playing content from michael@0: * a real-time MediaStream. michael@0: */ michael@0: var canPlayThroughCallback = function() { michael@0: // Disable the canplaythrough event listener to prevent multiple calls michael@0: canPlayThroughFired = true; michael@0: self.mediaElement.removeEventListener('canplaythrough', michael@0: canPlayThroughCallback, false); michael@0: michael@0: is(self.mediaElement.paused, false, michael@0: "Media element should be playing"); michael@0: is(self.mediaElement.duration, Number.POSITIVE_INFINITY, michael@0: "Duration should be infinity"); michael@0: michael@0: // When the media element is playing with a real-time stream, we michael@0: // constantly switch between having data to play vs. queuing up data, michael@0: // so we can only check that the ready state is one of those two values michael@0: ok(self.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA || michael@0: self.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA, michael@0: "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA"); michael@0: michael@0: is(self.mediaElement.seekable.length, 0, michael@0: "Seekable length shall be zero"); michael@0: is(self.mediaElement.buffered.length, 0, michael@0: "Buffered length shall be zero"); michael@0: michael@0: is(self.mediaElement.seeking, false, michael@0: "MediaElement is not seekable with MediaStream"); michael@0: ok(isNaN(self.mediaElement.startOffsetTime), michael@0: "Start offset time shall not be a number"); michael@0: is(self.mediaElement.loop, false, "Loop shall be false"); michael@0: is(self.mediaElement.preload, "", "Preload should not exist"); michael@0: is(self.mediaElement.src, "", "No src should be defined"); michael@0: is(self.mediaElement.currentSrc, "", michael@0: "Current src should still be an empty string"); michael@0: michael@0: var timeUpdateFired = false; michael@0: michael@0: var timeUpdateCallback = function() { michael@0: if (self.mediaStream.currentTime > 0 && michael@0: self.mediaElement.currentTime > 0) { michael@0: timeUpdateFired = true; michael@0: self.mediaElement.removeEventListener('timeupdate', michael@0: timeUpdateCallback, false); michael@0: onSuccess(); michael@0: } michael@0: }; michael@0: michael@0: // When timeupdate fires, we validate time has passed and move michael@0: // onto the success condition michael@0: self.mediaElement.addEventListener('timeupdate', timeUpdateCallback, michael@0: false); michael@0: michael@0: // If timeupdate doesn't fire in enough time, we fail the test michael@0: setTimeout(function() { michael@0: if (!timeUpdateFired) { michael@0: self.mediaElement.removeEventListener('timeupdate', michael@0: timeUpdateCallback, false); michael@0: onError("timeUpdate event never fired"); michael@0: } michael@0: }, TIMEUPDATE_TIMEOUT_LENGTH); michael@0: }; michael@0: michael@0: // Adds a listener intended to be fired when playback is available michael@0: // without further buffering. michael@0: this.mediaElement.addEventListener('canplaythrough', canPlayThroughCallback, michael@0: false); michael@0: michael@0: // Hooks up the media stream to the media element and starts playing it michael@0: this.mediaElement.mozSrcObject = this.mediaStream; michael@0: this.mediaElement.play(); michael@0: michael@0: // If canplaythrough doesn't fire in enough time, we fail the test michael@0: setTimeout(function() { michael@0: if (!canPlayThroughFired) { michael@0: self.mediaElement.removeEventListener('canplaythrough', michael@0: canPlayThroughCallback, false); michael@0: onError("canplaythrough event never fired"); michael@0: } michael@0: }, CANPLAYTHROUGH_TIMEOUT_LENGTH); michael@0: }, michael@0: michael@0: /** michael@0: * Stops the media with the associated stream. michael@0: * michael@0: * Precondition: The media stream and element should both be actively michael@0: * being played. michael@0: */ michael@0: stopMediaElement : function MSP_stopMediaElement() { michael@0: this.mediaElement.pause(); michael@0: this.mediaElement.mozSrcObject = null; michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * This class is basically the same as MediaStreamPlayback except michael@0: * ensures that the instance provided startMedia is a MediaStream. michael@0: * michael@0: * @param {HTMLMediaElement} mediaElement the media element for playback michael@0: * @param {LocalMediaStream} mediaStream the media stream used in michael@0: * the mediaElement for playback michael@0: */ michael@0: function LocalMediaStreamPlayback(mediaElement, mediaStream) { michael@0: ok(mediaStream instanceof LocalMediaStream, michael@0: "Stream should be a LocalMediaStream"); michael@0: MediaStreamPlayback.call(this, mediaElement, mediaStream); michael@0: } michael@0: michael@0: LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, { michael@0: michael@0: /** michael@0: * Starts media with a media stream, runs it until a canplaythrough and michael@0: * timeupdate event fires, and calls stop() on the stream. michael@0: * michael@0: * @param {Boolean} isResume specifies if this media element is being resumed michael@0: * from a previous run michael@0: * @param {Function} onSuccess the success callback if the media element michael@0: * successfully fires ended on a stop() call michael@0: * on the stream michael@0: * @param {Function} onError the error callback if the media element fails michael@0: * to fire an ended callback on a stop() call michael@0: * on the stream michael@0: */ michael@0: playMediaWithStreamStop : { michael@0: value: function (isResume, onSuccess, onError) { michael@0: var self = this; michael@0: michael@0: this.startMedia(isResume, function() { michael@0: self.stopStreamInMediaPlayback(function() { michael@0: self.stopMediaElement(); michael@0: onSuccess(); michael@0: }, onError); michael@0: }, onError); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Stops the local media stream while it's currently in playback in michael@0: * a media element. michael@0: * michael@0: * Precondition: The media stream and element should both be actively michael@0: * being played. michael@0: * michael@0: * @param {Function} onSuccess the success callback if the media element michael@0: * fires an ended event from stop() being called michael@0: * @param {Function} onError the error callback if the media element michael@0: * fails to fire an ended event from stop() being michael@0: * called michael@0: */ michael@0: stopStreamInMediaPlayback : { michael@0: value: function (onSuccess, onError) { michael@0: var endedFired = false; michael@0: var self = this; michael@0: michael@0: /** michael@0: * Callback fired when the ended event fires when stop() is called on the michael@0: * stream. michael@0: */ michael@0: var endedCallback = function() { michael@0: endedFired = true; michael@0: self.mediaElement.removeEventListener('ended', endedCallback, false); michael@0: ok(true, "ended event successfully fired"); michael@0: onSuccess(); michael@0: }; michael@0: michael@0: this.mediaElement.addEventListener('ended', endedCallback, false); michael@0: this.mediaStream.stop(); michael@0: michael@0: // If ended doesn't fire in enough time, then we fail the test michael@0: setTimeout(function() { michael@0: if (!endedFired) { michael@0: onError("ended event never fired"); michael@0: } michael@0: }, ENDED_TIMEOUT_LENGTH); michael@0: } michael@0: } michael@0: });