summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/media-source/dedicated-worker
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/media-source/dedicated-worker
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/media-source/dedicated-worker')
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-message-util.js16
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.html75
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.js79
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.html86
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.js290
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-get-objecturl.js13
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer-to-main.js10
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.html316
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.js19
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.html61
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.js70
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-must-fail-if-unsupported.js18
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.html52
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.js33
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.html85
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.js15
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.html49
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.js74
-rw-r--r--testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-util.js60
19 files changed, 1421 insertions, 0 deletions
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-message-util.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-message-util.js
new file mode 100644
index 0000000000..c62eb8e3f7
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-message-util.js
@@ -0,0 +1,16 @@
+// This script provides an object with common message subjects to assist main
+// and worker thread communication.
+
+const messageSubject = {
+ ERROR: "error", // info field may contain more detail
+ OBJECT_URL: "object url", // info field contains object URL
+ HANDLE: "handle", // info field contains the MediaSourceHandle
+ STARTED_BUFFERING: "started buffering",
+ FINISHED_BUFFERING: "finished buffering",
+ VERIFY_DURATION: "verify duration", // info field contains expected duration
+ AWAIT_DURATION: "await duration", // wait for element duration to match the expected duration in the info field
+ VERIFY_HAVE_NOTHING: "verify have nothing readyState",
+ VERIFY_AT_LEAST_HAVE_METADATA: "verify readyState is at least HAVE_METADATA",
+ ACK_VERIFIED: "verified", // info field contains the message values that requested the verification
+ WORKER_DONE: "worker done", // this lets worker signal main to successfully end the test
+};
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.html
new file mode 100644
index 0000000000..0f74d95372
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<title>MediaSource-in-Worker buffering test case with media element detachment at various places</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="mediasource-message-util.js"></script>
+<body>
+<script>
+
+const AFTER_SETTING_SRCOBJECT = "after setting srcObject";
+const AFTER_STARTED_BUFFERING = "after receiving Started Buffering message from worker";
+const AFTER_FINISHED_BUFFERING = "after receiving Finished Buffering message from worker";
+
+[ AFTER_SETTING_SRCOBJECT, AFTER_STARTED_BUFFERING, AFTER_FINISHED_BUFFERING ].forEach(when => {
+ for (let timeouts = 0; timeouts < 5; ++timeouts) {
+ async_test(test => { startWorkerAndDetachElement(test, when, timeouts); },
+ "Test element detachment from worker MediaSource after at least " + timeouts +
+ " main thread setTimeouts, starting counting " + when);
+ }
+});
+
+function detachElementAfterMultipleSetTimeouts(test, element, timeouts_remaining) {
+ if (timeouts_remaining <= 0) {
+ // While not the best way to detach, this triggers interoperable logic that
+ // includes detachment.
+ element.srcObject = null;
+ test.step_timeout(() => { test.done(); }, 10);
+ } else {
+ test.step_timeout(() => {
+ detachElementAfterMultipleSetTimeouts(test, element, --timeouts_remaining);
+ }, 0);
+ }
+}
+
+function startWorkerAndDetachElement(test, when_to_start_timeouts, timeouts_to_await) {
+ // Fail fast if MSE-in-Workers is not supported.
+ assert_true(MediaSource.hasOwnProperty("canConstructInDedicatedWorker"), "MediaSource hasOwnProperty 'canConstructInDedicatedWorker'");
+ assert_true(MediaSource.canConstructInDedicatedWorker, "MediaSource.canConstructInDedicatedWorker");
+
+ const worker = new Worker("mediasource-worker-detach-element.js");
+ worker.onerror = test.unreached_func("worker error");
+
+ const video = document.createElement("video");
+ document.body.appendChild(video);
+
+ worker.onmessage = test.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;
+ video.srcObject = handle;
+ if (when_to_start_timeouts == AFTER_SETTING_SRCOBJECT) {
+ detachElementAfterMultipleSetTimeouts(test, video, timeouts_to_await);
+ }
+ break;
+ case messageSubject.STARTED_BUFFERING:
+ if (when_to_start_timeouts == AFTER_STARTED_BUFFERING)
+ detachElementAfterMultipleSetTimeouts(test, video, timeouts_to_await);
+ break;
+ case messageSubject.FINISHED_BUFFERING:
+ if (when_to_start_timeouts == AFTER_FINISHED_BUFFERING)
+ detachElementAfterMultipleSetTimeouts(test, video, timeouts_to_await);
+ break;
+ default:
+ assert_unreached("Unrecognized message subject: " + subject);
+ }
+ });
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.js
new file mode 100644
index 0000000000..54b1d815f2
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-detach-element.js
@@ -0,0 +1,79 @@
+// This is similar to mediasource-worker-play.js, except that the buffering is
+// longer and done in tiny chunks to enable a better chance of the main thread
+// detaching the element while interesting buffering work is still occurring. To
+// assist the main thread understanding when the buffering has started already
+// or has completed already, we also perform extra messaging.
+importScripts("mediasource-worker-util.js");
+
+onmessage = function(evt) {
+ postMessage({ subject: messageSubject.ERROR, info: "No message expected by Worker" });
+};
+
+let util = new MediaSourceWorkerUtil();
+
+let sentStartedBufferingMessage = false;
+
+util.mediaSource.addEventListener("sourceopen", () => {
+ let sourceBuffer;
+ try {
+ sourceBuffer = util.mediaSource.addSourceBuffer(util.mediaMetadata.type);
+ } catch(e) {
+ // Detachment may have already begun, so allow exception here.
+ // TODO(https://crbug.com/878133): Consider a distinct readyState for the case
+ // where exception occurs due to "Worker MediaSource attachment is closing".
+ // That would assist API users and narrow the exception handling here.
+ return;
+ }
+
+ sourceBuffer.onerror = (err) => {
+ postMessage({ subject: messageSubject.ERROR, info: err });
+ };
+ util.mediaLoadPromise.then(mediaData => bufferInto(sourceBuffer, mediaData, 100, 0),
+ err => { postMessage({ subject: messageSubject.ERROR, info: err }) } );
+}, { once : true });
+
+let handle = util.mediaSource.handle;
+
+postMessage({ subject: messageSubject.HANDLE, info: handle }, { transfer: [handle] } );
+
+// Append increasingly large pieces at a time, starting/continuing at |position|.
+// This allows buffering the test media without timeout, but also with enough
+// operations to gain coverage on detachment concurrency with append.
+function bufferInto(sourceBuffer, mediaData, appendSize, position) {
+ if (position >= mediaData.byteLength) {
+ postMessage({ subject: messageSubject.FINISHED_BUFFERING });
+ try {
+ util.mediaSource.endOfStream();
+ } catch(e) {
+ // Detachment may have already begun, so allow exception here.
+ // TODO(https://crbug.com/878133): Consider a distinct readyState for the case
+ // where exception occurs due to "Worker MediaSource attachment is closing".
+ // That would assist API users and narrow the exception handling here.
+ // FALL-THROUGH - return.
+ }
+ return;
+ }
+
+ var nextPosition = position + appendSize;
+ const pieceToAppend = mediaData.slice(position, nextPosition);
+ position = nextPosition;
+ appendSize += 100;
+
+ sourceBuffer.addEventListener("updateend", () => {
+ if (!sentStartedBufferingMessage) {
+ postMessage({ subject: messageSubject.STARTED_BUFFERING});
+ sentStartedBufferingMessage = true;
+ }
+ bufferInto(sourceBuffer, mediaData, appendSize, position);
+ }, { once : true });
+
+ try {
+ sourceBuffer.appendBuffer(pieceToAppend);
+ } catch(e) {
+ // Detachment may have already begun, so allow exception here.
+ // TODO(https://crbug.com/878133): Consider a distinct readyState for the case
+ // where exception occurs due to "Worker MediaSource attachment is closing".
+ // That would assist API users and narrow the exception handling here.
+ // FALL-THROUGH - return.
+ }
+}
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.html
new file mode 100644
index 0000000000..c195775beb
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<title>Test MediaSource-in-Worker duration updates before and after HAVE_METADATA</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="mediasource-message-util.js"></script>
+<body>
+<script>
+
+function awaitDuration(t, video, worker, requestingMessage, expectedDuration) {
+ let durationAwaiter = t.step_func(() => {
+ if ((!Number.isNaN(expectedDuration) && video.duration === expectedDuration) ||
+ (Number.isNaN(expectedDuration) && Number.isNaN(video.duration))) {
+ worker.postMessage({ subject: messageSubject.ACK_VERIFIED, info: requestingMessage });
+ return;
+ }
+
+ // Otherwise, wait for one or more 'durationchange' events to see if video
+ // eventually has the expectedDuration.
+ video.addEventListener('durationchange', durationAwaiter, { once: true });
+ });
+
+ durationAwaiter();
+}
+
+async_test(t => {
+ // Fail fast if MSE-in-Workers is not supported.
+ assert_true(MediaSource.hasOwnProperty("canConstructInDedicatedWorker"), "MediaSource hasOwnProperty 'canConstructInDedicatedWorker'");
+ assert_true(MediaSource.canConstructInDedicatedWorker, "MediaSource.canConstructInDedicatedWorker");
+
+ const video = document.createElement("video");
+ document.body.appendChild(video);
+ video.onerror = t.unreached_func("video element error");
+ video.onended = t.unreached_func("video element ended");
+ assert_equals(video.duration, NaN, "initial video duration before attachment should be NaN");
+ assert_equals(video.readyState, HTMLMediaElement.HAVE_NOTHING, "initial video readyState before attachment should be HAVE_NOTHING");
+
+ let worker = new Worker("mediasource-worker-duration.js");
+ worker.onerror = t.step_func(e => {
+ assert_unreached("worker error: [" + e.filename + ":" + e.lineno + ":" + e.colno + ":" + e.error + ":" + e.message + "]");
+ });
+ worker.onmessage = 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_equals(video.duration, NaN, "initial video duration before attachment should still be NaN");
+ assert_equals(video.readyState, HTMLMediaElement.HAVE_NOTHING,
+ "initial video readyState before attachment should still be HAVE_NOTHING");
+ video.srcObject = handle;
+ break;
+ case messageSubject.VERIFY_DURATION:
+ assert_equals(video.duration, e.data.info, "duration should match expectation");
+ worker.postMessage({ subject: messageSubject.ACK_VERIFIED, info: e.data });
+ break;
+ case messageSubject.AWAIT_DURATION:
+ awaitDuration(t, video, worker, e.data, e.data.info);
+ break;
+ case messageSubject.VERIFY_HAVE_NOTHING:
+ assert_equals(video.readyState, HTMLMediaElement.HAVE_NOTHING, "readyState should match expectation");
+ worker.postMessage({ subject: messageSubject.ACK_VERIFIED, info: e.data });
+ break;
+ case messageSubject.VERIFY_AT_LEAST_HAVE_METADATA:
+ assert_greater_than_equal(video.readyState, HTMLMediaElement.HAVE_METADATA, "readyState should match expectation");
+ worker.postMessage({ subject: messageSubject.ACK_VERIFIED, info: e.data });
+ break;
+ case messageSubject.WORKER_DONE:
+ // This test is a worker-driven set of verifications, and it will send
+ // this message when it is complete. See comment in the worker script
+ // that describes the phases of this test case.
+ assert_not_equals(video.srcObject, null, "test should at least have set srcObject.");
+ t.done();
+ break;
+ default:
+ assert_unreached("Unexpected message subject: " + subject);
+ }
+ });
+}, "Test worker MediaSource duration updates before and after HAVE_METADATA");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.js
new file mode 100644
index 0000000000..2a2c7bac0b
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-duration.js
@@ -0,0 +1,290 @@
+importScripts("mediasource-worker-util.js");
+
+// Note, we do not use testharness.js utilities within the worker context
+// because it also communicates using postMessage to the main HTML document's
+// harness, and would confuse the test case message parsing there.
+
+let util = new MediaSourceWorkerUtil();
+let sourceBuffer;
+
+// Phases of this test case, in sequence:
+const testPhase = {
+ // Main thread verifies initial unattached HTMLMediaElement duration is NaN
+ // and readyState is HAVE_NOTHING, then starts this worker.
+ // This worker creates a MediaSource, verifies its initial duration
+ // is NaN, creates an object URL for the MediaSource and sends the URL to the
+ // main thread.
+ kInitial: "Initial",
+
+ // Main thread receives MediaSourceHandle, re-verifies that the media element
+ // duration is still NaN and readyState is still HAVE_NOTHING, and then sets
+ // the handle as the srcObject of the media element, eventually causing worker
+ // mediaSource 'onsourceopen' event dispatch.
+ kAttaching: "Awaiting sourceopen event that signals attachment is setup",
+
+ kRequestNaNDurationCheck:
+ "Sending request to main thread to verify expected duration of the freshly setup attachment",
+ kConfirmNaNDurationResult:
+ "Checking that main thread correctly ACK'ed the freshly setup attachment's duration verification request",
+
+ kRequestHaveNothingReadyStateCheck:
+ "Sending request to main thread to verify expected readyState of HAVE_NOTHING of the freshly setup attachment",
+ kConfirmHaveNothingReadyStateResult:
+ "Checking that main thread correctly ACK'ed the freshly setup attachment's readyState HAVE_NOTHING verification request",
+
+ kRequestSetDurationCheck:
+ "Sending request to main thread to verify explicitly set duration before any media data has been appended",
+ kConfirmSetDurationResult:
+ "Checking that main thread correctly ACK'ed the duration verification request of explicitly set duration before any media data has been appended",
+
+ kRequestHaveNothingReadyStateRecheck:
+ "Sending request to main thread to recheck that the readyState is still HAVE_NOTHING",
+ kConfirmHaveNothingReadyStateRecheckResult:
+ "Checking that main thread correctly ACK'ed the request to recheck readyState of HAVE_NOTHING",
+
+ kRequestAwaitNewDurationCheck:
+ "Buffering media and then sending request to main thread to await duration reaching the expected value due to buffering",
+ kConfirmAwaitNewDurationResult:
+ "Checking that main thread correctly ACK'ed the request to await duration reaching the expected value due to buffering",
+
+ kRequestAtLeastHaveMetadataReadyStateCheck:
+ "Sending request to main thread to verify expected readyState of at least HAVE_METADATA due to buffering",
+ kConfirmAtLeastHaveMetadataReadyStateResult:
+ "Checking that main thread correctly ACK'ed the request to verify expected readyState of at least HAVE_METADATA due to buffering",
+
+};
+
+let phase = testPhase.kInitial;
+
+// Setup handler for receipt of attachment completion.
+util.mediaSource.addEventListener("sourceopen", () => {
+ assert(phase === testPhase.kAttaching, "Unexpected sourceopen received by Worker mediaSource.");
+ phase = testPhase.kRequestNaNDurationCheck;
+ processPhase();
+}, { once : true });
+
+// Setup handler for receipt of acknowledgement of successful verifications from
+// main thread. |ackVerificationData| contains the round-tripped verification
+// request that the main thread just sent, and is used in processPhase to ensure
+// the ACK for this phase matched the request for verification.
+let ackVerificationData;
+onmessage = e => {
+ if (e.data === undefined || e.data.subject !== messageSubject.ACK_VERIFIED || e.data.info === undefined) {
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "Invalid message received by Worker"
+ });
+ return;
+ }
+
+ ackVerificationData = e.data.info;
+ processPhase(/* isResponseToAck */ true);
+};
+
+processPhase();
+
+
+// Returns true if checks succeed, false otherwise.
+function checkAckVerificationData(expectedRequest) {
+
+ // Compares only subject and info fields. Uses logic similar to testharness.js's
+ // same_value(x,y) to correctly handle NaN, but doesn't distinguish +0 from -0.
+ function messageValuesEqual(m1, m2) {
+ if (m1.subject !== m1.subject) {
+ // NaN case
+ if (m2.subject === m2.subject)
+ return false;
+ } else if (m1.subject !== m2.subject) {
+ return false;
+ }
+
+ if (m1.info !== m1.info) {
+ // NaN case
+ return (m2.info !== m2.info);
+ }
+
+ return m1.info === m2.info;
+ }
+
+ if (messageValuesEqual(expectedRequest, ackVerificationData)) {
+ ackVerificationData = undefined;
+ return true;
+ }
+
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "ACK_VERIFIED message from main thread was for a mismatching request for this phase. phase=[" + phase +
+ "], expected request that would produce ACK in this phase=[" + JSON.stringify(expectedRequest) +
+ "], actual request reported with the ACK=[" + JSON.stringify(ackVerificationData) + "]"
+ });
+
+ ackVerificationData = undefined;
+ return false;
+}
+
+function bufferMediaAndSendDurationVerificationRequest() {
+ sourceBuffer = util.mediaSource.addSourceBuffer(util.mediaMetadata.type);
+ sourceBuffer.onerror = (err) => {
+ postMessage({ subject: messageSubject.ERROR, info: err });
+ };
+ sourceBuffer.onupdateend = () => {
+ // Sanity check the duration.
+ // Unnecessary for this buffering, except helps with test coverage.
+ var duration = util.mediaSource.duration;
+ if (isNaN(duration) || duration <= 0.0) {
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "mediaSource.duration " + duration + " is not within expected range (0,1)"
+ });
+ return;
+ }
+
+ // Await the main thread media element duration matching the worker
+ // mediaSource duration.
+ postMessage(getAwaitCurrentDurationRequest());
+ };
+
+ util.mediaLoadPromise.then(mediaData => { sourceBuffer.appendBuffer(mediaData); },
+ err => { postMessage({ subject: messageSubject.ERROR, info: err }) });
+}
+
+
+function getAwaitCurrentDurationRequest() {
+ // Sanity check that we have a numeric duration value now.
+ const dur = util.mediaSource.duration;
+ assert(!Number.isNaN(dur), "Unexpected NaN duration in worker");
+ return { subject: messageSubject.AWAIT_DURATION, info: dur };
+}
+
+function assert(conditionBool, description) {
+ if (conditionBool !== true) {
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "Current test phase [" + phase + "] failed worker assertion. " + description
+ });
+ }
+}
+
+function processPhase(isResponseToAck = false) {
+ assert(!isResponseToAck || (phase !== testPhase.kInitial && phase !== testPhase.kAttaching),
+ "Phase does not expect verification ack receipt from main thread");
+
+ // Some static request messages useful in transmission and ACK verification.
+ const nanDurationCheckRequest = { subject: messageSubject.VERIFY_DURATION, info: NaN };
+ const haveNothingReadyStateCheckRequest = { subject: messageSubject.VERIFY_HAVE_NOTHING };
+ const setDurationCheckRequest = { subject: messageSubject.AWAIT_DURATION, info: 0.1 };
+ const atLeastHaveMetadataReadyStateCheckRequest = { subject: messageSubject.VERIFY_AT_LEAST_HAVE_METADATA };
+
+ switch (phase) {
+
+ case testPhase.kInitial:
+ assert(Number.isNaN(util.mediaSource.duration), "Initial unattached MediaSource duration must be NaN, but instead is " + util.mediaSource.duration);
+ phase = testPhase.kAttaching;
+ let handle = util.mediaSource.handle;
+ postMessage({ subject: messageSubject.HANDLE, info: handle }, { transfer: [handle] } );
+ break;
+
+ case testPhase.kAttaching:
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "kAttaching phase is handled by main thread and by worker onsourceopen, not this switch case."
+ });
+ break;
+
+ case testPhase.kRequestNaNDurationCheck:
+ assert(!isResponseToAck);
+ postMessage(nanDurationCheckRequest);
+ phase = testPhase.kConfirmNaNDurationResult;
+ break;
+
+ case testPhase.kConfirmNaNDurationResult:
+ assert(isResponseToAck);
+ if (checkAckVerificationData(nanDurationCheckRequest)) {
+ phase = testPhase.kRequestHaveNothingReadyStateCheck;
+ processPhase();
+ }
+ break;
+
+ case testPhase.kRequestHaveNothingReadyStateCheck:
+ assert(!isResponseToAck);
+ postMessage(haveNothingReadyStateCheckRequest);
+ phase = testPhase.kConfirmHaveNothingReadyStateResult;
+ break;
+
+ case testPhase.kConfirmHaveNothingReadyStateResult:
+ assert(isResponseToAck);
+ if (checkAckVerificationData(haveNothingReadyStateCheckRequest)) {
+ phase = testPhase.kRequestSetDurationCheck;
+ processPhase();
+ }
+ break;
+
+ case testPhase.kRequestSetDurationCheck:
+ assert(!isResponseToAck);
+ const newDuration = setDurationCheckRequest.info;
+ assert(!Number.isNaN(newDuration) && newDuration > 0);
+
+ // Set the duration, then request verification.
+ util.mediaSource.duration = newDuration;
+ postMessage(setDurationCheckRequest);
+ phase = testPhase.kConfirmSetDurationResult;
+ break;
+
+ case testPhase.kConfirmSetDurationResult:
+ assert(isResponseToAck);
+ if (checkAckVerificationData(setDurationCheckRequest)) {
+ phase = testPhase.kRequestHaveNothingReadyStateRecheck;
+ processPhase();
+ }
+ break;
+
+ case testPhase.kRequestHaveNothingReadyStateRecheck:
+ assert(!isResponseToAck);
+ postMessage(haveNothingReadyStateCheckRequest);
+ phase = testPhase.kConfirmHaveNothingReadyStateRecheckResult;
+ break;
+
+ case testPhase.kConfirmHaveNothingReadyStateRecheckResult:
+ assert(isResponseToAck);
+ if (checkAckVerificationData(haveNothingReadyStateCheckRequest)) {
+ phase = testPhase.kRequestAwaitNewDurationCheck;
+ processPhase();
+ }
+ break;
+
+ case testPhase.kRequestAwaitNewDurationCheck:
+ assert(!isResponseToAck);
+ bufferMediaAndSendDurationVerificationRequest();
+ phase = testPhase.kConfirmAwaitNewDurationResult;
+ break;
+
+ case testPhase.kConfirmAwaitNewDurationResult:
+ assert(isResponseToAck);
+ if (checkAckVerificationData(getAwaitCurrentDurationRequest())) {
+ phase = testPhase.kRequestAtLeastHaveMetadataReadyStateCheck;
+ processPhase();
+ }
+ break;
+
+ case testPhase.kRequestAtLeastHaveMetadataReadyStateCheck:
+ assert(!isResponseToAck);
+ postMessage(atLeastHaveMetadataReadyStateCheckRequest);
+ phase = testPhase.kConfirmAtLeastHaveMetadataReadyStateResult;
+ break;
+
+ case testPhase.kConfirmAtLeastHaveMetadataReadyStateResult:
+ assert(isResponseToAck);
+ if (checkAckVerificationData(atLeastHaveMetadataReadyStateCheckRequest)) {
+ postMessage({ subject: messageSubject.WORKER_DONE });
+ }
+ phase = "No further phase processing should occur once WORKER_DONE message has been sent";
+ break;
+
+ default:
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "Unexpected test phase in worker:" + phase,
+ });
+ }
+
+}
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-get-objecturl.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-get-objecturl.js
new file mode 100644
index 0000000000..e9a5af6c81
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-get-objecturl.js
@@ -0,0 +1,13 @@
+importScripts("mediasource-worker-util.js");
+
+// Note, we do not use testharness.js utilities within the worker context
+// because it also communicates using postMessage to the main HTML document's
+// harness, and would confuse the test case message parsing there.
+
+onmessage = function(evt) {
+ postMessage({ subject: messageSubject.ERROR, info: "No message expected by Worker"});
+};
+
+let util = new MediaSourceWorkerUtil();
+
+postMessage({ subject: messageSubject.OBJECT_URL, info: URL.createObjectURL(util.mediaSource) });
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer-to-main.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer-to-main.js
new file mode 100644
index 0000000000..15cccb1a0e
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer-to-main.js
@@ -0,0 +1,10 @@
+importScripts('mediasource-message-util.js');
+
+// Note, we do not use testharness.js utilities within the worker context
+// because it also communicates using postMessage to the main HTML document's
+// harness, and would confuse the test case message parsing there.
+
+// Just obtain a MediaSourceHandle and transfer it to creator of our context.
+let handle = new MediaSource().handle;
+postMessage(
+ {subject: messageSubject.HANDLE, info: handle}, {transfer: [handle]});
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.html
new file mode 100644
index 0000000000..2db71c049d
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.html
@@ -0,0 +1,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>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.js
new file mode 100644
index 0000000000..803da44e23
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle-transfer.js
@@ -0,0 +1,19 @@
+importScripts('/resources/testharness.js');
+
+test(t => {
+ let handle = new MediaSource().handle;
+ assert_true(handle instanceof MediaSourceHandle);
+ assert_throws_dom('DataCloneError', function() {
+ postMessage(handle);
+ }, 'serializing handle without transfer');
+}, 'MediaSourceHandle serialization without transfer must fail, tested in worker');
+
+test(t => {
+ let handle = new MediaSource().handle;
+ assert_true(handle instanceof MediaSourceHandle);
+ assert_throws_dom('DataCloneError', function() {
+ 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 worker');
+
+done();
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.html
new file mode 100644
index 0000000000..6129e05ffb
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<title>Test MediaSource object and handle creation, with MediaSource in dedicated worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="mediasource-message-util.js"></script>
+<script>
+
+async_test(t => {
+ // 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");
+
+ let worker = new Worker("mediasource-worker-play.js");
+ worker.onmessage = 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");
+ assert_true(handle instanceof MediaSourceHandle, "must be a MediaSourceHandle");
+ t.done();
+ break;
+ default:
+ assert_unreached("Unexpected message subject: " + subject);
+
+ }
+ });
+}, "Test main context receipt of postMessage'd MediaSourceHandle from DedicatedWorker MediaSource");
+
+test(t => {
+ assert_true(window.hasOwnProperty("MediaSourceHandle"), "window must have MediaSourceHandle");
+}, "Test main-thread has MediaSourceHandle defined");
+
+test(t => {
+ // Note, MSE spec may eventually describe how a main-thread MediaSource can
+ // attach to an HTMLMediaElement using a MediaSourceHandle. For now, we
+ // ensure that the implementation of this is not available per current spec.
+ assert_false(
+ "handle" in MediaSource.prototype,
+ "window MediaSource must not have handle in prototype");
+}, "Test main-thread MediaSource does not have handle getter");
+
+if (MediaSource.hasOwnProperty("canConstructInDedicatedWorker") && MediaSource.canConstructInDedicatedWorker === true) {
+ // If implementation claims support for MSE-in-Workers, then fetch and run
+ // some tests directly in another dedicated worker and get their results
+ // merged into those from this page.
+ fetch_tests_from_worker(new Worker("mediasource-worker-handle.js"));
+} else {
+ // Otherwise, fetch and run a test that verifies lack of support of
+ // MediaSource construction in another dedicated worker.
+ fetch_tests_from_worker(new Worker("mediasource-worker-must-fail-if-unsupported.js"));
+}
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.js
new file mode 100644
index 0000000000..d35cb877c2
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-handle.js
@@ -0,0 +1,70 @@
+importScripts("/resources/testharness.js");
+
+test(t => {
+ // The Window test html conditionally fetches and runs these tests only if the
+ // implementation exposes a true-valued static canConstructInDedicatedWorker
+ // attribute on MediaSource in the Window context. So, the implementation must
+ // agree on support here in the dedicated worker context.
+
+ // Ensure we're executing in a dedicated worker context.
+ assert_true(self instanceof DedicatedWorkerGlobalScope, "self instanceof DedicatedWorkerGlobalScope");
+ assert_true(MediaSource.hasOwnProperty("canConstructInDedicatedWorker", "DedicatedWorker MediaSource hasOwnProperty 'canConstructInDedicatedWorker'"));
+ assert_true(MediaSource.canConstructInDedicatedWorker, "DedicatedWorker MediaSource.canConstructInDedicatedWorker");
+}, "MediaSource in DedicatedWorker context must have true-valued canConstructInDedicatedWorker if Window context had it");
+
+test(t => {
+ assert_true(
+ 'handle' in MediaSource.prototype,
+ 'dedicated worker MediaSource must have handle in prototype');
+ assert_true(self.hasOwnProperty("MediaSourceHandle"), "dedicated worker must have MediaSourceHandle visibility");
+}, 'MediaSource prototype in DedicatedWorker context must have \'handle\', and worker must have MediaSourceHandle');
+
+test(t => {
+ const ms = new MediaSource();
+ assert_equals(ms.readyState, "closed");
+}, "MediaSource construction succeeds with initial closed readyState in DedicatedWorker");
+
+test(t => {
+ const ms = new MediaSource();
+ const handle = ms.handle;
+ assert_not_equals(handle, null, 'must have a non-null \'handle\' attribute');
+ assert_true(handle instanceof MediaSourceHandle, "must be a MediaSourceHandle");
+}, 'mediaSource.handle in DedicatedWorker returns a MediaSourceHandle');
+
+test(t => {
+ const msA = new MediaSource();
+ const msB = new MediaSource();
+ const handleA1 = msA.handle;
+ const handleA2 = msA.handle;
+ const handleA3 = msA['handle'];
+ const handleB1 = msB.handle;
+ const handleB2 = msB.handle;
+ assert_true(
+ handleA1 === handleA2 && handleB1 === handleB2 && handleA1 != handleB1,
+ 'SameObject is observed for mediaSource.handle, and different MediaSource instances have different handles');
+ assert_true(
+ handleA1 === handleA3,
+ 'SameObject is observed even when accessing handle differently');
+ assert_true(
+ handleA1 instanceof MediaSourceHandle &&
+ handleB1 instanceof MediaSourceHandle,
+ 'handle property returns MediaSourceHandles');
+}, 'mediaSource.handle observes SameObject property correctly');
+
+test(t => {
+ const ms1 = new MediaSource();
+ const handle1 = ms1.handle;
+ const ms2 = new MediaSource();
+ const handle2 = ms2.handle;
+ assert_true(
+ handle1 !== handle2,
+ 'distinct MediaSource instances must have distinct handles');
+
+ // Verify attempt to change value of the handle property does not succeed.
+ ms1.handle = handle2;
+ assert_true(
+ ms1.handle === handle1 && ms2.handle === handle2,
+ 'MediaSource handle is readonly, so should not have changed');
+}, 'Attempt to set MediaSource handle property should fail to change it, since it is readonly');
+
+done();
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-must-fail-if-unsupported.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-must-fail-if-unsupported.js
new file mode 100644
index 0000000000..69c65f6aa2
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-must-fail-if-unsupported.js
@@ -0,0 +1,18 @@
+importScripts("/resources/testharness.js");
+
+test(t => {
+ // The Window test html conditionally fetches and runs these tests only if the
+ // implementation does not have a true-valued static
+ // canConstructInDedicatedWorker property on MediaSource in the Window
+ // context. So, the implementation must agree on lack of support here in the
+ // dedicated worker context.
+
+ // Ensure we're executing in a dedicated worker context.
+ assert_true(self instanceof DedicatedWorkerGlobalScope, "self instanceof DedicatedWorkerGlobalScope");
+ assert_true(self.MediaSource === undefined, "MediaSource is undefined in DedicatedWorker");
+ assert_throws_js(ReferenceError,
+ function() { var ms = new MediaSource(); },
+ "MediaSource construction in DedicatedWorker throws exception");
+}, "MediaSource construction in DedicatedWorker context must fail if Window context did not claim MSE supported in DedicatedWorker");
+
+done();
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.html
new file mode 100644
index 0000000000..ae60199672
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<title>Test MediaSource object and objectUrl creation and load via that url should fail, with MediaSource in dedicated worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="mediasource-message-util.js"></script>
+<script>
+
+async_test(t => {
+ // Fail fast if MSE-in-Workers is not supported.
+ assert_true(MediaSource.hasOwnProperty("canConstructInDedicatedWorker"), "MediaSource hasOwnProperty 'canConstructInDedicatedWorker'");
+ assert_true(MediaSource.canConstructInDedicatedWorker, "MediaSource.canConstructInDedicatedWorker");
+
+ let worker = new Worker("mediasource-worker-get-objecturl.js");
+ worker.onmessage = 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.OBJECT_URL:
+ const url = e.data.info;
+ const video = document.createElement("video");
+ document.body.appendChild(video);
+ video.onerror = t.step_func((target) => {
+ assert_true(video.error != null);
+ assert_equals(video.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
+ t.done();
+ });
+ video.onended = t.unreached_func("video should not have successfully loaded and played to end");
+ video.src = url;
+ break;
+ default:
+ assert_unreached("Unexpected message subject: " + subject);
+ }
+ });
+}, "Test main context load of a DedicatedWorker MediaSource object URL should fail");
+
+if (MediaSource.hasOwnProperty("canConstructInDedicatedWorker") && MediaSource.canConstructInDedicatedWorker === true) {
+ // If implementation claims support for MSE-in-Workers, then fetch and run
+ // some tests directly in another dedicated worker and get their results
+ // merged into those from this page.
+ fetch_tests_from_worker(new Worker("mediasource-worker-objecturl.js"));
+} else {
+ // Otherwise, fetch and run a test that verifies lack of support of
+ // MediaSource construction in another dedicated worker.
+ fetch_tests_from_worker(new Worker("mediasource-worker-must-fail-if-unsupported.js"));
+}
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.js
new file mode 100644
index 0000000000..2e70d99418
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-objecturl.js
@@ -0,0 +1,33 @@
+importScripts("/resources/testharness.js");
+
+test(t => {
+ // The Window test html conditionally fetches and runs these tests only if the
+ // implementation exposes a true-valued static canConstructInDedicatedWorker
+ // attribute on MediaSource in the Window context. So, the implementation must
+ // agree on support here in the dedicated worker context.
+
+ // Ensure we're executing in a dedicated worker context.
+ assert_true(self instanceof DedicatedWorkerGlobalScope, "self instanceof DedicatedWorkerGlobalScope");
+ assert_true(MediaSource.hasOwnProperty("canConstructInDedicatedWorker", "DedicatedWorker MediaSource hasOwnProperty 'canConstructInDedicatedWorker'"));
+ assert_true(MediaSource.canConstructInDedicatedWorker, "DedicatedWorker MediaSource.canConstructInDedicatedWorker");
+}, "MediaSource in DedicatedWorker context must have true-valued canConstructInDedicatedWorker if Window context had it");
+
+test(t => {
+ const ms = new MediaSource();
+ assert_equals(ms.readyState, "closed");
+}, "MediaSource construction succeeds with initial closed readyState in DedicatedWorker");
+
+test(t => {
+ const ms = new MediaSource();
+ const url = URL.createObjectURL(ms);
+}, "URL.createObjectURL(mediaSource) in DedicatedWorker does not throw exception");
+
+test(t => {
+ const ms = new MediaSource();
+ const url1 = URL.createObjectURL(ms);
+ const url2 = URL.createObjectURL(ms);
+ URL.revokeObjectURL(url1);
+ URL.revokeObjectURL(url2);
+}, "URL.revokeObjectURL(mediaSource) in DedicatedWorker with two url for same MediaSource");
+
+done();
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.html
new file mode 100644
index 0000000000..d6496afd6f
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<title>MediaSource-in-Worker looped playback test case with worker termination at various places</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="mediasource-message-util.js"></script>
+<body>
+<script>
+
+function terminateWorkerAfterMultipleSetTimeouts(test, worker, timeouts_remaining) {
+ if (timeouts_remaining <= 0) {
+ worker.terminate();
+ test.step_timeout(() => { test.done(); }, 0);
+ } else {
+ test.step_timeout(() => {
+ terminateWorkerAfterMultipleSetTimeouts(test, worker, --timeouts_remaining);
+ }, 0);
+ }
+}
+
+function startWorkerAndTerminateWorker(test, when_to_start_timeouts, timeouts_to_await) {
+ // Fail fast if MSE-in-Workers is not supported.
+ assert_true(MediaSource.hasOwnProperty("canConstructInDedicatedWorker"), "MediaSource hasOwnProperty 'canConstructInDedicatedWorker'");
+ assert_true(MediaSource.canConstructInDedicatedWorker, "MediaSource.canConstructInDedicatedWorker");
+
+ const worker = new Worker("mediasource-worker-play-terminate-worker.js");
+ worker.onerror = test.unreached_func("worker error");
+
+ const video = document.createElement("video");
+ document.body.appendChild(video);
+ video.onerror = test.unreached_func("video element error");
+
+ if (when_to_start_timeouts == "after first ended event") {
+ video.addEventListener("ended", test.step_func(() => {
+ terminateWorkerAfterMultipleSetTimeouts(test, worker, timeouts_to_await);
+ video.currentTime = 0;
+ video.loop = true;
+ }), { once : true });
+ } else {
+ video.loop = true;
+ }
+
+ if (when_to_start_timeouts == "before setting srcObject") {
+ terminateWorkerAfterMultipleSetTimeouts(test, worker, timeouts_to_await);
+ }
+
+ worker.onmessage = test.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;
+ video.srcObject = handle;
+ if (when_to_start_timeouts == "after setting srcObject") {
+ terminateWorkerAfterMultipleSetTimeouts(test, worker, timeouts_to_await);
+ }
+ video.play().catch(error => {
+ // Only rejections due to MEDIA_ERR_SRC_NOT_SUPPORTED are expected to possibly
+ // occur, except if we expect to reach at least 1 'ended' event.
+ assert_not_equals(when_to_start_timeouts, "after first ended event");
+ assert_true(video.error != null);
+ assert_equals(video.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
+ // Do not rethrow. Instead, wait for the step_timeouts to finish the test.
+ });
+ break;
+ default:
+ assert_unreached("Unexpected message subject: " + subject);
+ }
+ });
+}
+
+[ "before setting srcObject", "after setting srcObject", "after first ended event" ].forEach(when => {
+ for (let timeouts = 0; timeouts < 10; ++timeouts) {
+ async_test(test => { startWorkerAndTerminateWorker(test, when, timeouts); },
+ "Test worker MediaSource termination after at least " + timeouts +
+ " main thread setTimeouts, starting counting " + when);
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.js
new file mode 100644
index 0000000000..b453818191
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.js
@@ -0,0 +1,15 @@
+// This worker script is intended to be used by the
+// mediasource-worker-play-terminate-worker.html test case. The script import
+// may itself be terminated by the main thread terminating our context,
+// producing a NetworkError, so we catch and ignore a NetworkError here. Note
+// that any dependency on globals defined in the imported scripts may result in
+// test harness error flakiness if an undefined variable (due to termination
+// causing importScripts to fail) is accessed. Hence this script just imports
+// and handles import errors, since such nondeterministic worker termination is
+// central to the test case.
+try {
+ importScripts("mediasource-worker-play.js");
+} catch(e) {
+ if (e.name != "NetworkError")
+ throw e;
+}
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.html b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.html
new file mode 100644
index 0000000000..455a224069
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<title>Simple MediaSource-in-Worker playback test case</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="mediasource-message-util.js"></script>
+<body>
+<script>
+
+async_test(t => {
+ // Fail fast if MSE-in-Workers is not supported.
+ assert_true(
+ MediaSource.hasOwnProperty('canConstructInDedicatedWorker'),
+ 'MediaSource hasOwnProperty \'canConstructInDedicatedWorker\'');
+ assert_true(
+ MediaSource.canConstructInDedicatedWorker,
+ 'MediaSource.canConstructInDedicatedWorker');
+
+ const video = document.createElement('video');
+ document.body.appendChild(video);
+ video.onerror = t.unreached_func('video element error');
+ video.onended = t.step_func_done();
+
+ let worker = new Worker('mediasource-worker-play.js');
+ worker.onerror = t.unreached_func('worker error');
+ worker.onmessage = 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;
+ video.srcObject = handle;
+ video.play();
+ break;
+ default:
+ assert_unreached('Unexpected message subject: ' + subject);
+ }
+ });
+}, 'Test worker MediaSource construction, attachment, buffering and basic playback');
+
+// See mediasource-worker-handle-transfer.html for a case that tests race of
+// multiple simultaneous attachments of same handle to multiple elements.
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.js
new file mode 100644
index 0000000000..5c4760bf7b
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play.js
@@ -0,0 +1,74 @@
+importScripts("mediasource-worker-util.js");
+
+// Note, we do not use testharness.js utilities within the worker context
+// because it also communicates using postMessage to the main HTML document's
+// harness, and would confuse the test case message parsing there.
+
+onmessage = function(evt) {
+ postMessage({ subject: messageSubject.ERROR, info: "No message expected by Worker"});
+};
+
+let util = new MediaSourceWorkerUtil();
+let handle = util.mediaSource.handle;
+
+util.mediaSource.addEventListener('sourceopen', () => {
+ // Immediately re-verify the SameObject property of the handle we transferred.
+ if (handle !== util.mediaSource.handle) {
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: 'mediaSource.handle changed from the original value'
+ });
+ }
+
+ // Also verify that transferring the already-transferred handle instance is
+ // prevented correctly.
+ try {
+ postMessage(
+ {
+ subject: messageSubject.ERROR,
+ info:
+ 'This postMessage should fail: the handle has already been transferred',
+ extra_info: util.mediaSource.handle
+ },
+ {transfer: [util.mediaSource.handle]});
+ } catch (e) {
+ if (e.name != 'DataCloneError') {
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: 'Expected handle retransfer exception did not occur'
+ });
+ }
+ }
+
+ sourceBuffer = util.mediaSource.addSourceBuffer(util.mediaMetadata.type);
+ sourceBuffer.onerror = (err) => {
+ postMessage({ subject: messageSubject.ERROR, info: err });
+ };
+ sourceBuffer.onupdateend = () => {
+ // Reset the parser. Unnecessary for this buffering, except helps with test
+ // coverage.
+ sourceBuffer.abort();
+ // Shorten the buffered media and test playback duration to avoid timeouts.
+ sourceBuffer.remove(0.5, Infinity);
+ sourceBuffer.onupdateend = () => {
+ util.mediaSource.duration = 0.5;
+ // Issue changeType to the same type that we've already buffered.
+ // Unnecessary for this buffering, except helps with test coverage.
+ sourceBuffer.changeType(util.mediaMetadata.type);
+ util.mediaSource.endOfStream();
+ // Sanity check the duration.
+ // Unnecessary for this buffering, except helps with test coverage.
+ var duration = util.mediaSource.duration;
+ if (isNaN(duration) || duration <= 0.0 || duration >= 1.0) {
+ postMessage({
+ subject: messageSubject.ERROR,
+ info: "mediaSource.duration " + duration + " is not within expected range (0,1)"
+ });
+ }
+ };
+ };
+ util.mediaLoadPromise.then(mediaData => { sourceBuffer.appendBuffer(mediaData); },
+ err => { postMessage({ subject: messageSubject.ERROR, info: err }) });
+}, {once: true});
+
+postMessage({ subject: messageSubject.HANDLE, info: handle }, { transfer: [handle] });
diff --git a/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-util.js b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-util.js
new file mode 100644
index 0000000000..7adaf82508
--- /dev/null
+++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-util.js
@@ -0,0 +1,60 @@
+// This script is intended to be imported into a worker's script, and provides
+// common preparation for multiple test cases. Errors encountered are either
+// postMessaged with subject of messageSubject.ERROR, or in the case of failed
+// mediaLoadPromise, result in promise rejection.
+
+importScripts("mediasource-message-util.js");
+
+if (!this.MediaSource)
+ postMessage({ subject: messageSubject.ERROR, info: "MediaSource API missing from Worker" });
+
+let MEDIA_LIST = [
+ {
+ url: '../mp4/test.mp4',
+ type: 'video/mp4; codecs="mp4a.40.2,avc1.4d400d"',
+ },
+ {
+ url: '../webm/test.webm',
+ type: 'video/webm; codecs="vp8, vorbis"',
+ },
+];
+
+class MediaSourceWorkerUtil {
+ constructor() {
+ this.mediaSource = new MediaSource();
+
+ // Find supported test media, if any.
+ this.foundSupportedMedia = false;
+ for (let i = 0; i < MEDIA_LIST.length; ++i) {
+ this.mediaMetadata = MEDIA_LIST[i];
+ if (MediaSource.isTypeSupported(this.mediaMetadata.type)) {
+ this.foundSupportedMedia = true;
+ break;
+ }
+ }
+
+ // Begin asynchronous fetch of the test media.
+ if (this.foundSupportedMedia) {
+ this.mediaLoadPromise = MediaSourceWorkerUtil.loadBinaryAsync(this.mediaMetadata.url);
+ } else {
+ postMessage({ subject: messageSubject.ERROR, info: "No supported test media" });
+ }
+ }
+
+ static loadBinaryAsync(url) {
+ return new Promise((resolve, reject) => {
+ let request = new XMLHttpRequest();
+ request.open("GET", url, true);
+ request.responseType = "arraybuffer";
+ request.onerror = event => { reject(event); };
+ request.onload = () => {
+ if (request.status != 200) {
+ reject("Unexpected loadData_ status code : " + request.status);
+ }
+ let response = new Uint8Array(request.response);
+ resolve(response);
+ };
+ request.send();
+ });
+ }
+}