summaryrefslogtreecommitdiffstats
path: root/dom/media/tests/mochitest/mediaStreamPlayback.js
blob: a72d65f3f50e44a7b02f34fa3dbfde7d3bac91f9 (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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/* 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 : function(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 : function () {
    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 : function(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 : function(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 : function() {
    var lastStreamTime = this.mediaStream.currentTime;
    var lastElementTime = this.mediaElement.currentTime;

    var mediaTimeProgressed = listenUntil(this.mediaElement, 'timeupdate',
        () => this.mediaStream.currentTime > lastStreamTime &&
              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.loop, false, "Loop shall be false");
        is(this.mediaElement.preload, "", "Preload should not exist");
        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 : function() {
    this.mediaElement.pause();
    this.mediaElement.srcObject = null;
  }
}


/**
 * This class is basically the same as MediaStreamPlayback except
 * ensures that the instance provided startMedia is a MediaStream.
 *
 * @param {HTMLMediaElement} mediaElement the media element for playback
 * @param {LocalMediaStream} mediaStream the media stream used in
 *                                       the mediaElement for playback
 */
function LocalMediaStreamPlayback(mediaElement, mediaStream) {
  ok(mediaStream instanceof LocalMediaStream,
     "Stream should be a LocalMediaStream");
  MediaStreamPlayback.call(this, mediaElement, mediaStream);
}

LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, {

  /**
   * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()!
   *
   * Starts media with a media stream, runs it until a canplaythrough and
   * timeupdate event fires, and calls stop() on the stream.
   *
   * @param {Boolean} isResume specifies if this media element is being resumed
   *                           from a previous run
   */
  playMediaWithDeprecatedStreamStop : {
    value: function(isResume) {
      this.startMedia(isResume);
      return this.verifyPlaying()
        .then(() => this.deprecatedStopStreamInMediaPlayback())
        .then(() => this.detachFromMediaElement());
    }
  },

  /**
   * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()!
   *
   * Stops the local media stream while it's currently in playback in
   * a media element.
   *
   * Precondition: The media stream and element should both be actively
   *               being played.
   *
   */
  deprecatedStopStreamInMediaPlayback : {
    value: function () {
      return new Promise((resolve, reject) => {
        /**
         * Callback fired when the ended event fires when stop() is called on the
         * stream.
         */
        var endedCallback = () => {
          this.mediaElement.removeEventListener('ended', endedCallback, false);
          ok(true, "ended event successfully fired");
          resolve();
        };

        this.mediaElement.addEventListener('ended', endedCallback, false);
        this.mediaStream.stop();

        // If ended doesn't fire in enough time, then we fail the test
        setTimeout(() => {
          reject(new Error("ended event never fired"));
        }, ENDED_TIMEOUT_LENGTH);
      });
    }
  }
});

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

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));
}

var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));

// 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.

var noGum = () => pushPrefs(["media.navigator.permission.disabled", false],
                            ["media.navigator.permission.fake", true])
  .then(() => navigator.mediaDevices.enumerateDevices())
  .then(([device]) => device &&
      is(device.label, "", "Test must leave no active gUM streams behind."));

var runTest = testFunction => scriptsReady
  .then(() => runTestWhenReady(testFunction))
  .then(() => noGum())
  .then(() => finish());