diff options
Diffstat (limited to 'dom/media/tests/mochitest/mediaStreamPlayback.js')
-rw-r--r-- | dom/media/tests/mochitest/mediaStreamPlayback.js | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/dom/media/tests/mochitest/mediaStreamPlayback.js b/dom/media/tests/mochitest/mediaStreamPlayback.js new file mode 100644 index 000000000..a72d65f3f --- /dev/null +++ b/dom/media/tests/mochitest/mediaStreamPlayback.js @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const ENDED_TIMEOUT_LENGTH = 30000; + +/* The time we wait depends primarily on the canplaythrough event firing + * Note: this needs to be at least 30s because the + * B2G emulator in VMs is really slow. */ +const VERIFYPLAYING_TIMEOUT_LENGTH = 60000; + +/** + * This class manages playback of a HTMLMediaElement with a MediaStream. + * When constructed by a caller, an object instance is created with + * a media element and a media stream object. + * + * @param {HTMLMediaElement} mediaElement the media element for playback + * @param {MediaStream} mediaStream the media stream used in + * the mediaElement for playback + */ +function MediaStreamPlayback(mediaElement, mediaStream) { + this.mediaElement = mediaElement; + this.mediaStream = mediaStream; +} + +MediaStreamPlayback.prototype = { + + /** + * Starts media element with a media stream, runs it until a canplaythrough + * and timeupdate event fires, and calls stop() on all its tracks. + * + * @param {Boolean} isResume specifies if this media element is being resumed + * from a previous run + */ + playMedia : function(isResume) { + this.startMedia(isResume); + return this.verifyPlaying() + .then(() => this.stopTracksForStreamInMediaPlayback()) + .then(() => this.detachFromMediaElement()); + }, + + /** + * Stops the local media stream's tracks while it's currently in playback in + * a media element. + * + * Precondition: The media stream and element should both be actively + * being played. All the stream's tracks must be local. + */ + stopTracksForStreamInMediaPlayback : function () { + var elem = this.mediaElement; + return Promise.all([ + haveEvent(elem, "ended", wait(ENDED_TIMEOUT_LENGTH, new Error("Timeout"))), + ...this.mediaStream.getTracks().map(t => (t.stop(), haveNoEvent(t, "ended"))) + ]); + }, + + /** + * Starts media with a media stream, runs it until a canplaythrough and + * timeupdate event fires, and detaches from the element without stopping media. + * + * @param {Boolean} isResume specifies if this media element is being resumed + * from a previous run + */ + playMediaWithoutStoppingTracks : function(isResume) { + this.startMedia(isResume); + return this.verifyPlaying() + .then(() => this.detachFromMediaElement()); + }, + + /** + * Starts the media with the associated stream. + * + * @param {Boolean} isResume specifies if the media element playback + * is being resumed from a previous run + */ + startMedia : function(isResume) { + + // If we're playing media element for the first time, check that time is zero. + if (!isResume) { + is(this.mediaElement.currentTime, 0, + "Before starting the media element, currentTime = 0"); + } + this.canPlayThroughFired = listenUntil(this.mediaElement, 'canplaythrough', + () => true); + + // Hooks up the media stream to the media element and starts playing it + this.mediaElement.srcObject = this.mediaStream; + this.mediaElement.play(); + }, + + /** + * Verifies that media is playing. + */ + verifyPlaying : function() { + var lastStreamTime = this.mediaStream.currentTime; + var lastElementTime = this.mediaElement.currentTime; + + var mediaTimeProgressed = listenUntil(this.mediaElement, 'timeupdate', + () => this.mediaStream.currentTime > lastStreamTime && + this.mediaElement.currentTime > lastElementTime); + + return timeout(Promise.all([this.canPlayThroughFired, mediaTimeProgressed]), + VERIFYPLAYING_TIMEOUT_LENGTH, "verifyPlaying timed out") + .then(() => { + is(this.mediaElement.paused, false, + "Media element should be playing"); + is(this.mediaElement.duration, Number.POSITIVE_INFINITY, + "Duration should be infinity"); + + // When the media element is playing with a real-time stream, we + // constantly switch between having data to play vs. queuing up data, + // so we can only check that the ready state is one of those two values + ok(this.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA || + this.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA, + "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA"); + + is(this.mediaElement.seekable.length, 0, + "Seekable length shall be zero"); + is(this.mediaElement.buffered.length, 0, + "Buffered length shall be zero"); + + is(this.mediaElement.seeking, false, + "MediaElement is not seekable with MediaStream"); + ok(isNaN(this.mediaElement.startOffsetTime), + "Start offset time shall not be a number"); + is(this.mediaElement.loop, false, "Loop shall be false"); + is(this.mediaElement.preload, "", "Preload should not exist"); + is(this.mediaElement.src, "", "No src should be defined"); + is(this.mediaElement.currentSrc, "", + "Current src should still be an empty string"); + }); + }, + + /** + * Detaches from the element without stopping the media. + * + * Precondition: The media stream and element should both be actively + * being played. + */ + detachFromMediaElement : function() { + this.mediaElement.pause(); + this.mediaElement.srcObject = null; + } +} + + +/** + * This class is basically the same as MediaStreamPlayback except + * ensures that the instance provided startMedia is a MediaStream. + * + * @param {HTMLMediaElement} mediaElement the media element for playback + * @param {LocalMediaStream} mediaStream the media stream used in + * the mediaElement for playback + */ +function LocalMediaStreamPlayback(mediaElement, mediaStream) { + ok(mediaStream instanceof LocalMediaStream, + "Stream should be a LocalMediaStream"); + MediaStreamPlayback.call(this, mediaElement, mediaStream); +} + +LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, { + + /** + * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()! + * + * Starts media with a media stream, runs it until a canplaythrough and + * timeupdate event fires, and calls stop() on the stream. + * + * @param {Boolean} isResume specifies if this media element is being resumed + * from a previous run + */ + playMediaWithDeprecatedStreamStop : { + value: function(isResume) { + this.startMedia(isResume); + return this.verifyPlaying() + .then(() => this.deprecatedStopStreamInMediaPlayback()) + .then(() => this.detachFromMediaElement()); + } + }, + + /** + * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()! + * + * Stops the local media stream while it's currently in playback in + * a media element. + * + * Precondition: The media stream and element should both be actively + * being played. + * + */ + deprecatedStopStreamInMediaPlayback : { + value: function () { + return new Promise((resolve, reject) => { + /** + * Callback fired when the ended event fires when stop() is called on the + * stream. + */ + var endedCallback = () => { + this.mediaElement.removeEventListener('ended', endedCallback, false); + ok(true, "ended event successfully fired"); + resolve(); + }; + + this.mediaElement.addEventListener('ended', endedCallback, false); + this.mediaStream.stop(); + + // If ended doesn't fire in enough time, then we fail the test + setTimeout(() => { + reject(new Error("ended event never fired")); + }, ENDED_TIMEOUT_LENGTH); + }); + } + } +}); + +// haxx to prevent SimpleTest from failing at window.onload +function addLoadEvent() {} + +var scriptsReady = Promise.all([ + "/tests/SimpleTest/SimpleTest.js", + "head.js" +].map(script => { + var el = document.createElement("script"); + el.src = script; + document.head.appendChild(el); + return new Promise(r => el.onload = r); +})); + +function createHTML(options) { + return scriptsReady.then(() => realCreateHTML(options)); +} + +var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r)); + +// noGum - Helper to detect whether active guM tracks still exist. +// +// It relies on the fact that, by spec, device labels from enumerateDevices are +// only visible during active gum calls. They're also visible when persistent +// permissions are granted, so turn off media.navigator.permission.disabled +// (which is normally on otherwise in our tests). Lastly, we must turn on +// media.navigator.permission.fake otherwise fake devices don't count as active. + +var noGum = () => pushPrefs(["media.navigator.permission.disabled", false], + ["media.navigator.permission.fake", true]) + .then(() => navigator.mediaDevices.enumerateDevices()) + .then(([device]) => device && + is(device.label, "", "Test must leave no active gUM streams behind.")); + +var runTest = testFunction => scriptsReady + .then(() => runTestWhenReady(testFunction)) + .then(() => noGum()) + .then(() => finish()); |