summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.html
blob: 2db71c049d8b4bfc4ad86964ac02bf661b1d49e5 (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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
<!DOCTYPE html>
<html>
<title>Test MediaSourceHandle transfer characteristics</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="mediasource-message-util.js"></script>
<body>
<script>

function assert_mseiw_supported() {
  // Fail fast if MSE-in-Workers is not supported.
  assert_true(
      MediaSource.hasOwnProperty('canConstructInDedicatedWorker'),
      'MediaSource hasOwnProperty \'canConstructInDedicatedWorker\'');
  assert_true(
      MediaSource.canConstructInDedicatedWorker,
      'MediaSource.canConstructInDedicatedWorker');
  assert_true(
      window.hasOwnProperty('MediaSourceHandle'),
      'window must have MediaSourceHandle visibility');
}

function get_handle_from_new_worker(
    t, script = 'mediasource-worker-handle-transfer-to-main.js') {
  return new Promise((r) => {
    let worker = new Worker(script);
    worker.addEventListener('message', t.step_func(e => {
      let subject = e.data.subject;
      assert_true(subject != undefined, 'message must have a subject field');
      switch (subject) {
        case messageSubject.ERROR:
          assert_unreached('Worker error: ' + e.data.info);
          break;
        case messageSubject.HANDLE:
          const handle = e.data.info;
          assert_not_equals(
              handle, null, 'must have a non-null MediaSourceHandle');
          r({worker, handle});
          break;
        default:
          assert_unreached('Unexpected message subject: ' + subject);
      }
    }));
  });
}

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle);
  }, 'serializing handle without transfer');
}, 'MediaSourceHandle serialization without transfer must fail, tested in window context');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle, handle]);
  }, 'transferring same handle more than once in same postMessage');
}, 'Same MediaSourceHandle transferred multiple times in single postMessage must fail, tested in window context');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  // Transferring handle to worker without including it in the message is still
  // a valid transfer, though the recipient will not be able to obtain the
  // handle itself. Regardless, the handle in this sender's context will be
  // detached.
  worker.postMessage(null, [handle]);

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(null, [handle]);
  }, 'transferring handle that was already detached should fail');

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was already detached should fail, even if this time it\'s included in the message');
}, 'Attempt to transfer detached MediaSourceHandle must fail, tested in window context');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let video = document.createElement('video');
  document.body.appendChild(video);
  video.srcObject = handle;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that is currently srcObject fails');
  assert_equals(video.srcObject, handle);

  // Clear |handle| from being the srcObject value.
  video.srcObject = null;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was briefly srcObject before srcObject was reset to null should also fail');
  assert_equals(video.srcObject, null);
}, 'MediaSourceHandle cannot be transferred, immediately after set as srcObject, even if srcObject immediately reset to null');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let video = document.createElement('video');
  document.body.appendChild(video);
  video.srcObject = handle;
  assert_not_equals(video.networkState, HTMLMediaElement.NETWORK_LOADING);
  // Initial step of resource selection algorithm sets networkState to
  // NETWORK_NO_SOURCE. networkState only becomes NETWORK_LOADING after stable
  // state awaited and resource selection algorithm continues with, in this
  // case, an assigned media provider object (which is the MediaSource
  // underlying the handle).
  assert_equals(video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE);

  // Wait until 'loadstart' media element event is dispatched.
  await new Promise((r) => {
    video.addEventListener(
        'loadstart', t.step_func(e => {
          r();
        }),
        {once: true});
  });
  assert_equals(video.networkState, HTMLMediaElement.NETWORK_LOADING);

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that is currently srcObject, after loadstart, fails');
  assert_equals(video.srcObject, handle);

  // Clear |handle| from being the srcObject value.
  video.srcObject = null;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was srcObject until \'loadstart\' when srcObject was reset to null should also fail');
  assert_equals(video.srcObject, null);
}, 'MediaSourceHandle cannot be transferred, if it was srcObject when asynchronous load starts (loadstart), even if srcObject is then immediately reset to null');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let video = document.createElement('video');
  document.body.appendChild(video);

  // Transfer the handle away so that our instance of it is detached.
  worker.postMessage(null, [handle]);

  // Now assign handle to srcObject to attempt load. 'loadstart' event should
  // occur, but then media element error should occur due to failure to attach
  // to the underlying MediaSource of a detached MediaSourceHandle.

  video.srcObject = handle;
  assert_equals(
      video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE,
      'before async load start, networkState should be NETWORK_NO_SOURCE');

  // Before 'loadstart' dispatch, we don't expect the media element error.
  video.onerror = t.unreached_func(
      'Error is unexpected before \'loadstart\' event dispatch');

  // Wait until 'loadstart' media element event is dispatched.
  await new Promise((r) => {
    video.addEventListener(
        'loadstart', t.step_func(e => {
          r();
        }),
        {once: true});
  });

  // Now wait until 'error' media element event is dispatched.
  video.onerror = null;
  await new Promise((r) => {
    video.addEventListener(
        'error', t.step_func(e => {
          r();
        }),
        {once: true});
  });

  // Confirm expected error and states resulting from the "dedicated media
  // source failure steps":
  // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps
  let e = video.error;
  assert_true(e instanceof MediaError);
  assert_equals(e.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
  assert_equals(
      video.readyState, HTMLMediaElement.HAVE_NOTHING,
      'load failure should occur long before parsing any appended metadata.');
  assert_equals(video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE);

  // Even if the handle is detached and attempt to load it failed, the handle is
  // still detached, and as well, has also been used as srcObject now. Re-verify
  // that such a handle instance must fail transfer attempt.
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring detached handle that is currently srcObject, after loadstart and load failure, fails');
  assert_equals(video.srcObject, handle);

  // Clear |handle| from being the srcObject value.
  video.srcObject = null;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring detached handle that was srcObject until \'loadstart\' and load failure when srcObject was reset to null should also fail');
  assert_equals(video.srcObject, null);
}, 'A detached (already transferred away) MediaSourceHandle cannot successfully load when assigned to srcObject');

