diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/media-source/dedicated-worker | |
parent | Initial commit. (diff) | |
download | firefox-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')
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(); + }); + } +} |