tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }