Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 9 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /media-source/dedicated-worker/mediasource-worker-handle-transfer.html - WPT Dashboard Interop Dashboard
<!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":
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":
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>