browser_media_control_position_state.js (8950B)
1 const PAGE_URL = 2 "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html"; 3 const IFRAME_URL = 4 "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html"; 5 6 const testVideoId = "video"; 7 const videoDuration = 5.589333; 8 9 add_task(async function setupTestingPref() { 10 await SpecialPowers.pushPrefEnv({ 11 set: [["media.mediacontrol.testingevents.enabled", true]], 12 }); 13 }); 14 15 /** 16 * This test is used to check if we can receive correct position state change, 17 * when we set the position state to the media session. 18 */ 19 add_task(async function testSetPositionState() { 20 info(`open media page`); 21 const tab = await createLoadedTabWrapper(PAGE_URL); 22 logPositionStateChangeEvents(tab); 23 24 info(`apply initial position state`); 25 await applyPositionState(tab, { duration: 10 }); 26 27 info(`start media`); 28 const initialPositionState = isNextPositionState(tab, { duration: 10 }); 29 await playMedia(tab, testVideoId); 30 await initialPositionState; 31 32 info(`set duration only`); 33 await setPositionState(tab, { 34 duration: 60, 35 }); 36 37 info(`set duration and playback rate`); 38 await setPositionState(tab, { 39 duration: 50, 40 playbackRate: 2.0, 41 }); 42 43 info(`set duration, playback rate and position`); 44 await setPositionState(tab, { 45 duration: 40, 46 playbackRate: 3.0, 47 position: 10, 48 }); 49 50 info(`remove tab`); 51 await tab.close(); 52 }); 53 54 add_task(async function testSetPositionStateFromInactiveMediaSession() { 55 info(`open media page`); 56 const tab = await createLoadedTabWrapper(PAGE_URL); 57 logPositionStateChangeEvents(tab); 58 59 info(`apply initial position state`); 60 await applyPositionState(tab, { duration: 10 }); 61 62 info(`start media`); 63 const initialPositionState = isNextPositionState(tab, { duration: 10 }); 64 await playMedia(tab, testVideoId); 65 await initialPositionState; 66 67 info( 68 `add an event listener to measure how many times the position state changes` 69 ); 70 let positionChangedNum = 0; 71 const controller = tab.linkedBrowser.browsingContext.mediaController; 72 controller.onpositionstatechange = () => positionChangedNum++; 73 74 info(`set position state on the main page which has an active media session`); 75 await setPositionState(tab, { 76 duration: 60, 77 }); 78 79 info(`set position state on the iframe which has an inactive media session`); 80 await setPositionStateOnInactiveMediaSession(tab); 81 82 info(`set position state on the main page again`); 83 await setPositionState(tab, { 84 duration: 60, 85 }); 86 is( 87 positionChangedNum, 88 2, 89 `We should only receive two times of position change, because ` + 90 `the second one which performs on inactive media session is effectless` 91 ); 92 93 info(`remove tab`); 94 await tab.close(); 95 }); 96 97 /** 98 * 99 * @param {boolean} withMetadata 100 * Specifies if the tab should set metadata for the playing video 101 */ 102 async function testGuessedPositionState(withMetadata) { 103 info(`open media page`); 104 const tab = await createLoadedTabWrapper(PAGE_URL); 105 logPositionStateChangeEvents(tab); 106 107 if (withMetadata) { 108 info(`set media metadata`); 109 await setMediaMetadata(tab, { title: "A Video" }); 110 } 111 112 info(`start media`); 113 await emitsPositionState(() => playMedia(tab, testVideoId), tab, { 114 duration: videoDuration, 115 position: 0, 116 playbackRate: 1.0, 117 }); 118 119 info(`set playback rate to 2x`); 120 await emitsPositionState(() => setPlaybackRate(tab, testVideoId, 2.0), tab, { 121 duration: videoDuration, 122 position: null, // ignored, 123 playbackRate: 2.0, 124 }); 125 126 info(`seek to 1s`); 127 await emitsPositionState(() => setCurrentTime(tab, testVideoId, 1.0), tab, { 128 duration: videoDuration, 129 position: 1.0, 130 playbackRate: 2.0, 131 }); 132 133 info(`pause media`); 134 await emitsPositionState(() => pauseMedia(tab, testVideoId), tab, { 135 duration: videoDuration, 136 position: null, 137 playbackRate: 0.0, 138 }); 139 140 info(`seek to 2s`); 141 await emitsPositionState(() => setCurrentTime(tab, testVideoId, 2.0), tab, { 142 duration: videoDuration, 143 position: 2.0, 144 playbackRate: 0.0, 145 }); 146 147 info(`start media`); 148 await emitsPositionState(() => playMedia(tab, testVideoId), tab, { 149 duration: videoDuration, 150 position: 2.0, 151 playbackRate: 2.0, 152 }); 153 154 info(`remove tab`); 155 await tab.close(); 156 } 157 158 add_task(async function testGuessedPositionStateWithMetadata() { 159 await testGuessedPositionState(true); 160 }); 161 162 add_task(async function testGuessedPositionStateWithoutMetadata() { 163 await testGuessedPositionState(false); 164 }); 165 166 /** 167 * @typedef {{ 168 * duration: number, 169 * playbackRate?: number | null, 170 * position?: number | null, 171 * }} ExpectedPositionState 172 */ 173 174 /** 175 * Checks if the next received position state matches the expected one. 176 * 177 * @param {tab} tab 178 * The tab that contains the media 179 * @param {ExpectedPositionState} positionState 180 * The expected position state. `duration` is mandatory. `playbackRate` 181 * and `position` are optional. If they're `null`, they're ignored, 182 * otherwise if they're not present or undefined, they're expected to 183 * be the default value. 184 * @returns {Promise} 185 * Resolves when the event has been received 186 */ 187 async function isNextPositionState(tab, positionState) { 188 const got = await nextPositionState(tab); 189 isPositionState(got, positionState); 190 } 191 192 /** 193 * Waits for the next position state and returns it 194 * 195 * @param {tab} tab The tab to receive position state from 196 * @returns {Promise<MediaPositionState>} The emitted position state 197 */ 198 function nextPositionState(tab) { 199 const controller = tab.linkedBrowser.browsingContext.mediaController; 200 return new Promise(r => { 201 controller.addEventListener("positionstatechange", r, { once: true }); 202 }); 203 } 204 205 /** 206 * @param {MediaPositionState} got 207 * The received position state 208 * @param {ExpectedPositionState} expected 209 * The expected position state. `duration` is mandatory. `playbackRate` 210 * and `position` are optional. If they're `null`, they're ignored, 211 * otherwise if they're not present or undefined, they're expected to 212 * be the default value. 213 */ 214 function isPositionState(got, expected) { 215 const { duration, playbackRate, position } = expected; 216 // duration is mandatory. 217 isFuzzyEq(got.duration, duration, "duration"); 218 219 // Playback rate is optional, if it's not present, default should be 1.0 220 if (typeof playbackRate === "number") { 221 isFuzzyEq(got.playbackRate, playbackRate, "playbackRate"); 222 } else if (playbackRate !== null) { 223 is(got.playbackRate, 1.0, `expected default playbackRate is 1.0`); 224 } 225 226 // Position is optional, if it's not present, default should be 0.0 227 if (typeof position === "number") { 228 isFuzzyEq(got.position, position, "position"); 229 } else if (position !== null) { 230 is(got.position, 0.0, `expected default position is 0.0`); 231 } 232 } 233 234 /** 235 * Checks if two numbers are equal within one significant digit 236 * 237 * @param {number} got 238 * The value received while testing 239 * @param {number} expected 240 * The expected value 241 * @param {string} role 242 * The role of the check (used for formatting) 243 */ 244 function isFuzzyEq(got, expected, role) { 245 expected = expected.toFixed(1); 246 got = got.toFixed(1); 247 is(got, expected, `expected ${role} ${got} to equal ${expected}`); 248 } 249 250 /** 251 * Test if `cb` emits a position state event. 252 * 253 * @param {() => (void | Promise<void>)} cb 254 * A callback that is expected to generate a position state event 255 * @param {tab} tab 256 * The tab that contains the media 257 * @param {ExpectedPositionState} positionState 258 * The expected position state to be generated. 259 */ 260 async function emitsPositionState(cb, tab, positionState) { 261 const positionStateChanged = isNextPositionState(tab, positionState); 262 await cb(); 263 await positionStateChanged; 264 } 265 266 /** 267 * The following are helper functions. 268 */ 269 async function setPositionState(tab, positionState) { 270 await emitsPositionState( 271 () => applyPositionState(tab, positionState), 272 tab, 273 positionState 274 ); 275 } 276 277 async function applyPositionState(tab, positionState) { 278 await SpecialPowers.spawn( 279 tab.linkedBrowser, 280 [positionState], 281 positionState => { 282 content.navigator.mediaSession.setPositionState(positionState); 283 } 284 ); 285 } 286 287 async function setMediaMetadata(tab, metadata) { 288 await SpecialPowers.spawn(tab.linkedBrowser, [metadata], data => { 289 content.navigator.mediaSession.metadata = new content.MediaMetadata(data); 290 }); 291 } 292 293 async function setPositionStateOnInactiveMediaSession(tab) { 294 return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => { 295 info(`create iframe and wait until it finishes loading`); 296 const iframe = content.document.getElementById("iframe"); 297 iframe.src = url; 298 await new Promise(r => (iframe.onload = r)); 299 300 info(`trigger media in iframe entering into fullscreen`); 301 iframe.contentWindow.postMessage("setPositionState", "*"); 302 }); 303 }