promise_test(async t => {
  assert_mseiw_supported();
  // Get a handle from a worker that is prepared to buffer real media once its
  // MediaSource instance attaches and 'sourceopen' is dispatched. Unlike
  // earlier cases in this file, we need positive indication from precisely one
  // of multiple media elements that the attachment and playback succeeded.
  let {worker, handle} =
      await get_handle_from_new_worker(t, 'mediasource-worker-play.js');
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let videos = [];
  const NUM_ELEMENTS = 5;
  for (let i = 0; i < NUM_ELEMENTS; ++i) {
    let v = document.createElement('video');
    videos.push(v);
    document.body.appendChild(v);
  }

  await new Promise((r) => {
    let errors = 0;
    let endeds = 0;

    // Setup handlers to expect precisely 1 ended and N-1 errors.
    videos.forEach((v) => {
      v.addEventListener(
          'error', t.step_func(e => {
            // Confirm expected error and states resulting from the "dedicated
            // media source failure steps":
            // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps
            let err = v.error;
            assert_true(err instanceof MediaError);
            assert_equals(err.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
            assert_equals(
                v.readyState, HTMLMediaElement.HAVE_NOTHING,
                'load failure should occur long before parsing any appended metadata.');
            assert_equals(v.networkState, HTMLMediaElement.NETWORK_NO_SOURCE);

            errors++;
            if (errors + endeds == videos.length && endeds == 1)
              r();
          }),
          {once: true});
      v.addEventListener(
          'ended', t.step_func(e => {
            endeds++;
            if (errors + endeds == videos.length && endeds == 1)
              r();
          }),
          {once: true});
      v.srcObject = handle;
      assert_equals(
          v.networkState, HTMLMediaElement.NETWORK_NO_SOURCE,
          'before async load start, networkState should be NETWORK_NO_SOURCE');
    });

    let playPromises = [];
    videos.forEach((v) => {
      playPromises.push(v.play());
    });

    // Ignore playPromise success/rejection, if any.
    playPromises.forEach((p) => {
      if (p !== undefined) {
        p.then(_ => {}).catch(_ => {});
      }
    });
  });

  // Once the handle has been assigned as srcObject, it must fail transfer
  // steps.
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that is currently srcObject on multiple elements, fails');
  videos.forEach((v) => {
    assert_equals(v.srcObject, handle);
    v.srcObject = null;
  });

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was srcObject on multiple elements, then was unset on them, should also fail');
  videos.forEach((v) => {
    assert_equals(v.srcObject, null);
  });
}, 'Precisely one load of the same MediaSourceHandle assigned synchronously to multiple media element srcObjects succeeds');

fetch_tests_from_worker(new Worker('mediasource-worker-handle-transfer.js'));

</script>
</body>
</html>