tor-browser

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

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 }