mediaStreamPlayback.js (7556B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const ENDED_TIMEOUT_LENGTH = 30000; 6 7 /* The time we wait depends primarily on the canplaythrough event firing 8 * Note: this needs to be at least 30s because the 9 * B2G emulator in VMs is really slow. */ 10 const VERIFYPLAYING_TIMEOUT_LENGTH = 60000; 11 12 /** 13 * This class manages playback of a HTMLMediaElement with a MediaStream. 14 * When constructed by a caller, an object instance is created with 15 * a media element and a media stream object. 16 * 17 * @param {HTMLMediaElement} mediaElement the media element for playback 18 * @param {MediaStream} mediaStream the media stream used in 19 * the mediaElement for playback 20 */ 21 function MediaStreamPlayback(mediaElement, mediaStream) { 22 this.mediaElement = mediaElement; 23 this.mediaStream = mediaStream; 24 } 25 26 MediaStreamPlayback.prototype = { 27 /** 28 * Starts media element with a media stream, runs it until a canplaythrough 29 * and timeupdate event fires, and calls stop() on all its tracks. 30 * 31 * @param {boolean} isResume specifies if this media element is being resumed 32 * from a previous run 33 */ 34 playMedia(isResume) { 35 this.startMedia(isResume); 36 return this.verifyPlaying() 37 .then(() => this.stopTracksForStreamInMediaPlayback()) 38 .then(() => this.detachFromMediaElement()); 39 }, 40 41 /** 42 * Stops the local media stream's tracks while it's currently in playback in 43 * a media element. 44 * 45 * Precondition: The media stream and element should both be actively 46 * being played. All the stream's tracks must be local. 47 */ 48 stopTracksForStreamInMediaPlayback() { 49 var elem = this.mediaElement; 50 return Promise.all([ 51 haveEvent( 52 elem, 53 "ended", 54 wait(ENDED_TIMEOUT_LENGTH, new Error("Timeout")) 55 ), 56 ...this.mediaStream.getTracks().map(t => { 57 t.stop(); 58 return haveNoEvent(t, "ended"); 59 }), 60 ]); 61 }, 62 63 /** 64 * Starts media with a media stream, runs it until a canplaythrough and 65 * timeupdate event fires, and detaches from the element without stopping media. 66 * 67 * @param {boolean} isResume specifies if this media element is being resumed 68 * from a previous run 69 */ 70 playMediaWithoutStoppingTracks(isResume) { 71 this.startMedia(isResume); 72 return this.verifyPlaying().then(() => this.detachFromMediaElement()); 73 }, 74 75 /** 76 * Starts the media with the associated stream. 77 * 78 * @param {boolean} isResume specifies if the media element playback 79 * is being resumed from a previous run 80 */ 81 startMedia(isResume) { 82 // If we're playing media element for the first time, check that time is zero. 83 if (!isResume) { 84 is( 85 this.mediaElement.currentTime, 86 0, 87 "Before starting the media element, currentTime = 0" 88 ); 89 } 90 this.canPlayThroughFired = listenUntil( 91 this.mediaElement, 92 "canplaythrough", 93 () => true 94 ); 95 96 // Hooks up the media stream to the media element and starts playing it 97 this.mediaElement.srcObject = this.mediaStream; 98 this.mediaElement.play(); 99 }, 100 101 /** 102 * Verifies that media is playing. 103 */ 104 verifyPlaying() { 105 var lastElementTime = this.mediaElement.currentTime; 106 107 var mediaTimeProgressed = listenUntil( 108 this.mediaElement, 109 "timeupdate", 110 () => this.mediaElement.currentTime > lastElementTime 111 ); 112 113 return timeout( 114 Promise.all([this.canPlayThroughFired, mediaTimeProgressed]), 115 VERIFYPLAYING_TIMEOUT_LENGTH, 116 "verifyPlaying timed out" 117 ).then(() => { 118 is(this.mediaElement.paused, false, "Media element should be playing"); 119 is( 120 this.mediaElement.duration, 121 Number.POSITIVE_INFINITY, 122 "Duration should be infinity" 123 ); 124 125 // When the media element is playing with a real-time stream, we 126 // constantly switch between having data to play vs. queuing up data, 127 // so we can only check that the ready state is one of those two values 128 ok( 129 this.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA || 130 this.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA, 131 "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA" 132 ); 133 134 is(this.mediaElement.seekable.length, 0, "Seekable length shall be zero"); 135 is(this.mediaElement.buffered.length, 0, "Buffered length shall be zero"); 136 137 is( 138 this.mediaElement.seeking, 139 false, 140 "MediaElement is not seekable with MediaStream" 141 ); 142 ok( 143 isNaN(this.mediaElement.startOffsetTime), 144 "Start offset time shall not be a number" 145 ); 146 is( 147 this.mediaElement.defaultPlaybackRate, 148 1, 149 "DefaultPlaybackRate should be 1" 150 ); 151 is(this.mediaElement.playbackRate, 1, "PlaybackRate should be 1"); 152 is(this.mediaElement.preload, "none", 'Preload should be "none"'); 153 is(this.mediaElement.src, "", "No src should be defined"); 154 is( 155 this.mediaElement.currentSrc, 156 "", 157 "Current src should still be an empty string" 158 ); 159 }); 160 }, 161 162 /** 163 * Detaches from the element without stopping the media. 164 * 165 * Precondition: The media stream and element should both be actively 166 * being played. 167 */ 168 detachFromMediaElement() { 169 this.mediaElement.pause(); 170 this.mediaElement.srcObject = null; 171 }, 172 }; 173 174 // haxx to prevent SimpleTest from failing at window.onload 175 function addLoadEvent() {} 176 177 /* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */ 178 /* import-globals-from head.js */ 179 const scriptsReady = Promise.all( 180 ["/tests/SimpleTest/SimpleTest.js", "head.js"].map(script => { 181 const el = document.createElement("script"); 182 el.src = script; 183 document.head.appendChild(el); 184 return new Promise(r => (el.onload = r)); 185 }) 186 ); 187 188 function createHTML(options) { 189 return scriptsReady.then(() => realCreateHTML(options)); 190 } 191 192 async function runTest(testFunction) { 193 await Promise.all([ 194 scriptsReady, 195 SpecialPowers.pushPrefEnv({ 196 set: [["media.navigator.permission.fake", true]], 197 }), 198 ]); 199 await runTestWhenReady(async (...args) => { 200 await testFunction(...args); 201 await noGum(); 202 }); 203 } 204 205 // noGum - Helper to detect whether active guM tracks still exist. 206 // 207 // Note it relies on the permissions system to detect active tracks, so it won't 208 // catch getUserMedia use while media.navigator.permission.disabled is true 209 // (which is common in automation), UNLESS we set 210 // media.navigator.permission.fake to true also, like runTest() does above. 211 async function noGum() { 212 if (!navigator.mediaDevices) { 213 // No mediaDevices, then gUM cannot have been called either. 214 return; 215 } 216 const mediaManagerService = Cc[ 217 "@mozilla.org/mediaManagerService;1" 218 ].getService(Ci.nsIMediaManagerService); 219 220 const hasCamera = {}; 221 const hasMicrophone = {}; 222 mediaManagerService.mediaCaptureWindowState( 223 window, 224 hasCamera, 225 hasMicrophone, 226 {}, 227 {}, 228 {}, 229 {}, 230 false 231 ); 232 is( 233 hasCamera.value, 234 mediaManagerService.STATE_NOCAPTURE, 235 "Test must leave no active camera gUM tracks behind." 236 ); 237 is( 238 hasMicrophone.value, 239 mediaManagerService.STATE_NOCAPTURE, 240 "Test must leave no active microphone gUM tracks behind." 241 ); 242 }