summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js
blob: ba644390d6bcb3ec0e93eeb2a986797ca41fda66 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/* 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(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() {
    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(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(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() {
    var lastElementTime = this.mediaElement.currentTime;

    var mediaTimeProgressed = listenUntil(
      this.mediaElement,
      "timeupdate",
      () => 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.defaultPlaybackRate,
        1,
        "DefaultPlaybackRate should be 1"
      );
      is(this.mediaElement.playbackRate, 1, "PlaybackRate should be 1");
      is(this.mediaElement.preload, "none", 'Preload should be "none"');
      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() {
    this.mediaElement.pause();
    this.mediaElement.srcObject = null;
  },
};

// haxx to prevent SimpleTest from failing at window.onload
function addLoadEvent() {}

/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */
/* import-globals-from head.js */
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));
}

async function runTest(testFunction) {
  await scriptsReady;
  await runTestWhenReady(async (...args) => {
    await testFunction(...args);
    await noGum();
  });
}

// 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.
async function noGum() {
  await pushPrefs(
    ["media.navigator.permission.disabled", false],
    ["media.navigator.permission.fake", true]
  );
  if (!navigator.mediaDevices) {
    // No mediaDevices, then gUM cannot have been called either.
    return;
  }
  const [device] = await navigator.mediaDevices.enumerateDevices();
  if (device) {
    is(device.label, "", "Test must leave no active gUM streams behind.");
  }
}