diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/media-source | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/media-source')
151 files changed, 9746 insertions, 0 deletions
diff --git a/testing/web-platform/tests/media-source/META.yml b/testing/web-platform/tests/media-source/META.yml new file mode 100644 index 0000000000..d1252f5af1 --- /dev/null +++ b/testing/web-platform/tests/media-source/META.yml @@ -0,0 +1,3 @@ +spec: https://w3c.github.io/media-source/ +suggested_reviewers: + - wolenetz diff --git a/testing/web-platform/tests/media-source/SourceBuffer-abort-readyState.html b/testing/web-platform/tests/media-source/SourceBuffer-abort-readyState.html new file mode 100644 index 0000000000..5942379d08 --- /dev/null +++ b/testing/web-platform/tests/media-source/SourceBuffer-abort-readyState.html @@ -0,0 +1,72 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>SourceBuffer#abort() when readyState attribute is not in the "open"</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +var contents = {'/media/white.webm': 'video/webm; codecs="vorbis,vp8"', + '/media/white.mp4' : 'video/mp4'}; + +//check the browser supports the MIME used in this test +function isTypeSupported(mime) { + if(!MediaSource.isTypeSupported(mime)) { + this.step(function() { + assert_unreached("Browser doesn't support the MIME used in this test: " + mime); + }); + this.done(); + return false; + } + return true; +} +function GET(url, processBody) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.send(); + xhr.onload = function(e) { + if (xhr.status != 200) { + alert("Unexpected status code " + xhr.status + " for " + url); + return false; + } + processBody(new Uint8Array(xhr.response)); + }; +} +function mediaTest(file, mime) { + async_test(function(t) { + if(!isTypeSupported.bind(t)(mime)) { + return; + } + GET(file, function(data) { + var mediaSource = new MediaSource(); + var sourceBuffer = null; + mediaSource.addEventListener('sourceopen', function(e) { + sourceBuffer = mediaSource.addSourceBuffer(mime); + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, 'ended', + 'mediaSource.readyState is "ended" after endOfStream()'); + }); + mediaSource.addEventListener('sourceended', t.step_func_done(function(e) { + assert_throws_dom('InvalidStateError', function() { + sourceBuffer.abort(); + }); + })); + var video = document.createElement('video'); + video.src = window.URL.createObjectURL(mediaSource); + }); + }, 'SourceBuffer#abort() (' + mime + ') : If the readyState attribute ' + + 'of the parent media source is not in the "open" state then throw ' + + 'an INVALID_STATE_ERR exception and abort these steps.'); +} +for(var file in contents) { + mediaTest(file, contents[file]); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/SourceBuffer-abort-removed.html b/testing/web-platform/tests/media-source/SourceBuffer-abort-removed.html new file mode 100644 index 0000000000..4782412ccd --- /dev/null +++ b/testing/web-platform/tests/media-source/SourceBuffer-abort-removed.html @@ -0,0 +1,52 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>SourceBuffer#abort() for already removed buffer from parent media source</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +var mimes = ['video/webm; codecs="vorbis,vp8"', 'video/mp4']; + +//check the browser supports the MIME used in this test +function isTypeSupported(mime) { + if(!MediaSource.isTypeSupported(mime)) { + this.step(function() { + assert_unreached("Browser doesn't support the MIME used in this test: " + mime); + }); + this.done(); + return false; + } + return true; +} +function mediaTest(mime) { + async_test(function(t) { + if(!isTypeSupported.bind(t)(mime)) { + return; + } + var mediaSource = new MediaSource(); + mediaSource.addEventListener('sourceopen', t.step_func_done(function(e) { + var sourceBuffer = mediaSource.addSourceBuffer(mime); + mediaSource.removeSourceBuffer(sourceBuffer); + assert_throws_dom('InvalidStateError', + function() { + sourceBuffer.abort(); + }, + 'SourceBuffer#abort() after removing the SourceBuffer object'); + }), false); + var video = document.createElement('video'); + video.src = window.URL.createObjectURL(mediaSource); + }, 'SourceBuffer#abort (' + mime + ') : ' + + 'if this object has been removed from the sourceBuffers attribute of the parent media source, ' + + 'then throw an INVALID_STATE_ERR exception and abort these steps.'); +} +mimes.forEach(function(mime) { + mediaTest(mime); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/SourceBuffer-abort-updating.html b/testing/web-platform/tests/media-source/SourceBuffer-abort-updating.html new file mode 100644 index 0000000000..1132d14663 --- /dev/null +++ b/testing/web-platform/tests/media-source/SourceBuffer-abort-updating.html @@ -0,0 +1,92 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>Check SourceBuffer#abort() when the updating attribute is true</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +var contents = {'/media/white.webm': 'video/webm; codecs="vorbis,vp8"', + '/media/white.mp4' : 'video/mp4'}; + +function GET(url, processBody) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.send(); + xhr.onload = function(e) { + if (xhr.status != 200) { + alert("Unexpected status code " + xhr.status + " for " + url); + return false; + } + processBody(new Uint8Array(xhr.response)); + }; +} +//check the browser supports the MIME used in this test +function isTypeSupported(mime) { + if(!MediaSource.isTypeSupported(mime)) { + this.step(function() { + assert_unreached("Browser doesn't support the MIME used in this test: " + mime); + }); + this.done(); + return false; + } + return true; +} +function mediaTest(file, mime) { + async_test(function(t) { + if(!isTypeSupported.bind(t)(mime)) { + return; + } + GET(file, function(data) { + var mediaSource = new MediaSource(); + var num_updateend = 0; + var events = []; + mediaSource.addEventListener('sourceopen', t.step_func(function(e) { + var sourceBuffer = mediaSource.addSourceBuffer(mime); + assert_equals(sourceBuffer.updating, false); + sourceBuffer.addEventListener('updatestart', t.step_func(function(e) { + events.push('updatestart'); + //abort when sourceBuffer#updating is true + sourceBuffer.abort(); + + assert_equals(sourceBuffer.updating, false, + 'Check updating value after calling abort.'); + assert_equals(sourceBuffer.appendWindowStart, 0); + assert_equals(sourceBuffer.appendWindowEnd, Number.POSITIVE_INFINITY); + })); + sourceBuffer.addEventListener('update', t.step_func(function(e) { + assert_unreached("Can't touch this"); + })); + sourceBuffer.addEventListener('updateend', function(e) { + events.push('updateend'); + mediaSource.endOfStream(); + }); + sourceBuffer.addEventListener('abort', function(e) { + events.push('abort'); + }); + sourceBuffer.addEventListener('error', t.step_func(function(e) { + assert_unreached("Can't touch this"); + })); + sourceBuffer.appendBuffer(data); + })); + mediaSource.addEventListener('sourceended', t.step_func_done(function(e) { + assert_array_equals(events, + ['updatestart', 'abort', 'updateend'], + 'Check the sequence of fired events.'); + })); + var video = document.createElement('video'); + video.src = window.URL.createObjectURL(mediaSource); + }); + }, 'SourceBuffer#abort() (' + mime + ') : Check the algorithm when the updating attribute is true.'); +} +for(var file in contents) { + mediaTest(file, contents[file]); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/SourceBuffer-abort.html b/testing/web-platform/tests/media-source/SourceBuffer-abort.html new file mode 100644 index 0000000000..7d7c9ff1de --- /dev/null +++ b/testing/web-platform/tests/media-source/SourceBuffer-abort.html @@ -0,0 +1,34 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>Check the values of appendWindowStart and appendWindowEnd after abort()</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +var mimes = ['video/webm; codecs="vorbis,vp8"', 'video/mp4']; + +mimes.forEach(function(mime) { + async_test(function() { + assert_true(MediaSource.isTypeSupported(mime), + "Browser doesn't support the MIME used in this test: " + mime); + + var mediaSource = new MediaSource(); + mediaSource.addEventListener('sourceopen', this.step_func_done(function(e) { + var sourceBuffer = mediaSource.addSourceBuffer(mime); + sourceBuffer.abort(); + assert_equals(sourceBuffer.appendWindowStart, 0); + assert_equals(sourceBuffer.appendWindowEnd, Number.POSITIVE_INFINITY); + })); + + var video = document.createElement('video'); + video.src = window.URL.createObjectURL(mediaSource); + }, 'SourceBuffer#abort() (' + mime + '): Check the values of appendWindowStart and appendWindowEnd.'); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/URL-createObjectURL-null.html b/testing/web-platform/tests/media-source/URL-createObjectURL-null.html new file mode 100644 index 0000000000..f2f973a776 --- /dev/null +++ b/testing/web-platform/tests/media-source/URL-createObjectURL-null.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>URL.createObjectURL(null)</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function() { + assert_throws_js(TypeError, function() { + window.URL.createObjectURL(null); + }); +}, "URL.createObjectURL(null)"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/URL-createObjectURL-revoke.html b/testing/web-platform/tests/media-source/URL-createObjectURL-revoke.html new file mode 100644 index 0000000000..c5e18d4fd5 --- /dev/null +++ b/testing/web-platform/tests/media-source/URL-createObjectURL-revoke.html @@ -0,0 +1,59 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>Revoking a created URL with URL.revokeObjectURL(url)</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +async_test(function(t) { + var mediaSource = new MediaSource(); + var url = window.URL.createObjectURL(mediaSource); + window.URL.revokeObjectURL(url); + mediaSource.addEventListener('sourceopen', + t.unreached_func("url should not reference MediaSource.")); + var video = document.createElement('video'); + video.src = url; + video.addEventListener('error', t.step_func_done(function(e) { + assert_equals(e.target.error.code, + MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, + 'Expected error code'); + assert_equals(mediaSource.readyState, 'closed'); + })); +}, "Check revoking behavior of URL.revokeObjectURL(url)."); +async_test(function(t) { + var mediaSource = new MediaSource(); + var url = window.URL.createObjectURL(mediaSource); + var video = document.createElement('video'); + var unexpectedErrorHandler = t.unreached_func("Unexpected error.") + video.addEventListener('error', unexpectedErrorHandler); + video.src = url; + window.URL.revokeObjectURL(url); + mediaSource.addEventListener('sourceopen', t.step_func_done(function(e) { + assert_equals(mediaSource.readyState, 'open'); + mediaSource.endOfStream(); + video.removeEventListener('error', unexpectedErrorHandler); + })); +}, "Check referenced MediaSource can open after URL.revokeObjectURL(url)."); +async_test(function(t) { + var mediaSource = new MediaSource(); + var url = window.URL.createObjectURL(mediaSource); + setTimeout(function() { + mediaSource.addEventListener('sourceopen', + t.unreached_func("url should not reference MediaSource.")); + var video = document.createElement('video'); + video.src = url; + video.addEventListener('error', t.step_func_done(function(e) { + assert_equals(e.target.error.code, + MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, + 'Expected error code'); + assert_equals(mediaSource.readyState, 'closed'); + })); + }, 0); +}, "Check auto-revoking behavior with URL.createObjectURL(MediaSource)."); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/URL-createObjectURL.html b/testing/web-platform/tests/media-source/URL-createObjectURL.html new file mode 100644 index 0000000000..da82806349 --- /dev/null +++ b/testing/web-platform/tests/media-source/URL-createObjectURL.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>URL.createObjectURL(mediaSource)</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +test(function() { + var mediaSource = new MediaSource(); + var url = window.URL.createObjectURL(mediaSource); + assert_true(url != null); + assert_true(url.match(/^blob:.+/) != null); +}, "URL.createObjectURL(mediaSource) should return a unique Blob URI."); +</script> +</body> +</html> 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..fb3dc8add1 --- /dev/null +++ b/testing/web-platform/tests/media-source/dedicated-worker/mediasource-worker-play-terminate-worker.html @@ -0,0 +1,90 @@ +<!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(video, test, worker, timeouts_remaining) { + if (timeouts_remaining <= 0) { + // Terminating the worker may introduce errors in the video element we don't care about. + video.onerror = null; + worker.terminate(); + test.step_timeout(() => { test.done(); }, 0); + } else { + test.step_timeout(() => { + terminateWorkerAfterMultipleSetTimeouts(video, 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 = _ => { + assert_unreached( + `video element error: \"${video.error.message}\": ${video.error.code}`); + } + + if (when_to_start_timeouts == "after first ended event") { + video.addEventListener("ended", test.step_func(() => { + terminateWorkerAfterMultipleSetTimeouts(video, 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(video, 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(video, 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(); + }); + } +} diff --git a/testing/web-platform/tests/media-source/generate-config-change-tests.py b/testing/web-platform/tests/media-source/generate-config-change-tests.py new file mode 100755 index 0000000000..6ab2c8bf46 --- /dev/null +++ b/testing/web-platform/tests/media-source/generate-config-change-tests.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# Copyright (C) 2013 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +This is a script that generates the content and HTML files for Media Source +codec config change LayoutTests. +""" +import json +import os + +DURATION = 2 +MEDIA_FORMATS = ['webm', 'mp4'] +ENCODE_SETTINGS = [ + ## Video-only files + # Frame rate changes + {'fs': '320x240', 'fr': 24, 'kfr': 8, 'c': '#ff0000', 'vbr': 128, 'abr': 0, 'asr': 0, 'ach': 0, 'afreq': 0}, + {'fs': '320x240', 'fr': 30, 'kfr': 10, 'c': '#ff0000', 'vbr': 128, 'abr': 0, 'asr': 0, 'ach': 0, 'afreq': 0}, + # Frame size change + {'fs': '640x480', 'fr': 30, 'kfr': 10, 'c': '#00ff00', 'vbr': 128, 'abr': 0, 'asr': 0, 'ach': 0, 'afreq': 0}, + # Bitrate change + {'fs': '320x240', 'fr': 30, 'kfr': 10, 'c': '#ff00ff', 'vbr': 256, 'abr': 0, 'asr': 0, 'ach': 0, 'afreq': 0}, + + ## Audio-only files + # Bitrate/Codebook changes + {'fs': '0x0', 'fr': 0, 'kfr': 0, 'c': '#000000', 'vbr': 0, 'abr': 128, 'asr': 44100, 'ach': 1, 'afreq': 2000}, + {'fs': '0x0', 'fr': 0, 'kfr': 0, 'c': '#000000', 'vbr': 0, 'abr': 192, 'asr': 44100, 'ach': 1, 'afreq': 4000}, + + ## Audio-Video files + # Frame size change. + {'fs': '320x240', 'fr': 30, 'kfr': 10, 'c': '#ff0000', 'vbr': 256, 'abr': 128, 'asr': 44100, 'ach': 1, 'afreq': 2000}, + {'fs': '640x480', 'fr': 30, 'kfr': 10, 'c': '#00ff00', 'vbr': 256, 'abr': 128, 'asr': 44100, 'ach': 1, 'afreq': 2000}, + # Audio bitrate change. + {'fs': '640x480', 'fr': 30, 'kfr': 10, 'c': '#00ff00', 'vbr': 256, 'abr': 192, 'asr': 44100, 'ach': 1, 'afreq': 4000}, + # Video bitrate change. + {'fs': '640x480', 'fr': 30, 'kfr': 10, 'c': '#00ffff', 'vbr': 512, 'abr': 128, 'asr': 44100, 'ach': 1, 'afreq': 2000}, +] + +CONFIG_CHANGE_TESTS = [ + ["v-framerate", 0, 1, "Tests %s video-only frame rate changes."], + ["v-framesize", 1, 2, "Tests %s video-only frame size changes."], + ["v-bitrate", 1, 3, "Tests %s video-only bitrate changes."], + ["a-bitrate", 4, 5, "Tests %s audio-only bitrate changes."], + ["av-framesize", 6, 7, "Tests %s frame size changes in multiplexed content."], + ["av-audio-bitrate", 7, 8, "Tests %s audio bitrate changes in multiplexed content."], + ["av-video-bitrate", 7, 9, "Tests %s video bitrate changes in multiplexed content."] +] + +CODEC_INFO = { + "mp4": {"audio": "mp4a.40.2", "video": "avc1.4D4001"}, + "webm": {"audio": "vorbis", "video": "vp8"} +} + +HTML_TEMPLATE = """<!DOCTYPE html> +<html> + <head> + <script src="/w3c/resources/testharness.js"></script> + <script src="/w3c/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("%(media_format)s", "%(idA)s", "%(idB)s", "%(description)s"); + </script> + </body> +</html> +""" + +def run(cmd_line): + os.system(" ".join(cmd_line)) + +def generate_manifest(filename, media_filename, media_format, has_audio, has_video): + major_type = "audio" + if has_video: + major_type = "video" + + codecs = [] + if has_video: + codecs.append(CODEC_INFO[media_format]["video"]) + + if has_audio: + codecs.append(CODEC_INFO[media_format]["audio"]) + + mimetype = "%s/%s;codecs=\"%s\"" % (major_type, media_format, ",".join(codecs)) + + manifest = { 'url': media_filename, 'type': mimetype} + + f = open(filename, "wb") + f.write(json.dumps(manifest, indent=4, separators=(',', ': '))) + f.close() + +def generate_test_html(media_format, config_change_tests, encoding_ids): + for test_info in config_change_tests: + filename = "../../media-source/mediasource-config-change-%s-%s.html" % (media_format, test_info[0]) + html = HTML_TEMPLATE % {'media_format': media_format, + 'idA': encoding_ids[test_info[1]], + 'idB': encoding_ids[test_info[2]], + 'description': test_info[3] % (media_format)} + f = open(filename, "wb") + f.write(html) + f.close() + + +def main(): + encoding_ids = [] + + for media_format in MEDIA_FORMATS: + run(["mkdir ", media_format]) + + for settings in ENCODE_SETTINGS: + video_bitrate = settings['vbr'] + has_video = (video_bitrate > 0) + + audio_bitrate = settings['abr'] + has_audio = (audio_bitrate > 0) + bitrate = video_bitrate + audio_bitrate + + frame_size = settings['fs'] + frame_rate = settings['fr'] + keyframe_rate = settings['kfr'] + color = settings['c'] + + sample_rate = settings['asr'] + channels = settings['ach'] + frequency = settings['afreq'] + + cmdline = ["ffmpeg", "-y"] + + id_prefix = "" + id_params = "" + if has_audio: + id_prefix += "a" + id_params += "-%sHz-%sch" % (sample_rate, channels) + + channel_layout = "FC" + sin_func = "sin(%s*2*PI*t)" % frequency + func = sin_func + if channels == 2: + channel_layout += "|BC" + func += "|" + sin_func + + cmdline += ["-f", "lavfi", "-i", "aevalsrc=\"%s:s=%s:c=%s:d=%s\"" % (func, sample_rate, channel_layout, DURATION)] + + if has_video: + id_prefix += "v" + id_params += "-%s-%sfps-%skfr" % (frame_size, frame_rate, keyframe_rate) + + cmdline += ["-f", "lavfi", "-i", "color=%s:duration=%s:size=%s:rate=%s" % (color, DURATION, frame_size, frame_rate)] + + if has_audio: + cmdline += ["-b:a", "%sk" % audio_bitrate] + + if has_video: + cmdline += ["-b:v", "%sk" % video_bitrate] + cmdline += ["-keyint_min", "%s" % keyframe_rate] + cmdline += ["-g", "%s" % keyframe_rate] + + + textOverlayInfo = "'drawtext=fontfile=Mono:fontsize=32:text=Time\\\\:\\\\ %{pts}" + textOverlayInfo += ",drawtext=fontfile=Mono:fontsize=32:y=32:text=Size\\\\:\\\\ %s" % (frame_size) + textOverlayInfo += ",drawtext=fontfile=Mono:fontsize=32:y=64:text=Bitrate\\\\:\\\\ %s" % (bitrate) + textOverlayInfo += ",drawtext=fontfile=Mono:fontsize=32:y=96:text=FrameRate\\\\:\\\\ %s" % (frame_rate) + textOverlayInfo += ",drawtext=fontfile=Mono:fontsize=32:y=128:text=KeyFrameRate\\\\:\\\\ %s" % (keyframe_rate) + + if has_audio: + textOverlayInfo += ",drawtext=fontfile=Mono:fontsize=32:y=160:text=SampleRate\\\\:\\\\ %s" % (sample_rate) + textOverlayInfo += ",drawtext=fontfile=Mono:fontsize=32:y=192:text=Channels\\\\:\\\\ %s" % (channels) + + textOverlayInfo += "'" + cmdline += ["-vf", textOverlayInfo] + + encoding_id = "%s-%sk%s" % (id_prefix, bitrate, id_params) + + if len(encoding_ids) < len(ENCODE_SETTINGS): + encoding_ids.append(encoding_id) + + filename_base = "%s/test-%s" % (media_format, encoding_id) + media_filename = filename_base + "." + media_format + manifest_filename = filename_base + "-manifest.json" + + cmdline.append(media_filename) + run(cmdline) + + # Remux file so it conforms to MSE bytestream requirements. + if media_format == "webm": + tmp_filename = media_filename + ".tmp" + run(["mse_webm_remuxer", media_filename, tmp_filename]) + run(["mv", tmp_filename, media_filename]) + elif media_format == "mp4": + run(["MP4Box", "-dash", "250", "-rap", media_filename]) + run(["mv", filename_base + "_dash.mp4", media_filename]) + run(["rm", filename_base + "_dash.mpd"]) + + generate_manifest(manifest_filename, media_filename, media_format, has_audio, has_video) + generate_test_html(media_format, CONFIG_CHANGE_TESTS, encoding_ids) + +if '__main__' == __name__: + main() diff --git a/testing/web-platform/tests/media-source/idlharness.window.js b/testing/web-platform/tests/media-source/idlharness.window.js new file mode 100644 index 0000000000..9300f67fe0 --- /dev/null +++ b/testing/web-platform/tests/media-source/idlharness.window.js @@ -0,0 +1,35 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +// https://w3c.github.io/media-source/ + +'use strict'; + +idl_test( + ['media-source'], + ['dom', 'html', 'url'], + async idl_array => { + idl_array.add_objects({ + MediaSource: ['mediaSource'], + SourceBuffer: ['sourceBuffer'], + SourceBufferList: ['mediaSource.sourceBuffers'], + }); + + const video = document.createElement('video'); + self.mediaSource = new MediaSource(); + video.src = URL.createObjectURL(mediaSource); + + self.sourceBuffer = await new Promise((resolve, reject) => { + mediaSource.addEventListener('sourceopen', () => { + var defaultType = 'video/webm;codecs="vp8,vorbis"'; + if (MediaSource.isTypeSupported(defaultType)) { + resolve(mediaSource.addSourceBuffer(defaultType)); + } else { + resolve(mediaSource.addSourceBuffer('video/mp4')); + } + }); + step_timeout(() => reject(new Error('sourceopen event not fired')), 3000); + }); + } +); diff --git a/testing/web-platform/tests/media-source/import_tests.sh b/testing/web-platform/tests/media-source/import_tests.sh new file mode 100755 index 0000000000..a87619c024 --- /dev/null +++ b/testing/web-platform/tests/media-source/import_tests.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +if [ $# -lt 1 ] +then + echo "Usage: $0 <Blink directory>" + exit -1 +fi + +BLINK_ROOT=$1 +LAYOUT_TEST_DIR=$BLINK_ROOT/LayoutTests +HTTP_MEDIA_TEST_DIR=$LAYOUT_TEST_DIR/http/tests/media + +if [ ! -d "$BLINK_ROOT" ] +then + echo "$BLINK_ROOT is not a directory or doesn't exist" + exit -1 +fi + +if [ ! -d "$LAYOUT_TEST_DIR" ] +then + echo "$LAYOUT_TEST_DIR is not a directory or doesn't exist" + exit -1 +fi + +#rm -rf *.html *.js webm mp4 manifest.txt + +cp $HTTP_MEDIA_TEST_DIR/media-source/mediasource-*.html $HTTP_MEDIA_TEST_DIR/media-source/mediasource-*.js . +cp -r $HTTP_MEDIA_TEST_DIR/resources/media-source/webm . +cp -r $HTTP_MEDIA_TEST_DIR/resources/media-source/mp4 . + +# Remove Blink-specific files +rm mediasource-gc-after-decode-error-crash.html + +sed -i 's/\/w3c\/resources\//\/resources\//g' *.html +sed -i 's/\/media\/resources\/media-source\///g' *.html +sed -i 's/\/media\/resources\/media-source\///g' *.js +sed -i 's/\/media\/resources\/media-source\///g' webm/* + + +for TEST_FILE in `ls *.html` +do + if [ "$TEST_FILE" = "index.html" ] + then + continue + fi + echo -e "$TEST_FILE" >> manifest.txt +done + +cp import_tests-template.txt index.html + +chmod -R a+r *.html *.js webm mp4 manifest.txt +chmod a+rx webm mp4 diff --git a/testing/web-platform/tests/media-source/manifest.txt b/testing/web-platform/tests/media-source/manifest.txt new file mode 100644 index 0000000000..3ca784f17a --- /dev/null +++ b/testing/web-platform/tests/media-source/manifest.txt @@ -0,0 +1,55 @@ +interfaces.html +mediasource-activesourcebuffers.html +mediasource-addsourcebuffer.html +mediasource-addsourcebuffer-mode.html +mediasource-append-buffer.html +mediasource-appendbuffer-quota-exceeded.html +mediasource-appendwindow.html +mediasource-attach-stops-delaying-load-event.html +mediasource-avtracks.html +mediasource-buffered.html +mediasource-closed.html +mediasource-config-change-mp4-a-bitrate.html +mediasource-config-change-mp4-av-audio-bitrate.html +mediasource-config-change-mp4-av-framesize.html +mediasource-config-change-mp4-av-video-bitrate.html +mediasource-config-change-mp4-v-bitrate.html +mediasource-config-change-mp4-v-framerate.html +mediasource-config-change-mp4-v-framesize.html +mediasource-config-change-webm-a-bitrate.html +mediasource-config-change-webm-av-audio-bitrate.html +mediasource-config-change-webm-av-framesize.html +mediasource-config-change-webm-av-video-bitrate.html +mediasource-config-change-webm-v-bitrate.html +mediasource-config-change-webm-v-framerate.html +mediasource-config-change-webm-v-framesize.html +mediasource-detach.html +mediasource-duration-boundaryconditions.html +mediasource-duration.html +mediasource-endofstream.html +mediasource-endofstream-invaliderror.html +mediasource-errors.html +mediasource-is-type-supported.html +mediasource-liveseekable.html +mediasource-multiple-attach.html +mediasource-play.html +mediasource-play-then-seek-back.html +mediasource-preload.html +mediasource-redundant-seek.html +mediasource-remove.html +mediasource-removesourcebuffer.html +mediasource-seekable.html +mediasource-seek-beyond-duration.html +mediasource-seek-during-pending-seek.html +mediasource-sequencemode-append-buffer.html +mediasource-sourcebufferlist.html +mediasource-sourcebuffer-mode.html +mediasource-sourcebuffer-mode-timestamps.html +mediasource-timestamp-offset.html +SourceBuffer-abort.html +SourceBuffer-abort-readyState.html +SourceBuffer-abort-removed.html +SourceBuffer-abort-updating.html +URL-createObjectURL.html +URL-createObjectURL-null.html +URL-createObjectURL-revoke.html diff --git a/testing/web-platform/tests/media-source/mediasource-activesourcebuffers.html b/testing/web-platform/tests/media-source/mediasource-activesourcebuffers.html new file mode 100644 index 0000000000..02ebecc773 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-activesourcebuffers.html @@ -0,0 +1,238 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Checks MediaSource.activeSourceBuffers and changes to selected/enabled track state</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + // Audio / Video files supported by the user agent under test + var subType = MediaSourceUtil.getSubType(MediaSourceUtil.AUDIO_ONLY_TYPE); + var manifestFilenameAudio = subType + "/test-a-128k-44100Hz-1ch-manifest.json"; + var manifestFilenameVideo = subType + "/test-v-128k-320x240-30fps-10kfr-manifest.json"; + var manifestFilenameAV = subType + "/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json"; + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAudio, function (typeAudio, dataAudio) + { + var sourceBuffer = mediaSource.addSourceBuffer(typeAudio); + assert_equals(mediaSource.sourceBuffers.length, 1, + "sourceBuffers list contains one SourceBuffer"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, + "activeSourceBuffers is empty to start with"); + + test.expectEvent(mediaSource.activeSourceBuffers, "addsourcebuffer"); + test.expectEvent(mediaElement, "loadedmetadata"); + sourceBuffer.appendBuffer(dataAudio); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "activeSourceBuffers updated when media element is loaded"); + assert_equals(mediaSource.activeSourceBuffers[0], sourceBuffer, + "activeSourceBuffers contains sourceBuffer when media element is loaded"); + test.done(); + }); + }); + }, "SourceBuffer added to activeSourceBuffers list when its only audio track gets loaded (and thus becomes enabled)."); + + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameVideo, function (typeVideo, dataVideo) + { + var sourceBuffer = mediaSource.addSourceBuffer(typeVideo); + assert_equals(mediaSource.sourceBuffers.length, 1, + "sourceBuffers list contains one SourceBuffer"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, + "activeSourceBuffers is empty to start with"); + + test.expectEvent(mediaSource.activeSourceBuffers, "addsourcebuffer"); + test.expectEvent(mediaElement, "loadedmetadata"); + sourceBuffer.appendBuffer(dataVideo); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "activeSourceBuffers updated when media element is loaded"); + assert_equals(mediaSource.activeSourceBuffers[0], sourceBuffer, + "activeSourceBuffers contains sourceBuffer when media element is loaded"); + test.done(); + }); + }); + }, "SourceBuffer added to activeSourceBuffers list when its only video track gets loaded (and thus becomes selected)."); + + function mediaSourceActiveSourceBufferOrderTest(addAudioFirst, appendAudioFirst) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAudio, function (typeAudio, dataAudio) + { + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameVideo, function (typeVideo, dataVideo) + { + var sourceBufferAudio, sourceBufferVideo, expectedFirstSB, expectedSecondSB; + if (addAudioFirst) { + expectedFirstSB = sourceBufferAudio = mediaSource.addSourceBuffer(typeAudio); + expectedSecondSB = sourceBufferVideo = mediaSource.addSourceBuffer(typeVideo); + } else { + expectedFirstSB = sourceBufferVideo = mediaSource.addSourceBuffer(typeVideo); + expectedSecondSB = sourceBufferAudio = mediaSource.addSourceBuffer(typeAudio); + } + + assert_equals(mediaSource.activeSourceBuffers.length, 0, + "activeSourceBuffers is empty to start with"); + assert_equals(mediaSource.sourceBuffers.length, 2, + "sourceBuffers list contains both SourceBuffers"); + assert_equals(mediaSource.sourceBuffers[0], expectedFirstSB, + "first SourceBuffer matches expectation"); + assert_equals(mediaSource.sourceBuffers[1], expectedSecondSB, + "second SourceBuffer matches expectation"); + test.expectEvent(mediaSource.activeSourceBuffers, "addsourcebuffer"); + test.expectEvent(mediaSource.activeSourceBuffers, "addsourcebuffer"); + test.expectEvent(mediaElement, "loadedmetadata"); + if (appendAudioFirst) { + sourceBufferAudio.appendBuffer(dataAudio); + sourceBufferVideo.appendBuffer(dataVideo); + } else { + sourceBufferVideo.appendBuffer(dataVideo); + sourceBufferAudio.appendBuffer(dataAudio); + } + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 2, + "activeSourceBuffers list updated when tracks are loaded"); + assert_equals(mediaSource.activeSourceBuffers[0], mediaSource.sourceBuffers[0], + "first active SourceBuffer matches first SourceBuffer"); + assert_equals(mediaSource.activeSourceBuffers[1], mediaSource.sourceBuffers[1], + "second active SourceBuffer matches second SourceBuffer"); + test.done(); + }); + }); + }); + }, + "Active SourceBuffers must appear in the same order as they appear in the sourceBuffers attribute: " + + (addAudioFirst ? "audio is first sourceBuffer" : "video is first sourceBuffer") + ", " + + (appendAudioFirst ? "audio media appended first" : "video media appended first")); + } + + mediaSourceActiveSourceBufferOrderTest(true, true); + mediaSourceActiveSourceBufferOrderTest(true, false); + mediaSourceActiveSourceBufferOrderTest(false, true); + mediaSourceActiveSourceBufferOrderTest(false, false); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAudio, function (typeAudio, dataAudio) + { + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameVideo, function (typeVideo, dataVideo) + { + var sourceBufferAudio = mediaSource.addSourceBuffer(typeAudio); + var sourceBufferVideo = mediaSource.addSourceBuffer(typeVideo); + + test.expectEvent(sourceBufferAudio.audioTracks, "addtrack"); + test.expectEvent(sourceBufferVideo.videoTracks, "addtrack"); + sourceBufferAudio.appendBuffer(dataAudio); + sourceBufferVideo.appendBuffer(dataVideo); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 2, + "activeSourceBuffers list updated when tracks are loaded"); + assert_equals(sourceBufferAudio.audioTracks.length, 1, + "audio track list contains loaded audio track"); + assert_equals(sourceBufferVideo.videoTracks.length, 1, + "video track list contains loaded video track"); + + test.expectEvent(mediaSource.activeSourceBuffers, "removesourcebuffer"); + sourceBufferAudio.audioTracks[0].enabled = false; + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "audio source buffer no longer in the activeSourceBuffers list"); + assert_equals(mediaSource.activeSourceBuffers[0], sourceBufferVideo, + "activeSourceBuffers list only contains the video SourceBuffer"); + + test.expectEvent(mediaSource.activeSourceBuffers, "addsourcebuffer"); + test.expectEvent(mediaSource.activeSourceBuffers, "removesourcebuffer"); + sourceBufferAudio.audioTracks[0].enabled = true; + sourceBufferVideo.videoTracks[0].selected = false; + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "video source buffer no longer in the activeSourceBuffers list"); + assert_equals(mediaSource.activeSourceBuffers[0], sourceBufferAudio, + "activeSourceBuffers list only contains the audio SourceBuffer"); + test.done(); + }); + }); + }); + }, "Active SourceBuffers list reflects changes to selected audio/video tracks associated with separate SourceBuffers."); + + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAV, function (typeAV, dataAV) + { + var sourceBuffer = mediaSource.addSourceBuffer(typeAV); + + test.expectEvent(sourceBuffer.audioTracks, "addtrack"); + test.expectEvent(sourceBuffer.videoTracks, "addtrack"); + sourceBuffer.appendBuffer(dataAV); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "activeSourceBuffers list updated when tracks are loaded"); + assert_equals(sourceBuffer.audioTracks.length, 1, + "audio track list contains loaded audio track"); + assert_equals(sourceBuffer.videoTracks.length, 1, + "video track list contains loaded video track"); + + mediaSource.activeSourceBuffers.addEventListener("removesourcebuffer", test.unreached_func( + "Unexpected removal from activeSourceBuffers list")); + mediaSource.activeSourceBuffers.addEventListener("addsourcebuffer", test.unreached_func( + "Unexpected insertion in activeSourceBuffers list")); + + // Changes should only trigger events at the + // AudioTrack/VideoTrack instance + test.expectEvent(sourceBuffer.audioTracks, "change"); + sourceBuffer.audioTracks[0].enabled = false; + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "activeSourceBuffers list unchanged"); + + test.expectEvent(sourceBuffer.videoTracks, "change"); + sourceBuffer.audioTracks[0].enabled = true; + sourceBuffer.videoTracks[0].selected = false; + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.activeSourceBuffers.length, 1, + "activeSourceBuffers list unchanged"); + test.done(); + }); + }); + }, "Active SourceBuffers list ignores changes to selected audio/video tracks " + + "that do not affect the activation of the SourceBuffer."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-addsourcebuffer-mode.html b/testing/web-platform/tests/media-source/mediasource-addsourcebuffer-mode.html new file mode 100644 index 0000000000..cf7f57f8e2 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-addsourcebuffer-mode.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Checks MediaSource.addSourceBuffer() sets SourceBuffer.mode appropriately</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + mediasource_test(function(test, mediaElement, mediaSource) + { + // Note all mime types in mediasource-util.js + // set the "generate timestamps flag" to false + var mime = MediaSourceUtil.VIDEO_ONLY_TYPE; + var sourceBuffer = mediaSource.addSourceBuffer(mime); + assert_equals(sourceBuffer.mode, "segments"); + test.done(); + }, "addSourceBuffer() sets SourceBuffer.mode to 'segments' when the generate timestamps flag is false"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var mime = 'audio/aac'; + if (!MediaSource.isTypeSupported(mime)) { + mime = 'audio/mpeg'; + if (!MediaSource.isTypeSupported(mime)) { + assert_unreached("Browser does not support the audio/aac and audio/mpeg MIME types used in this test"); + } + } + sourceBuffer = mediaSource.addSourceBuffer(mime); + assert_equals(sourceBuffer.mode, "sequence"); + test.done(); + }, "addSourceBuffer() sets SourceBuffer.mode to 'sequence' when the generate timestamps flag is true"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mediasource-addsourcebuffer.html b/testing/web-platform/tests/media-source/mediasource-addsourcebuffer.html new file mode 100644 index 0000000000..a95155aefc --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-addsourcebuffer.html @@ -0,0 +1,133 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MediaSource.addSourceBuffer() test cases</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaSource.endOfStream(); + assert_throws_dom("InvalidStateError", + function() { mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); }, + "addSourceBuffer() threw an exception when in 'ended' state."); + test.done(); + }, "Test addSourceBuffer() in 'ended' state."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + assert_throws_js(TypeError, + function() { mediaSource.addSourceBuffer(""); }, + "addSourceBuffer() threw an exception when passed an empty string."); + test.done(); + }, "Test addSourceBuffer() with empty type"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + assert_throws_dom("NotSupportedError", + function() { mediaSource.addSourceBuffer(null); }, + "addSourceBuffer() threw an exception when passed null."); + test.done(); + }, "Test addSourceBuffer() with null"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + assert_throws_dom("NotSupportedError", + function() { mediaSource.addSourceBuffer("invalidType"); }, + "addSourceBuffer() threw an exception for an unsupported type."); + test.done(); + }, "Test addSourceBuffer() with unsupported type"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var mimetype = 'video/webm;codecs="vp8,vorbis"'; + + assert_true(MediaSource.isTypeSupported(mimetype), mimetype + " is supported"); + + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + assert_equals(mediaSource.sourceBuffers[0], sourceBuffer, "SourceBuffer is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBuffer is not in mediaSource.activeSourceBuffers"); + test.done(); + }, "Test addSourceBuffer() with Vorbis and VP8"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var videoMimetype = 'video/webm;codecs="vp8"'; + var audioMimetype = 'audio/webm;codecs="vorbis"'; + + assert_true(MediaSource.isTypeSupported(videoMimetype), videoMimetype + " is supported"); + assert_true(MediaSource.isTypeSupported(audioMimetype), audioMimetype + " is supported"); + + var sourceBufferA = mediaSource.addSourceBuffer(videoMimetype); + var sourceBufferB = mediaSource.addSourceBuffer(audioMimetype); + assert_equals(mediaSource.sourceBuffers[0], sourceBufferA, "sourceBufferA is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBufferA is not in mediaSource.activeSourceBuffers"); + assert_equals(mediaSource.sourceBuffers[1], sourceBufferB, "sourceBufferB is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBufferB is not in mediaSource.activeSourceBuffers"); + test.done(); + }, "Test addSourceBuffer() with Vorbis and VP8 in separate SourceBuffers"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var mimetype = MediaSourceUtil.VIDEO_ONLY_TYPE; + + assert_true(MediaSource.isTypeSupported(mimetype), mimetype + " is supported"); + + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + assert_equals(mediaSource.sourceBuffers[0], sourceBuffer, "SourceBuffer is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBuffer is not in mediaSource.activeSourceBuffers"); + test.done(); + }, "Test addSourceBuffer() video only"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var mimetype = MediaSourceUtil.AUDIO_ONLY_TYPE; + + assert_true(MediaSource.isTypeSupported(mimetype), mimetype + " is supported"); + + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + assert_equals(mediaSource.sourceBuffers[0], sourceBuffer, "SourceBuffer is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBuffer is not in mediaSource.activeSourceBuffers"); + test.done(); + }, "Test addSourceBuffer() audio only"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var mimetype = 'video/mp4;codecs="avc1.4D4001,mp4a.40.2"'; + + assert_true(MediaSource.isTypeSupported(mimetype), mimetype + " is supported"); + + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + assert_equals(mediaSource.sourceBuffers[0], sourceBuffer, "SourceBuffer is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBuffer is not in mediaSource.activeSourceBuffers"); + test.done(); + }, "Test addSourceBuffer() with AAC and H.264"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var videoMimetype = 'video/mp4;codecs="avc1.4D4001"'; + var audioMimetype = 'audio/mp4;codecs="mp4a.40.2"'; + + assert_true(MediaSource.isTypeSupported(videoMimetype), videoMimetype + " is supported"); + assert_true(MediaSource.isTypeSupported(audioMimetype), audioMimetype + " is supported"); + + var sourceBufferA = mediaSource.addSourceBuffer(videoMimetype); + var sourceBufferB = mediaSource.addSourceBuffer(audioMimetype); + assert_equals(mediaSource.sourceBuffers[0], sourceBufferA, "sourceBufferA is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBufferA is not in mediaSource.activeSourceBuffers"); + assert_equals(mediaSource.sourceBuffers[1], sourceBufferB, "sourceBufferB is in mediaSource.sourceBuffers"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "SourceBufferB is not in mediaSource.activeSourceBuffers"); + test.done(); + }, "Test addSourceBuffer() with AAC and H.264 in separate SourceBuffers"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-append-buffer.html b/testing/web-platform/tests/media-source/mediasource-append-buffer.html new file mode 100644 index 0000000000..750ccaf456 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-append-buffer.html @@ -0,0 +1,623 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>SourceBuffer.appendBuffer() test cases</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test SourceBuffer.appendBuffer() event dispatching."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.appendBuffer(mediaData); }, + "appendBuffer() throws an exception there is a pending append."); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test SourceBuffer.appendBuffer() call during a pending appendBuffer()."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "abort", "Append aborted."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + sourceBuffer.abort(); + + assert_false(sourceBuffer.updating, "updating attribute is false"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test SourceBuffer.abort() call during a pending appendBuffer()."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + + test.expectEvent(mediaSource, "sourceended", "MediaSource sourceended event"); + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, "ended", "MediaSource readyState is 'ended'"); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "ended", "MediaSource readyState is 'ended'"); + + test.expectEvent(mediaSource, "sourceopen", "MediaSource sourceopen event"); + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_equals(mediaSource.readyState, "open", "MediaSource readyState is 'open'"); + assert_true(sourceBuffer.updating, "updating attribute is true"); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "open", "MediaSource readyState is 'open'"); + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test SourceBuffer.appendBuffer() triggering an 'ended' to 'open' transition."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + + test.expectEvent(mediaSource, "sourceended", "MediaSource sourceended event"); + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, "ended", "MediaSource readyState is 'ended'"); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "ended", "MediaSource readyState is 'ended'"); + + test.expectEvent(mediaSource, "sourceopen", "MediaSource sourceopen event"); + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(new Uint8Array(0)); + + assert_equals(mediaSource.readyState, "open", "MediaSource readyState is 'open'"); + assert_true(sourceBuffer.updating, "updating attribute is true"); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "open", "MediaSource readyState is 'open'"); + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test zero byte SourceBuffer.appendBuffer() call triggering an 'ended' to 'open' transition."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "abort", "Append aborted."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "activeSourceBuffers.length"); + + test.expectEvent(mediaSource.sourceBuffers, "removesourcebuffer", "sourceBuffers"); + mediaSource.removeSourceBuffer(sourceBuffer); + + assert_false(sourceBuffer.updating, "updating attribute is false"); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.appendBuffer(mediaData); }, + "appendBuffer() throws an exception because it isn't attached to the mediaSource anymore."); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test MediaSource.removeSourceBuffer() call during a pending appendBuffer()."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + assert_throws_dom("InvalidStateError", + function() { mediaSource.duration = 1.0; }, + "set duration throws an exception when updating attribute is true."); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test set MediaSource.duration during a pending appendBuffer() for one of its SourceBuffers."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + mediaSource.addEventListener("sourceended", test.unreached_func("Unexpected event 'sourceended'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + assert_throws_dom("InvalidStateError", + function() { mediaSource.endOfStream(); }, + "endOfStream() throws an exception when updating attribute is true."); + + assert_equals(mediaSource.readyState, "open"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + assert_equals(mediaSource.readyState, "open"); + test.done(); + }); + }, "Test MediaSource.endOfStream() during a pending appendBuffer() for one of its SourceBuffers."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.timestampOffset = 10.0; }, + "set timestampOffset throws an exception when updating attribute is true."); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test set SourceBuffer.timestampOffset during a pending appendBuffer()."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(new Uint8Array(0)); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test appending an empty ArrayBufferView."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + + var arrayBufferView = new Uint8Array(mediaData); + + assert_equals(arrayBufferView.length, mediaData.length, "arrayBufferView.length before transfer."); + + // Send the buffer as in a message so it gets neutered. + window.postMessage( "test", "*", [arrayBufferView.buffer]); + + assert_equals(arrayBufferView.length, 0, "arrayBufferView.length after transfer."); + + sourceBuffer.appendBuffer(arrayBufferView); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test appending a neutered ArrayBufferView."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendBuffer(new ArrayBuffer(0)); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test appending an empty ArrayBuffer."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + + var arrayBuffer = mediaData.buffer.slice(0); + + assert_equals(arrayBuffer.byteLength, mediaData.buffer.byteLength, "arrayBuffer.byteLength before transfer."); + + // Send the buffer as in a message so it gets neutered. + window.postMessage( "test", "*", [arrayBuffer]); + + assert_equals(arrayBuffer.byteLength, 0, "arrayBuffer.byteLength after transfer."); + + sourceBuffer.appendBuffer(arrayBuffer); + + assert_true(sourceBuffer.updating, "updating attribute is true"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test appending a neutered ArrayBuffer."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var halfIndex = (initSegment.length + 1) / 2; + var partialInitSegment = initSegment.subarray(0, halfIndex); + var remainingInitSegment = initSegment.subarray(halfIndex); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + test.expectEvent(sourceBuffer, "updateend", "partialInitSegment append ended."); + sourceBuffer.appendBuffer(partialInitSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, mediaElement.HAVE_NOTHING); + assert_equals(mediaSource.duration, Number.NaN); + test.expectEvent(sourceBuffer, "updateend", "remainingInitSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "loadedmetadata event received."); + sourceBuffer.appendBuffer(remainingInitSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA); + assert_equals(mediaSource.duration, segmentInfo.duration); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadeddata", "loadeddata fired."); + sourceBuffer.appendBuffer(mediaSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA); + assert_equals(sourceBuffer.updating, false); + assert_equals(mediaSource.readyState, "open"); + test.done(); + }); + }, "Test appendBuffer with partial init segments."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + var halfIndex = (mediaSegment.length + 1) / 2; + var partialMediaSegment = mediaSegment.subarray(0, halfIndex); + var remainingMediaSegment = mediaSegment.subarray(halfIndex); + + test.expectEvent(sourceBuffer, "updateend", "InitSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "loadedmetadata done."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA); + assert_equals(mediaSource.duration, segmentInfo.duration); + test.expectEvent(sourceBuffer, "updateend", "partial media segment append ended."); + sourceBuffer.appendBuffer(partialMediaSegment); + }); + + test.waitForExpectedEvents(function() + { + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadeddata", "loadeddata fired."); + sourceBuffer.appendBuffer(remainingMediaSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA); + assert_equals(mediaSource.readyState, "open"); + assert_equals(sourceBuffer.updating, false); + test.done(); + }); + }, "Test appendBuffer with partial media segments."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + assert_equals(mediaElement.readyState, mediaElement.HAVE_NOTHING); + assert_equals(mediaSource.duration, Number.NaN); + + // readyState is changing as per the Initialization Segment Received algorithm. + var loadedmetadataCalled = false; + mediaElement.addEventListener("loadedmetadata", function metadata(e) { + loadedmetadataCalled = true; + e.target.removeEventListener(e.type, metadata); + }); + sourceBuffer.addEventListener("updateend", test.step_func(function updateend(e) { + assert_true(loadedmetadataCalled); + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA); + e.target.removeEventListener(e.type, updateend); + })); + test.expectEvent(sourceBuffer, "updateend", "remainingInitSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "loadedmetadata event received."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA); + assert_equals(mediaSource.duration, segmentInfo.duration); + // readyState is changing as per the Coded Frame Processing algorithm. + var loadeddataCalled = false; + mediaElement.addEventListener("loadeddata", function loadeddata(e) { + loadeddataCalled = true; + e.target.removeEventListener(e.type, loadeddata); + }); + sourceBuffer.addEventListener("updateend", test.step_func(function updateend(e) { + assert_true(loadeddataCalled); + assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA); + e.target.removeEventListener(e.type, updateend); + })); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadeddata", "loadeddata fired."); + sourceBuffer.appendBuffer(mediaSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA); + assert_equals(sourceBuffer.updating, false); + assert_equals(mediaSource.readyState, "open"); + test.done(); + }); + }, "Test appendBuffer events order."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var partialInitSegment = initSegment.subarray(0, initSegment.length / 2); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + test.expectEvent(sourceBuffer, "updateend", "partialInitSegment append ended."); + sourceBuffer.appendBuffer(partialInitSegment); + + test.waitForExpectedEvents(function() + { + // Call abort to reset the parser. + sourceBuffer.abort(); + + // Append the full intiialization segment. + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + }); + + test.waitForExpectedEvents(function() + { + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadeddata", "loadeddata fired."); + sourceBuffer.appendBuffer(mediaSegment); + }); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + }, "Test abort in the middle of an initialization segment."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(mediaSource.sourceBuffers, "removesourcebuffer", "SourceBuffer removed."); + mediaSource.removeSourceBuffer(sourceBuffer); + test.waitForExpectedEvents(function() + { + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.abort(); }, + "sourceBuffer.abort() throws an exception for InvalidStateError."); + + test.done(); + }); + }, "Test abort after removing sourcebuffer."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "open", "readyState is open after init segment appended."); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + sourceBuffer.appendBuffer(mediaSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(sourceBuffer.buffered.length, 1, "sourceBuffer has a buffered range"); + assert_equals(mediaSource.readyState, "open", "readyState is open after media segment appended."); + test.expectEvent(mediaSource, "sourceended", "source ended"); + mediaSource.endOfStream(); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "ended", "readyState is ended."); + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.abort(); }, + "sourceBuffer.abort() throws an exception for InvalidStateError."); + test.done(); + }); + + }, "Test abort after readyState is ended following init segment and media segment."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + sourceBuffer.appendWindowStart = 1; + sourceBuffer.appendWindowEnd = 100; + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + sourceBuffer.abort(); + assert_equals(sourceBuffer.appendWindowStart, 0, "appendWindowStart is reset to 0"); + assert_equals(sourceBuffer.appendWindowEnd, Number.POSITIVE_INFINITY, + "appendWindowEnd is reset to +INFINITY"); + test.done(); + }); + }, "Test abort after appendBuffer update ends."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + + test.expectEvent(sourceBuffer, "updatestart", "Append started."); + test.expectEvent(sourceBuffer, "update", "Append success."); + test.expectEvent(sourceBuffer, "updateend", "Append ended."); + + assert_throws_js( TypeError, + function() { sourceBuffer.appendBuffer(null); }, + "appendBuffer(null) throws an exception."); + test.done(); + }, "Test appending null."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaSource.removeSourceBuffer(sourceBuffer); + + assert_throws_dom( "InvalidStateError", + function() { sourceBuffer.appendBuffer(mediaData); }, + "appendBuffer() throws an exception when called after removeSourceBuffer()."); + test.done(); + }, "Test appending after removeSourceBuffer()."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + // Media elements using MSE should not fire the stalled event. See discussion at + // https://github.com/w3c/media-source/issues/88#issuecomment-374406928 + mediaElement.addEventListener("stalled", test.unreached_func("Unexpected 'stalled' event.")); + + // Prime the media element with initial appends. + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "open", "readyState is open after init segment appended."); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + sourceBuffer.appendBuffer(mediaSegment); + }); + + // Verify state and wait for the 'stalled' event. + test.waitForExpectedEvents(function() + { + assert_equals(sourceBuffer.buffered.length, 1, "sourceBuffer has a buffered range"); + assert_equals(mediaSource.readyState, "open", "readyState is open after media segment appended."); + + // Set timeout to 4 seconds. This creates an opportunity for UAs to _improperly_ fire the stalled event. + // For media elements doing progressive download (not MSE), stalled is thrown after ~3 seconds of the + // download failing to progress. + test.step_timeout(function() { test.done(); }, 4000); + }); + }, "Test slow appending does not trigger stalled events."); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-appendbuffer-quota-exceeded.html b/testing/web-platform/tests/media-source/mediasource-appendbuffer-quota-exceeded.html new file mode 100644 index 0000000000..c90d8448c5 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-appendbuffer-quota-exceeded.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + var subType = MediaSourceUtil.getSubType(MediaSourceUtil.AUDIO_ONLY_TYPE); + var manifestFilenameAudio = subType + '/test-a-128k-44100Hz-1ch-manifest.json'; + + // Fill up a given SourceBuffer by appending data repeatedly via doAppendDataFunc until + // an exception is thrown. The thrown exception is passed to onCaughtExceptionCallback. + function fillUpSourceBuffer(test, sourceBuffer, doAppendDataFunc, onCaughtExceptionCallback) { + assert_false(sourceBuffer.updating, 'updating should be false before attempting an append operation'); + + // We are appending data repeatedly in sequence mode, there should be no gaps. + let sbLength = sourceBuffer.buffered.length; + assert_false(sbLength > 1, 'unexpected gap in buffered ranges.'); + + let previousBufferedStart = sbLength == 0 ? -Infinity : sourceBuffer.buffered.start(0); + let previousBufferedEnd = sbLength == 0 ? -Infinity : sourceBuffer.buffered.end(0); + let appendSucceeded = true; + try { + doAppendDataFunc(); + } catch(ex) { + onCaughtExceptionCallback(ex, previousBufferedStart, previousBufferedEnd); + appendSucceeded = false; + } + if (appendSucceeded) { + assert_true(sourceBuffer.updating, 'updating should be true if synchronous portion of appendBuffer succeeded'); + test.expectEvent(sourceBuffer, 'updateend', 'append ended.'); + test.waitForExpectedEvents(function() { fillUpSourceBuffer(test, sourceBuffer, doAppendDataFunc, onCaughtExceptionCallback); }); + } + } + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener('error', test.unreached_func('Unexpected media element error event')); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAudio, function(typeAudio, dataAudio) + { + var sourceBuffer = mediaSource.addSourceBuffer(typeAudio); + sourceBuffer.mode = 'sequence'; + fillUpSourceBuffer(test, sourceBuffer, + function () { // doAppendDataFunc + sourceBuffer.appendBuffer(dataAudio); + }, + function (ex, previousBufferedStart, previousBufferedEnd) { // onCaughtExceptionCallback + assert_equals(ex.name, 'QuotaExceededError'); + // Verify that the state looks like appendBuffer stops executing its steps if the prepare append + // algorithm aborts after throwing `QuotaExceededError`. + + assert_false(sourceBuffer.updating, 'updating should be false if appendBuffer throws QuotaExceededError'); + + sourceBuffer.onupdatestart = test.unreached_func('buffer append, signalled by updatestart, should not be in progress'); + sourceBuffer.onupdate = test.unreached_func('buffer append, signalled by update, should not be in progress'); + sourceBuffer.onupdateend = test.unreached_func('buffer append, signalled by updateend, should not be in progress'); + + // Ensure the async append was not actually run by letting their event handlers have some time before we proceed. + test.step_timeout(function() { + // At least the first doAppendDataFunc call shouldn't throw QuotaExceededError, unless the audio + // test media itself is too large for one append. If that's the case, then many other tests should + // fail and the choice of test media may need to be changed. + assert_equals(sourceBuffer.buffered.length, 1, 'test expects precisely one buffered range here'); + assert_equals(sourceBuffer.buffered.start(0), previousBufferedStart, 'QuotaExceededError should not update buffered media'); + assert_equals(sourceBuffer.buffered.end(0), previousBufferedEnd, 'QuotaExceededError should not update buffered media'); + + // Note, it's difficult to verify that the user agent does not "Add |data| to the end of the |input buffer|" if + // the attempted appendBuffer() of that |data| caused QuotaExceededError. + test.done(); + }, 1000 /* 1 second, modifiable by harness multiplier */ ); + }); + }); + }, 'Appending data repeatedly should fill up the buffer and throw a QuotaExceededError when buffer is full.'); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-appendwindow.html b/testing/web-platform/tests/media-source/mediasource-appendwindow.html new file mode 100644 index 0000000000..ba15bd6322 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-appendwindow.html @@ -0,0 +1,176 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>SourceBuffer.appendWindowStart and SourceBuffer.appendWindowEnd test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + + sourceBuffer.appendWindowStart = 100.0; + sourceBuffer.appendWindowEnd = 500.0; + assert_equals(sourceBuffer.appendWindowStart, 100.0, "appendWindowStart is correctly set'"); + assert_equals(sourceBuffer.appendWindowEnd, 500.0, "appendWindowEnd is correctly set'"); + + sourceBuffer.appendWindowStart = 200.0; + sourceBuffer.appendWindowEnd = 400.0; + assert_equals(sourceBuffer.appendWindowStart, 200.0, "appendWindowStart is correctly reset'"); + assert_equals(sourceBuffer.appendWindowEnd, 400.0, "appendWindowEnd is correctly reset'"); + test.done(); + }, "Test correctly reset appendWindowStart and appendWindowEnd values"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + sourceBuffer.appendWindowEnd = 500.0; + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = Number.NEGATIVE_INFINITY; }, + "set appendWindowStart throws an exception for Number.NEGATIVE_INFINITY."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = Number.POSITIVE_INFINITY; }, + "set appendWindowStart throws an exception for Number.POSITIVE_INFINITY."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = Number.NaN; }, + "set appendWindowStart throws an exception for Number.NaN."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = 600.0; }, + "set appendWindowStart throws an exception when greater than appendWindowEnd."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = sourceBuffer.appendWindowEnd; }, + "set appendWindowStart throws an exception when equal to appendWindowEnd."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowEnd = sourceBuffer.appendWindowStart; }, + "set appendWindowEnd throws an exception when equal to appendWindowStart."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowEnd = sourceBuffer.appendWindowStart - 1; }, + "set appendWindowEnd throws an exception if less than appendWindowStart."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = -100.0; }, + "set appendWindowStart throws an exception when less than 0."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowEnd = -100.0; }, + "set appendWindowEnd throws an exception when less than 0."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowEnd = Number.NaN; }, + "set appendWindowEnd throws an exception if NaN."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowEnd = undefined; }, + "set appendWindowEnd throws an exception if undefined."); + + assert_throws_js(TypeError, + function() { sourceBuffer.appendWindowStart = undefined; }, + "set appendWindowStart throws an exception if undefined."); + + test.done(); + }, "Test set wrong values to appendWindowStart and appendWindowEnd."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + + sourceBuffer.appendWindowStart = ""; + assert_equals(sourceBuffer.appendWindowStart, 0, "appendWindowStart is 0"); + + sourceBuffer.appendWindowStart = "10"; + assert_equals(sourceBuffer.appendWindowStart, 10, "appendWindowStart is 10"); + + sourceBuffer.appendWindowStart = null; + assert_equals(sourceBuffer.appendWindowStart, 0, "appendWindowStart is 0"); + + sourceBuffer.appendWindowStart = true; + assert_equals(sourceBuffer.appendWindowStart, 1, "appendWindowStart is 1"); + + sourceBuffer.appendWindowStart = false; + assert_equals(sourceBuffer.appendWindowStart, 0, "appendWindowStart is 0"); + + sourceBuffer.appendWindowEnd = "100"; + assert_equals(sourceBuffer.appendWindowEnd, 100, "appendWindowEnd is 100"); + + test.done(); + + }, "Test set correct values to appendWindowStart and appendWindowEnd."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaSource.removeSourceBuffer(sourceBuffer); + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.appendWindowStart = 100.0; }, + "set appendWindowStart throws an exception when mediasource object is not associated with a buffer."); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.appendWindowEnd = 500.0; }, + "set appendWindowEnd throws an exception when mediasource object is not associated with a buffer."); + test.done(); + + }, "Test appendwindow throw error when mediasource object is not associated with a sourebuffer."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updateend", "sourceBuffer"); + sourceBuffer.appendBuffer(mediaData); + assert_true(sourceBuffer.updating, "updating attribute is true"); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.appendWindowStart = 100.0; }, + "set appendWindowStart throws an exception when there is a pending append."); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.appendWindowEnd = 500.0; }, + "set appendWindowEnd throws an exception when there is a pending append."); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test set appendWindowStart and appendWindowEnd when source buffer updating."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updateend", "sourceBuffer"); + sourceBuffer.appendBuffer(mediaData); + assert_true(sourceBuffer.updating, "updating attribute is true"); + + sourceBuffer.abort(); + assert_equals(sourceBuffer.appendWindowStart, 0, "appendWindowStart is 0 after an abort'"); + assert_equals(sourceBuffer.appendWindowEnd, Number.POSITIVE_INFINITY, + "appendWindowStart is POSITIVE_INFINITY after an abort"); + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.done(); + }); + }, "Test appendWindowStart and appendWindowEnd value after a sourceBuffer.abort()."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_equals(sourceBuffer.appendWindowStart, 0, "appendWindowStart is 0 initially"); + assert_equals(sourceBuffer.appendWindowEnd, Number.POSITIVE_INFINITY, + "appendWindowStart is POSITIVE_INFINITY initially"); + test.done(); + }, "Test read appendWindowStart and appendWindowEnd initial values."); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-attach-stops-delaying-load-event.html b/testing/web-platform/tests/media-source/mediasource-attach-stops-delaying-load-event.html new file mode 100644 index 0000000000..6b5d5b0ad1 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-attach-stops-delaying-load-event.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<title>Tests that MediaSource attachment stops delaying the load event.</title> +<link rel="author" title="Matthew Wolenetz" href="mailto:wolenetz@chromium.org"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +async_test(function(test) +{ + var receivedLoadEvent = false; + var receivedSourceOpenEvent = false; + + window.addEventListener("load", test.step_func(function() { + assert_false(receivedLoadEvent, "window should not receive multiple load events"); + receivedLoadEvent = true; + assert_equals(document.readyState, "complete", "document should be complete"); + if (receivedLoadEvent && receivedSourceOpenEvent) { + test.done(); + } + })); + + assert_equals(document.readyState, "loading", "document should not be complete yet"); + var video = document.createElement("video"); + var mediaSource = new MediaSource(); + + // |video| should stop delaying the load event long and complete the MediaSource attachment + // before either a "progress", "stalled" or "suspend" event are enqueued. + video.addEventListener("suspend", test.unreached_func("unexpected 'suspend' event")); + video.addEventListener("stalled", test.unreached_func("unexpected 'stalled' event")); + video.addEventListener("progress", test.unreached_func("unexpected 'progress' event")); + + // No error is expected. + video.addEventListener("error", test.unreached_func("unexpected 'error' event")); + + mediaSource.addEventListener("sourceopen", test.step_func(function() { + assert_false(receivedSourceOpenEvent, "only one sourceopen event should occur in this test"); + receivedSourceOpenEvent = true; + assert_equals(video.networkState, video.NETWORK_LOADING); + assert_equals(video.readyState, video.HAVE_NOTHING); + if (receivedLoadEvent && receivedSourceOpenEvent) { + test.done(); + } + })); + + var mediaSourceURL = URL.createObjectURL(mediaSource); + video.src = mediaSourceURL; + test.add_cleanup(function() { URL.revokeObjectURL(mediaSourceURL); }); +}, "MediaSource attachment should immediately stop delaying the load event"); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-avtracks.html b/testing/web-platform/tests/media-source/mediasource-avtracks.html new file mode 100644 index 0000000000..26ae5bcfe3 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-avtracks.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + sourceBuffer.appendBuffer(initSegment); + test.expectEvent(sourceBuffer.audioTracks, "addtrack", "sourceBuffer.audioTracks addtrack event"); + test.expectEvent(sourceBuffer.videoTracks, "addtrack", "sourceBuffer.videoTracks addtrack event"); + test.expectEvent(mediaElement.audioTracks, "addtrack", "mediaElement.audioTracks addtrack event"); + test.expectEvent(mediaElement.videoTracks, "addtrack", "mediaElement.videoTracks addtrack event"); + test.expectEvent(mediaElement, "loadedmetadata", "loadedmetadata done."); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + test.waitForExpectedEvents(function() + { + assert_equals(sourceBuffer.videoTracks.length, 1, "videoTracks.length"); + assert_equals(sourceBuffer.videoTracks[0].kind, "main", "videoTrack.kind"); + assert_equals(sourceBuffer.videoTracks[0].label, "", "videoTrack.label"); + assert_equals(sourceBuffer.videoTracks[0].language, "eng", "videoTrack.language"); + assert_equals(sourceBuffer.videoTracks[0].sourceBuffer, sourceBuffer, "videoTrack.sourceBuffer"); + // The first video track is selected by default. + assert_true(sourceBuffer.videoTracks[0].selected, "sourceBuffer.videoTracks[0].selected"); + + assert_equals(sourceBuffer.audioTracks.length, 1, "audioTracks.length"); + assert_equals(sourceBuffer.audioTracks[0].kind, "main", "audioTrack.kind"); + assert_equals(sourceBuffer.audioTracks[0].label, "", "audioTrack.label"); + assert_equals(sourceBuffer.audioTracks[0].language, "eng", "audioTrack.language"); + assert_equals(sourceBuffer.audioTracks[0].sourceBuffer, sourceBuffer, "audioTrack.sourceBuffer"); + // The first audio track is enabled by default. + assert_true(sourceBuffer.audioTracks[0].enabled, "sourceBuffer.audioTracks[0].enabled"); + + assert_not_equals(sourceBuffer.audioTracks[0].id, sourceBuffer.videoTracks[0].id, "track ids must be unique"); + + assert_equals(mediaElement.videoTracks.length, 1, "videoTracks.length"); + assert_equals(mediaElement.videoTracks[0], sourceBuffer.videoTracks[0], "mediaElement.videoTrack == sourceBuffer.videoTrack"); + + assert_equals(mediaElement.audioTracks.length, 1, "audioTracks.length"); + assert_equals(mediaElement.audioTracks[0], sourceBuffer.audioTracks[0], "mediaElement.audioTrack == sourceBuffer.audioTrack"); + + test.done(); + }); + }, "Check that media tracks and their properties are populated properly"); + + function verifyTrackRemoval(test, mediaElement, mediaSource, sourceBuffer, trackRemovalAction, successCallback) { + assert_equals(sourceBuffer.audioTracks.length, 1, "audioTracks.length"); + assert_true(sourceBuffer.audioTracks[0].enabled, "sourceBuffer.audioTracks[0].enabled"); + assert_equals(sourceBuffer.videoTracks.length, 1, "videoTracks.length"); + assert_true(sourceBuffer.videoTracks[0].selected, "sourceBuffer.videoTracks[0].selected"); + + var audioTrack = sourceBuffer.audioTracks[0]; + var videoTrack = sourceBuffer.videoTracks[0]; + + // Verify removetrack events. + test.expectEvent(sourceBuffer.audioTracks, "removetrack", "sourceBuffer.audioTracks removetrack event"); + test.expectEvent(sourceBuffer.videoTracks, "removetrack", "sourceBuffer.videoTracks removetrack event"); + test.expectEvent(mediaElement.audioTracks, "removetrack", "mediaElement.audioTracks removetrack event"); + test.expectEvent(mediaElement.videoTracks, "removetrack", "mediaElement.videoTracks removetrack event"); + + // Removing enabled audio track and selected video track should fire "change" events on mediaElement track lists. + test.expectEvent(mediaElement.audioTracks, "change", "mediaElement.audioTracks changed."); + test.expectEvent(mediaElement.videoTracks, "change", "mediaElement.videoTracks changed."); + + trackRemovalAction(); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.sourceBuffers.length, 0, "mediaSource.sourceBuffers.length"); + assert_equals(mediaElement.videoTracks.length, 0, "videoTracks.length"); + assert_equals(mediaElement.audioTracks.length, 0, "audioTracks.length"); + assert_equals(sourceBuffer.videoTracks.length, 0, "videoTracks.length"); + assert_equals(sourceBuffer.audioTracks.length, 0, "audioTracks.length"); + // Since audio and video tracks have been removed, their .sourceBuffer property should be null now. + assert_equals(audioTrack.sourceBuffer, null, "audioTrack.sourceBuffer"); + assert_equals(videoTrack.sourceBuffer, null, "videoTrack.sourceBuffer"); + test.done(); + }); + } + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + sourceBuffer.appendBuffer(initSegment); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + test.waitForExpectedEvents(function() + { + verifyTrackRemoval(test, mediaElement, mediaSource, sourceBuffer, test.step_func(function () + { + mediaSource.removeSourceBuffer(sourceBuffer); + })); + }); + }, "Media tracks must be removed when the SourceBuffer is removed from the MediaSource"); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + sourceBuffer.appendBuffer(initSegment); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + test.waitForExpectedEvents(function() + { + verifyTrackRemoval(test, mediaElement, mediaSource, sourceBuffer, test.step_func(function () + { + mediaElement.src = ""; + })); + }); + }, "Media tracks must be removed when the HTMLMediaElement.src is changed"); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + sourceBuffer.appendBuffer(initSegment); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + test.waitForExpectedEvents(function() + { + verifyTrackRemoval(test, mediaElement, mediaSource, sourceBuffer, test.step_func(function () + { + mediaElement.load(); + })); + }); + }, "Media tracks must be removed when HTMLMediaElement.load() is called"); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-buffered.html b/testing/web-platform/tests/media-source/mediasource-buffered.html new file mode 100644 index 0000000000..159eaf532f --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-buffered.html @@ -0,0 +1,244 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>SourceBuffer.buffered test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + var subType = MediaSourceUtil.getSubType(MediaSourceUtil.AUDIO_ONLY_TYPE); + + var manifestFilenameA = subType + "/test-a-128k-44100Hz-1ch-manifest.json"; + var manifestFilenameB = subType + "/test-v-128k-320x240-30fps-10kfr-manifest.json"; + + // Audio track expectations + var expectationsA = { + webm: "{ [0.000, 2.023) }", + mp4: "{ [0.000, 2.043) }", + }; + + // Video track expectations + var expectationsB = { + webm: "{ [0.000, 2.001) }", + mp4: "{ [0.067, 2.067) }", + }; + + // Audio and Video intersection expectations. + // https://w3c.github.io/media-source/index.html#dom-sourcebuffer-buffered + // When mediaSource.readyState is "ended", then set the end time on the last range in track ranges to highest end time. + var expectationsC = { + webm: ["{ [0.000, 2.001) }", "{ [0.000, 2.023) }"], + mp4: ["{ [0.067, 2.043) }", "{ [0.067, 2.067) }"] + }; + + function mediaSourceDemuxedTest(callback, description) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.pause(); + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener("ended", test.step_func_done()); + + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameA, function(typeA, dataA) + { + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameB, function(typeB, dataB) + { + mediaSource.addSourceBuffer(typeA); + mediaSource.addSourceBuffer(typeB); + assert_equals(mediaSource.sourceBuffers.length, 2); + + callback(test, mediaElement, mediaSource, dataA, dataB); + }); + }); + }, description); + }; + + function appendData(test, mediaSource, dataA, dataB, callback) + { + var sourceBufferA = mediaSource.sourceBuffers[0]; + var sourceBufferB = mediaSource.sourceBuffers[1]; + + test.expectEvent(sourceBufferA, "update"); + test.expectEvent(sourceBufferA, "updateend"); + sourceBufferA.appendBuffer(dataA); + + test.expectEvent(sourceBufferB, "update"); + test.expectEvent(sourceBufferB, "updateend"); + sourceBufferB.appendBuffer(dataB); + + test.waitForExpectedEvents(function() + { + callback(); + }); + } + + mediaSourceDemuxedTest(function(test, mediaElement, mediaSource, dataA, dataB) { + test.expectEvent(mediaElement, "loadedmetadata"); + appendData(test, mediaSource, dataA, dataB, function() + { + var expectedBeforeEndOfStreamIntersection = expectationsC[subType][0]; + var expectedAfterEndOfStreamIntersection = expectationsC[subType][1]; + + assertBufferedEquals(mediaSource.activeSourceBuffers[0], expectationsA[subType], "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaSource.activeSourceBuffers[1], expectationsB[subType], "mediaSource.activeSourceBuffers[1]"); + assertBufferedEquals(mediaElement, expectedBeforeEndOfStreamIntersection, "mediaElement.buffered"); + + mediaSource.endOfStream(); + + assertBufferedEquals(mediaSource.activeSourceBuffers[0], expectationsA[subType], "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaSource.activeSourceBuffers[1], expectationsB[subType], "mediaSource.activeSourceBuffers[1]"); + assertBufferedEquals(mediaElement, expectedAfterEndOfStreamIntersection, "mediaElement.buffered"); + + test.done(); + }); + }, "Demuxed content with different lengths"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.pause(); + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener("ended", test.step_func_done()); + + MediaSourceUtil.fetchManifestAndData(test, subType + "/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json", function(type, data) + { + var sourceBuffer = mediaSource.addSourceBuffer(type); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(data); + + test.waitForExpectedEvents(function() + { + var expectationsAV = { + webm: ["{ [0.003, 2.004) }", "{ [0.003, 2.023) }"], + mp4: ["{ [0.067, 2.043) }", "{ [0.067, 2.067) }"], + }; + + var expectedBeforeEndOfStream = expectationsAV[subType][0]; + var expectedAfterEndOfStream = expectationsAV[subType][1]; + + assertBufferedEquals(mediaSource.activeSourceBuffers[0], expectedBeforeEndOfStream, "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaElement, expectedBeforeEndOfStream, "mediaElement.buffered"); + + mediaSource.endOfStream(); + + assertBufferedEquals(mediaSource.activeSourceBuffers[0], expectedAfterEndOfStream, "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaElement, expectedAfterEndOfStream, "mediaElement.buffered"); + + test.done(); + }); + }); + }, "Muxed content with different lengths"); + + mediaSourceDemuxedTest(function(test, mediaElement, mediaSource, dataA, dataB) { + var dataBSize = { + webm: 318, + mp4: 835, + }; + test.expectEvent(mediaElement, "loadedmetadata"); + appendData(test, mediaSource, dataA, dataB.subarray(0, dataBSize[subType]), function() + { + assertBufferedEquals(mediaSource.activeSourceBuffers[0], expectationsA[subType], "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaSource.activeSourceBuffers[1], "{ }", "mediaSource.activeSourceBuffers[1]"); + assertBufferedEquals(mediaElement, "{ }", "mediaElement.buffered"); + + mediaSource.endOfStream(); + + assertBufferedEquals(mediaSource.activeSourceBuffers[0], expectationsA[subType], "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaSource.activeSourceBuffers[1], "{ }", "mediaSource.activeSourceBuffers[1]"); + assertBufferedEquals(mediaElement, "{ }", "mediaElement.buffered"); + + test.done(); + }); + }, "Demuxed content with an empty buffered range on one SourceBuffer"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.pause(); + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener("ended", test.step_func_done()); + + MediaSourceUtil.fetchManifestAndData(test, subType + "/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json", function(type, data) + { + var sourceBuffer = mediaSource.addSourceBuffer(type); + test.expectEvent(mediaElement, "loadedmetadata"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(data.subarray(0, 4052)); + + test.waitForExpectedEvents(function() + { + assertBufferedEquals(mediaSource.activeSourceBuffers[0], "{ }", "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaElement, "{ }", "mediaElement.buffered"); + + mediaSource.endOfStream(); + + assertBufferedEquals(mediaSource.activeSourceBuffers[0], "{ }", "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaElement, "{ }", "mediaElement.buffered"); + + test.done(); + }); + }); + }, "Muxed content empty buffered ranges."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.pause(); + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener("ended", test.step_func_done()); + + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + assertBufferedEquals(mediaSource.sourceBuffers[0], "{ }", "mediaSource.sourceBuffers[0]"); + assertBufferedEquals(mediaElement, "{ }", "mediaElement.buffered"); + test.done(); + + }, "Get buffered range when sourcebuffer is empty."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + + test.expectEvent(mediaElement, "loadedmetadata"); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + test.waitForExpectedEvents(function() + { + assertBufferedEquals(mediaSource.sourceBuffers[0], "{ }", "mediaSource.sourceBuffers[0]"); + assertBufferedEquals(mediaSource.activeSourceBuffers[0], "{ }", "mediaSource.activeSourceBuffers[0]"); + assertBufferedEquals(mediaElement, "{ }", "mediaElement.buffered"); + test.done(); + }); + + }, "Get buffered range when only init segment is appended."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(mediaSource.sourceBuffers, "removesourcebuffer", "SourceBuffer removed."); + mediaSource.removeSourceBuffer(sourceBuffer); + + test.waitForExpectedEvents(function() + { + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.buffered; }, + "get sourceBuffer.buffered throws an exception for InvalidStateError."); + test.done(); + }); + }, "Get buffered range after removing sourcebuffer."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.pause(); + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener("ended", test.step_func_done()); + + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + assert_equals(mediaSource.sourceBuffers[0].buffered , mediaSource.sourceBuffers[0].buffered); + test.done(); + }, "buffered return the same object over multiple calls."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-changetype-play-implicit.html b/testing/web-platform/tests/media-source/mediasource-changetype-play-implicit.html new file mode 100644 index 0000000000..c186361e79 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-changetype-play-implicit.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- Copyright © 2019 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Exercise implicit changeType for supported test types, using mime types WITH and WITHOUT codecs for addSourceBuffer.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-changetype-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + +// Helper that generates implicit codec switching tests for a pair of media +// types, with full codecs in the original addSourceBuffer calls, and +// separately without full codecs parameters in the original addSourceBuffer +// calls. +function generateTestsForMediaPair(type1, type2) { + // Implicit changeType across bytestream formats is not expected to be + // supported, so skip those tests' generation. + if (type1.mime_subtype != type2.mime_subtype) + return; + + assert_equals(type1.is_video, type2.is_video, + "Types must both be audio or both be video"); + test_description_prefix = "Test " + (type1.is_video ? "video" : "audio") + + "-only implicit changeType for " + type1.type + " <-> " + type2.type; + + mediaSourceChangeTypeTest( + type1, type2, + test_description_prefix, + { implicit_changetype: true } ); + + // Skip test generation if the relaxed types are already fully specified and + // tested, above. + if (type1.type == type1.relaxed_type && + type2.type == type2.relaxed_type) { + return; + } + + mediaSourceChangeTypeTest( + type1, type2, + test_description_prefix + + " (using types without codecs parameters for addSourceBuffer)", + { use_relaxed_mime_types: true, implicit_changetype: true } ); +} + +function generateImplicitChangeTypeTests(audio_types, video_types) { + async_test((test) => { + // Require at least 1 pair of different audio-only or video-only test media + // files sharing same bytestream format. + assert_true(audio_types.length > 1 || video_types.length > 1, + "Browser doesn't support enough test media"); + + // Count the number of unique bytestream formats used in each of audio_types + // and video_types. + let audio_formats = new Set(Array.from(audio_types, t => t.mime_subtype)); + let video_formats = new Set(Array.from(video_types, t => t.mime_subtype)); + assert_true(audio_types.length > audio_formats.size || + video_types.length > video_formats.size, + "Browser doesn't support at least 2 audio-only or 2 video-only test " + + "media with same bytestream format"); + + test.done(); + }, "Check if browser supports enough test media types and pairs of " + + "audio-only or video-only media with same bytestream format"); + + // Generate audio-only tests + for (let audio1 of audio_types) { + for (let audio2 of audio_types) { + generateTestsForMediaPair(audio1, audio2); + } + } + + // Generate video-only tests + for (let video1 of video_types) { + for (let video2 of video_types) { + generateTestsForMediaPair(video1, video2); + } + } +} + +findSupportedChangeTypeTestTypes(generateImplicitChangeTypeTests); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-changetype-play-negative.html b/testing/web-platform/tests/media-source/mediasource-changetype-play-negative.html new file mode 100644 index 0000000000..f74e12945a --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-changetype-play-negative.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<!-- Copyright © 2019 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Exercise scenarios expected to fail for changeType for supported test types, using mime types WITH and WITHOUT codecs.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-changetype-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + +function generateNegativeChangeTypeTests(audio_types, video_types) { + async_test((test) => { + assert_true(audio_types.length > 0 && video_types.length > 0, + "Browser doesn't support at least one audio and one video test media for audio<->video changeType negative tests"); + + let audio_formats = new Set(Array.from(audio_types, t => t.mime_subtype)); + let video_formats = new Set(Array.from(video_types, t => t.mime_subtype)); + + let has_intersected_av_format = false; + for (let elem of audio_formats) { + if (video_formats.has(elem)) + has_intersected_av_format = true; + } + assert_true(has_intersected_av_format, + "Browser doesn't support at least 1 audio-only and 1 video-only test media with same bytestream formats"); + + test.done(); + }, "Check if browser supports enough test media types across audio and video for changeType negative tests"); + + // Generate audio<->video changeType tests that should not succeed in + // reaching successful end of playback because the class of media (audio or + // video) must remain the same across either an implicit or explicit + // changeType. + for (let audio_type of audio_types) { + for (let video_type of video_types) { + // For implicit changeType negative tests, only pairs of test media files + // using the same bytestream format are used, because it is not + // guaranteed that all implementations can be expected to reliably detect + // an implicit switch of bytestream format (for example, MP3 parsers + // might skip invalid input bytes without issuing error.) + let do_implicit_changetype = (audio_type.mime_subtype == + video_type.mime_subtype); + + mediaSourceChangeTypeTest( + audio_type, video_type, + "Negative test audio<->video changeType for " + + audio_type.type + " <-> " + video_type.type, + { negative_test: true } ); + mediaSourceChangeTypeTest( + video_type, audio_type, + "Negative test video<->audio changeType for " + + video_type.type + " <-> " + audio_type.type, + { negative_test: true } ); + + if (do_implicit_changetype) { + mediaSourceChangeTypeTest( + audio_type, video_type, + "Negative test audio<->video implicit changeType for " + + audio_type.type + " <-> " + video_type.type, + { implicit_changetype: true, negative_test: true } ); + mediaSourceChangeTypeTest( + video_type, audio_type, + "Negative test video<->audio implicit changeType for " + + video_type.type + " <-> " + audio_type.type, + { implicit_changetype: true, negative_test: true } ); + } + + // Skip tests where the relaxed type is already fully specified and + // tested, above. + if (audio_type.type == audio_type.relaxed_type && + video_type.type == video_type.relaxed_type) { + continue; + } + + mediaSourceChangeTypeTest( + audio_type, video_type, + "Negative test audio<->video changeType for " + + audio_type.type + " <-> " + video_type.type + + " (using types without codecs parameters)", + { use_relaxed_mime_types: true, negative_test: true } ); + mediaSourceChangeTypeTest( + video_type, audio_type, + "Negative test video<->audio changeType for " + + video_type.type + " <-> " + audio_type.type + + " (using types without codecs parameters)", + { use_relaxed_mime_types: true, negative_test: true } ); + + if (do_implicit_changetype) { + mediaSourceChangeTypeTest( + audio_type, video_type, + "Negative test audio<->video implicit changeType for " + + audio_type.type + " <-> " + video_type.type + + " (without codecs parameters for addSourceBuffer)", + { use_relaxed_mime_types: true, + implicit_changetype: true, + negative_test: true + } ); + + mediaSourceChangeTypeTest( + video_type, audio_type, + "Negative test video<->audio implicit changeType for " + + video_type.type + " <-> " + audio_type.type + + " (without codecs parameters for addSourceBuffer)", + { use_relaxed_mime_types: true, + implicit_changetype: true, + negative_test: true + } ); + } + } + } +} + +findSupportedChangeTypeTestTypes(generateNegativeChangeTypeTests); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-changetype-play-without-codecs-parameter.html b/testing/web-platform/tests/media-source/mediasource-changetype-play-without-codecs-parameter.html new file mode 100644 index 0000000000..f802b155f7 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-changetype-play-without-codecs-parameter.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<!-- Copyright © 2019 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Exercise changeType for supported test types, using mime types WITHOUT codecs for addSourceBuffer and changeType.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-changetype-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + +function generateChangeTypeTests(audio_types, video_types) { + async_test((test) => { + assert_true(audio_types.length > 1, "Browser doesn't support at least 2 types of audio test media"); + assert_true(video_types.length > 1, "Browser doesn't support at least 2 types of video test media"); + test.done(); + }, "Check if browser supports enough test media types"); + + // Generate audio-only changeType tests + for (let audio1 of audio_types) { + for (let audio2 of audio_types) { + mediaSourceChangeTypeTest( + audio1, audio2, + "Test audio-only changeType for " + + audio1.type + " <-> " + audio2.type + + " (using types without codecs parameters)", + { use_relaxed_mime_types: true } ); + } + } + + // Generate video-only changeType tests + for (let video1 of video_types) { + for (let video2 of video_types) { + mediaSourceChangeTypeTest( + video1, video2, + "Test video-only changeType for " + + video1.type + " <-> " + video2.type + + " (using types without codecs parameters)", + { use_relaxed_mime_types: true }); + } + } +} + +findSupportedChangeTypeTestTypes(generateChangeTypeTests); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-changetype-play.html b/testing/web-platform/tests/media-source/mediasource-changetype-play.html new file mode 100644 index 0000000000..26a67c3270 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-changetype-play.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- Copyright © 2018 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Exercise changeType for supported test types, using mime types WITH codecs (if applicable) for addSourceBuffer and changeType.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-changetype-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + +function generateChangeTypeTests(audio_types, video_types) { + async_test((test) => { + assert_true(audio_types.length > 1, "Browser doesn't support at least 2 types of audio test media"); + assert_true(video_types.length > 1, "Browser doesn't support at least 2 types of video test media"); + test.done(); + }, "Check if browser supports enough test media types"); + + // Generate audio-only changeType tests + for (let audio1 of audio_types) { + for (let audio2 of audio_types) { + mediaSourceChangeTypeTest( + audio1, audio2, + "Test audio-only changeType for " + + audio1.type + " <-> " + audio2.type); + } + } + + // Generate video-only changeType tests + for (let video1 of video_types) { + for (let video2 of video_types) { + mediaSourceChangeTypeTest( + video1, video2, + "Test video-only changeType for " + + video1.type + " <-> " + video2.type); + } + } +} + +findSupportedChangeTypeTestTypes(generateChangeTypeTests); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-changetype-util.js b/testing/web-platform/tests/media-source/mediasource-changetype-util.js new file mode 100644 index 0000000000..024af027ed --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-changetype-util.js @@ -0,0 +1,359 @@ +// Copyright © 2018 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). + +function findSupportedChangeTypeTestTypes(cb) { + // Changetype test media metadata. + // type: fully specified mime type (and codecs substring if the bytestream + // format does not forbid codecs parameter). This is required for use with + // isTypeSupported, and if supported, should work with both addSourceBuffer + // and changeType (unless implementation has restrictions). + // + // relaxed_type: possibly ambiguous mime type/subtype without any codecs + // substring. This is the same as type minus any codecs substring. + // + // mime_subtype: the subtype of the mime type in type and relaxed_type. Across + // types registered in the bytestream format registry + // (https://www.w3.org/TR/mse-byte-stream-format-registry/), this is + // currently sufficient to describe uniquely which test media share the same + // bytestream format for use in implicit changeType testing. + // + // is_video: All test media currently is single track. This describes whether + // or not the track is video. + // + // url: Relative location of the test media file. + // + // The next two items enable more reliable test media splicing test logic that + // prevents buffered range gaps at the splice points. + // start_time: Some test media begins at a time later than 0.0 seconds. This + // is the start time of the media. + // keyframe_interval: Some test media contains out-of-order PTS versus DTS + // coded frames. In those cases, a constant keyframe_interval is needed to + // prevent severely truncating out-of-order GOPs at splice points. + let CHANGE_TYPE_MEDIA_LIST = [ + { + type: 'video/webm; codecs="vp8"', + relaxed_type: 'video/webm', + mime_subtype: 'webm', + is_video: true, + url: 'webm/test-v-128k-320x240-24fps-8kfr.webm', + start_time: 0.0 + // keyframe_interval: N/A since DTS==PTS so overlap-removal of + // non-keyframe should not produce a buffered range gap. + }, + { + type: 'video/webm; codecs="vp9"', + relaxed_type: 'video/webm', + mime_subtype: 'webm', + is_video: true, + url: 'webm/test-vp9.webm', + start_time: 0.0 + // keyframe_interval: N/A since DTS==PTS so overlap-removal of + // non-keyframe should not produce a buffered range gap. + }, + { + type: 'video/mp4; codecs="avc1.4D4001"', + relaxed_type: 'video/mp4', + mime_subtype: 'mp4', + is_video: true, + url: 'mp4/test-v-128k-320x240-24fps-8kfr.mp4', + start_time: 0.083333, + keyframe_interval: 0.333333 + }, + { + type: 'audio/webm; codecs="vorbis"', + relaxed_type: 'audio/webm', + mime_subtype: 'webm', + is_video: false, + url: 'webm/test-a-128k-44100Hz-1ch.webm', + start_time: 0.0 + // keyframe_interval: N/A since DTS==PTS so overlap-removal of + // non-keyframe should not produce a buffered range gap. Also, all frames + // in this media are key-frames (it is audio). + }, + { + type: 'audio/mp4; codecs="mp4a.40.2"', + relaxed_type: 'audio/mp4', + mime_subtype: 'mp4', + is_video: false, + url: 'mp4/test-a-128k-44100Hz-1ch.mp4', + start_time: 0.0 + // keyframe_interval: N/A since DTS==PTS so overlap-removal of + // non-keyframe should not produce a buffered range gap. Also, all frames + // in this media are key-frames (it is audio). + }, + { + type: 'audio/mpeg', + relaxed_type: 'audio/mpeg', + mime_subtype: 'mpeg', + is_video: false, + url: 'mp3/sound_5.mp3', + start_time: 0.0 + // keyframe_interval: N/A since DTS==PTS so overlap-removal of + // non-keyframe should not produce a buffered range gap. Also, all frames + // in this media are key-frames (it is audio). + } + ]; + + let audio_result = []; + let video_result = []; + + for (let i = 0; i < CHANGE_TYPE_MEDIA_LIST.length; ++i) { + let media = CHANGE_TYPE_MEDIA_LIST[i]; + if (window.MediaSource && MediaSource.isTypeSupported(media.type)) { + if (media.is_video === true) { + video_result.push(media); + } else { + audio_result.push(media); + } + } + } + + cb(audio_result, video_result); +} + +function appendBuffer(test, sourceBuffer, data) { + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(data); +} + +function trimBuffered(test, mediaSource, sourceBuffer, minimumPreviousDuration, newDuration, skip_duration_prechecks) { + if (!skip_duration_prechecks) { + assert_less_than(newDuration, minimumPreviousDuration, + "trimBuffered newDuration must be less than minimumPreviousDuration"); + assert_less_than(minimumPreviousDuration, mediaSource.duration, + "trimBuffered minimumPreviousDuration must be less than mediaSource.duration"); + } + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.remove(newDuration, Infinity); +} + +function trimDuration(test, mediaElement, mediaSource, newDuration, skip_duration_prechecks) { + if (!skip_duration_prechecks) { + assert_less_than(newDuration, mediaSource.duration, + "trimDuration newDuration must be less than mediaSource.duration"); + } + test.expectEvent(mediaElement, "durationchange"); + mediaSource.duration = newDuration; +} + +function runChangeTypeTest(test, mediaElement, mediaSource, metadataA, typeA, dataA, metadataB, typeB, dataB, + implicit_changetype, negative_test) { + // Some streams, like the MP4 video stream, contain presentation times for + // frames out of order versus their decode times. If we overlap-append the + // latter part of such a stream's GOP presentation interval, a significant + // portion of decode-dependent non-keyframes with earlier presentation + // intervals could be removed and a presentation time buffered range gap could + // be introduced. Therefore, we test overlap appends with the overlaps + // occurring very near to a keyframe's presentation time to reduce the + // possibility of such a gap. None of the test media is SAP-Type-2, so we + // don't take any extra care to avoid gaps that may occur when + // splice-overlapping such GOP sequences that aren't SAP-Type-1. + // TODO(wolenetz): https://github.com/w3c/media-source/issues/160 could + // greatly simplify this problem by allowing us play through these small gaps. + // + // typeA and typeB may be underspecified for use with isTypeSupported, but + // this helper does not use isTypeSupported. typeA and typeB must work (even + // if missing codec specific substrings) with addSourceBuffer (just typeA) and + // changeType (both typeA and typeB). + // + // See also mediaSourceChangeTypeTest's options argument for the meanings of + // implicit_changetype and negative_test. + + function findSafeOffset(targetTime, overlappedMediaMetadata, overlappedStartTime, overlappingMediaMetadata) { + assert_greater_than_equal(targetTime, overlappedStartTime, + "findSafeOffset targetTime must be greater than or equal to overlappedStartTime"); + + let offset = targetTime; + if ("start_time" in overlappingMediaMetadata) { + offset -= overlappingMediaMetadata["start_time"]; + } + + // If the media being overlapped is not out-of-order decode, then we can + // safely use the supplied times. + if (!("keyframe_interval" in overlappedMediaMetadata)) { + return { "offset": offset, "adjustedTime": targetTime }; + } + + // Otherwise, we're overlapping media that needs care to prevent introducing + // a gap. Adjust offset and adjustedTime to make the overlapping media start + // at the next overlapped media keyframe at or after targetTime. + let gopsToRetain = Math.ceil((targetTime - overlappedStartTime) / overlappedMediaMetadata["keyframe_interval"]); + let adjustedTime = overlappedStartTime + gopsToRetain * overlappedMediaMetadata["keyframe_interval"]; + + assert_greater_than_equal(adjustedTime, targetTime, + "findSafeOffset adjustedTime must be greater than or equal to targetTime"); + offset += adjustedTime - targetTime; + return { "offset": offset, "adjustedTime": adjustedTime }; + } + + // Note, none of the current negative changeType tests should fail the initial addSourceBuffer. + let sourceBuffer = mediaSource.addSourceBuffer(typeA); + + // Add error event listeners to sourceBuffer. The caller of this helper may + // also have installed error event listeners on mediaElement. + if (negative_test) { + sourceBuffer.addEventListener("error", test.step_func_done()); + } else { + sourceBuffer.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + } + + // In either negative test or not, the first appendBuffer should succeed. + appendBuffer(test, sourceBuffer, dataA); + let lastStart = metadataA["start_time"]; + if (lastStart == null) { + lastStart = 0.0; + } + + // changeType A->B and append the first media of B effectively at 0.5 seconds + // (or at the first keyframe in A at or after 0.5 seconds if it has + // keyframe_interval defined). + test.waitForExpectedEvents(() => { + let safeOffset = findSafeOffset(0.5, metadataA, lastStart, metadataB); + lastStart = safeOffset["adjustedTime"]; + if (!implicit_changetype) { + try { sourceBuffer.changeType(typeB); } catch(err) { + if (negative_test) + test.done(); + else + throw err; + } + } + sourceBuffer.timestampOffset = safeOffset["offset"]; + appendBuffer(test, sourceBuffer, dataB); + }); + + // changeType B->B and append B starting at 1.0 seconds (or at the first + // keyframe in B at or after 1.0 seconds if it has keyframe_interval defined). + test.waitForExpectedEvents(() => { + assert_less_than(lastStart, 1.0, + "changeType B->B lastStart must be less than 1.0"); + let safeOffset = findSafeOffset(1.0, metadataB, lastStart, metadataB); + lastStart = safeOffset["adjustedTime"]; + if (!implicit_changetype) { + try { sourceBuffer.changeType(typeB); } catch(err) { + if (negative_test) + test.done(); + else + throw err; + } + } + sourceBuffer.timestampOffset = safeOffset["offset"]; + appendBuffer(test, sourceBuffer, dataB); + }); + + // changeType B->A and append A starting at 1.5 seconds (or at the first + // keyframe in B at or after 1.5 seconds if it has keyframe_interval defined). + test.waitForExpectedEvents(() => { + assert_less_than(lastStart, 1.5, + "changeType B->A lastStart must be less than 1.5"); + let safeOffset = findSafeOffset(1.5, metadataB, lastStart, metadataA); + // Retain the previous lastStart because the next block will append data + // which begins between that start time and this block's start time. + if (!implicit_changetype) { + try { sourceBuffer.changeType(typeA); } catch(err) { + if (negative_test) + test.done(); + else + throw err; + } + } + sourceBuffer.timestampOffset = safeOffset["offset"]; + appendBuffer(test, sourceBuffer, dataA); + }); + + // changeType A->A and append A starting at 1.3 seconds (or at the first + // keyframe in B at or after 1.3 seconds if it has keyframe_interval defined). + test.waitForExpectedEvents(() => { + assert_less_than(lastStart, 1.3, + "changeType A->A lastStart must be less than 1.3"); + // Our next append will begin by overlapping some of metadataB, then some of + // metadataA. + let safeOffset = findSafeOffset(1.3, metadataB, lastStart, metadataA); + if (!implicit_changetype) { + try { sourceBuffer.changeType(typeA); } catch(err) { + if (negative_test) + test.done(); + else + throw err; + } + } + sourceBuffer.timestampOffset = safeOffset["offset"]; + appendBuffer(test, sourceBuffer, dataA); + }); + + // Trim duration to 2 seconds, then play through to end. + test.waitForExpectedEvents(() => { + // If negative testing, then skip fragile assertions. + trimBuffered(test, mediaSource, sourceBuffer, 2.1, 2, negative_test); + }); + + test.waitForExpectedEvents(() => { + // If negative testing, then skip fragile assertions. + trimDuration(test, mediaElement, mediaSource, 2, negative_test); + }); + + test.waitForExpectedEvents(() => { + assert_equals(mediaElement.currentTime, 0, "currentTime must be 0"); + test.expectEvent(mediaSource, "sourceended"); + test.expectEvent(mediaElement, "play"); + test.expectEvent(mediaElement, "ended"); + mediaSource.endOfStream(); + mediaElement.play(); + }); + + test.waitForExpectedEvents(() => { + if (negative_test) + assert_unreached("Received 'ended' while negative testing."); + else + test.done(); + }); +} + +// options.use_relaxed_mime_types : boolean (defaults to false). +// If true, the initial addSourceBuffer and any changeType calls will use the +// relaxed_type in metadataA and metadataB instead of the full type in the +// metadata. +// options.implicit_changetype : boolean (defaults to false). +// If true, no changeType calls will be used. Instead, the test media files +// are expected to begin with an initialization segment and end at a segment +// boundary (no abort() call is issued by this test to reset the +// SourceBuffer's parser). +// options.negative_test : boolean (defaults to false). +// If true, the test is expected to hit error amongst one of the following +// areas: addSourceBuffer, appendBuffer (synchronous or asynchronous error), +// changeType, playback to end of buffered media. If 'ended' is received +// without error otherwise already occurring, then fail the test. Otherwise, +// pass the test on receipt of error. Continue to consider timeouts as test +// failures. +function mediaSourceChangeTypeTest(metadataA, metadataB, description, options = {}) { + mediasource_test((test, mediaElement, mediaSource) => { + let typeA = metadataA.type; + let typeB = metadataB.type; + if (options.hasOwnProperty("use_relaxed_mime_types") && + options.use_relaxed_mime_types === true) { + typeA = metadataA.relaxed_type; + typeB = metadataB.relaxed_type; + } + let implicit_changetype = options.hasOwnProperty("implicit_changetype") && + options.implicit_changetype === true; + let negative_test = options.hasOwnProperty("negative_test") && + options.negative_test === true; + + mediaElement.pause(); + if (negative_test) { + mediaElement.addEventListener("error", test.step_func_done()); + } else { + mediaElement.addEventListener("error", + test.unreached_func("Unexpected event 'error'")); + } + MediaSourceUtil.loadBinaryData(test, metadataA.url, (dataA) => { + MediaSourceUtil.loadBinaryData(test, metadataB.url, (dataB) => { + runChangeTypeTest( + test, mediaElement, mediaSource, + metadataA, typeA, dataA, metadataB, typeB, dataB, + implicit_changetype, negative_test); + }); + }); + }, description); +} diff --git a/testing/web-platform/tests/media-source/mediasource-changetype.html b/testing/web-platform/tests/media-source/mediasource-changetype.html new file mode 100644 index 0000000000..25618cdc1e --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-changetype.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<!-- Copyright © 2018 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <meta charset="utf-8"> + <title>SourceBuffer.changeType() test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> +mediasource_test(function(test, mediaElement, mediaSource) +{ + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + assert_throws_js(TypeError, function() + { + sourceBuffer.changeType(""); + }, "changeType"); + + test.done(); +}, "Test changeType with an empty type."); + +mediasource_test(function(test, mediaElement, mediaSource) +{ + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.removeSourceBuffer(sourceBuffer); + + assert_throws_dom("InvalidStateError", function() + { + sourceBuffer.changeType(MediaSourceUtil.AUDIO_VIDEO_TYPE); + }, "changeType"); + + test.done(); +}, "Test changeType after SourceBuffer removed from mediaSource."); + +mediasource_test(function(test, mediaElement, mediaSource) +{ + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + sourceBuffer.appendBuffer(new Uint8Array(0)); + assert_true(sourceBuffer.updating, "Updating flag set when a buffer is appended."); + + assert_throws_dom("InvalidStateError", function() + { + sourceBuffer.changeType(MediaSourceUtil.AUDIO_VIDEO_TYPE); + }, "changeType"); + + test.done(); +}, "Test changeType while update pending."); + +mediasource_test(function(test, mediaElement, mediaSource) +{ + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + var unsupported_type = null; + assert_false(MediaSource.isTypeSupported(unsupported_type), "null MIME type is not expected to be supported."); + + assert_throws_dom("NotSupportedError", function() + { + sourceBuffer.changeType(unsupported_type); + }, "changeType"); + + test.done(); +}, "Test changeType with null type."); + +mediasource_test(function(test, mediaElement, mediaSource) +{ + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + var unsupported_type = 'invalidType'; + assert_false(MediaSource.isTypeSupported(unsupported_type), unsupported_type + " is not expected to be supported."); + + assert_throws_dom("NotSupportedError", function() + { + sourceBuffer.changeType(unsupported_type); + }, "changeType"); + + test.done(); +}, "Test changeType with unsupported type."); + +mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) +{ + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, "ended"); + + test.expectEvent(mediaSource, "sourceopen"); + sourceBuffer.changeType(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_equals(mediaSource.readyState, "open"); + }); + + test.waitForExpectedEvents(function() + { + test.done(); + }); +}, "Test changeType transitioning readyState from 'ended' to 'open'."); + +mediasource_test(function(test, mediaElement, mediaSource) { + var sequenceType = "audio/aac"; + if (!MediaSource.isTypeSupported(sequenceType)) { + sequenceType = "audio/mpeg"; + assert_true(MediaSource.isTypeSupported(sequenceType), + "No bytestream that generates timestamps is supported, aborting test"); + } + + assert_not_equals(MediaSourceUtil.AUDIO_ONLY_TYPE, sequenceType, + "This test requires distinct audio-only types"); + + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + assert_equals(sourceBuffer.mode, "segments", + "None of the audioOnlyTypes in the test util generate timestamps, but mode is incorrectly set"); + + sourceBuffer.changeType(sequenceType); + assert_equals(sourceBuffer.mode, "sequence", + "Mode is not updated correctly for a bytestream that generates timestamps"); + + test.done(); +}, "Test changeType sets mode to sequence for change to type that generates timestamps"); + +mediasource_test(function(test, mediaElement, mediaSource) { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + assert_equals(sourceBuffer.mode, "segments", + "None of the audioOnlyTypes in the test util generate timestamps, but mode is incorrectly set"); + sourceBuffer.changeType(MediaSourceUtil.AUDIO_ONLY_TYPE); + assert_equals(sourceBuffer.mode, "segments", + "Previous segments mode is not retained correctly for changeType to one that doesn't generate timestamps"); + + sourceBuffer.mode = "sequence"; + assert_equals(sourceBuffer.mode, "sequence", "mode should be sequence now"); + sourceBuffer.changeType(MediaSourceUtil.AUDIO_ONLY_TYPE); + assert_equals(sourceBuffer.mode, "sequence", + "Previous sequence mode is not retained correctly for changeType to one that doesn't generate timestamps"); + + test.done(); +}, "Test changeType retains previous mode when changing to type that doesn't generate timestamps"); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-closed.html b/testing/web-platform/tests/media-source/mediasource-closed.html new file mode 100644 index 0000000000..79d522f2f9 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-closed.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MediaSource.readyState equals "closed" test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + test(function () + { + var mediaSource = new MediaSource(); + assert_equals(mediaSource.sourceBuffers.length, 0, "sourceBuffers is empty"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "activeSourceBuffers is empty"); + assert_equals(mediaSource.readyState, "closed", "readyState is 'closed'"); + assert_true(isNaN(mediaSource.duration), "duration is NaN"); + }, "Test attribute values on a closed MediaSource object."); + + test(function () + { + var mediaSource = new MediaSource(); + assert_throws_dom("InvalidStateError", + function() { mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); }, + "addSourceBuffer() throws an exception when closed."); + }, "Test addSourceBuffer() while closed."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + // Setup a handler to run when the MediaSource closes. + mediaSource.addEventListener('sourceclose', test.step_func(function (event) + { + assert_equals(mediaSource.sourceBuffers.length, 0, "sourceBuffers is empty"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "activeSourceBuffers is empty"); + assert_equals(mediaSource.readyState, "closed", "readyState is 'closed'"); + assert_throws_dom("NotFoundError", + function() { mediaSource.removeSourceBuffer(sourceBuffer); }, + "removeSourceBuffer() throws an exception when closed."); + test.done(); + })); + + // Trigger the MediaSource to close. + mediaElement.src = ""; + }, "Test removeSourceBuffer() while closed."); + + test(function () + { + var mediaSource = new MediaSource(); + assert_throws_dom("InvalidStateError", + function() { mediaSource.endOfStream(); }, + "endOfStream() throws an exception when closed."); + }, "Test endOfStream() while closed."); + + test(function () + { + var mediaSource = new MediaSource(); + assert_throws_dom("InvalidStateError", + function() { mediaSource.endOfStream("decode"); }, + "endOfStream(decode) throws an exception when closed."); + }, "Test endOfStream(decode) while closed."); + + test(function () + { + var mediaSource = new MediaSource(); + assert_throws_dom("InvalidStateError", + function() { mediaSource.endOfStream("network"); }, + "endOfStream(network) throws an exception when closed."); + }, "Test endOfStream(network) while closed."); + + test(function () + { + var mediaSource = new MediaSource(); + assert_throws_dom("InvalidStateError", + function() { mediaSource.duration = 10; }, + "Setting duration throws an exception when closed."); + }, "Test setting duration while closed."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + assert_equals(mediaSource.readyState, "open", "readyState is 'open'"); + // Setup a handler to run when the MediaSource closes. + mediaSource.addEventListener("sourceclose", test.step_func(function (event) + { + assert_equals(mediaSource.readyState, "closed", "readyState is 'closed'"); + assert_throws_dom("InvalidStateError", + function() { mediaSource.duration = 10; }, + "Setting duration when closed throws an exception"); + test.done(); + })); + + // Trigger the MediaSource to close. + mediaElement.src = ""; + }, "Test setting duration while open->closed."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + assert_equals(mediaSource.readyState, "open", "readyState is 'open'"); + // Setup a handler to run when the MediaSource closes. + mediaSource.addEventListener("sourceclose", test.step_func(function (event) + { + assert_equals(mediaSource.readyState, "closed", "readyState is 'closed'"); + assert_true(isNaN(mediaSource.duration), "duration is NaN"); + test.done(); + })); + + // Trigger the MediaSource to close. + mediaElement.src = ""; + }, "Test getting duration while open->closed."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + assert_equals(mediaSource.readyState, "open", "readyState is open"); + + // Setup a handler to run when the MediaSource closes. + mediaSource.addEventListener("sourceclose", test.step_func(function (event) + { + assert_equals(mediaSource.readyState, "closed", "readyState is closed"); + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.abort(); }, + "sourceBuffer.abort() throws INVALID_STATE_ERROR"); + test.done(); + })); + + // Trigger the MediaSource to close. + mediaElement.src = ""; + }, "Test sourcebuffer.abort when closed."); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-a-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-a-bitrate.html new file mode 100644 index 0000000000..47e4c804ee --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-a-bitrate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 audio-only bitrate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "a-128k-44100Hz-1ch", "a-192k-44100Hz-1ch", "Tests mp4 audio-only bitrate changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-audio-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-audio-bitrate.html new file mode 100644 index 0000000000..960720768b --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-audio-bitrate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 muxed audio & video with an audio bitrate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "av-384k-44100Hz-1ch-640x480-30fps-10kfr", "av-448k-44100Hz-1ch-640x480-30fps-10kfr", "Tests mp4 audio bitrate changes in multiplexed content."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-framesize.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-framesize.html new file mode 100644 index 0000000000..7ef2bb0bbf --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-framesize.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 muxed audio & video with a video frame size change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "av-384k-44100Hz-1ch-320x240-30fps-10kfr", "av-384k-44100Hz-1ch-640x480-30fps-10kfr", "Tests mp4 frame size changes in multiplexed content."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-video-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-video-bitrate.html new file mode 100644 index 0000000000..8c74e75389 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-av-video-bitrate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 muxed audio & video with a video bitrate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "av-384k-44100Hz-1ch-640x480-30fps-10kfr", "av-640k-44100Hz-1ch-640x480-30fps-10kfr", "Tests mp4 video bitrate changes in multiplexed content."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-bitrate.html new file mode 100644 index 0000000000..705c5bd3ca --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-bitrate.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 video-only bitrate change.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "v-128k-320x240-30fps-10kfr", "v-256k-320x240-30fps-10kfr", "Tests mp4 video-only bitrate changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-framerate.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-framerate.html new file mode 100644 index 0000000000..1d07fa9482 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-framerate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 video-only frame rate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "v-128k-320x240-24fps-8kfr", "v-128k-320x240-30fps-10kfr", "Tests mp4 video-only frame rate changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-framesize.html b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-framesize.html new file mode 100644 index 0000000000..78e6823e23 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-mp4-v-framesize.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MP4 video-only frame size change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("mp4", "v-128k-320x240-30fps-10kfr", "v-128k-640x480-30fps-10kfr", "Tests mp4 video-only frame size changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-a-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-a-bitrate.html new file mode 100644 index 0000000000..cc351cd307 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-a-bitrate.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM audio-only bitrate change.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "a-128k-44100Hz-1ch", "a-192k-44100Hz-1ch", "Tests webm audio-only bitrate changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-audio-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-audio-bitrate.html new file mode 100644 index 0000000000..d98069d072 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-audio-bitrate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM muxed audio & video with an audio bitrate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "av-384k-44100Hz-1ch-640x480-30fps-10kfr", "av-448k-44100Hz-1ch-640x480-30fps-10kfr", "Tests webm audio bitrate changes in multiplexed content."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-framesize.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-framesize.html new file mode 100644 index 0000000000..c37f8c2ed4 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-framesize.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM muxed audio & video with a video frame size change.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "av-384k-44100Hz-1ch-320x240-30fps-10kfr", "av-384k-44100Hz-1ch-640x480-30fps-10kfr", "Tests webm frame size changes in multiplexed content."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-video-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-video-bitrate.html new file mode 100644 index 0000000000..96037c736a --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-av-video-bitrate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM muxed audio & video with a video bitrate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "av-384k-44100Hz-1ch-640x480-30fps-10kfr", "av-640k-44100Hz-1ch-640x480-30fps-10kfr", "Tests webm video bitrate changes in multiplexed content."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-bitrate.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-bitrate.html new file mode 100644 index 0000000000..e194e267d2 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-bitrate.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM video-only bitrate change.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "v-128k-320x240-30fps-10kfr", "v-256k-320x240-30fps-10kfr", "Tests webm video-only bitrate changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-framerate.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-framerate.html new file mode 100644 index 0000000000..7dbdc9c802 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-framerate.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM video-only frame rate change.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "v-128k-320x240-24fps-8kfr", "v-128k-320x240-30fps-10kfr", "Tests webm video-only frame rate changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-framesize.html b/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-framesize.html new file mode 100644 index 0000000000..ca13c78a35 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-change-webm-v-framesize.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>WebM video-only frame size change.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + <script src="mediasource-config-changes.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediaSourceConfigChangeTest("webm", "v-128k-320x240-30fps-10kfr", "v-128k-640x480-30fps-10kfr", "Tests webm video-only frame size changes."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-config-changes.js b/testing/web-platform/tests/media-source/mediasource-config-changes.js new file mode 100644 index 0000000000..b28aa90f1f --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-config-changes.js @@ -0,0 +1,116 @@ +// Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). + +// Extract & return the resolution string from a filename, if any. +function resolutionFromFilename(filename) +{ + resolution = filename.replace(/^.*[^0-9]([0-9]+x[0-9]+)[^0-9].*$/, "$1"); + if (resolution != filename) { + return resolution; + } + return ""; +} + +function appendBuffer(test, sourceBuffer, data) +{ + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(data); +} + +function mediaSourceConfigChangeTest(directory, idA, idB, description) +{ + var manifestFilenameA = directory + "/test-" + idA + "-manifest.json"; + var manifestFilenameB = directory + "/test-" + idB + "-manifest.json"; + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.pause(); + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + var expectResizeEvents = resolutionFromFilename(manifestFilenameA) != resolutionFromFilename(manifestFilenameB); + var expectedResizeEventCount = 0; + + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameA, function(typeA, dataA) + { + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameB, function(typeB, dataB) + { + assert_equals(typeA, typeB, "Media format types match"); + + var sourceBuffer = mediaSource.addSourceBuffer(typeA); + + appendBuffer(test, sourceBuffer, dataA); + ++expectedResizeEventCount; + + test.waitForExpectedEvents(function() + { + // Add the second buffer starting at 0.5 second. + sourceBuffer.timestampOffset = 0.5; + appendBuffer(test, sourceBuffer, dataB); + ++expectedResizeEventCount; + }); + + test.waitForExpectedEvents(function() + { + // Add the first buffer starting at 1 second. + sourceBuffer.timestampOffset = 1; + appendBuffer(test, sourceBuffer, dataA); + ++expectedResizeEventCount; + }); + + test.waitForExpectedEvents(function() + { + // Add the second buffer starting at 1.5 second. + sourceBuffer.timestampOffset = 1.5; + appendBuffer(test, sourceBuffer, dataB); + ++expectedResizeEventCount; + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + + // Truncate the presentation to a duration of 2 seconds. + // First, explicitly remove the media beyond 2 seconds. + sourceBuffer.remove(2, Infinity); + + assert_true(sourceBuffer.updating, "sourceBuffer.updating during range removal"); + test.expectEvent(sourceBuffer, 'updatestart', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'update', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "sourceBuffer.updating prior to duration reduction"); + assert_greater_than(mediaSource.duration, 2, "duration"); + + // Complete the truncation of presentation to 2 second + // duration. + mediaSource.duration = 2; + assert_false(sourceBuffer.updating, "sourceBuffer.updating synchronously after duration reduction"); + + test.expectEvent(mediaElement, "durationchange"); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + + mediaSource.endOfStream(); + + assert_false(sourceBuffer.updating, "updating"); + + if (expectResizeEvents) { + for (var i = 0; i < expectedResizeEventCount; ++i) { + test.expectEvent(mediaElement, "resize"); + } + } + test.expectEvent(mediaElement, "ended"); + mediaElement.play(); + }); + + test.waitForExpectedEvents(function() { + test.done(); + }); + }); + }); + }, description); +}; diff --git a/testing/web-platform/tests/media-source/mediasource-correct-frames-after-reappend.html b/testing/web-platform/tests/media-source/mediasource-correct-frames-after-reappend.html new file mode 100644 index 0000000000..5c0f2e1119 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-correct-frames-after-reappend.html @@ -0,0 +1,162 @@ +<!DOCTYPE html> +<!-- Copyright © 2019 Igalia. --> +<html> +<head> + <title>Frame checking test for MSE playback in presence of a reappend.</title> + <meta name="timeout" content="long"> + <meta name="charset" content="UTF-8"> + <link rel="author" title="Alicia Boya GarcÃa" href="mailto:aboya@igalia.com"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> +</head> +<body> +<div id="log"></div> +<canvas id="test-canvas"></canvas> +<script> + function waitForEventPromise(element, event) { + return new Promise(resolve => { + function handler(ev) { + element.removeEventListener(event, handler); + resolve(ev); + } + element.addEventListener(event, handler); + }); + } + + function appendBufferPromise(sourceBuffer, data) { + sourceBuffer.appendBuffer(data); + return waitForEventPromise(sourceBuffer, "update"); + } + + function waitForPlayerToReachTimePromise(mediaElement, time) { + return new Promise(resolve => { + function timeupdate() { + if (mediaElement.currentTime < time) + return; + + mediaElement.removeEventListener("timeupdate", timeupdate); + resolve(); + } + mediaElement.addEventListener("timeupdate", timeupdate); + }); + } + + function readPixel(imageData, x, y) { + return { + r: imageData.data[4 * (y * imageData.width + x)], + g: imageData.data[1 + 4 * (y * imageData.width + x)], + b: imageData.data[2 + 4 * (y * imageData.width + x)], + a: imageData.data[3 + 4 * (y * imageData.width + x)], + }; + } + + function isPixelLit(pixel) { + const threshold = 200; // out of 255 + return pixel.r >= threshold && pixel.g >= threshold && pixel.b >= threshold; + } + + // The test video has a few gray boxes. Each box interval (1 second) a new box is lit white and a different note + // is played. This test makes sure the right number of lit boxes and the right note are played at the right time. + const totalBoxes = 7; + const boxInterval = 1; // seconds + + const videoWidth = 320; + const videoHeight = 240; + const boxesY = 210; + const boxSide = 20; + const boxMargin = 20; + const allBoxesWidth = totalBoxes * boxSide + (totalBoxes - 1) * boxMargin; + const boxesX = new Array(totalBoxes).fill(undefined) + .map((_, i) => (videoWidth - allBoxesWidth) / 2 + boxSide / 2 + i * (boxSide + boxMargin)); + + // Sound starts playing A4 (440 Hz) and goes one chromatic note up with every box lit. + // By comparing the player position to both the amount of boxes lit and the note played we can detect A/V + // synchronization issues automatically. + const noteFrequencies = new Array(1 + totalBoxes).fill(undefined) + .map((_, i) => 440 * Math.pow(Math.pow(2, 1 / 12), i)); + + // We also check the first second [0, 1) where no boxes are lit, therefore we start counting at -1 to do the check + // for zero lit boxes. + let boxesLitSoFar = -1; + + mediasource_test(async function (test, mediaElement, mediaSource) { + const canvas = document.getElementById("test-canvas"); + const canvasCtx = canvas.getContext("2d"); + canvas.width = videoWidth; + canvas.height = videoHeight; + + const videoData = await (await fetch("mp4/test-boxes-video.mp4")).arrayBuffer(); + const audioData = (await (await fetch("mp4/test-boxes-audio.mp4")).arrayBuffer()); + + const videoSb = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.4d401f"'); + const audioSb = mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"'); + + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener('ended', onEnded); + mediaElement.addEventListener('timeupdate', onTimeUpdate); + + await appendBufferPromise(videoSb, videoData); + await appendBufferPromise(audioSb, audioData); + mediaElement.play(); + + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + source = audioCtx.createMediaElementSource(mediaElement); + analyser = audioCtx.createAnalyser(); + analyser.fftSize = 8192; + source.connect(analyser); + analyser.connect(audioCtx.destination); + + const freqDomainArray = new Float32Array(analyser.frequencyBinCount); + + function checkNoteBeingPlayed() { + const expectedNoteFrequency = noteFrequencies[boxesLitSoFar]; + + analyser.getFloatFrequencyData(freqDomainArray); + const maxBin = freqDomainArray.reduce((prev, curValue, i) => + curValue > prev.value ? {index: i, value: curValue} : prev, + {index: -1, value: -Infinity}); + const binFrequencyWidth = audioCtx.sampleRate / analyser.fftSize; + const binFreq = maxBin.index * binFrequencyWidth; + + assert_true(Math.abs(expectedNoteFrequency - binFreq) <= binFrequencyWidth, + `The note being played matches the expected one (boxes lit: ${boxesLitSoFar}, ${expectedNoteFrequency.toFixed(1)} Hz)` + + `, found ~${binFreq.toFixed(1)} Hz`); + } + + function countLitBoxesInCurrentVideoFrame() { + canvasCtx.drawImage(mediaElement, 0, 0); + const imageData = canvasCtx.getImageData(0, 0, videoWidth, videoHeight); + const lights = boxesX.map(boxX => isPixelLit(readPixel(imageData, boxX, boxesY))); + let litBoxes = 0; + for (let i = 0; i < lights.length; i++) { + if (lights[i]) + litBoxes++; + } + for (let i = litBoxes; i < lights.length; i++) { + assert_false(lights[i], 'After the first non-lit box, all boxes must non-lit'); + } + return litBoxes; + } + + await waitForPlayerToReachTimePromise(mediaElement, 2.5); + await appendBufferPromise(audioSb, audioData); + mediaSource.endOfStream(); + + function onTimeUpdate() { + const graceTime = 0.5; + if (mediaElement.currentTime >= (1 + boxesLitSoFar) * boxInterval + graceTime && boxesLitSoFar < totalBoxes) { + assert_equals(countLitBoxesInCurrentVideoFrame(), boxesLitSoFar + 1, "Num of lit boxes:"); + boxesLitSoFar++; + checkNoteBeingPlayed(); + } + } + + function onEnded() { + assert_equals(boxesLitSoFar, totalBoxes, "Boxes lit at video ended event"); + test.done(); + } + }, "Test the expected frames are played at the expected times, even in presence of reappends"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-correct-frames.html b/testing/web-platform/tests/media-source/mediasource-correct-frames.html new file mode 100644 index 0000000000..4ef3f4605e --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-correct-frames.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<!-- Copyright © 2019 Igalia. --> +<html> +<head> + <title>Frame checking test for simple MSE playback.</title> + <meta name="timeout" content="long"> + <meta name="charset" content="UTF-8"> + <link rel="author" title="Alicia Boya GarcÃa" href="mailto:aboya@igalia.com"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> +</head> +<body> +<div id="log"></div> +<canvas id="test-canvas"></canvas> +<script> + function waitForEventPromise(element, event) { + return new Promise(resolve => { + function handler(ev) { + element.removeEventListener(event, handler); + resolve(ev); + } + element.addEventListener(event, handler); + }); + } + + function appendBufferPromise(sourceBuffer, data) { + sourceBuffer.appendBuffer(data); + return waitForEventPromise(sourceBuffer, "update"); + } + + function readPixel(imageData, x, y) { + return { + r: imageData.data[4 * (y * imageData.width + x)], + g: imageData.data[1 + 4 * (y * imageData.width + x)], + b: imageData.data[2 + 4 * (y * imageData.width + x)], + a: imageData.data[3 + 4 * (y * imageData.width + x)], + }; + } + + function isPixelLit(pixel) { + const threshold = 200; // out of 255 + return pixel.r >= threshold && pixel.g >= threshold && pixel.b >= threshold; + } + + // The test video has a few gray boxes. Each box interval (1 second) a new box is lit white and a different note + // is played. This test makes sure the right number of lit boxes and the right note are played at the right time. + const totalBoxes = 7; + const boxInterval = 1; // seconds + + const videoWidth = 320; + const videoHeight = 240; + const boxesY = 210; + const boxSide = 20; + const boxMargin = 20; + const allBoxesWidth = totalBoxes * boxSide + (totalBoxes - 1) * boxMargin; + const boxesX = new Array(totalBoxes).fill(undefined) + .map((_, i) => (videoWidth - allBoxesWidth) / 2 + boxSide / 2 + i * (boxSide + boxMargin)); + + // Sound starts playing A4 (440 Hz) and goes one chromatic note up with every box lit. + // By comparing the player position to both the amount of boxes lit and the note played we can detect A/V + // synchronization issues automatically. + const noteFrequencies = new Array(1 + totalBoxes).fill(undefined) + .map((_, i) => 440 * Math.pow(Math.pow(2, 1 / 12), i)); + + // We also check the first second [0, 1) where no boxes are lit, therefore we start counting at -1 to do the check + // for zero lit boxes. + let boxesLitSoFar = -1; + + mediasource_test(async function (test, mediaElement, mediaSource) { + const canvas = document.getElementById("test-canvas"); + const canvasCtx = canvas.getContext("2d"); + canvas.width = videoWidth; + canvas.height = videoHeight; + + const videoData = await (await fetch("mp4/test-boxes-video.mp4")).arrayBuffer(); + const audioData = (await (await fetch("mp4/test-boxes-audio.mp4")).arrayBuffer()); + + const videoSb = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.4d401f"'); + const audioSb = mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"'); + + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener('ended', onEnded); + mediaElement.addEventListener('timeupdate', onTimeUpdate); + + await appendBufferPromise(videoSb, videoData); + await appendBufferPromise(audioSb, audioData); + mediaSource.endOfStream(); + mediaElement.play(); + + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + const source = audioCtx.createMediaElementSource(mediaElement); + const analyser = audioCtx.createAnalyser(); + analyser.fftSize = 8192; + source.connect(analyser); + analyser.connect(audioCtx.destination); + + const freqDomainArray = new Float32Array(analyser.frequencyBinCount); + + function checkNoteBeingPlayed() { + const expectedNoteFrequency = noteFrequencies[boxesLitSoFar]; + + analyser.getFloatFrequencyData(freqDomainArray); + const maxBin = freqDomainArray.reduce((prev, curValue, i) => + curValue > prev.value ? {index: i, value: curValue} : prev, + {index: -1, value: -Infinity}); + const binFrequencyWidth = audioCtx.sampleRate / analyser.fftSize; + const binFreq = maxBin.index * binFrequencyWidth; + + assert_true(Math.abs(expectedNoteFrequency - binFreq) <= binFrequencyWidth, + `The note being played matches the expected one (boxes lit: ${boxesLitSoFar}, ${expectedNoteFrequency.toFixed(1)} Hz)` + + `, found ~${binFreq.toFixed(1)} Hz`); + } + + function countLitBoxesInCurrentVideoFrame() { + canvasCtx.drawImage(mediaElement, 0, 0); + const imageData = canvasCtx.getImageData(0, 0, videoWidth, videoHeight); + const lights = boxesX.map(boxX => isPixelLit(readPixel(imageData, boxX, boxesY))); + let litBoxes = 0; + for (let i = 0; i < lights.length; i++) { + if (lights[i]) + litBoxes++; + } + for (let i = litBoxes; i < lights.length; i++) { + assert_false(lights[i], 'After the first non-lit box, all boxes must non-lit'); + } + return litBoxes; + } + + function onTimeUpdate() { + const graceTime = 0.5; + if (mediaElement.currentTime >= (1 + boxesLitSoFar) * boxInterval + graceTime && boxesLitSoFar < totalBoxes) { + assert_equals(countLitBoxesInCurrentVideoFrame(), boxesLitSoFar + 1, "Num of lit boxes:"); + boxesLitSoFar++; + checkNoteBeingPlayed(); + } + } + + function onEnded() { + assert_equals(boxesLitSoFar, totalBoxes, "Boxes lit at video ended event"); + test.done(); + } + }, "Test the expected frames are played at the expected times"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-detach.html b/testing/web-platform/tests/media-source/mediasource-detach.html new file mode 100644 index 0000000000..3f87d9a3d5 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-detach.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + function mediasource_detach_test(testFunction, description) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + var segmentInfo = MediaSourceUtil.SEGMENT_INFO; + var sourceBuffer = mediaSource.addSourceBuffer(segmentInfo.type); + + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + assert_equals(mediaSource.readyState, 'open'); + + mediaSource.addEventListener('sourceclose', test.step_func(function (event) + { + assert_equals(mediaSource.sourceBuffers.length, 0, 'sourceBuffers is empty'); + assert_equals(mediaSource.activeSourceBuffers.length, 0, 'activeSourceBuffers is empty'); + assert_equals(mediaSource.readyState, 'closed', 'readyState is "closed"'); + assert_true(Number.isNaN(mediaSource.duration), 'duration is NaN'); + test.done(); + })); + + MediaSourceUtil.loadBinaryData(test, segmentInfo.url, function(mediaData) + { + testFunction(test, mediaElement, mediaSource, sourceBuffer, mediaData); + }); + }, description); + } + + mediasource_detach_test(function(test, mediaElement, mediaSource, sourceBuffer, mediaData) + { + mediaElement.load(); + }, 'Test media.load() before appending data will trigger MediaSource detaching from a media element.'); + + mediasource_detach_test(function(test, mediaElement, mediaSource, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, 'updateend', 'updateend after appending data'); + test.expectEvent(mediaElement, 'loadedmetadata', 'media element loadedmetata'); + test.waitForExpectedEvents(() => + { + assert_greater_than(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING, + 'media element readyState is greater than "HAVE_NOTHING"'); + assert_false(sourceBuffer.updating, 'updating attribute is false'); + assert_equals(mediaSource.readyState, 'open'); + mediaElement.load(); + }); + + sourceBuffer.appendBuffer(mediaData); + }, 'Test media.load() after appending data will trigger MediaSource detaching from a media element.'); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-duration-boundaryconditions.html b/testing/web-platform/tests/media-source/mediasource-duration-boundaryconditions.html new file mode 100644 index 0000000000..e5be9f18fc --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-duration-boundaryconditions.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MediaSource.duration boundary condition test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + function DurationBoundaryConditionTest(testDurationValue, expectedError, description) + { + return mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + // Append initialization segment. + test.expectEvent(sourceBuffer, "updateend", "sourceBuffer"); + test.expectEvent(mediaElement, "loadedmetadata", "mediaElement"); + sourceBuffer.appendBuffer(MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init)); + test.waitForExpectedEvents(function() + { + if (expectedError) { + assert_throws_js(expectedError, + function() { mediaSource.duration = testDurationValue; }, + "mediaSource.duration assignment throws an exception for " + testDurationValue); + test.done(); + return; + } + + mediaSource.duration = testDurationValue; + + assert_equals(mediaSource.duration, testDurationValue, "mediaSource.duration"); + assert_equals(mediaElement.duration, testDurationValue, "mediaElement.duration"); + + test.expectEvent(mediaElement, "durationchange", "mediaElement"); + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.duration, testDurationValue, "mediaSource.duration"); + assert_equals(mediaElement.duration, testDurationValue, "mediaElement.duration"); + test.done(); + }); + }); + + }, description); + } + + DurationBoundaryConditionTest(Math.pow(2, 31) - 1, null, "Set duration to 2^31 - 1"); + DurationBoundaryConditionTest(1, null, "Set duration to 1"); + DurationBoundaryConditionTest(Number.MAX_VALUE, null, "Set duration to Number.MAX_VALUE"); + DurationBoundaryConditionTest(Number.MIN_VALUE, null, "Set duration to Number.MIN_VALUE"); + DurationBoundaryConditionTest(Number.MAX_VALUE - 1, null, "Set duration to Number.MAX_VALUE - 1"); + DurationBoundaryConditionTest(Number.MIN_VALUE - 1, TypeError, "Set duration to Number.MIN_VALUE - 1"); + DurationBoundaryConditionTest(Number.POSITIVE_INFINITY, null, "Set duration to Number.POSITIVE_INFINITY"); + DurationBoundaryConditionTest(Number.NEGATIVE_INFINITY, TypeError, "Set duration to Number.NEGATIVE_INFINITY"); + DurationBoundaryConditionTest(-1 * Number.MAX_VALUE, TypeError, "Set duration to lowest value."); + DurationBoundaryConditionTest(-101.9, TypeError, "Set duration to a negative double."); + DurationBoundaryConditionTest(101.9, null, "Set duration to a positive double."); + DurationBoundaryConditionTest(0, null, "Set duration to zero"); + DurationBoundaryConditionTest(NaN, TypeError, "Set duration to NaN"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-duration.html b/testing/web-platform/tests/media-source/mediasource-duration.html new file mode 100644 index 0000000000..b4619da38b --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-duration.html @@ -0,0 +1,383 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MediaSource.duration & HTMLMediaElement.duration test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + + var subType = MediaSourceUtil.getSubType(MediaSourceUtil.AUDIO_ONLY_TYPE); + var manifestFilenameAudio = subType + "/test-a-128k-44100Hz-1ch-manifest.json"; + var manifestFilenameVideo = subType + "/test-v-128k-320x240-30fps-10kfr-manifest.json"; + + function mediasource_truncated_duration_seek_test(testFunction, description, options) + { + return mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration'); + + var fullDuration = segmentInfo.duration; + var seekTo = fullDuration / 2.0; + var truncatedDuration = seekTo / 2.0; + + mediaElement.play(); + + // Append all the segments + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'playing', 'Playing triggered'); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + test.expectEvent(mediaElement, 'seeking', 'seeking to seekTo'); + test.expectEvent(mediaElement, 'timeupdate', 'timeupdate while seeking to seekTo'); + test.expectEvent(mediaElement, 'seeked', 'seeked to seekTo'); + mediaElement.currentTime = seekTo; + assert_true(mediaElement.seeking, 'mediaElement.seeking (to seekTo)'); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.currentTime, seekTo, 'Playback time has reached seekTo'); + assert_false(mediaElement.seeking, 'mediaElement.seeking after seeked to seekTo'); + + assert_false(sourceBuffer.updating, 'sourceBuffer.updating'); + + sourceBuffer.remove(truncatedDuration, Infinity); + + assert_true(sourceBuffer.updating, 'sourceBuffer.updating'); + test.expectEvent(sourceBuffer, 'updatestart', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'update', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.currentTime, seekTo, 'Playback time has reached seekTo'); + assert_false(sourceBuffer.updating, 'sourceBuffer.updating'); + + // Remove will not remove partial frames, so the resulting duration is the highest end time + // of the track buffer ranges, and is greater than or equal to the highest coded frame + // presentation time across all track buffer ranges. We first obtain the intersected track buffer + // ranges end time and set the duration to that value. + truncatedDuration = sourceBuffer.buffered.end(sourceBuffer.buffered.length-1); + assert_less_than(truncatedDuration, seekTo, + 'remove has removed the current playback position from at least one track buffer'); + + mediaSource.duration = truncatedDuration; + test.expectEvent(mediaElement, 'seeking', 'Seeking to truncated duration'); + + // The actual duration may be slightly higher than truncatedDuration because the + // duration is adjusted upwards if necessary to be the highest end time across all track buffer + // ranges. Allow that increase here. + assert_less_than_equal(truncatedDuration, mediaSource.duration, + 'Duration should not be less than what was set'); + // Here, we assume no test media coded frame duration is longer than 100ms. + assert_less_than(mediaSource.duration - truncatedDuration, 0.1); + + // Update our truncatedDuration to be the actual new duration. + truncatedDuration = mediaSource.duration; + + assert_true(mediaElement.seeking, 'Seeking after setting truncatedDuration'); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.currentTime, truncatedDuration, + 'Playback time is truncatedDuration while seeking'); + assert_true(mediaElement.seeking, 'mediaElement.seeking while seeking to truncatedDuration'); + assert_equals(mediaElement.duration, truncatedDuration, + 'mediaElement truncatedDuration during seek to it'); + assert_equals(mediaSource.duration, truncatedDuration, + 'mediaSource truncatedDuration during seek to it'); + + testFunction(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData, + truncatedDuration); + }); + }, description, options); + } + + mediasource_truncated_duration_seek_test(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, + mediaData, truncatedDuration) + { + // Tests that duration truncation below current playback position + // starts seek to new duration. + test.done(); + }, 'Test seek starts on duration truncation below currentTime'); + + mediasource_truncated_duration_seek_test(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, + mediaData, truncatedDuration) + { + // The duration has been truncated at this point, and there is an + // outstanding seek pending. + test.expectEvent(sourceBuffer, 'updateend', 'updateend after appending more data'); + + test.expectEvent(mediaElement, 'timeupdate', 'timeupdate while finishing seek to truncatedDuration'); + test.expectEvent(mediaElement, 'seeked', 'seeked to truncatedDuration'); + + // Allow seek to complete by appending more data beginning at the + // truncated duration timestamp. + sourceBuffer.timestampOffset = truncatedDuration; + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.currentTime, truncatedDuration, + 'Playback time has reached truncatedDuration'); + assert_approx_equals(mediaElement.duration, truncatedDuration + segmentInfo.duration, 0.05, + 'mediaElement duration increased by new append'); + assert_equals(mediaSource.duration, mediaElement.duration, + 'mediaSource duration increased by new append'); + assert_false(mediaElement.seeking, 'mediaElement.seeking after seeked to truncatedDuration'); + + test.done(); + }); + }, 'Test appendBuffer completes previous seek to truncated duration'); + + mediasource_truncated_duration_seek_test(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, + mediaData, truncatedDuration) + { + // The duration has been truncated at this point, and there is an + // outstanding seek pending. + test.expectEvent(mediaSource, 'sourceended', 'endOfStream acknowledged'); + + test.expectEvent(mediaElement, 'timeupdate', 'timeupdate while finishing seek to truncatedDuration'); + test.expectEvent(mediaElement, 'seeked', 'seeked to truncatedDuration'); + + // Call endOfStream() to complete the pending seek. + mediaSource.endOfStream(); + + test.waitForExpectedEvents(function() + { + assert_greater_than_equal(mediaElement.currentTime, truncatedDuration, + 'Playback time has reached truncatedDuration'); + // The mediaSource.readyState is "ended". Buffered ranges have been adjusted to the longest track. + truncatedDuration = sourceBuffer.buffered.end(sourceBuffer.buffered.length-1); + assert_equals(mediaElement.duration, truncatedDuration, + 'mediaElement truncatedDuration after seek to it'); + assert_equals(mediaSource.duration, truncatedDuration, + 'mediaSource truncatedDuration after seek to it'); + assert_false(mediaElement.seeking, 'mediaElement.seeking after seeked to truncatedDuration'); + + test.done(); + }); + }, 'Test endOfStream completes previous seek to truncated duration'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration'); + + var fullDuration = segmentInfo.duration; + var newDuration = 0.5; + + var expectedDurationChangeEventCount = 1; + var durationchangeEventCounter = 0; + var durationchangeEventHandler = test.step_func(function(event) + { + assert_equals(mediaElement.duration, mediaSource.duration, 'mediaElement newDuration'); + // Final duration may be greater than originally set as per MSE's 2.4.6 Duration change + // Adjust newDuration accordingly. + assert_less_than_equal(newDuration, mediaSource.duration, 'mediaSource newDuration'); + durationchangeEventCounter++; + }); + + mediaElement.play(); + + // Append all the segments + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'playing', 'Playing triggered'); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + assert_less_than(mediaElement.currentTime, newDuration / 2, 'mediaElement currentTime'); + + assert_false(sourceBuffer.updating, "updating"); + + // Truncate duration. This should result in one 'durationchange' fired. + sourceBuffer.remove(newDuration, Infinity); + + assert_true(sourceBuffer.updating, "updating"); + test.expectEvent(sourceBuffer, 'updatestart', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'update', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + }); + + test.waitForExpectedEvents(function() + { + // Media load also fires 'durationchange' event, so only start counting them now. + mediaElement.addEventListener('durationchange', durationchangeEventHandler); + + assert_false(sourceBuffer.updating, "updating"); + + // Truncate duration. This should result in one 'durationchange' fired. + mediaSource.duration = newDuration; + + // Final duration may be greater than originally set as per MSE's 2.4.6 Duration change + // Adjust newDuration accordingly. + assert_true(newDuration <= mediaSource.duration, 'adjusted duration'); + newDuration = mediaSource.duration; + + // Set duration again to make sure it does not trigger another 'durationchange' event. + mediaSource.duration = newDuration; + + // Mark endOfStream so that playback can reach 'ended' at the new duration. + test.expectEvent(mediaSource, 'sourceended', 'endOfStream acknowledged'); + mediaSource.endOfStream(); + + // endOfStream can change duration slightly. + // Allow for one more 'durationchange' event only in this case. + var currentDuration = mediaSource.duration; + if (currentDuration != newDuration) { + newDuration = currentDuration; + ++expectedDurationChangeEventCount; + } + + // Allow media to play to end while counting 'durationchange' events. + test.expectEvent(mediaElement, 'ended', 'Playback ended'); + test.waitForExpectedEvents(function() + { + mediaElement.removeEventListener('durationchange', durationchangeEventHandler); + assert_equals(durationchangeEventCounter, expectedDurationChangeEventCount, 'durationchanges'); + test.done(); + }); + }); + }, 'Test setting same duration multiple times does not fire duplicate durationchange'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration'); + + var fullDuration = segmentInfo.duration; + var newDuration = fullDuration / 2; + + // Append all the segments + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'loadedmetadata', 'mediaElement'); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + + assert_throws_dom("InvalidStateError", function() + { + mediaSource.duration = newDuration; + }, "duration"); + + test.done(); + }); + }, 'Test setting the duration to less than the highest starting presentation timestamp will throw'); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAudio, function(typeAudio, dataAudio) + { + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameVideo, function(typeVideo, dataVideo) + { + var sourceBufferAudio = mediaSource.addSourceBuffer(typeAudio); + var sourceBufferVideo = mediaSource.addSourceBuffer(typeVideo); + var newDuration = 1.2; + + sourceBufferAudio.appendWindowEnd = 2.0; + sourceBufferAudio.appendWindowStart = newDuration / 2.0; + sourceBufferAudio.appendBuffer(dataAudio); + + sourceBufferVideo.appendWindowEnd = 2.0; + sourceBufferVideo.appendWindowStart = newDuration * 1.3; + sourceBufferVideo.appendBuffer(dataVideo); + + test.expectEvent(sourceBufferAudio, "updateend"); + test.expectEvent(sourceBufferVideo, "updateend"); + test.waitForExpectedEvents(function() + { + assert_equals(sourceBufferAudio.buffered.length, 1); + assert_equals(sourceBufferVideo.buffered.length, 1); + assert_less_than(sourceBufferAudio.buffered.start(0), newDuration); + assert_greater_than(sourceBufferVideo.buffered.start(0), newDuration); + assert_throws_dom("InvalidStateError", function () { mediaSource.duration = newDuration; }); + test.done(); + }); + }); + }); + }, "Truncating the duration throws an InvalidStateError exception when new duration is less than the highest buffered range start time of one of the track buffers"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameAudio, function(typeAudio, dataAudio) + { + MediaSourceUtil.fetchManifestAndData(test, manifestFilenameVideo, function(typeVideo, dataVideo) + { + var sourceBufferAudio = mediaSource.addSourceBuffer(typeAudio); + var sourceBufferVideo = mediaSource.addSourceBuffer(typeVideo); + + // Buffer audio [0.8,1.8) + sourceBufferAudio.timestampOffset = 0.8; + sourceBufferAudio.appendWindowEnd = 1.8; + sourceBufferAudio.appendBuffer(dataAudio); + + // Buffer video [1.5,3) + sourceBufferVideo.timestampOffset = 1.5; + sourceBufferVideo.appendWindowEnd = 3; + sourceBufferVideo.appendBuffer(dataVideo); + + test.expectEvent(sourceBufferAudio, "updateend"); + test.expectEvent(sourceBufferVideo, "updateend"); + test.waitForExpectedEvents(function() + { + var newDuration = 2.0; + + // Verify the test setup + assert_equals(sourceBufferAudio.buffered.length, 1); + assert_equals(sourceBufferVideo.buffered.length, 1); + assert_greater_than(sourceBufferAudio.buffered.end(0), 1.5); + assert_less_than(sourceBufferAudio.buffered.end(0), newDuration); + assert_less_than(sourceBufferVideo.buffered.start(0), newDuration); + assert_greater_than(sourceBufferVideo.buffered.end(0), newDuration + 0.5); + + // Verify the expected error + // We assume relocated test video has at least one coded + // frame presentation interval which fits in [>2.0,>2.5) + assert_throws_dom("InvalidStateError", function () { mediaSource.duration = newDuration; }); + test.done(); + }); + }); + }); + }, "Truncating the duration throws an InvalidStateError exception when new duration is less than a buffered coded frame presentation time"); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_less_than(segmentInfo.duration, 60, 'Sufficient test media duration'); + sourceBuffer.appendBuffer(mediaData); + test.expectEvent(sourceBuffer, 'updateend', 'Media data appended to the SourceBuffer'); + test.waitForExpectedEvents(function() + { + mediaSource.duration = 60; + assert_false(sourceBuffer.updating, 'No SourceBuffer update when duration is increased'); + test.done(); + }); + }, 'Increasing the duration does not trigger any SourceBuffer update'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_greater_than(segmentInfo.duration, 2, 'Sufficient test media duration'); + mediaElement.play(); + sourceBuffer.appendBuffer(mediaData); + test.expectEvent(sourceBuffer, 'updateend', 'Media data appended to the SourceBuffer'); + test.waitForExpectedEvents(function() + { + mediaSource.duration = 60; + assert_false(sourceBuffer.updating, 'No SourceBuffer update when duration is increased'); + test.done(); + }); + }, 'Increasing the duration during media playback does not trigger any SourceBuffer update'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-endofstream-invaliderror.html b/testing/web-platform/tests/media-source/mediasource-endofstream-invaliderror.html new file mode 100644 index 0000000000..20a118d717 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-endofstream-invaliderror.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Invalid MediaSource.endOfStream() parameter test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + assert_equals(mediaSource.readyState, 'open'); + + assert_throws_js(TypeError, + function() { mediaSource.endOfStream('garbage'); }, + 'endOfStream(\'garbage\') throws TypeError'); + + assert_equals(mediaSource.readyState, 'open'); + test.done(); + }, 'Test MediaSource.endOfStream() with invalid non-empty error string.'); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + assert_equals(mediaSource.readyState, 'open'); + + assert_throws_js(TypeError, + function() { mediaSource.endOfStream(''); }, + 'endOfStream(\'\') throws TypeError'); + + assert_equals(mediaSource.readyState, 'open'); + test.done(); + }, 'Test MediaSource.endOfStream() with invalid empty error string.'); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + assert_equals(mediaSource.readyState, 'open'); + + assert_throws_js(TypeError, + function() { mediaSource.endOfStream(null); }, + 'endOfStream(null) throws TypeError'); + + assert_equals(mediaSource.readyState, 'open'); + test.done(); + }, 'Test MediaSource.endOfStream() with invalid null error parameter.'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-endofstream.html b/testing/web-platform/tests/media-source/mediasource-endofstream.html new file mode 100644 index 0000000000..3af190ea3e --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-endofstream.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Calls to MediaSource.endOfStream() without error</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaSource.duration = 2; + mediaSource.endOfStream(); + assert_equals(mediaSource.duration, 0); + test.done(); + }, 'MediaSource.endOfStream(): duration truncated to 0 when there are no buffered coded frames'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + sourceBuffer.appendBuffer(mediaData); + test.expectEvent(sourceBuffer, 'updateend', + 'Media buffer appended to SourceBuffer'); + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + test.expectEvent(mediaElement, 'canplaythrough', + 'Media element may render the media content until the end'); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_ENOUGH_DATA, + 'Media element has enough data to render the content'); + test.done(); + }); + }, 'MediaSource.endOfStream(): media element notified that it now has all of the media data'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + sourceBuffer.appendBuffer(mediaData); + test.expectEvent(sourceBuffer, 'updateend', + 'Media buffer appended to SourceBuffer'); + test.waitForExpectedEvents(function() + { + assert_equals(sourceBuffer.buffered.length, 1, + 'Media data properly buffered'); + var highestEndTime = sourceBuffer.buffered.end(0); + + // Note that segmentInfo.duration is expected to also be the + // highest track buffer range end time. Therefore, endOfStream() should + // not change duration with this media. + assert_approx_equals(segmentInfo.duration, mediaSource.duration, 0.001, + 'SegmentInfo duration should initially roughly match mediaSource duration'); + assert_less_than_equal(highestEndTime, mediaSource.duration, + 'Media duration may be slightly longer than intersected track buffered ranges'); + + // Set the duration even higher, then confirm that endOfStream() drops it back to be + // the highest track buffer range end time. + mediaSource.duration += 10; + mediaSource.endOfStream(); + + assert_equals(sourceBuffer.buffered.length, 1, + 'Media data properly buffered after endOfStream'); + + assert_approx_equals(segmentInfo.duration, mediaSource.duration, 0.001, + 'SegmentInfo duration should still roughly match mediaSource duration'); + assert_less_than_equal(highestEndTime, mediaSource.duration, + 'Media duration may be slightly longer than intersected track buffered ranges'); + assert_equals(sourceBuffer.buffered.end(0), mediaSource.duration, + 'After endOfStream(), highest buffered range end time must be the highest track buffer range end time'); + + test.done(); + }); + }, 'MediaSource.endOfStream(): duration and buffered range end time before and after endOfStream'); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-errors.html b/testing/web-platform/tests/media-source/mediasource-errors.html new file mode 100644 index 0000000000..b2224aa5f6 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-errors.html @@ -0,0 +1,273 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + function ErrorTest(testFunction, description) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + var segmentInfo = MediaSourceUtil.SEGMENT_INFO; + + if (!segmentInfo) { + assert_unreached("No segment info compatible with this MediaSource implementation."); + return; + } + + var sourceBuffer = mediaSource.addSourceBuffer(segmentInfo.type); + MediaSourceUtil.loadBinaryData(test, segmentInfo.url, function(mediaData) + { + testFunction(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData); + }); + }, description); + } + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + test.expectEvent(sourceBuffer, "error", "sourceBuffer error."); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "error", "mediaElement error."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + test.expectEvent(mediaSource, "sourceclose", "mediaSource closed."); + sourceBuffer.appendBuffer(mediaSegment); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + + assert_equals(mediaSource.sourceBuffers.length, 0); + assert_equals(mediaSource.readyState, "closed"); + test.done(); + }); + }, "Appending media segment before the first initialization segment."); + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + + // Fail if the append error algorithm occurs, since the decode + // error will be provided by us directly via endOfStream(). + sourceBuffer.addEventListener("error", test.unreached_func("'error' should not be fired on sourceBuffer")); + + test.expectEvent(mediaElement, "error", "mediaElement error."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + test.expectEvent(mediaSource, "sourceclose", "mediaSource closed."); + + mediaSource.endOfStream("decode"); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + + assert_equals(mediaSource.sourceBuffers.length, 0); + assert_equals(mediaSource.readyState, "closed"); + + // Give a short time for a broken implementation to errantly fire + // "error" on sourceBuffer. + test.step_timeout(test.step_func_done(), 0); + }); + }, "Signaling 'decode' error via endOfStream() before initialization segment has been appended."); + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + + // Fail if the append error algorithm occurs, since the network + // error will be provided by us directly via endOfStream(). + sourceBuffer.addEventListener("error", test.unreached_func("'error' should not be fired on sourceBuffer")); + + test.expectEvent(mediaElement, "error", "mediaElement error."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + test.expectEvent(mediaSource, "sourceclose", "mediaSource closed."); + + mediaSource.endOfStream("network"); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + + assert_equals(mediaSource.sourceBuffers.length, 0); + assert_equals(mediaSource.readyState, "closed"); + + // Give a short time for a broken implementation to errantly fire + // "error" on sourceBuffer. + test.step_timeout(test.step_func_done(), 0); + }); + }, "Signaling 'network' error via endOfStream() before initialization segment has been appended."); + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + + // Fail if the append error algorithm occurs, since the decode + // error will be provided by us directly via endOfStream(). + sourceBuffer.addEventListener("error", test.unreached_func("'error' should not be fired on sourceBuffer")); + + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "mediaElement metadata."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_METADATA); + + test.expectEvent(mediaElement, "error", "mediaElement error."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + mediaSource.endOfStream("decode"); + }); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_DECODE); + assert_equals(mediaSource.readyState, "ended"); + + // Give a short time for a broken implementation to errantly fire + // "error" on sourceBuffer. + test.step_timeout(test.step_func_done(), 0); + }); + + }, "Signaling 'decode' error via endOfStream() after initialization segment has been appended and the HTMLMediaElement has reached HAVE_METADATA."); + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + + // Fail if the append error algorithm occurs, since the network + // error will be provided by us directly via endOfStream(). + sourceBuffer.addEventListener("error", test.unreached_func("'error' should not be fired on sourceBuffer")); + + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "mediaElement metadata."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_METADATA); + test.expectEvent(mediaElement, "error", "mediaElement error."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + mediaSource.endOfStream("network"); + }); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_NETWORK); + assert_equals(mediaSource.readyState, "ended"); + + // Give a short time for a broken implementation to errantly fire + // "error" on sourceBuffer. + test.step_timeout(test.step_func_done(), 0); + }); + }, "Signaling 'network' error via endOfStream() after initialization segment has been appended and the HTMLMediaElement has reached HAVE_METADATA."); + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "mediaElement metadata."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_METADATA); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + var index = segmentInfo.init.size + (mediaSegment.length - 1) / 2; + // Corrupt the media data from index of mediaData, so it can signal 'decode' error. + // Here use mediaSegment to replace the original mediaData[index, index + mediaSegment.length] + mediaData.set(mediaSegment, index); + + test.expectEvent(sourceBuffer, "error", "sourceBuffer error."); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaElement, "error", "mediaElement error."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + sourceBuffer.appendBuffer(mediaData); + }); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_DECODE); + test.done(); + }); + }, "Signaling 'decode' error via segment parser loop algorithm after initialization segment has been appended."); + + ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + var index = segmentInfo.init.size + (mediaSegment.length - 1) / 2; + // Corrupt the media data from index of mediaData, so it can signal 'decode' error. + // Here use mediaSegment to replace the original mediaData[index, index + mediaSegment.length] + mediaData.set(mediaSegment, index); + + // Depending on implementation, mediaElement transition to + // HAVE_METADATA and dispatching 'loadedmetadata' may occur, since the + // initialization segment is uncorrupted and forms the initial part of + // the appended bytes. The segment parser loop continues and + // eventually should observe decode error. Other implementations may + // delay such transition until some larger portion of the append's + // parsing is completed or until the media element is configured to + // handle the playback of media with the associated metadata (which may + // not occur in this case before the MSE append error algorithm executes.) + // So we cannot reliably expect the lack or presence of + // 'loadedmetadata' before the MSE append error algortihm executes in + // this case; similarly, mediaElement's resulting readyState may be + // either HAVE_NOTHING or HAVE_METADATA after the append error + // algorithm executes, and the resulting MediaError code would + // respectively be MEDIA_ERR_SRC_NOT_SUPPORTED or MEDIA_ERR_DECODE. + let loaded = false; + mediaElement.addEventListener("loadedmetadata", test.step_func(() => { loaded = true; })); + let errored = false; + mediaElement.addEventListener("error", test.step_func(() => { errored = true; })); + + test.expectEvent(sourceBuffer, "error", "sourceBuffer error."); + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + test.expectEvent(mediaSource, "sourceended", "mediaSource ended."); + sourceBuffer.appendBuffer(mediaData); + + let verifyFinalState = test.step_func(function() { + if (loaded) { + assert_greater_than(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_DECODE); + test.done(); + } else { + assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING); + assert_true(mediaElement.error != null); + assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + test.done(); + } + }); + + let awaitMediaElementError = test.step_func(function() { + if (!errored) { + test.step_timeout(awaitMediaElementError, 100); + } else { + verifyFinalState(); + } + }); + + test.waitForExpectedEvents(function() + { + // Not all implementations will reliably fire a "loadedmetadata" + // event, so we use custom logic to verify mediaElement state based + // on whether or not "loadedmetadata" was ever fired. But first + // we must ensure "error" was fired on the mediaElement. + awaitMediaElementError(); + }); + + }, "Signaling 'decode' error via segment parser loop algorithm of append containing init plus corrupted media segment."); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-getvideoplaybackquality.html b/testing/web-platform/tests/media-source/mediasource-getvideoplaybackquality.html new file mode 100644 index 0000000000..54b2a55799 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-getvideoplaybackquality.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>HTMLVideoElement.getVideoPlaybackQuality() test cases.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var previousQuality = mediaElement.getVideoPlaybackQuality(); + var timeUpdateCount = 0; + mediaElement.addEventListener("timeupdate", test.step_func(function (e) + { + var videoElement = e.target; + var newQuality = videoElement.getVideoPlaybackQuality(); + var now = window.performance.now(); + + assert_not_equals(previousQuality, newQuality, + "New quality object is different from the previous one"); + assert_greater_than(newQuality.creationTime, previousQuality.creationTime, + "creationTime increases monotonically"); + assert_approx_equals(newQuality.creationTime, now, 100, + "creationTime roughly equals current time"); + + assert_greater_than_equal(newQuality.totalVideoFrames, 0, "totalVideoFrames >= 0"); + assert_greater_than_equal(newQuality.totalVideoFrames, previousQuality.totalVideoFrames, + "totalVideoFrames increases monotonically"); + assert_less_than(newQuality.totalVideoFrames, 300, + "totalVideoFrames should remain low as duration is less than 10s and framerate less than 30fps"); + + assert_greater_than_equal(newQuality.droppedVideoFrames, 0, "droppedVideoFrames >= 0"); + assert_greater_than_equal(newQuality.droppedVideoFrames, previousQuality.droppedVideoFrames, + "droppedVideoFrames increases monotonically"); + assert_less_than_equal(newQuality.droppedVideoFrames, newQuality.totalVideoFrames, + "droppedVideoFrames is only a portion of totalVideoFrames"); + + previousQuality = newQuality; + timeUpdateCount++; + })); + + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + sourceBuffer.appendBuffer(mediaData); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + mediaSource.endOfStream(); + assert_less_than(mediaSource.duration, 10, "duration"); + mediaElement.play().catch(test.unreached_func("Unexpected promise rejection"));; + test.expectEvent(mediaElement, 'ended', 'mediaElement'); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than(timeUpdateCount, 2, "timeUpdateCount"); + test.done(); + }); + }, "Test HTMLVideoElement.getVideoPlaybackQuality() with MediaSource API"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-h264-play-starved.html b/testing/web-platform/tests/media-source/mediasource-h264-play-starved.html new file mode 100644 index 0000000000..a3cdf3cb26 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-h264-play-starved.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test MediaSource behavior when the decoder is starved.</title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"> + <meta name="timeout" content="long"> + <link rel="author" title="Alicia Boya GarcÃa" href="mailto:aboya@igalia.com"/> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> +</head> +<body> +<div id="log"></div> +<script> + mediasource_test(function (test, video, mediaSource) { + if (!MediaSource.isTypeSupported('video/mp4; codecs="avc1.4d001e"')) { + // Format not supported, nothing to test in this platform. + test.done(); + return; + } + + let initSegment; + let mediaSegment; + + const videoSB = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.4d001e"'); + + MediaSourceUtil.loadBinaryData(test, "mp4/h264-starvation-init.mp4", initDownloaded); + + function initDownloaded(data) { + initSegment = data; + MediaSourceUtil.loadBinaryData(test, "mp4/h264-starvation-media.mp4", mediaDownloaded); + } + + function mediaDownloaded(data) { + mediaSegment = data; + videoSB.appendBuffer(initSegment); + videoSB.addEventListener("updateend", initSegmentAppended); + } + + function initSegmentAppended() { + videoSB.removeEventListener("updateend", initSegmentAppended); + videoSB.appendBuffer(mediaSegment); + videoSB.addEventListener("updateend", mediaSegmentAppended) + } + + function mediaSegmentAppended() { + video.play(); + + video.addEventListener('timeupdate', function onTimeUpdate() { + if (video.currentTime >= 2) + test.done(); + }); + } + }, "Enough frames are played when the decoder is starved.") +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-invalid-codec.html b/testing/web-platform/tests/media-source/mediasource-invalid-codec.html new file mode 100644 index 0000000000..19aa00c4d5 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-invalid-codec.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>SourceBuffer handling of invalid codecs in the initialization segment</title> + <link rel="author" title="Alicia Boya GarcÃa" href="mailto:aboya@igalia.com"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> +</head> +<body> +<div id="log"></div> +<script> + function testInvalidCodec(test, mediaElement, mediaSource, mediaType, url) { + assert_true(MediaSource.isTypeSupported(mediaType), `Media type not supported in this browser: isTypeSupported('${mediaType}')`); + + MediaSourceUtil.loadBinaryData(test, url, (mediaData) => { + _testInvalidCodecWithData(test, mediaElement, mediaSource, mediaType, mediaData); + }); + } + + function _testInvalidCodecWithData(test, mediaElement, mediaSource, mediaType, mediaData) { + const sourceBuffer = mediaSource.addSourceBuffer(mediaType); + sourceBuffer.appendBuffer(mediaData); + test.expectEvent(sourceBuffer, 'error', 'Append ended with error'); + test.waitForExpectedEvents(() => { + test.done(); + }) + } + + // These test cases provide a typical media MIME type, but the actual files have been mangled to declare a different, + // unsupported, fictitious codec (MP4 fourcc: 'zzzz', WebM codec id 'V_ZZZ'). The browser should report a parsing + // error. + + mediasource_test((test, mediaElement, mediaSource) => { + testInvalidCodec(test, mediaElement, mediaSource, 'video/mp4;codecs="avc1.4D4001"', 'mp4/invalid-codec.mp4'); + }, 'Test an MP4 with an invalid codec results in an error.'); + + mediasource_test((test, mediaElement, mediaSource) => { + testInvalidCodec(test, mediaElement, mediaSource, 'video/webm; codecs="vp8"', 'webm/invalid-codec.webm'); + }, 'Test a WebM with an invalid codec results in an error.'); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-is-type-supported.html b/testing/web-platform/tests/media-source/mediasource-is-type-supported.html new file mode 100644 index 0000000000..93b067c692 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-is-type-supported.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MediaSource.isTypeSupported() test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <div id="log"></div> + <script> + // Generate a distinct test for each type in types + function test_type_support(types, expectation, description) + { + for (var i = 0; i < types.length; ++i) { + test(function() + { + assert_equals(MediaSource.isTypeSupported(types[i]), + expectation, 'supported'); + }, description + ' "' + types[i] + '"'); + } + }; + + test_type_support([ + 'video', + 'video/', + 'video/webm', + 'video/webm;', + 'video/webm;codecs', + 'video/webm;codecs=', + 'video/webm;codecs="', + 'video/webm;codecs=""', + 'video/webm;codecs=","', + 'audio/webm;aaacodecsbbb=opus', + 'unsupported_mediatype', + '', + null + ], false, 'Test invalid MIME format'); + + test_type_support([ + 'xxx', + 'text/html', + 'image/jpeg' + ], false, 'Test invalid MSE MIME media type'); + + test_type_support([ + 'audio/webm;codecs="vp8"', + 'audio/mp4;codecs="avc1.4d001e"', + 'audio/mp4;codecs="vorbis"', + 'audio/webm;codecs="mp4a.40.2"', + 'video/mp4;codecs="vp8"', + 'video/mp4;codecs="vorbis"', + 'video/webm;codecs="mp4a.40.2"', + ], false, 'Test invalid mismatch between MIME type and codec ID'); + + // Note that, though the user agent might support some subset of + // these for progressive non-MSE playback, the MSE mpeg audio + // bytestream format specification requires there to be no codecs + // parameter. + test_type_support([ + 'audio/mpeg;codecs="mp3"', + 'audio/mpeg;codecs="mp4a.69"', + 'audio/mpeg;codecs="mp4a.6B"', + 'audio/aac;codecs="aac"', + 'audio/aac;codecs="adts"', + 'audio/aac;codecs="mp4a.40"', + ], false, 'Test invalid inclusion of codecs parameter for mpeg audio types'); + + test_type_support([ + 'audio/mp4;codecs="mp4a"', + 'audio/mp4;codecs="mp4a.40"', + 'audio/mp4;codecs="mp4a.40."', + 'audio/mp4;codecs="mp4a.67.3"' + ], false, 'Test invalid codec ID'); + + test_type_support([ + 'video/webm;codecs="vp8"', + 'video/webm;codecs="vorbis"', + 'video/webm;codecs="vp8,vorbis"', + 'video/webm;codecs="vorbis, vp8"', + 'audio/webm;codecs="vorbis"', + 'AUDIO/WEBM;CODECS="vorbis"', + 'audio/webm;codecs=vorbis;test="6"', + 'audio/webm;codecs="opus"', + 'video/webm;codecs="opus"' + ], true, 'Test valid WebM type'); + + test_type_support([ + 'video/mp4;codecs="avc1.4d001e"', // H.264 Main Profile level 3.0 + 'video/mp4;codecs="avc1.42001e"', // H.264 Baseline Profile level 3.0 + 'audio/mp4;codecs="mp4a.40.2"', // MPEG4 AAC-LC + 'audio/mp4;codecs="mp4a.40.5"', // MPEG4 HE-AAC + 'audio/mp4;codecs="mp4a.67"', // MPEG2 AAC-LC + 'video/mp4;codecs="mp4a.40.2"', + 'video/mp4;codecs="avc1.4d001e,mp4a.40.2"', + 'video/mp4;codecs="mp4a.40.2 , avc1.4d001e "', + 'video/mp4;codecs="avc1.4d001e,mp4a.40.5"', + 'audio/mp4;codecs="Opus"', + 'video/mp4;codecs="Opus"', + 'audio/mp4;codecs="fLaC"', + 'video/mp4;codecs="fLaC"' + ], true, 'Test valid MP4 type'); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-liveseekable.html b/testing/web-platform/tests/media-source/mediasource-liveseekable.html new file mode 100644 index 0000000000..123a41e9e5 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-liveseekable.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<title>Checks setting/clearing the live seekable range and HTMLMediaElement.seekable</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> +test(function(test) +{ + var mediaSource = new MediaSource(); + assert_equals(mediaSource.readyState, "closed", "media source is closed."); + assert_throws_dom("InvalidStateError", function() { mediaSource.setLiveSeekableRange(0, 1); }); +}, "setLiveSeekableRange throws an InvalidStateError exception if the readyState attribute is not 'open'"); + + +test(function(test) +{ + var mediaSource = new MediaSource(); + assert_equals(mediaSource.readyState, "closed", "media source is closed."); + assert_throws_dom("InvalidStateError", function() { mediaSource.clearLiveSeekableRange(); }); +}, "clearLiveSeekableRange throws an InvalidStateError exception if the readyState attribute is not 'open'"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + var mimetype = MediaSourceUtil.AUDIO_VIDEO_TYPE; + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + sourceBuffer.appendBuffer(new Uint8Array(0)); + assert_true(sourceBuffer.updating, "Updating set when a buffer is appended."); + mediaSource.setLiveSeekableRange(0, 1); + test.done(); +}, "setLiveSeekableRange does not restrict to not currently updating"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + var mimetype = MediaSourceUtil.AUDIO_VIDEO_TYPE; + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + sourceBuffer.appendBuffer(new Uint8Array(0)); + assert_true(sourceBuffer.updating, "Updating set when a buffer is appended."); + mediaSource.clearLiveSeekableRange(); + test.done(); +}, "clearLiveSeekableRange does not restrict to not currently updating"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + assert_throws_js(TypeError, function() { mediaSource.setLiveSeekableRange(-1, 1); }); + test.done(); +}, "setLiveSeekableRange throws a TypeError if start is negative"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + assert_throws_js(TypeError, function() { mediaSource.setLiveSeekableRange(2, 1); }); + test.done(); +}, "setLiveSeekableRange throws a TypeError if start is greater than end"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + mediaSource.setLiveSeekableRange(0, 1); + test.done(); +}, "setLiveSeekableRange returns with no error when conditions are correct"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + mediaSource.clearLiveSeekableRange(); + test.done(); +}, "clearLiveSeekableRange returns with no error when conditions are correct"); + + +mediasource_test(function(test, mediaElement, mediaSource) +{ + mediaSource.duration = +Infinity; + mediaSource.setLiveSeekableRange(1, 2); + assert_equals(mediaElement.seekable.length, 1, + 'The seekable attribute contains a single range.'); + assertSeekableEquals(mediaElement, '{ [1.000, 2.000) }', + 'The seekable attribute returns the correct range.'); + + mediaSource.clearLiveSeekableRange(); + assertSeekableEquals(mediaElement, '{ }', + 'The seekable attribute now returns an empty range.'); + test.done(); +}, "HTMLMediaElement.seekable returns the live seekable range or an empty range if that range was cleared when nothing is buffered"); + + +mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) +{ + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + test.expectEvent(sourceBuffer, 'updateend', 'Init segment appended to SourceBuffer.'); + sourceBuffer.appendBuffer(initSegment); + test.waitForExpectedEvents(function() + { + mediaSource.duration = +Infinity; + mediaSource.setLiveSeekableRange(40, 42); + + // Append a segment that starts after 1s to ensure seekable + // won't use 0 as starting point. + var midSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[5]); + test.expectEvent(sourceBuffer, 'updateend'); + sourceBuffer.appendBuffer(midSegment); + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.seekable.length, 1, + 'The seekable attribute contains a single range.'); + assert_equals(mediaElement.buffered.length, 1, + 'The buffered attribute contains a single range.'); + assert_not_equals(mediaElement.seekable.start(0), 0, + 'The range starts after 0.'); + assert_equals(mediaElement.seekable.start(0), mediaElement.buffered.start(0), + 'The start time is the start time of the buffered range.'); + assert_equals(mediaElement.seekable.end(0), 42, + 'The end time is the end time of the seekable range.'); + + mediaSource.clearLiveSeekableRange(); + assert_equals(mediaElement.seekable.length, 1, + 'The seekable attribute contains a single range.'); + assert_equals(mediaElement.seekable.start(0), 0, + 'The start time is now 0.'); + assert_equals(mediaElement.seekable.end(0), mediaElement.buffered.end(0), + 'The end time is now the end time of the buffered range.'); + + test.done(); + }); + }); +}, 'HTMLMediaElement.seekable returns the union of the buffered range and the live seekable range, when set'); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-multiple-attach.html b/testing/web-platform/tests/media-source/mediasource-multiple-attach.html new file mode 100644 index 0000000000..4a95a42e83 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-multiple-attach.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Test Attaching a MediaSource to multiple HTMLMediaElements.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + function twoMediaElementTest(testFunction, description) + { + media_test(function(test) + { + var firstMediaTag = document.createElement('video'); + var secondMediaTag = document.createElement('video'); + document.body.appendChild(firstMediaTag); + document.body.appendChild(secondMediaTag); + + // Overload done() so that elements added to the document can be + // removed. + var removeMediaElements = true; + var oldTestDone = test.done.bind(test); + test.done = function() + { + if (removeMediaElements) { + document.body.removeChild(secondMediaTag); + document.body.removeChild(firstMediaTag); + removeMediaElements = false; + } + oldTestDone(); + }; + + testFunction(test, firstMediaTag, secondMediaTag); + }, description); + } + + twoMediaElementTest(function(test, firstMediaTag, secondMediaTag) + { + // When attachment of mediaSource to two MediaElements is done + // without an intervening stable state, exactly one of the two + // MediaElements should successfully attach, and the other one + // should get error event due to mediaSource already in 'open' + // readyState. + var mediaSource = new MediaSource(); + var mediaSourceURL = URL.createObjectURL(mediaSource); + var gotSourceOpen = false; + var gotError = false; + var doneIfFinished = test.step_func(function() + { + if (gotSourceOpen && gotError) + test.done(); + }); + var errorHandler = test.step_func(function(e) + { + firstMediaTag.removeEventListener('error', errorHandler); + secondMediaTag.removeEventListener('error', errorHandler); + + var eventTarget = e.target; + var otherTarget; + if (eventTarget == firstMediaTag) { + otherTarget = secondMediaTag; + } else { + assert_equals(eventTarget, secondMediaTag, 'Error target check'); + otherTarget = firstMediaTag; + } + + assert_true(eventTarget.error != null, 'Error state on one tag'); + assert_equals(eventTarget.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 'Expected error code'); + assert_equals(otherTarget.error, null, 'No error on other tag'); + + assert_equals(eventTarget.networkState, HTMLMediaElement.NETWORK_NO_SOURCE, + 'Tag with error state networkState'); + assert_equals(otherTarget.networkState, HTMLMediaElement.NETWORK_LOADING, + 'Tag without error state networkState'); + + gotError = true; + doneIfFinished(); + }); + + test.expectEvent(mediaSource, 'sourceopen', 'An attachment succeeded'); + firstMediaTag.addEventListener('error', errorHandler); + secondMediaTag.addEventListener('error', errorHandler); + + firstMediaTag.src = mediaSourceURL; + secondMediaTag.src = mediaSourceURL; + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, 'open', 'Source is opened'); + gotSourceOpen = true; + doneIfFinished(); + }); + }, 'Test exactly one succeeds when two MediaElements attach to same MediaSource'); + + mediasource_test(function(test, mediaElement, mediaSource) { + assert_equals(mediaSource.readyState, 'open', 'Source open'); + // Set the tag's src attribute. This should close mediaSource, + // reattach it to the tag, and initiate source reopening. + test.expectEvent(mediaSource, 'sourceopen', 'Source attached again'); + mediaElement.src = URL.createObjectURL(mediaSource); + assert_equals(mediaSource.readyState, 'closed', 'Source closed'); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, 'open', 'Source reopened'); + test.done(); + }); + }, 'Test that MediaSource can reattach if closed first'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-play-then-seek-back.html b/testing/web-platform/tests/media-source/mediasource-play-then-seek-back.html new file mode 100644 index 0000000000..66fdbe810d --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-play-then-seek-back.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Simple MediaSource playback & seek test case.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + + mediaElement.play(); + // Append all the segments + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'playing', 'Playing triggered'); + sourceBuffer.appendBuffer(mediaData); + + function confirmPlayThenEnd() + { + test.waitForCurrentTimeChange(mediaElement, function () + { + assert_greater_than(mediaElement.currentTime, 0.0, 'Playback has started after seek.'); + test.done(); + }); + } + + function finishSeekThenPlay() + { + test.expectEvent(mediaElement, 'seeked', 'mediaElement finished seek'); + + test.waitForExpectedEvents(confirmPlayThenEnd); + } + + function delayedPlayHandler() + { + assert_greater_than(mediaElement.currentTime, 0.0, 'Playback has started.'); + test.expectEvent(mediaElement, 'seeking', 'mediaElement'); + mediaElement.currentTime = 0.0; + assert_true(mediaElement.seeking, 'mediaElement is seeking'); + + test.waitForExpectedEvents(finishSeekThenPlay); + } + + test.waitForExpectedEvents(function() + { + test.waitForCurrentTimeChange(mediaElement, delayedPlayHandler); + }); + + }, 'Test playing then seeking back.'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-play.html b/testing/web-platform/tests/media-source/mediasource-play.html new file mode 100644 index 0000000000..2129b8f473 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-play.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Simple MediaSource playback test case.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener('ended', test.step_func_done()); + + test.expectEvent(sourceBuffer, 'updatestart', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'update', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + + assert_false(sourceBuffer.updating, "sourceBuffer.updating"); + + sourceBuffer.appendBuffer(mediaData); + + assert_true(sourceBuffer.updating, "sourceBuffer.updating"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "sourceBuffer.updating"); + + // Truncate the buffered media to about 1 second duration. + sourceBuffer.remove(1, +Infinity); + + assert_true(sourceBuffer.updating, "sourceBuffer.updating"); + test.expectEvent(sourceBuffer, 'updatestart', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'update', 'sourceBuffer'); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + assert_greater_than(mediaSource.duration, 1, "duration"); + + // Complete truncation of duration to 1 second. + mediaSource.duration = 1; + + test.expectEvent(mediaElement, "durationchange"); + }); + + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + mediaElement.play(); + }); + }, "Test normal playback case with MediaSource API"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-preload.html b/testing/web-platform/tests/media-source/mediasource-preload.html new file mode 100644 index 0000000000..e387b63737 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-preload.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Various MediaSource HTMLMediaElement preload tests.</title> + <link rel="author" title="Matthew Wolenetz" href="mailto:wolenetz@chromium.org"/> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + function attachWithPreloadTest(preload) + { + async_test(function(test) + { + var video = document.createElement("video"); + var mediaSource = new MediaSource(); + var mediaSourceURL = URL.createObjectURL(mediaSource); + + video.preload = preload; + document.body.appendChild(video); + test.add_cleanup(function() { + document.body.removeChild(video); + URL.revokeObjectURL(mediaSourceURL); + }); + + mediaSource.addEventListener("sourceopen", test.step_func_done()); + video.src = mediaSourceURL; + }, "sourceopen occurs with element preload=" + preload); + } + + attachWithPreloadTest("auto"); + attachWithPreloadTest("metadata"); + attachWithPreloadTest("none"); + + function errorWithPreloadTest(preload, bogusURLStyle) + { + async_test(function(test) + { + var mediaSource = new MediaSource(); + var bogusURL = URL.createObjectURL(mediaSource); + + if (bogusURLStyle == "corrupted") { + var goodURL = bogusURL; + test.add_cleanup(function() { URL.revokeObjectURL(goodURL); }); + bogusURL += "0"; + } else if (bogusURLStyle == "revoked") { + URL.revokeObjectURL(bogusURL); + } else { + assert_unreached("invalid case"); + } + + var video = document.createElement("video"); + video.preload = preload; + document.body.appendChild(video); + test.add_cleanup(function() { document.body.removeChild(video); }); + + mediaSource.addEventListener("sourceopen", test.unreached_func("'sourceopen' should not be fired")); + + video.onerror = test.step_func_done(); + video.src = bogusURL; + }, "error occurs with bogus blob URL (" + bogusURLStyle + " MediaSource object URL) and element preload=" + preload); + } + + errorWithPreloadTest("auto", "revoked"); + errorWithPreloadTest("metadata", "revoked"); + + errorWithPreloadTest("auto", "corrupted"); + errorWithPreloadTest("metadata", "corrupted"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-redundant-seek.html b/testing/web-platform/tests/media-source/mediasource-redundant-seek.html new file mode 100644 index 0000000000..05eae9714f --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-redundant-seek.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Test MediaSource behavior when receiving multiple seek requests during a pending seek.</title> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.play(); + + // Append all media data for complete playback. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer end update.'); + test.expectEvent(mediaElement, 'loadedmetadata', 'Reached HAVE_METADATA'); + test.expectEvent(mediaElement, 'playing', 'Playing media.'); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + var bufferedRanges = mediaElement.buffered; + + assert_greater_than_equal(mediaElement.duration, 4.0, 'Duration is >= 4.0s'); + assert_equals(bufferedRanges.length, 1, 'Just one buffered range'); + assert_less_than_equal(bufferedRanges.start(0), 1.0, 'Buffered range starts <= 1.0s'); + assert_greater_than_equal(bufferedRanges.end(0), 4.0, 'Buffered range ends >= 4.0s'); + + test.expectEvent(mediaElement, 'seeking', 'seeking'); + test.expectEvent(mediaElement, 'timeupdate', 'timeupdate'); + test.expectEvent(mediaElement, 'seeked', 'seeked'); + + // Request seeks. + mediaElement.currentTime = 1.0; + + // This 'ephemeral' seek should be invisible to javascript, except any latency incurred in its processing. + mediaElement.currentTime = 3.0; + + mediaElement.currentTime = 1.0; + + assert_true(mediaElement.seeking, 'Element is seeking'); + assert_equals(mediaElement.currentTime, 1.0, 'Element time is at last seek time'); + }); + + test.waitForExpectedEvents(function() + { + // No more seeking or seeked events should occur. + mediaElement.addEventListener('seeking', test.unreached_func("Unexpected event 'seeking'")); + mediaElement.addEventListener('seeked', test.unreached_func("Unexpected event 'seeked'")); + + assert_false(mediaElement.seeking, 'Element is not seeking'); + assert_greater_than_equal(mediaElement.currentTime, 1.0, 'Element time is at or after last seek time'); + assert_less_than(mediaElement.currentTime, 3.0, 'Element time is before the ephemeral seek time'); + + var timeBeforeWait = mediaElement.currentTime; + test.waitForCurrentTimeChange(mediaElement, function() + { + // Time should have advanced a little, but not yet reached the ephemeral seek time. + assert_greater_than(mediaElement.currentTime, timeBeforeWait, 'Element time has increased'); + assert_less_than(mediaElement.currentTime, 3.0, 'Element time is still before the ephemeral seek time'); + test.done(); + }); + }); + }, 'Test redundant fully prebuffered seek'); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-remove.html b/testing/web-platform/tests/media-source/mediasource-remove.html new file mode 100644 index 0000000000..64d5a31488 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-remove.html @@ -0,0 +1,353 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <meta charset="utf-8"> + <title>SourceBuffer.remove() test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(-1, 2); + }, "remove"); + + test.done(); + }, "Test remove with an negative start."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + [ undefined, NaN, Infinity, -Infinity ].forEach(function(item) + { + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(item, 2); + }, "remove"); + }); + + test.done(); + }, "Test remove with non-finite start."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(11, 12); + }, "remove"); + + test.done(); + }, "Test remove with a start beyond the duration."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(2, 1); + }, "remove"); + + test.done(); + }, "Test remove with a start larger than the end."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(0, Number.NEGATIVE_INFINITY); + }, "remove"); + + test.done(); + }, "Test remove with a NEGATIVE_INFINITY end."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(0, Number.NaN); + }, "remove"); + + test.done(); + }, "Test remove with a NaN end."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + mediaSource.removeSourceBuffer(sourceBuffer); + + assert_throws_dom("InvalidStateError", function() + { + sourceBuffer.remove(1, 2); + }, "remove"); + + test.done(); + }, "Test remove after SourceBuffer removed from mediaSource."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + assert_false(sourceBuffer.updating, "updating is false"); + assert_equals(mediaSource.duration, NaN, "duration isn't set"); + + assert_throws_js(TypeError, function() + { + sourceBuffer.remove(0, 0); + }, "remove"); + + test.done(); + }, "Test remove with a NaN duration."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.remove(1, 2); + + assert_true(sourceBuffer.updating, "updating"); + + assert_throws_dom("InvalidStateError", function() + { + sourceBuffer.remove(3, 4); + }, "remove"); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + }, "Test remove while update pending."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + + mediaSource.duration = 10; + + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.remove(1, 2); + + assert_true(sourceBuffer.updating, "updating"); + + assert_throws_dom("InvalidStateError", function() + { + sourceBuffer.abort(); + }, "abort"); + + assert_true(sourceBuffer.updating, "updating"); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + }, "Test aborting a remove operation."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + sourceBuffer.appendBuffer(mediaData); + + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + + test.waitForExpectedEvents(function() + { + assert_less_than(mediaSource.duration, 10) + + mediaSource.duration = 10; + + sourceBuffer.remove(mediaSource.duration, mediaSource.duration + 2); + + assert_true(sourceBuffer.updating, "updating"); + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + }); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + + }, "Test remove with a start at the duration."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + sourceBuffer.appendBuffer(mediaData); + + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + + test.waitForExpectedEvents(function() + { + assert_less_than(mediaSource.duration, 10) + + mediaSource.duration = 10; + + sourceBuffer.remove(1, 1 + 0.9 / (1000 * 1000)); + + assert_true(sourceBuffer.updating, "updating"); + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + }); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + + }, "Test remove with a nanosecond range."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + + assert_equals(mediaSource.readyState, "ended"); + + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + test.expectEvent(mediaSource, "sourceopen"); + sourceBuffer.remove(1, 2); + + assert_true(sourceBuffer.updating, "updating"); + assert_equals(mediaSource.readyState, "open"); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + test.done(); + }); + }, "Test remove transitioning readyState from 'ended' to 'open'."); + + function removeAppendedDataTests(callback, description) + { + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + assert_false(sourceBuffer.updating, "updating"); + + var start = Math.max(segmentInfo.media[0].timev, segmentInfo.media[0].timea).toFixed(3); + var duration = mediaElement.duration.toFixed(3); + var subType = MediaSourceUtil.getSubType(segmentInfo.type); + + assertBufferedEquals(sourceBuffer, "{ [" + start + ", " + duration + ") }", "Initial buffered range."); + callback(test, mediaSource, sourceBuffer, duration, subType, segmentInfo); + }); + }, description); + }; + function removeAndCheckBufferedRanges(test, mediaSource, sourceBuffer, start, end, expected) + { + test.expectEvent(sourceBuffer, "updatestart"); + test.expectEvent(sourceBuffer, "update"); + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.remove(start, end); + + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + assert_false(sourceBuffer.updating, "updating"); + + assertBufferedEquals(sourceBuffer, expected, "Buffered ranges after remove()."); + test.done(); + }); + } + + removeAppendedDataTests(function(test, mediaSource, sourceBuffer, duration, subType, segmentInfo) + { + removeAndCheckBufferedRanges(test, mediaSource, sourceBuffer, 0, Number.POSITIVE_INFINITY, "{ }"); + }, "Test removing all appended data."); + + removeAppendedDataTests(function(test, mediaSource, sourceBuffer, duration, subType, segmentInfo) + { + var expectations = { + webm: ("{ [3.315, " + duration + ") }"), + mp4: ("{ [3.298, " + duration + ") }"), + }; + + // Note: Range doesn't start exactly at the end of the remove range because there isn't + // a keyframe there. The resulting range starts at the first keyframe >= the end time. + removeAndCheckBufferedRanges(test, mediaSource, sourceBuffer, 0, 3, expectations[subType]); + }, "Test removing beginning of appended data."); + + removeAppendedDataTests(function(test, mediaSource, sourceBuffer, duration, subType, segmentInfo) + { + var start = Math.max(segmentInfo.media[0].timev, segmentInfo.media[0].timea).toFixed(3); + var expectations = { + webm: ("{ [" + start + ", 1.005) [3.315, " + duration + ") }"), + mp4: ("{ [" + start + ", 0.997) [3.298, " + duration + ") }"), + }; + + // Note: The first resulting range ends slightly after start because the removal algorithm only removes + // frames with a timestamp >= the start time. If a frame starts before and ends after the remove() start + // timestamp, then it stays in the buffer. + removeAndCheckBufferedRanges(test, mediaSource, sourceBuffer, 1, 3, expectations[subType]); + }, "Test removing the middle of appended data."); + + removeAppendedDataTests(function(test, mediaSource, sourceBuffer, duration, subType, segmentInfo) + { + var start = Math.max(segmentInfo.media[0].timev, segmentInfo.media[0].timea).toFixed(3); + var expectations = { + webm: "{ [" + start + ", 1.013) }", + mp4: "{ [" + start + ", 1.022) }", + }; + + removeAndCheckBufferedRanges(test, mediaSource, sourceBuffer, 1, Number.POSITIVE_INFINITY, expectations[subType]); + }, "Test removing the end of appended data."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-removesourcebuffer.html b/testing/web-platform/tests/media-source/mediasource-removesourcebuffer.html new file mode 100644 index 0000000000..30ec930cbe --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-removesourcebuffer.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>MediaSource.removeSourceBuffer() test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_class_string(sourceBuffer, "SourceBuffer", "New SourceBuffer returned"); + + mediaSource.removeSourceBuffer(sourceBuffer); + + var sourceBuffer2 = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_class_string(sourceBuffer2, "SourceBuffer", "New SourceBuffer returned"); + assert_not_equals(sourceBuffer, sourceBuffer2, "SourceBuffers are different instances."); + assert_equals(mediaSource.sourceBuffers.length, 1, "sourceBuffers.length == 1"); + + test.done(); + }, "Test addSourceBuffer(), removeSourceBuffer(), addSourceBuffer() sequence."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + assert_throws_js(TypeError, + function() { mediaSource.removeSourceBuffer(null); }, + "removeSourceBuffer() threw an exception when passed null."); + test.done(); + }, "Test removeSourceBuffer() with null"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_class_string(sourceBuffer, "SourceBuffer", "New SourceBuffer returned"); + + mediaSource.removeSourceBuffer(sourceBuffer); + + assert_throws_dom("NotFoundError", + function() { mediaSource.removeSourceBuffer(sourceBuffer); }, + "removeSourceBuffer() threw an exception for a SourceBuffer that was already removed."); + + test.done(); + }, "Test calling removeSourceBuffer() twice with the same object."); + + mediasource_test(function(test, mediaElement1, mediaSource1) + { + var sourceBuffer1 = mediaSource1.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + assert_class_string(sourceBuffer1, "SourceBuffer", "New SourceBuffer returned"); + + var mediaElement2 = document.createElement("video"); + document.body.appendChild(mediaElement2); + test.add_cleanup(function() { document.body.removeChild(mediaElement2); }); + + var mediaSource2 = new MediaSource(); + var mediaSource2URL = URL.createObjectURL(mediaSource2); + mediaElement2.src = mediaSource2URL; + test.expectEvent(mediaSource2, "sourceopen", "Second MediaSource opened"); + test.waitForExpectedEvents(function() + { + URL.revokeObjectURL(mediaSource2URL); + + var sourceBuffer2 = mediaSource2.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + assert_class_string(sourceBuffer2, "SourceBuffer", "Second new SourceBuffer returned"); + assert_not_equals(mediaSource1, mediaSource2, "MediaSources are different instances"); + assert_not_equals(sourceBuffer1, sourceBuffer2, "SourceBuffers are different instances"); + assert_equals(mediaSource1.sourceBuffers[0], sourceBuffer1); + assert_equals(mediaSource2.sourceBuffers[0], sourceBuffer2); + assert_throws_dom("NotFoundError", + function() { mediaSource1.removeSourceBuffer(sourceBuffer2); }, + "MediaSource1.removeSourceBuffer() threw an exception for SourceBuffer2"); + assert_throws_dom("NotFoundError", + function() { mediaSource2.removeSourceBuffer(sourceBuffer1); }, + "MediaSource2.removeSourceBuffer() threw an exception for SourceBuffer1"); + mediaSource1.removeSourceBuffer(sourceBuffer1); + mediaSource2.removeSourceBuffer(sourceBuffer2); + test.done(); + }); + }, "Test calling removeSourceBuffer() for a sourceBuffer belonging to a different mediaSource instance."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_class_string(sourceBuffer, "SourceBuffer", "New SourceBuffer returned"); + + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, "ended", "MediaSource in ended state"); + mediaSource.removeSourceBuffer(sourceBuffer); + + assert_equals(mediaSource.sourceBuffers.length, 0, "MediaSource.sourceBuffers is empty"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "MediaSource.activesourceBuffers is empty"); + + test.done(); + }, "Test calling removeSourceBuffer() in ended state."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + test.expectEvent(mediaElement, "loadedmetadata", "loadedmetadata done."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.sourceBuffers.length, 1, "MediaSource.sourceBuffers is not empty"); + assert_equals(mediaSource.activeSourceBuffers.length, 1, "MediaSource.activesourceBuffers is not empty"); + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA); + assert_equals(mediaSource.duration, segmentInfo.duration); + test.expectEvent(mediaSource.activeSourceBuffers, "removesourcebuffer", "SourceBuffer removed from activeSourceBuffers."); + test.expectEvent(mediaSource.sourceBuffers, "removesourcebuffer", "SourceBuffer removed."); + mediaSource.removeSourceBuffer(sourceBuffer); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.sourceBuffers.length, 0, "MediaSource.sourceBuffers is empty"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "MediaSource.activesourceBuffers is empty"); + test.done(); + }); + }, "Test removesourcebuffer event on activeSourceBuffers."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + var mimetype = MediaSourceUtil.AUDIO_VIDEO_TYPE; + var sourceBuffer = mediaSource.addSourceBuffer(mimetype); + sourceBuffer.appendBuffer(new Uint8Array(0)); + assert_true(sourceBuffer.updating, "Updating flag set when a buffer is appended."); + test.expectEvent(sourceBuffer, 'abort'); + test.expectEvent(sourceBuffer, 'updateend'); + + mediaSource.removeSourceBuffer(sourceBuffer); + assert_false(sourceBuffer.updating, "Updating flag reset after abort."); + test.waitForExpectedEvents(function() + { + test.done(); + }); + }, "Test abort event when removeSourceBuffer() called while SourceBuffer is updating"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-replay.html b/testing/web-platform/tests/media-source/mediasource-replay.html new file mode 100644 index 0000000000..05a8c0a918 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-replay.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- Copyright © 2019 Igalia S.L --> +<html> +<head> + <title>MediaSource replay test case.</title> + <meta name="timeout" content="long"> + <meta charset="utf-8"> + <link rel="author" title="Alicia Boya GarcÃa" href="mailto:aboya@igalia.com"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> +</head> +<body> +<div id="log"></div> +<script> + mediasource_testafterdataloaded(function (test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function () { + mediaSource.endOfStream(); + + // Start playing near the end. + mediaElement.currentTime = 6.2; + mediaElement.play(); + test.expectEvent(mediaElement, 'ended', 'mediaElement'); + }); + + test.waitForExpectedEvents(function () { + mediaElement.play(); + assert_equals(mediaElement.currentTime, 0, "currentTime"); + // If currentTime is able to advance, the player did not get stuck and it's a pass. + test.waitForCurrentTimeChange(mediaElement, test.step_func_done()); + }); + }, "Test replaying video after 'ended'"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-seek-beyond-duration.html b/testing/web-platform/tests/media-source/mediasource-seek-beyond-duration.html new file mode 100644 index 0000000000..8b07c9f801 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-seek-beyond-duration.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Test MediaSource behavior when seeking beyond the duration of the clip.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + + function seekToSpecifiedTimeSetEOSAndVerifyDone(test, mediaElement, mediaSource, seekToTime) + { + assert_less_than(mediaElement.currentTime, mediaElement.duration, 'Not at the end yet.'); + test.expectEvent(mediaElement, 'seeking', 'mediaElement seeking'); + // Seek to specified time. + mediaElement.currentTime = seekToTime; + if (seekToTime >= mediaSource.duration) { + assert_equals(mediaElement.currentTime, mediaSource.duration, 'Current time equals duration.'); + } else { + assert_equals(mediaElement.currentTime, seekToTime, 'Current time equals specified seek time.'); + } + + test.waitForExpectedEvents(function() + { + test.expectEvent(mediaElement, 'timeupdate', 'mediaElement time updated.'); + test.expectEvent(mediaElement, 'seeked', 'mediaElement seeked'); + test.expectEvent(mediaElement, 'ended', 'mediaElement ended.'); + test.expectEvent(mediaSource, 'sourceended', 'mediaSource ended.'); + mediaSource.endOfStream(); + assert_true(mediaElement.seeking, 'mediaElement seeking.'); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.currentTime, mediaSource.duration, 'Current time equals duration.'); + test.done(); + }); + }; + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.play(); + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + + // Append the initialization segment to trigger a transition to HAVE_METADATA. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer end update.'); + test.expectEvent(mediaElement, 'loadedmetadata', 'Reached HAVE_METADATA'); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + // Add sufficient segments to have at least 2s of play-time. + var playbackData = MediaSourceUtil.getMediaDataForPlaybackTime(mediaData, segmentInfo, 2.0); + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'playing', 'Playing media.'); + sourceBuffer.appendBuffer(playbackData); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.duration, segmentInfo.duration); + assert_greater_than_equal(mediaElement.duration, 2.0, 'Duration is >2.0s.'); + + test.expectEvent(sourceBuffer, "updateend"); + sourceBuffer.remove(1.5, Infinity); + assert_true(sourceBuffer.updating, "updating"); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating"); + test.waitForCurrentTimeChange(mediaElement, function() + { + // Update duration. + mediaSource.duration = 1.5; + seekToSpecifiedTimeSetEOSAndVerifyDone(test, mediaElement, mediaSource, 1.8); + }); + }); + }, 'Test seeking beyond updated media duration.'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.play(); + + // Append all media data for complete playback. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer end update.'); + test.expectEvent(mediaElement, 'loadedmetadata', 'Reached HAVE_METADATA'); + test.expectEvent(mediaElement, 'playing', 'Playing media.'); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + test.waitForCurrentTimeChange(mediaElement, function() + { + seekToSpecifiedTimeSetEOSAndVerifyDone(test, mediaElement, mediaSource, mediaSource.duration, mediaSource.duration + 0.1); + }); + }); + + }, 'Test seeking beyond media duration.'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-seek-during-pending-seek.html b/testing/web-platform/tests/media-source/mediasource-seek-during-pending-seek.html new file mode 100644 index 0000000000..60c5eec1c7 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-seek-during-pending-seek.html @@ -0,0 +1,189 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>Test MediaSource behavior when a seek is requested while another seek is pending.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.play(); + + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var firstSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + var segmentIndex = 2; + var secondSegmentInfo = segmentInfo.media[segmentIndex]; + + // Append the initialization segment to trigger a transition to HAVE_METADATA. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'loadedmetadata', 'Reached HAVE_METADATA'); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_false(mediaElement.seeking, 'mediaElement is not seeking'); + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA, 'Still in HAVE_METADATA'); + + // Seek to a new position before letting the initial seek to 0 completes. + test.expectEvent(mediaElement, 'seeking', 'mediaElement'); + mediaElement.currentTime = Math.max(secondSegmentInfo.timev, secondSegmentInfo.timea); + assert_true(mediaElement.seeking, 'mediaElement is seeking'); + + // Append media data for time 0. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + sourceBuffer.appendBuffer(firstSegment); + }); + + test.waitForExpectedEvents(function() + { + // Verify that the media data didn't trigger a 'seeking' event or a transition beyond HAVE_METADATA. + assert_true(mediaElement.seeking, 'mediaElement is still seeking'); + assert_equals(mediaElement.readyState, mediaElement.HAVE_METADATA, 'Still in HAVE_METADATA'); + + // Append media data for the current position until the element starts playing. + test.expectEvent(mediaElement, 'seeked', 'mediaElement finished seek'); + test.expectEvent(mediaElement, 'playing', 'mediaElement playing'); + + MediaSourceUtil.appendUntilEventFires(test, mediaElement, 'playing', sourceBuffer, mediaData, segmentInfo, segmentIndex); + }); + + test.waitForExpectedEvents(function() + { + if (sourceBuffer.updating) + { + // The event playing was fired prior to the appendBuffer completing. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, 'append have compleded'); + test.expectEvent(mediaSource, 'sourceended', 'mediaSource ended'); + mediaSource.endOfStream(); + }); + } + else + { + test.expectEvent(mediaSource, 'sourceended', 'mediaSource ended'); + mediaSource.endOfStream(); + } + }); + + test.waitForExpectedEvents(function() + { + // Note: we just completed the seek. However, we only have less than a second worth of data to play. It is possible that + // playback has reached the end since the seek completed. + if (!mediaElement.paused) + { + assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA, 'Greater or equal than HAVE_CURRENT_DATA'); + } + else + { + assert_true(mediaElement.ended); + } + test.done(); + }); + + }, 'Test seeking to a new location before transitioning beyond HAVE_METADATA.'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaElement.play(); + + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var firstSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + var secondSegmentInfo = segmentInfo.media[2]; + var secondSegment = MediaSourceUtil.extractSegmentData(mediaData, secondSegmentInfo); + var segmentIndex = 4; + var thirdSegmentInfo = segmentInfo.media[segmentIndex]; + + // Append the initialization segment to trigger a transition to HAVE_METADATA. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'loadedmetadata', 'Reached HAVE_METADATA'); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.expectEvent(mediaElement, 'playing', 'mediaElement playing'); + sourceBuffer.appendBuffer(firstSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_greater_than(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA, 'Greater than HAVE_CURRENT_DATA'); + + // Seek to a new position. + test.expectEvent(mediaElement, 'seeking', 'mediaElement'); + mediaElement.currentTime = Math.max(secondSegmentInfo.timev, secondSegmentInfo.timea); + assert_true(mediaElement.seeking, 'mediaElement is seeking'); + + }); + + test.waitForExpectedEvents(function() + { + assert_true(mediaElement.seeking, 'mediaElement is still seeking'); + + // Seek to a second position while the first seek is still pending. + test.expectEvent(mediaElement, 'seeking', 'mediaElement'); + mediaElement.currentTime = Math.max(thirdSegmentInfo.timev, thirdSegmentInfo.timea); + assert_true(mediaElement.seeking, 'mediaElement is seeking'); + + // Append media data for the first seek position. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + sourceBuffer.appendBuffer(secondSegment); + }); + + test.waitForExpectedEvents(function() + { + // Note that we can't assume that the element is still seeking + // when the seeking event is fired as the operation is asynchronous. + + // Append media data for the second seek position. + test.expectEvent(mediaElement, 'seeked', 'mediaElement finished seek'); + MediaSourceUtil.appendUntilEventFires(test, mediaElement, 'seeked', sourceBuffer, mediaData, segmentInfo, segmentIndex); + }); + + test.waitForExpectedEvents(function() + { + assert_false(mediaElement.seeking, 'mediaElement is no longer seeking'); + + if (sourceBuffer.updating) + { + // The event seeked was fired prior to the appendBuffer completing. + test.expectEvent(sourceBuffer, 'updateend', 'sourceBuffer'); + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, 'append have compleded'); + test.expectEvent(mediaSource, 'sourceended', 'mediaSource ended'); + mediaSource.endOfStream(); + }); + } + else + { + test.expectEvent(mediaSource, 'sourceended', 'mediaSource ended'); + mediaSource.endOfStream(); + } + }); + + test.waitForExpectedEvents(function() + { + // Note: we just completed the seek. However, we only have less than a second worth of data to play. It is possible that + // playback has reached the end since the seek completed. + if (!mediaElement.paused) + { + assert_greater_than_equal(mediaElement.readyState, mediaElement.HAVE_CURRENT_DATA, 'Greater or equal than HAVE_CURRENT_DATA'); + } + else + { + assert_true(mediaElement.ended); + } + test.done(); + }); + }, 'Test seeking to a new location during a pending seek.'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-seekable.html b/testing/web-platform/tests/media-source/mediasource-seekable.html new file mode 100644 index 0000000000..8e228d3466 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-seekable.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + mediasource_test(function(test, mediaElement, mediaSource) + { + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + mediaElement.addEventListener('ended', test.step_func_done(function () {})); + + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + assertSeekableEquals(mediaElement, '{ }', 'mediaElement.seekable'); + test.done(); + }, 'Get seekable time ranges when the sourcebuffer is empty.'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + test.expectEvent(mediaElement, 'durationchange', 'mediaElement got duration'); + sourceBuffer.appendBuffer(initSegment); + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.duration, segmentInfo.duration); + assertSeekableEquals(mediaElement, '{ [0.000, ' + segmentInfo.duration.toFixed(3) + ') }', 'mediaElement.seekable'); + test.done(); + }); + }, 'Get seekable time ranges after init segment received.'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + test.expectEvent(mediaElement, 'durationchange', 'mediaElement got duration after initsegment'); + test.expectEvent(sourceBuffer, 'update'); + test.expectEvent(sourceBuffer, 'updateend'); + sourceBuffer.appendBuffer(initSegment); + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "updating attribute is false"); + test.expectEvent(mediaElement, 'durationchange', 'mediaElement got infinity duration'); + mediaSource.duration = Infinity; + test.waitForExpectedEvents(function() + { + assertSeekableEquals(mediaElement, '{ }', 'mediaElement.seekable'); + + // Append a segment from the middle of the stream to make sure that seekable does not use buffered.start(0) or duration as first or last value + var midSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[2]); + test.expectEvent(sourceBuffer, 'update'); + test.expectEvent(sourceBuffer, 'updateend'); + sourceBuffer.appendBuffer(midSegment); + test.waitForExpectedEvents(function() + { + assert_equals(mediaElement.seekable.length, 1, 'mediaElement.seekable.length'); + assert_equals(mediaElement.buffered.length, 1, 'mediaElement.buffered.length'); + assert_not_equals(mediaElement.seekable.start(0), mediaElement.buffered.start(0)); + assert_equals(mediaElement.seekable.start(0), 0); + assert_not_equals(mediaElement.seekable.end(0), mediaElement.duration); + assert_not_equals(mediaElement.seekable.end(0), mediaElement.buffered.start(0)); + assert_equals(mediaElement.seekable.end(0), mediaElement.buffered.end(0)); + test.done(); + }); + }); + }); + }, 'Get seekable time ranges on an infinite stream.'); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-sequencemode-append-buffer.html b/testing/web-platform/tests/media-source/mediasource-sequencemode-append-buffer.html new file mode 100644 index 0000000000..46008eeb25 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-sequencemode-append-buffer.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>SourceBuffer.mode == "sequence" test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + function mediasource_sequencemode_test(testFunction, description, options) + { + return mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_greater_than(segmentInfo.media.length, 3, "at least 3 media segments for supported type"); + mediaElement.addEventListener("error", test.unreached_func("Unexpected event 'error'")); + sourceBuffer.mode = "sequence"; + assert_equals(sourceBuffer.mode, "sequence", "mode after setting it to \"sequence\""); + + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + test.expectEvent(sourceBuffer, "updatestart", "initSegment append started."); + test.expectEvent(sourceBuffer, "update", "initSegment append success."); + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + test.waitForExpectedEvents(function() + { + assert_equals(sourceBuffer.timestampOffset, 0, "timestampOffset initially 0"); + testFunction(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData); + }); + }, description, options); + } + + function append_segment(test, sourceBuffer, mediaData, info, callback) + { + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, info); + test.expectEvent(sourceBuffer, "updatestart", "media segment append started."); + test.expectEvent(sourceBuffer, "update", "media segment append success."); + test.expectEvent(sourceBuffer, "updateend", "media segment append ended."); + sourceBuffer.appendBuffer(mediaSegment); + test.waitForExpectedEvents(callback); + } + + // Verifies expected times to 3 decimal places before and after mediaSource.endOfStream(), + // and calls |callback| on success. + function verify_offset_and_buffered(test, mediaSource, sourceBuffer, + expectedTimestampOffset, expectedBufferedRangeStartTime, + expectedBufferedRangeMaxEndTimeBeforeEOS, + expectedBufferedRangeEndTimeAfterEOS, + callback) { + assert_approx_equals(sourceBuffer.timestampOffset, + expectedTimestampOffset, + 0.001, + "expectedTimestampOffset"); + + // Prior to EOS, the buffered range end time may not have fully reached the next media + // segment's timecode (adjusted by any timestampOffset). It should not exceed it though. + // Therefore, an exact assertBufferedEquals() will not work here. + assert_greater_than(sourceBuffer.buffered.length, 0, "sourceBuffer.buffered has at least 1 range before EOS"); + assert_approx_equals(sourceBuffer.buffered.start(0), + expectedBufferedRangeStartTime, + 0.001, + "sourceBuffer.buffered range begins where expected before EOS"); + assert_less_than_equal(sourceBuffer.buffered.end(0), + expectedBufferedRangeMaxEndTimeBeforeEOS + 0.001, + "sourceBuffer.buffered range ends at or before expected upper bound before EOS"); + + test.expectEvent(mediaSource, "sourceended", "mediaSource endOfStream"); + mediaSource.endOfStream(); + test.waitForExpectedEvents(function() + { + assertBufferedEquals(sourceBuffer, + "{ [" + expectedBufferedRangeStartTime.toFixed(3) + ", " + expectedBufferedRangeEndTimeAfterEOS.toFixed(3) + ") }", + "sourceBuffer.buffered after EOS"); + callback(); + }); + } + + mediasource_sequencemode_test(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var offset = Math.min(segmentInfo.media[0].timev, segmentInfo.media[0].timea); + var expectedStart = Math.max(segmentInfo.media[0].timev, segmentInfo.media[0].timea) - offset; + var expectedEnd = Math.min(segmentInfo.media[0].endtimev, segmentInfo.media[0].endtimea) - offset; + var expectedEndEOS = Math.max(segmentInfo.media[0].endtimev, segmentInfo.media[0].endtimea) - offset; + append_segment(test, sourceBuffer, mediaData, segmentInfo.media[0], function() + { + verify_offset_and_buffered(test, mediaSource, sourceBuffer, + -offset, expectedStart, + expectedEnd, expectedEndEOS, + test.done); + }); + }, "Test sequence AppendMode appendBuffer(first media segment)"); + + mediasource_sequencemode_test(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var offset = Math.min(segmentInfo.media[1].timev, segmentInfo.media[1].timea); + var expectedStart = Math.max(segmentInfo.media[1].timev, segmentInfo.media[1].timea) - offset; + var expectedEnd = Math.min(segmentInfo.media[1].endtimev, segmentInfo.media[1].endtimea) - offset; + var expectedEndEOS = Math.max(segmentInfo.media[1].endtimev, segmentInfo.media[1].endtimea) - offset; + assert_greater_than(Math.min(segmentInfo.media[1].timev, segmentInfo.media[1].timea), 0, + "segment starts after time 0"); + append_segment(test, sourceBuffer, mediaData, segmentInfo.media[1], function() + { + verify_offset_and_buffered(test, mediaSource, sourceBuffer, + -offset, expectedStart, + expectedEnd, expectedEndEOS, + test.done); + }); + }, "Test sequence AppendMode appendBuffer(second media segment)"); + + mediasource_sequencemode_test(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_greater_than(Math.min(segmentInfo.media[1].timev, segmentInfo.media[1].timea), 0, + "segment starts after time 0"); + append_segment(test, sourceBuffer, mediaData, segmentInfo.media[1], function() + { + append_segment(test, sourceBuffer, mediaData, segmentInfo.media[0], function() + { + var firstOffset = Math.min(segmentInfo.media[1].timev, segmentInfo.media[1].timea); + var secondOffset = Math.max(segmentInfo.media[1].endtimev, segmentInfo.media[1].endtimea) - firstOffset; + var expectedStart = Math.max(segmentInfo.media[1].timev, segmentInfo.media[1].timea) - firstOffset; + var expectedEnd = Math.min(segmentInfo.media[0].endtimev, segmentInfo.media[0].endtimea) + secondOffset; + var expectedEndEOS = Math.max(segmentInfo.media[0].endtimev, segmentInfo.media[0].endtimea) + secondOffset; + // Current timestampOffset should reflect offset required to put media[0] + // immediately after media[1]'s highest frame end timestamp (as was adjusted + // by an earlier timestampOffset). + verify_offset_and_buffered(test, mediaSource, sourceBuffer, + secondOffset, expectedStart, + expectedEnd, expectedEndEOS, + test.done); + }) + }); + }, "Test sequence AppendMode appendBuffer(second media segment, then first media segment)"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-sourcebuffer-mode-timestamps.html b/testing/web-platform/tests/media-source/mediasource-sourcebuffer-mode-timestamps.html new file mode 100644 index 0000000000..e7e9b8ca5f --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-sourcebuffer-mode-timestamps.html @@ -0,0 +1,52 @@ +<!doctype html> +<html> +<head> + <meta charset='utf-8'> + <title>SourceBuffer#mode with generate timestamps flag true</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +var mimes = ['audio/aac', 'audio/mpeg']; + +//check the browser supports the MIME used in this test +function isTypeSupported(mime) { + if(!MediaSource.isTypeSupported(mime)) { + this.step(function() { + assert_unreached("Browser doesn't support the MIME used in this test: " + mime); + }); + this.done(); + return false; + } + return true; +} +function mediaTest(mime) { + async_test(function(t) { + if(!isTypeSupported.bind(t)(mime)) { + return; + } + var mediaSource = new MediaSource(); + mediaSource.addEventListener('sourceopen', t.step_func_done(function(e) { + var sourceBuffer = mediaSource.addSourceBuffer(mime); + assert_equals(sourceBuffer.updating, false, "SourceBuffer.updating is false"); + assert_throws_js(TypeError, + function() { + sourceBuffer.mode = "segments"; + }, + 'SourceBuffer#mode with generate timestamps flag true'); + }), false); + var video = document.createElement('video'); + video.src = window.URL.createObjectURL(mediaSource); + }, mime + ' : ' + + 'If generate timestamps flag equals true and new mode equals "segments", ' + + 'then throw a TypeError exception and abort these steps.'); +} +mimes.forEach(function(mime) { + mediaTest(mime); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-sourcebuffer-mode.html b/testing/web-platform/tests/media-source/mediasource-sourcebuffer-mode.html new file mode 100644 index 0000000000..2d84fa9753 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-sourcebuffer-mode.html @@ -0,0 +1,142 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <meta charset="utf-8"> + <title>SourceBuffer.mode test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_equals(sourceBuffer.mode, 'segments', 'default append mode should be \'segments\''); + test.done(); + }, 'Test initial value of SourceBuffer.mode is "segments"'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + sourceBuffer.mode = 'sequence'; + assert_equals(sourceBuffer.mode, 'sequence', 'mode after setting it to \'sequence\''); + + // Setting a mode that is not in AppendMode IDL enum should be ignored and not cause exception. + sourceBuffer.mode = 'invalidmode'; + sourceBuffer.mode = null; + sourceBuffer.mode = ''; + sourceBuffer.mode = 'Segments'; + assert_equals(sourceBuffer.mode, 'sequence', 'mode unchanged by attempts to set invalid modes'); + + sourceBuffer.mode = 'segments'; + assert_equals(sourceBuffer.mode, 'segments', 'mode after setting it to \'segments\''); + test.done(); + }, 'Test setting SourceBuffer.mode'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + mediaSource.removeSourceBuffer(sourceBuffer); + assert_throws_dom('InvalidStateError', + function() { sourceBuffer.mode = 'segments'; }, + 'Setting valid sourceBuffer.mode on removed SourceBuffer should throw InvalidStateError.'); + test.done(); + }, 'Test setting a removed SourceBuffer\'s mode'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + sourceBuffer.appendBuffer(mediaData); + assert_true(sourceBuffer.updating, 'updating attribute is true'); + assert_throws_dom('InvalidStateError', + function() { sourceBuffer.mode = 'segments'; }, + 'Setting valid sourceBuffer.mode on updating SourceBuffer threw InvalidStateError.'); + test.done(); + }, 'Test setting SourceBuffer.mode while still updating'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + test.expectEvent(sourceBuffer, 'updateend', 'Append ended.'); + sourceBuffer.appendBuffer(mediaData); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, 'updating attribute is false'); + + test.expectEvent(mediaSource, 'sourceended', 'MediaSource sourceended event'); + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, 'ended', 'MediaSource readyState is \'ended\''); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, 'ended', 'MediaSource readyState is \'ended\''); + + test.expectEvent(mediaSource, 'sourceopen', 'MediaSource sourceopen event'); + sourceBuffer.mode = 'segments'; + + assert_equals(mediaSource.readyState, 'open', 'MediaSource readyState is \'open\''); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, 'open', 'MediaSource readyState is \'open\''); + test.done(); + }); + }, 'Test setting SourceBuffer.mode triggers parent MediaSource \'ended\' to \'open\' transition.'); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var fullMediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + var truncateAt = Math.floor(segmentInfo.media[0].size * 0.5); // Pick first 50% of segment bytes. + var partialMediaSegment = fullMediaSegment.subarray(0, truncateAt); + var mediaSegmentRemainder = fullMediaSegment.subarray(truncateAt); + + // Append init segment. + test.expectEvent(sourceBuffer, 'updateend', 'Init segment append ended.'); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, 'updating attribute is false'); + assert_equals(sourceBuffer.mode, 'segments'); + sourceBuffer.mode = 'segments'; // No exception should occur. + assert_equals(sourceBuffer.timestampOffset, 0.0); + sourceBuffer.timestampOffset = 10.123456789; // No exception should occur. + assert_equals(sourceBuffer.timestampOffset, 10.123456789); // Super-precise offsets should round-trip. + + // Append first part of media segment. + test.expectEvent(sourceBuffer, 'updateend', 'Partial media segment append ended.'); + sourceBuffer.appendBuffer(partialMediaSegment); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, 'updating attribute is false'); + assert_equals(sourceBuffer.mode, 'segments'); + assert_throws_dom('InvalidStateError', + function() { sourceBuffer.mode = 'segments'; }, + 'Setting valid sourceBuffer.mode while still parsing media segment threw InvalidStateError.'); + assert_equals(sourceBuffer.timestampOffset, 10.123456789); + assert_throws_dom('InvalidStateError', + function() { sourceBuffer.timestampOffset = 20.0; }, + 'Setting valid sourceBuffer.timestampOffset while still parsing media segment threw InvalidStateError.'); + + // Append remainder of media segment. + test.expectEvent(sourceBuffer, 'updateend', 'Append ended of remainder of media segment.'); + sourceBuffer.appendBuffer(mediaSegmentRemainder); + }); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, 'updating attribute is false'); + assert_equals(sourceBuffer.mode, 'segments'); + sourceBuffer.mode = 'segments'; // No exception should occur. + assert_equals(sourceBuffer.timestampOffset, 10.123456789); + sourceBuffer.timestampOffset = 20.0; // No exception should occur. + test.done(); + }); + }, 'Test setting SourceBuffer.mode and SourceBuffer.timestampOffset while parsing media segment.'); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-sourcebuffer-trackdefaults.html b/testing/web-platform/tests/media-source/mediasource-sourcebuffer-trackdefaults.html new file mode 100644 index 0000000000..7b45486d71 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-sourcebuffer-trackdefaults.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="mediasource-util.js"></script> +<script> + function sourceBufferTrackDefaultsTest(callback, description) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_array_equals(sourceBuffer.trackDefaults, [], "Empty initial SourceBuffer.trackDefaults"); + callback(test, mediaElement, mediaSource, sourceBuffer); + }, description); + }; + + sourceBufferTrackDefaultsTest(function(test, mediaElement, mediaSource, sourceBuffer) + { + var emptyList = new TrackDefaultList([]); + assert_not_equals(sourceBuffer.trackDefaults, emptyList, "Initial trackDefaults object differs from new empty list"); + + sourceBuffer.trackDefaults = emptyList; + + assert_array_equals(sourceBuffer.trackDefaults, [], "Round-tripped empty trackDefaults"); + assert_equals(sourceBuffer.trackDefaults, emptyList, "Round-tripped the empty TrackDefaultList object"); + test.done(); + }, "Test round-trip of empty SourceBuffer.trackDefaults"); + + sourceBufferTrackDefaultsTest(function(test, mediaElement, mediaSource, sourceBuffer) + { + var trackDefault = new TrackDefault("audio", "en-US", "audio label", ["main"], "1"); + var trackDefaults = new TrackDefaultList([ trackDefault ]); + + sourceBuffer.trackDefaults = trackDefaults; + + assert_array_equals(sourceBuffer.trackDefaults, trackDefaults, "Round-tripped non-empty trackDefaults"); + assert_equals(sourceBuffer.trackDefaults.length, 1, "Confirmed non-empty trackDefaults"); + assert_equals(sourceBuffer.trackDefaults, trackDefaults, "Round-tripped the non-empty TrackDefaultList object"); + test.done(); + }, "Test round-trip of non-empty SourceBuffer.trackDefaults"); + + sourceBufferTrackDefaultsTest(function(test, mediaElement, mediaSource, sourceBuffer) + { + mediaSource.removeSourceBuffer(sourceBuffer); + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.trackDefaults = new TrackDefaultList([]); }, + "Exception thrown when setting trackDefaults on SourceBuffer that is removed from MediaSource"); + test.done(); + }, "Test setting trackDefaults on an already-removed SourceBuffer"); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + assert_array_equals(sourceBuffer.trackDefaults, [], "Empty initial SourceBuffer.trackDefaults"); + test.expectEvent(sourceBuffer, "updateend", "Append ended"); + sourceBuffer.appendBuffer(mediaData); + assert_true(sourceBuffer.updating, "SourceBuffer is updating"); + + assert_throws_dom("InvalidStateError", + function() { sourceBuffer.trackDefaults = new TrackDefaultList([]); }, + "Exception thrown when setting trackDefaults on SourceBuffer that is updating"); + + test.waitForExpectedEvents(function() + { + assert_false(sourceBuffer.updating, "SourceBuffer is not updating"); + sourceBuffer.trackDefaults = new TrackDefaultList([]); + test.done(); + }); + }, "Test setting trackDefaults on a SourceBuffer that is updating"); + + sourceBufferTrackDefaultsTest(function(test, mediaElement, mediaSource, sourceBuffer) + { + assert_throws_js(TypeError, + function() { sourceBuffer.trackDefaults = null; }, + "null should be disallowed by trackDefaults setter"); + test.done(); + }, "Test setting null SourceBuffer.trackDefaults"); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-sourcebufferlist.html b/testing/web-platform/tests/media-source/mediasource-sourcebufferlist.html new file mode 100644 index 0000000000..760e6df46c --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-sourcebufferlist.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>SourceBufferList test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + function verifySourceBufferLists(mediaSource, expected) + { + assert_equals(mediaSource.sourceBuffers.length, expected.length, "sourceBuffers length"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "activeSourceBuffers length"); + for (var i = 0; i < expected.length; ++i) { + assert_equals(mediaSource.sourceBuffers[i], expected[i], "Verifying mediaSource.sourceBuffers[" + i + "]"); + } + assert_equals(mediaSource.sourceBuffers[expected.length], undefined, + "If index is greater than or equal to the length attribute then return undefined."); + } + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBufferA = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + verifySourceBufferLists(mediaSource, [sourceBufferA]); + + var sourceBufferB = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + verifySourceBufferLists(mediaSource, [sourceBufferA, sourceBufferB]); + test.done(); + }, "Test SourceBufferList getter method"); + + mediasource_test(function(test, mediaElement, mediaSource) + { + test.expectEvent(mediaSource.sourceBuffers, "addsourcebuffer", "sourceBuffers"); + var sourceBufferA = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + var sourceBufferB = null; + + test.waitForExpectedEvents(function() + { + test.expectEvent(mediaSource.sourceBuffers, "addsourcebuffer", "sourceBuffers"); + sourceBufferB = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + verifySourceBufferLists(mediaSource, [sourceBufferA, sourceBufferB]); + }); + + test.waitForExpectedEvents(function() + { + test.expectEvent(mediaSource.sourceBuffers, "removesourcebuffer", "sourceBuffers"); + mediaSource.removeSourceBuffer(sourceBufferA); + + verifySourceBufferLists(mediaSource, [sourceBufferB]); + + test.expectEvent(mediaSource.sourceBuffers, "addsourcebuffer", "sourceBuffers"); + sourceBufferA = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + + verifySourceBufferLists(mediaSource, [sourceBufferB, sourceBufferA]); + }); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + }, "Test SourceBufferList event dispatching."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + test.expectEvent(mediaSource.sourceBuffers, "addsourcebuffer", "sourceBuffers"); + test.expectEvent(mediaSource.sourceBuffers, "addsourcebuffer", "sourceBuffers"); + var sourceBufferA = mediaSource.addSourceBuffer(MediaSourceUtil.VIDEO_ONLY_TYPE); + var sourceBufferB = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_ONLY_TYPE); + + verifySourceBufferLists(mediaSource, [sourceBufferA, sourceBufferB]); + + test.waitForExpectedEvents(function() + { + verifySourceBufferLists(mediaSource, [sourceBufferA, sourceBufferB]); + + // Force the media element to close the MediaSource object. + test.expectEvent(mediaSource.sourceBuffers, "removesourcebuffer", "sourceBuffers"); + test.expectEvent(mediaSource, "sourceclose", "mediaSource closing"); + mediaElement.src = ""; + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.readyState, "closed", "mediaSource is closed."); + + verifySourceBufferLists(mediaSource, []); + test.done(); + }); + }, "Test that only 1 removesourcebuffer event fires on each SourceBufferList when the MediaSource closes."); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-timestamp-offset.html b/testing/web-platform/tests/media-source/mediasource-timestamp-offset.html new file mode 100644 index 0000000000..8381aceeb0 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-timestamp-offset.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<html> + <head> + <title>SourceBuffer.timestampOffset test cases.</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="mediasource-util.js"></script> + </head> + <body> + <div id="log"></div> + <script> + function simpleTimestampOffsetTest(value, expected, description) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + var segmentInfo = MediaSourceUtil.SEGMENT_INFO; + var sourceBuffer = mediaSource.addSourceBuffer(segmentInfo.type); + + assert_equals(sourceBuffer.timestampOffset, 0, + "Initial timestampOffset of a SourceBuffer is 0"); + + if (expected == "TypeError") { + assert_throws_js(TypeError, + function() { sourceBuffer.timestampOffset = value; }, + "setting timestampOffset to " + description + " throws an exception."); + } else { + sourceBuffer.timestampOffset = value; + assert_equals(sourceBuffer.timestampOffset, expected); + } + + test.done(); + }, "Test setting SourceBuffer.timestampOffset to " + description + "."); + } + + simpleTimestampOffsetTest(10.5, 10.5, "a positive number"); + simpleTimestampOffsetTest(-10.4, -10.4, "a negative number"); + simpleTimestampOffsetTest(0, 0, "zero"); + simpleTimestampOffsetTest(Number.POSITIVE_INFINITY, "TypeError", "positive infinity"); + simpleTimestampOffsetTest(Number.NEGATIVE_INFINITY, "TypeError", "negative infinity"); + simpleTimestampOffsetTest(Number.NaN, "TypeError", "NaN"); + simpleTimestampOffsetTest(undefined, "TypeError", "undefined"); + simpleTimestampOffsetTest(null, 0, "null"); + simpleTimestampOffsetTest(false, 0, "false"); + simpleTimestampOffsetTest(true, 1, "true"); + simpleTimestampOffsetTest("10.5", 10.5, "a number string"); + simpleTimestampOffsetTest("", 0, "an empty string"); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + + test.waitForExpectedEvents(function() + { + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + sourceBuffer.appendBuffer(mediaSegment); + }); + + test.waitForExpectedEvents(function() + { + mediaSource.endOfStream(); + + assert_equals(mediaSource.readyState, "ended"); + + mediaSource.sourceBuffers[0].timestampOffset = 2; + + assert_equals(mediaSource.readyState, "open"); + + test.expectEvent(mediaSource, "sourceopen", "mediaSource fired 'sourceopen' event."); + }); + + test.waitForExpectedEvents(function() + { + test.done(); + }); + }, "Test setting timestampOffset in 'ended' state causes a transition to 'open'."); + + mediasource_testafterdataloaded(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData) + { + var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init); + var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]); + + test.expectEvent(sourceBuffer, "updateend", "initSegment append ended."); + sourceBuffer.appendBuffer(initSegment); + assert_equals(mediaSource.sourceBuffers[0].timestampOffset, 0, "read initial value"); + + test.waitForExpectedEvents(function() + { + test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended."); + sourceBuffer.appendBuffer(mediaSegment); + assert_equals(mediaSource.sourceBuffers[0].timestampOffset, 0, + "No change to timestampoffset after segments mode init segment append"); + }); + + test.waitForExpectedEvents(function() + { + assert_equals(mediaSource.sourceBuffers[0].timestampOffset, 0, + "No change to timestampoffset after segments mode media segment append"); + test.done(); + }); + }, "Test getting the initial value of timestampOffset."); + + mediasource_test(function(test, mediaElement, mediaSource) + { + var sourceBuffer = mediaSource.addSourceBuffer(MediaSourceUtil.AUDIO_VIDEO_TYPE); + assert_true(sourceBuffer != null, "New SourceBuffer returned"); + + mediaSource.removeSourceBuffer(sourceBuffer); + assert_equals(mediaSource.sourceBuffers.length, 0, "MediaSource.sourceBuffers is empty"); + assert_equals(mediaSource.activeSourceBuffers.length, 0, "MediaSource.activesourceBuffers is empty"); + + assert_throws_dom("InvalidStateError", function() + { + sourceBuffer.timestampOffset = 10; + }); + + test.done(); + }, "Test setting timestampoffset after removing the sourcebuffer."); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/media-source/mediasource-trackdefault.html b/testing/web-platform/tests/media-source/mediasource-trackdefault.html new file mode 100644 index 0000000000..e6c9e76ef9 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-trackdefault.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + function checkConstructionSucceeds(type, language, label, kinds, byteStreamTrackID) + { + var trackDefault = new TrackDefault(type, language, label, kinds, byteStreamTrackID); + assert_equals(trackDefault.type, type, "type"); + assert_equals(trackDefault.language, language, "language"); + assert_equals(trackDefault.label, label, "label"); + assert_equals(trackDefault.byteStreamTrackID, byteStreamTrackID, "byteStreamTrackID"); + assert_array_equals(trackDefault.kinds, kinds, "kinds"); + } + + function checkConstructionFails(type, language, label, kinds, byteStreamTrackID) + { + assert_throws_js(TypeError, + function() { new TrackDefault(type, language, label, kinds, byteStreamTrackID); }, + "TrackDefault construction threw an exception"); + } + + function trackDefaultConstructionTest(type, language, label, kinds, byteStreamTrackID, expectation, description) + { + test(function() + { + if (expectation) + checkConstructionSucceeds(type, language, label, kinds, byteStreamTrackID); + else + checkConstructionFails(type, language, label, kinds, byteStreamTrackID); + }, description + ": type '" + type + "', language '" + language + "', label '" + label + "', multiple kinds, byteStreamTrackID '" + byteStreamTrackID + "'"); + + // If all of |kinds| are expected to succeed, also test each kind individually. + if (!expectation || kinds.length <= 1) + return; + for (var i = 0; i < kinds.length; ++i) { + test(function() + { + checkConstructionSucceeds(type, language, label, new Array(kinds[i]), byteStreamTrackID); + }, description + ": type '" + type + "', language '" + language + "', label '" + label + "', kind '" + kinds[i] + "', byteStreamTrackID '" + byteStreamTrackID + "'"); + } + } + + var VALID_AUDIO_TRACK_KINDS = [ + "alternative", + "descriptions", + "main", + "main-desc", + "translation", + "commentary", + "", + ]; + + var VALID_VIDEO_TRACK_KINDS = [ + "alternative", + "captions", + "main", + "sign", + "subtitles", + "commentary", + "", + ]; + + var VALID_TEXT_TRACK_KINDS = [ + "subtitles", + "captions", + "descriptions", + "chapters", + "metadata", + ]; + + trackDefaultConstructionTest("audio", "en-US", "audio label", VALID_AUDIO_TRACK_KINDS, "1", true, "Test valid audio kinds"); + + trackDefaultConstructionTest("video", "en-US", "video label", VALID_VIDEO_TRACK_KINDS, "1", true, "Test valid video kinds"); + + trackDefaultConstructionTest("text", "en-US", "text label", VALID_TEXT_TRACK_KINDS, "1", true, "Test valid text kinds"); + + trackDefaultConstructionTest("audio", "en-US", "audio label", VALID_VIDEO_TRACK_KINDS, "1", false, "Test mixed valid and invalid audio kinds"); + + trackDefaultConstructionTest("video", "en-US", "video label", VALID_AUDIO_TRACK_KINDS, "1", false, "Test mixed valid and invalid video kinds"); + + trackDefaultConstructionTest("text", "en-US", "text label", VALID_VIDEO_TRACK_KINDS, "1", false, "Test mixed valid and invalid text kinds"); + + trackDefaultConstructionTest("invalid type", "en-US", "label", VALID_AUDIO_TRACK_KINDS, "1", false, "Test invalid 'type' parameter type passed to TrackDefault constructor"); + + test(function() + { + checkConstructionFails("audio", "en-US", "label", "this is not a valid sequence", "1"); + }, "Test invalid 'kinds' parameter type passed to TrackDefault constructor"); + + test(function() + { + var trackDefault = new TrackDefault("audio", "en-US", "label", VALID_AUDIO_TRACK_KINDS, "1"); + var kinds = trackDefault.kinds; + kinds[0] = "invalid kind"; + assert_equals(kinds[0], "invalid kind", "local kinds is updated"); + assert_equals(VALID_AUDIO_TRACK_KINDS[0], "alternative", "local original kinds unchanged"); + assert_array_equals(trackDefault.kinds, VALID_AUDIO_TRACK_KINDS, "trackDefault kinds unchanged"); + }, "Test updating the retval of TrackDefault.kinds does not modify TrackDefault.kinds"); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-trackdefaultlist.html b/testing/web-platform/tests/media-source/mediasource-trackdefaultlist.html new file mode 100644 index 0000000000..940260cfd2 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-trackdefaultlist.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<!-- Copyright © 2016 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + test(function() + { + var originalTrackDefaults = [ + // Same everything, but different byteStreamTrackID, should be allowed by the constructor. + new TrackDefault("audio", "en-US", "label", ["main"], ""), + new TrackDefault("audio", "en-US", "label", ["main"], "1"), + new TrackDefault("audio", "en-US", "label", ["main"], "2"), + new TrackDefault("audio", "en-US", "label", [""], "3"), + + // Same everything, but different type, should be allowed by the constructor. + new TrackDefault("video", "en-US", "label", ["main"], ""), + new TrackDefault("video", "en-US", "label", ["main"], "1"), + new TrackDefault("video", "en-US", "label", ["main"], "2"), + new TrackDefault("video", "en-US", "label", [""], "3") + ]; + + // Get a new array containing the same objects as |originalTrackDefaults|. + var trackDefaults = originalTrackDefaults.slice(); + + var trackDefaultList = new TrackDefaultList(trackDefaults); + assert_array_equals(trackDefaultList, originalTrackDefaults, "construction and read-back succeeded"); + assert_equals(trackDefaultList[trackDefaultList.length], undefined, "out of range indexed property getter result is undefined"); + assert_equals(trackDefaultList[trackDefaultList.length + 1], undefined, "out of range indexed property getter result is undefined"); + + // Introduce same-type, same-empty-string-byteStreamTrackId conflict in trackDefaults[0 vs 4]. + trackDefaults[4] = new TrackDefault("audio", "en-US", "label", ["main"], ""); + assert_equals(trackDefaults[0].type, trackDefaults[4].type, "same-type conflict setup"); + assert_equals(trackDefaults[0].byteStreamTrackID, trackDefaults[4].byteStreamTrackID, "same-byteStreamTrackID conflict setup"); + assert_throws_dom("InvalidAccessError", + function() { new TrackDefaultList(trackDefaults); }, + "TrackDefaultList construction should throw exception due to same type and byteStreamTrackID across at least 2 items in trackDefaults"); + + // Introduce same-type, same-non-empty-string-byteStreamTrackId conflict in trackDefaults[4 vs 5]. + trackDefaults[4] = new TrackDefault("video", "en-US", "label", ["main"], "1"); + assert_equals(trackDefaults[4].type, trackDefaults[5].type, "same-type conflict setup"); + assert_equals(trackDefaults[4].byteStreamTrackID, trackDefaults[5].byteStreamTrackID, "same-byteStreamTrackID conflict setup"); + assert_throws_dom("InvalidAccessError", + function() { new TrackDefaultList(trackDefaults); }, + "TrackDefaultList construction should throw exception due to same type and byteStreamTrackID across at least 2 items in trackDefaults"); + + // Confirm the constructed TrackDefaultList makes a shallow copy of the supplied TrackDefault sequence. + assert_array_equals(trackDefaultList, originalTrackDefaults, "read-back of original trackDefaultList unchanged"); + + }, "Test track default list construction, length, and indexed property getter"); + + test(function() + { + var trackDefaultList = new TrackDefaultList(); + assert_array_equals(trackDefaultList, [], "empty list constructable without supplying optional trackDefaults parameter"); + + trackDefaultList = new TrackDefaultList([]); + assert_array_equals(trackDefaultList, [], "empty list constructable by supplying empty sequence as optional trackDefaults parameter"); + }, "Test empty track default list construction with and without optional trackDefaults parameter"); +</script> diff --git a/testing/web-platform/tests/media-source/mediasource-util.js b/testing/web-platform/tests/media-source/mediasource-util.js new file mode 100644 index 0000000000..6b11210052 --- /dev/null +++ b/testing/web-platform/tests/media-source/mediasource-util.js @@ -0,0 +1,412 @@ +(function(window) { + var SEGMENT_INFO_LIST = [ + { + url: 'mp4/test.mp4', + type: 'video/mp4; codecs="mp4a.40.2,avc1.4d400d"', + duration: 6.549, + init: { offset: 0, size: 1413 }, + media: [ + { offset: 1413, size: 24034, timev: 0.095000, timea: 0, endtimev: 0.896666, endtimea: 0.882358 }, + { offset: 25447, size: 21757, timev: 0.896666, timea: 0.882358, endtimev: 1.696666, endtimea: 1.671836 }, + { offset: 47204, size: 23591, timev: 1.696666, timea: 1.671836, endtimev: 2.498333, endtimea: 2.461315 }, + { offset: 70795, size: 22614, timev: 2.498333, timea: 2.461315, endtimev: 3.298333, endtimea: 3.297233 }, + { offset: 93409, size: 18353, timev: 3.298333, timea: 3.297233, endtimev: 4.100000, endtimea: 4.086712}, + { offset: 111762, size: 23935, timev: 4.100000, timea: 4.086712, endtimev: 4.900000, endtimea: 4.876190 }, + { offset: 135697, size: 21911, timev: 4.900000, timea: 4.876190, endtimev: 5.701666, endtimea: 5.665668 }, + { offset: 157608, size: 23776, timev: 5.701666, timea: 5.665668, endtimev: 6.501666, endtimea: 6.501587 }, + { offset: 181384, size: 5843, timev: 6.501666, timea: 6.501587, endtimev: 6.501666, endtimea: 6.501678 }, + ] + }, + { + url: 'webm/test.webm', + type: 'video/webm; codecs="vp8, vorbis"', + duration: 6.552, + init: { offset: 0, size: 4116 }, + media: [ + { offset: 4116, size: 26583, timev: 0.112000, timea: 0, endtimev: 0.913000, endtimea: 0.912000 }, + { offset: 30699, size: 20555, timev: 0.913000, timea: 0.912000, endtimev: 1.714000, endtimea: 1.701000 }, + { offset: 51254, size: 22668, timev: 1.714000, timea: 1.701000, endtimev: 2.515000, endtimea: 2.514000 }, + { offset: 73922, size: 21943, timev: 2.515000, timea: 2.514000, endtimev: 3.315000, endtimea: 3.303000 }, + { offset: 95865, size: 23015, timev: 3.315000, timea: 3.303000, endtimev: 4.116000, endtimea: 4.093000}, + { offset: 118880, size: 20406, timev: 4.116000, timea: 4.093000, endtimev: 4.917000, endtimea: 4.906000 }, + { offset: 139286, size: 21537, timev: 4.917000, timea: 4.906000, endtimev: 5.718000, endtimea: 5.695000 }, + { offset: 160823, size: 24027, timev: 5.718000, timea: 5.695000, endtimev: 6.519000, endtimea: 6.508000 }, + { offset: 184850, size: 5955, timev: 6.519000, timea: 6.508000, endtimev: 6.577000, endtimea: 6.577000}, + ], + } + ]; + EventExpectationsManager = function(test) + { + this.test_ = test; + this.eventTargetList_ = []; + this.waitCallbacks_ = []; + }; + + EventExpectationsManager.prototype.expectEvent = function(object, eventName, description) + { + var eventInfo = { 'target': object, 'type': eventName, 'description': description}; + var expectations = this.getExpectations_(object); + expectations.push(eventInfo); + + var t = this; + var waitHandler = this.test_.step_func(this.handleWaitCallback_.bind(this)); + var eventHandler = this.test_.step_func(function(event) + { + object.removeEventListener(eventName, eventHandler); + var expected = expectations[0]; + assert_equals(event.target, expected.target, "Event target match."); + assert_equals(event.type, expected.type, "Event types match."); + assert_equals(eventInfo.description, expected.description, "Descriptions match for '" + event.type + "'."); + + expectations.shift(1); + if (t.waitCallbacks_.length > 1) + setTimeout(waitHandler, 0); + else if (t.waitCallbacks_.length == 1) { + // Immediately call the callback. + waitHandler(); + } + }); + object.addEventListener(eventName, eventHandler); + }; + + EventExpectationsManager.prototype.waitForExpectedEvents = function(callback) + { + this.waitCallbacks_.push(callback); + setTimeout(this.test_.step_func(this.handleWaitCallback_.bind(this)), 0); + }; + + EventExpectationsManager.prototype.expectingEvents = function() + { + for (var i = 0; i < this.eventTargetList_.length; ++i) { + if (this.eventTargetList_[i].expectations.length > 0) { + return true; + } + } + return false; + } + + EventExpectationsManager.prototype.handleWaitCallback_ = function() + { + if (this.waitCallbacks_.length == 0 || this.expectingEvents()) + return; + var callback = this.waitCallbacks_.shift(1); + callback(); + }; + + EventExpectationsManager.prototype.getExpectations_ = function(target) + { + for (var i = 0; i < this.eventTargetList_.length; ++i) { + var info = this.eventTargetList_[i]; + if (info.target == target) { + return info.expectations; + } + } + var expectations = []; + this.eventTargetList_.push({ 'target': target, 'expectations': expectations }); + return expectations; + }; + + function loadData_(test, url, callback, isBinary) + { + var request = new XMLHttpRequest(); + request.open("GET", url, true); + if (isBinary) { + request.responseType = 'arraybuffer'; + } + request.onload = test.step_func(function(event) + { + if (request.status != 200) { + assert_unreached("Unexpected status code : " + request.status); + return; + } + var response = request.response; + if (isBinary) { + response = new Uint8Array(response); + } + callback(response); + }); + request.onerror = test.step_func(function(event) + { + assert_unreached("Unexpected error"); + }); + request.send(); + } + + function openMediaSource_(test, mediaTag, callback) + { + var mediaSource = new MediaSource(); + var mediaSourceURL = URL.createObjectURL(mediaSource); + + var eventHandler = test.step_func(onSourceOpen); + function onSourceOpen(event) + { + mediaSource.removeEventListener('sourceopen', eventHandler); + URL.revokeObjectURL(mediaSourceURL); + callback(mediaSource); + } + + mediaSource.addEventListener('sourceopen', eventHandler); + mediaTag.src = mediaSourceURL; + } + + var MediaSourceUtil = {}; + + MediaSourceUtil.loadTextData = function(test, url, callback) + { + loadData_(test, url, callback, false); + }; + + MediaSourceUtil.loadBinaryData = function(test, url, callback) + { + loadData_(test, url, callback, true); + }; + + MediaSourceUtil.fetchManifestAndData = function(test, manifestFilename, callback) + { + var baseURL = ''; + var manifestURL = baseURL + manifestFilename; + MediaSourceUtil.loadTextData(test, manifestURL, function(manifestText) + { + var manifest = JSON.parse(manifestText); + + assert_true(MediaSource.isTypeSupported(manifest.type), manifest.type + " is supported."); + + var mediaURL = baseURL + manifest.url; + MediaSourceUtil.loadBinaryData(test, mediaURL, function(mediaData) + { + callback(manifest.type, mediaData); + }); + }); + }; + + MediaSourceUtil.extractSegmentData = function(mediaData, info) + { + var start = info.offset; + var end = start + info.size; + return mediaData.subarray(start, end); + } + + MediaSourceUtil.getMediaDataForPlaybackTime = function(mediaData, segmentInfo, playbackTimeToAdd) + { + assert_less_than_equal(playbackTimeToAdd, segmentInfo.duration); + var mediaInfo = segmentInfo.media; + var start = mediaInfo[0].offset; + var numBytes = 0; + var segmentIndex = 0; + while (segmentIndex < mediaInfo.length + && Math.min(mediaInfo[segmentIndex].timev, mediaInfo[segmentIndex].timea) <= playbackTimeToAdd) + { + numBytes += mediaInfo[segmentIndex].size; + ++segmentIndex; + } + return mediaData.subarray(start, numBytes + start); + } + + function getFirstSupportedType(typeList) + { + for (var i = 0; i < typeList.length; ++i) { + if (window.MediaSource && MediaSource.isTypeSupported(typeList[i])) + return typeList[i]; + } + return ""; + } + + function getSegmentInfo() + { + for (var i = 0; i < SEGMENT_INFO_LIST.length; ++i) { + var segmentInfo = SEGMENT_INFO_LIST[i]; + if (window.MediaSource && MediaSource.isTypeSupported(segmentInfo.type)) { + return segmentInfo; + } + } + return null; + } + + // To support mediasource-changetype tests, do not use any types that + // indicate automatic timestamp generation in this audioOnlyTypes list. + var audioOnlyTypes = ['audio/mp4;codecs="mp4a.40.2"', 'audio/webm;codecs="vorbis"']; + + var videoOnlyTypes = ['video/mp4;codecs="avc1.4D4001"', 'video/webm;codecs="vp8"']; + var audioVideoTypes = ['video/mp4;codecs="avc1.4D4001,mp4a.40.2"', 'video/webm;codecs="vp8,vorbis"']; + MediaSourceUtil.AUDIO_ONLY_TYPE = getFirstSupportedType(audioOnlyTypes); + MediaSourceUtil.VIDEO_ONLY_TYPE = getFirstSupportedType(videoOnlyTypes); + MediaSourceUtil.AUDIO_VIDEO_TYPE = getFirstSupportedType(audioVideoTypes); + MediaSourceUtil.SEGMENT_INFO = getSegmentInfo(); + + MediaSourceUtil.getSubType = function(mimetype) { + var slashIndex = mimetype.indexOf("/"); + var semicolonIndex = mimetype.indexOf(";"); + if (slashIndex <= 0) { + assert_unreached("Invalid mimetype '" + mimetype + "'"); + return; + } + + var start = slashIndex + 1; + if (semicolonIndex >= 0) { + if (semicolonIndex <= start) { + assert_unreached("Invalid mimetype '" + mimetype + "'"); + return; + } + + return mimetype.substr(start, semicolonIndex - start) + } + + return mimetype.substr(start); + }; + + MediaSourceUtil.append = function(test, sourceBuffer, data, callback) + { + function onUpdate() { + sourceBuffer.removeEventListener("update", onUpdate); + callback(); + } + sourceBuffer.addEventListener("update", onUpdate); + + sourceBuffer.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + sourceBuffer.appendBuffer(data); + }; + + MediaSourceUtil.appendUntilEventFires = function(test, mediaElement, eventName, sourceBuffer, mediaData, segmentInfo, startingIndex) + { + var eventFired = false; + function onEvent() { + mediaElement.removeEventListener(eventName, onEvent); + eventFired = true; + } + mediaElement.addEventListener(eventName, onEvent); + + var i = startingIndex; + var onAppendDone = function() { + if (eventFired || (i >= (segmentInfo.media.length - 1))) + return; + + i++; + if (i < segmentInfo.media.length) + { + MediaSourceUtil.append(test, sourceBuffer, MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[i]), onAppendDone); + } + }; + MediaSourceUtil.append(test, sourceBuffer, MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[i]), onAppendDone); + + }; + + function addExtraTestMethods(test) + { + test.eventExpectations_ = new EventExpectationsManager(test); + test.expectEvent = function(object, eventName, description) + { + test.eventExpectations_.expectEvent(object, eventName, description); + }; + + test.waitForExpectedEvents = function(callback) + { + test.eventExpectations_.waitForExpectedEvents(callback); + }; + + test.waitForCurrentTimeChange = function(mediaElement, callback) + { + var initialTime = mediaElement.currentTime; + + var onTimeUpdate = test.step_func(function() + { + if (mediaElement.currentTime != initialTime) { + mediaElement.removeEventListener('timeupdate', onTimeUpdate); + callback(); + } + }); + + mediaElement.addEventListener('timeupdate', onTimeUpdate); + } + + var oldTestDone = test.done.bind(test); + test.done = function() + { + if (test.status == test.PASS) { + test.step(function() { + assert_false(test.eventExpectations_.expectingEvents(), "No pending event expectations."); + }); + } + oldTestDone(); + }; + }; + + window['MediaSourceUtil'] = MediaSourceUtil; + window['media_test'] = function(testFunction, description, options) + { + options = options || {}; + return async_test(function(test) + { + addExtraTestMethods(test); + testFunction(test); + }, description, options); + }; + window['mediasource_test'] = function(testFunction, description, options) + { + return media_test(function(test) + { + var mediaTag = document.createElement("video"); + if (!document.body) { + document.body = document.createElement("body"); + } + document.body.appendChild(mediaTag); + + test.removeMediaElement_ = true; + test.add_cleanup(function() + { + if (test.removeMediaElement_) { + document.body.removeChild(mediaTag); + test.removeMediaElement_ = false; + } + }); + + openMediaSource_(test, mediaTag, function(mediaSource) + { + testFunction(test, mediaTag, mediaSource); + }); + }, description, options); + }; + + window['mediasource_testafterdataloaded'] = function(testFunction, description, options) + { + mediasource_test(function(test, mediaElement, mediaSource) + { + var segmentInfo = MediaSourceUtil.SEGMENT_INFO; + + if (!segmentInfo) { + assert_unreached("No segment info compatible with this MediaSource implementation."); + return; + } + + mediaElement.addEventListener('error', test.unreached_func("Unexpected event 'error'")); + + var sourceBuffer = mediaSource.addSourceBuffer(segmentInfo.type); + MediaSourceUtil.loadBinaryData(test, segmentInfo.url, function(mediaData) + { + testFunction(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData); + }); + }, description, options); + } + + function timeRangesToString(ranges) + { + var s = "{"; + for (var i = 0; i < ranges.length; ++i) { + s += " [" + ranges.start(i).toFixed(3) + ", " + ranges.end(i).toFixed(3) + ")"; + } + return s + " }"; + } + + window['assertBufferedEquals'] = function(obj, expected, description) + { + var actual = timeRangesToString(obj.buffered); + assert_equals(actual, expected, description); + }; + + window['assertSeekableEquals'] = function(obj, expected, description) + { + var actual = timeRangesToString(obj.seekable); + assert_equals(actual, expected, description); + }; + +})(window); diff --git a/testing/web-platform/tests/media-source/mp3/sound_5.mp3 b/testing/web-platform/tests/media-source/mp3/sound_5.mp3 Binary files differnew file mode 100644 index 0000000000..bd20291989 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp3/sound_5.mp3 diff --git a/testing/web-platform/tests/media-source/mp4/h264-starvation-init.mp4 b/testing/web-platform/tests/media-source/mp4/h264-starvation-init.mp4 Binary files differnew file mode 100644 index 0000000000..3724acd8ed --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/h264-starvation-init.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/h264-starvation-media.mp4 b/testing/web-platform/tests/media-source/mp4/h264-starvation-media.mp4 Binary files differnew file mode 100644 index 0000000000..8cc5c5e823 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/h264-starvation-media.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/invalid-codec.mp4 b/testing/web-platform/tests/media-source/mp4/invalid-codec.mp4 Binary files differnew file mode 100644 index 0000000000..6fcc7c21a6 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/invalid-codec.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-a-128k-44100Hz-1ch-manifest.json b/testing/web-platform/tests/media-source/mp4/test-a-128k-44100Hz-1ch-manifest.json new file mode 100644 index 0000000000..f3caa460e9 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-a-128k-44100Hz-1ch-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-a-128k-44100Hz-1ch.mp4", + "type": "audio/mp4;codecs=\"mp4a.40.2\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-a-128k-44100Hz-1ch.mp4 b/testing/web-platform/tests/media-source/mp4/test-a-128k-44100Hz-1ch.mp4 Binary files differnew file mode 100644 index 0000000000..fc7832a5b3 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-a-128k-44100Hz-1ch.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-a-192k-44100Hz-1ch-manifest.json b/testing/web-platform/tests/media-source/mp4/test-a-192k-44100Hz-1ch-manifest.json new file mode 100644 index 0000000000..41a6f339b7 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-a-192k-44100Hz-1ch-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-a-192k-44100Hz-1ch.mp4", + "type": "audio/mp4;codecs=\"mp4a.40.2\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-a-192k-44100Hz-1ch.mp4 b/testing/web-platform/tests/media-source/mp4/test-a-192k-44100Hz-1ch.mp4 Binary files differnew file mode 100644 index 0000000000..864a87d25b --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-a-192k-44100Hz-1ch.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..7731e3170e --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001,mp4a.40.2\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..e623e8ee4c --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..78ded611f6 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001,mp4a.40.2\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..946167b56e --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..ba46349f93 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001,mp4a.40.2\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..ace4bee53a --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..24da9b4ce3 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001,mp4a.40.2\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..f224a5426a --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-boxes-audio.mp4 b/testing/web-platform/tests/media-source/mp4/test-boxes-audio.mp4 Binary files differnew file mode 100644 index 0000000000..b1cabbfd21 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-boxes-audio.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-boxes-video.mp4 b/testing/web-platform/tests/media-source/mp4/test-boxes-video.mp4 Binary files differnew file mode 100644 index 0000000000..714c17ca12 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-boxes-video.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-24fps-8kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-24fps-8kfr-manifest.json new file mode 100644 index 0000000000..a31b6d0245 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-24fps-8kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-v-128k-320x240-24fps-8kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-24fps-8kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-24fps-8kfr.mp4 Binary files differnew file mode 100644 index 0000000000..cc55f40fa2 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-24fps-8kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..3e02844105 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-v-128k-320x240-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..68d02cdfec --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-128k-320x240-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-v-128k-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-v-128k-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..10c4f4bcbd --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-128k-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-v-128k-640x480-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-v-128k-640x480-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-v-128k-640x480-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..c4f47f0358 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-128k-640x480-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test-v-256k-320x240-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/mp4/test-v-256k-320x240-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..42d3e1e524 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-256k-320x240-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "mp4/test-v-256k-320x240-30fps-10kfr.mp4", + "type": "video/mp4;codecs=\"avc1.4D4001\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/mp4/test-v-256k-320x240-30fps-10kfr.mp4 b/testing/web-platform/tests/media-source/mp4/test-v-256k-320x240-30fps-10kfr.mp4 Binary files differnew file mode 100644 index 0000000000..6dc4972fd7 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test-v-256k-320x240-30fps-10kfr.mp4 diff --git a/testing/web-platform/tests/media-source/mp4/test.mp4 b/testing/web-platform/tests/media-source/mp4/test.mp4 Binary files differnew file mode 100644 index 0000000000..1b0e7b56a6 --- /dev/null +++ b/testing/web-platform/tests/media-source/mp4/test.mp4 diff --git a/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/media-source-webcodecs-util.js b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/media-source-webcodecs-util.js new file mode 100644 index 0000000000..f581da77b2 --- /dev/null +++ b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/media-source-webcodecs-util.js @@ -0,0 +1,136 @@ +const testIV = window.crypto.getRandomValues(new Uint8Array(16)); +const testKey = window.crypto.getRandomValues(new Uint8Array(16)); +const testKeyId = window.crypto.getRandomValues(new Uint8Array(8)); +var testEncodedKey = null; + +const keySystemConfig = [{ + initDataTypes: ['keyids'], + videoCapabilities: [{contentType: 'video/mp4; codecs="vp09.00.10.08"'}] +}]; + +// TODO(crbug.com/1144908): Consider extracting metadata into helper library +// shared with webcodecs tests. This metadata is adapted from +// webcodecs/video-decoder-any.js. +const vp9 = { + async buffer() { + return (await fetch('vp9.mp4')).arrayBuffer(); + }, + // Note, file might not actually be level 1. See original metadata in + // webcodecs test suite. + codec: 'vp09.00.10.08', + frames: [ + {offset: 44, size: 3315, type: 'key'}, + {offset: 3359, size: 203, type: 'delta'}, + {offset: 3562, size: 245, type: 'delta'}, + {offset: 3807, size: 172, type: 'delta'}, + {offset: 3979, size: 312, type: 'delta'}, + {offset: 4291, size: 170, type: 'delta'}, + {offset: 4461, size: 195, type: 'delta'}, + {offset: 4656, size: 181, type: 'delta'}, + {offset: 4837, size: 356, type: 'delta'}, + {offset: 5193, size: 159, type: 'delta'} + ] +}; + +async function getOpenMediaSource(t) { + return new Promise(async resolve => { + const v = document.createElement('video'); + document.body.appendChild(v); + const mediaSource = new MediaSource(); + const url = URL.createObjectURL(mediaSource); + mediaSource.addEventListener( + 'sourceopen', t.step_func(() => { + URL.revokeObjectURL(url); + assert_equals(mediaSource.readyState, 'open', 'MediaSource is open'); + resolve([v, mediaSource]); + }), + {once: true}); + v.src = url; + }); +} + +async function setupEme(t, video) { + testEncodedKey = await crypto.subtle.importKey( + 'raw', testKey.buffer, 'AES-CTR', false, ['encrypt', 'decrypt']); + + var handler = new MessageHandler( + 'org.w3.clearkey', {keys: [{kid: testKeyId, key: testKey}]}); + + function handleMessage(event) { + handler.messagehandler(event.messageType, event.message).then(response => { + event.target.update(response).catch(e => { + assert_unreached('Failed to update session: ' + e); + }); + }); + } + + return navigator + .requestMediaKeySystemAccess('org.w3.clearkey', keySystemConfig) + .then(keySystemAccess => { + return keySystemAccess.createMediaKeys(); + }) + .then(createdMediaKeys => { + return video.setMediaKeys(createdMediaKeys); + }) + .then(_ => { + let session = video.mediaKeys.createSession(); + session.addEventListener('message', handleMessage, false); + + let encoder = new TextEncoder(); + let initData = encoder.encode( + JSON.stringify({'kids': [base64urlEncode(testKeyId)]})); + session.generateRequest('keyids', initData).catch(e => { + assert_unreached('Failed to generate a license request: ' + e); + }); + }) + .catch(e => { + assert_unreached('Failed to setup EME: ', e); + }); +} + +async function runEncryptedChunksTest(t) { + let buffer = await vp9.buffer(); + let [videoElement, mediaSource] = await getOpenMediaSource(t); + + // Makes early prototype demo playback easier to control manually. + videoElement.controls = true; + + await setupEme(t, videoElement); + + let sourceBuffer = mediaSource.addSourceBuffer( + {videoConfig: {codec: vp9.codec, encryptionScheme: 'cenc'}}); + let nextTimestamp = 0; + let frameDuration = 100 * 1000; // 100 milliseconds + // forEach with async callbacks makes it too easy to have uncaught rejections + // that don't fail this promise_test or even emit harness error. + // Iterating explicitly instead. + for (i = 0; i < vp9.frames.length; i++, nextTimestamp += frameDuration) { + let frameMetadata = vp9.frames[i]; + let frameData = + new Uint8Array(buffer, frameMetadata.offset, frameMetadata.size); + let encryptedFrameData = await window.crypto.subtle.encrypt( + {name: 'AES-CTR', counter: testIV, length: 128}, testEncodedKey, + frameData); + + await sourceBuffer.appendEncodedChunks(new EncodedVideoChunk({ + type: frameMetadata.type, + timestamp: nextTimestamp, + duration: frameDuration, + data: encryptedFrameData, + decryptConfig: { + encryptionScheme: 'cenc', + keyId: testKeyId, + initializationVector: testIV, + subsampleLayout: [{clearBytes: 0, cypherBytes: frameMetadata.size}], + } + })); + } + + mediaSource.endOfStream(); + + return new Promise((resolve, reject) => { + videoElement.onended = resolve; + videoElement.onerror = reject; + videoElement.play(); + }); +} diff --git a/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-encrypted-webcodecs-appendencodedchunks-play.https.html b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-encrypted-webcodecs-appendencodedchunks-play.https.html new file mode 100644 index 0000000000..47238af30d --- /dev/null +++ b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-encrypted-webcodecs-appendencodedchunks-play.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<title>Test encoded chunk buffering and playback with EME + MediaSource</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/encrypted-media/util/utf8.js"></script> +<script src="/encrypted-media/util/utils.js"></script> +<script src="/encrypted-media/util/clearkey-messagehandler.js"></script> +<script src="media-source-webcodecs-util.js"></script> +<script> +setup(() => { + assert_implements( + SourceBuffer.prototype.hasOwnProperty('appendEncodedChunks'), + 'SourceBuffer prototype hasOwnProperty "appendEncodedChunks", used ' + + 'here to feature detect MSE-for-WebCodecs implementation.'); +}); + +promise_test(async t => { + return runEncryptedChunksTest(t); +}, "Buffer encrypted EncodedVideoChunks (VP9) one-by-one and play them with MSE"); + +// TODO(crbug.com/1144908): More exhaustive tests (multiple sourcebuffers, +// varying append patterns, invalid append patterns; eventually more codecs, +// out-of-order DTS, durations, etc.) + +</script> +</html> diff --git a/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-webcodecs-addsourcebuffer.html b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-webcodecs-addsourcebuffer.html new file mode 100644 index 0000000000..e29600f99c --- /dev/null +++ b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-webcodecs-addsourcebuffer.html @@ -0,0 +1,191 @@ +<!DOCTYPE html> +<html> + <title>Test MediaSource addSourceBuffer overloads for WebCodecs Audio/VideoDecoderConfigs</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +setup(() => { + assert_implements( + SourceBuffer.prototype.hasOwnProperty('appendEncodedChunks'), + 'SourceBuffer prototype hasOwnProperty "appendEncodedChunks", used ' + + 'here to feature detect MSE-for-WebCodecs implementation.'); +}); + +testInvalidArguments(); +testValidArguments(); + +function getValidAudioConfig() { + // TODO(crbug.com/1144908): Consider confirming with WebCodecs' + // isConfigSupported() once that API is available. + return { + codec: 'opus', + sampleRate: 48000, + numberOfChannels: 2 + }; +} + +function getValidVideoConfig() { + // TODO(crbug.com/1144908): Consider confirming with WebCodecs' + // isConfigSupported() once that API is available. + return { codec: 'vp09.00.10.08' }; +} + +function testInvalidArguments() { + const INVALID_CASES = [ + { arg: null, + name: 'null' }, + { arg: undefined, + name: 'undefined' }, + { arg: { }, + name: '{ empty dictionary }' }, + { + arg: { + audioConfig: getValidAudioConfig(), + videoConfig: getValidVideoConfig() + }, + name: '{ valid audioConfig and videoConfig }', + }, + { arg: { audioConfig: { sampleRate: 48000, numberOfChannels: 2 } }, + name: 'audio config missing required member "codec"' }, + { arg: { videoConfig: { } }, + name: 'video config missing required member "codec"' }, + ]; + + [ 'closed', 'open', 'ended' ].forEach(readyStateScenario => { + INVALID_CASES.forEach(invalidCase => { + runAddSourceBufferTest(invalidCase['arg'] /* argument */, + false /* isValidArgument */, + invalidCase['name'] /* argumentDescription */, + readyStateScenario); + }); + }); +} + +function testValidArguments() { + const VALID_CASES = [ + { + arg: { + audioConfig: getValidAudioConfig() + }, + name: 'valid audioConfig' + }, + { + arg: { + videoConfig: getValidVideoConfig() + }, + name: 'valid videoConfig' + }, + ]; + + [ 'closed', 'open', 'ended' ].forEach(readyStateScenario => { + VALID_CASES.forEach(validCase => { + runAddSourceBufferTest( + validCase['arg'] /* argument */, + true /* isValidArgument */, + validCase['name'] /* argumentDescription */, + readyStateScenario); + }); + }); +} + +async function getClosedMediaSource(test) { + let mediaSource = new MediaSource(); + assert_equals(mediaSource.readyState, 'closed'); + return mediaSource; +} + +async function getOpenMediaSource(test) { + return new Promise(async resolve => { + const v = document.createElement('video'); + const mediaSource = new MediaSource(); + const url = URL.createObjectURL(mediaSource); + mediaSource.addEventListener('sourceopen', test.step_func(() => { + URL.revokeObjectURL(url); + assert_equals(mediaSource.readyState, 'open', 'MediaSource is open'); + resolve(mediaSource); + }), { once: true }); + v.src = url; + }); +} + +async function getEndedMediaSource(test) { + let mediaSource = await getOpenMediaSource(test); + assert_equals(mediaSource.readyState, 'open', 'MediaSource is open'); + mediaSource.endOfStream(); + assert_equals(mediaSource.readyState, 'ended', 'MediaSource is ended'); + return mediaSource; +} + +function runAddSourceBufferTest(argument, isValidArgument, argumentDescription, readyStateScenario) { + const testDescription = 'addSourceBuffer call with ' + + (isValidArgument ? 'valid' : 'invalid') + + ' argument ' + argumentDescription + ' while MediaSource readyState is ' + + readyStateScenario; + + switch(readyStateScenario) { + case 'closed': + promise_test(async t => { + let mediaSource = await getClosedMediaSource(t); + assert_equals(mediaSource.readyState, 'closed'); + let sourceBuffer; + if (isValidArgument) { + assert_throws_dom('InvalidStateError', + () => { sourceBuffer = mediaSource.addSourceBuffer(argument); }, + 'addSourceBuffer(valid config) throws InvalidStateError if MediaSource is "closed"'); + assert_equals(sourceBuffer, undefined, + 'addSourceBuffer result for valid config while "closed" should be exception'); + } else { + assert_throws_js(TypeError, + () => { sourceBuffer = mediaSource.addSourceBuffer(argument); }, + 'addSourceBuffer(invalid config) throws TypeError if MediaSource is "closed"'); + assert_equals(sourceBuffer, undefined, + 'addSourceBuffer result for invalid config while "closed" should be exception'); + } + }, testDescription); + break; + case 'open': + promise_test(async t => { + let mediaSource = await getOpenMediaSource(t); + assert_equals(mediaSource.readyState, 'open', 'MediaSource is open'); + let sourceBuffer; + if (isValidArgument) { + sourceBuffer = mediaSource.addSourceBuffer(argument); + assert_true(sourceBuffer instanceof SourceBuffer, + 'addSourceBuffer result for valid config while "open" should be a SourceBuffer instance'); + } else { + assert_throws_js(TypeError, + () => { sourceBuffer = mediaSource.addSourceBuffer(argument); }, + 'addSourceBuffer(invalid config) throws TypeError if MediaSource is "open"'); + assert_equals(sourceBuffer, undefined, + 'addSourceBufferResult for invalid config while "open" should be exception'); + } + }, testDescription); + break; + case 'ended': + promise_test(async t => { + let mediaSource = await getEndedMediaSource(t); + let sourceBuffer; + if (isValidArgument) { + assert_throws_dom('InvalidStateError', + () => { sourceBuffer = mediaSource.addSourceBuffer(argument); }, + 'addSourceBuffer(valid config) throws InvalidStateError if MediaSource is "ended"'); + assert_equals(sourceBuffer, undefined, + 'addSourceBuffer result for valid config while "ended" should be exception'); + } else { + assert_throws_js(TypeError, + () => { sourceBuffer = mediaSource.addSourceBuffer(argument); }, + 'addSourceBuffer(invalid config) throws TypeError if MediaSource is "ended"'); + assert_equals(sourceBuffer, undefined, + 'addSourceBuffer result for invalid config while "ended" should be exception'); + } + }, testDescription); + break; + default: + assert_unreached('Invalid readyStateScenario ' + readyStateScenario); + break; + } +} + +</script> +</html> diff --git a/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-webcodecs-appendencodedchunks-play.html b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-webcodecs-appendencodedchunks-play.html new file mode 100644 index 0000000000..f93f5a9883 --- /dev/null +++ b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/mediasource-webcodecs-appendencodedchunks-play.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> + <title>Test basic encoded chunk buffering and playback with MediaSource</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="media-source-webcodecs-util.js"></script> +<script> +setup(() => { + assert_implements( + SourceBuffer.prototype.hasOwnProperty('appendEncodedChunks'), + 'SourceBuffer prototype hasOwnProperty "appendEncodedChunks", used ' + + 'here to feature detect MSE-for-WebCodecs implementation.'); +}); + +promise_test(async t => { + let buffer = await vp9.buffer(); + let [ videoElement, mediaSource ] = await getOpenMediaSource(t); + videoElement.controls = true; // Makes early prototype demo playback easier to control manually. + let sourceBuffer = mediaSource.addSourceBuffer({ videoConfig: { codec: vp9.codec } }); + let next_timestamp = 0; + let frame_duration = 100 * 1000; // 100 milliseconds + // forEach with async callbacks makes it too easy to have uncaught rejections + // that don't fail this promise_test or even emit harness error. + // Iterating explicitly instead. + for (i = 0; i < vp9.frames.length; i++, next_timestamp += frame_duration) { + let frame_metadata = vp9.frames[i]; + await sourceBuffer.appendEncodedChunks(new EncodedVideoChunk( { + type: frame_metadata.type, + timestamp: next_timestamp, + duration: frame_duration, + data: new Uint8Array(buffer, frame_metadata.offset, frame_metadata.size) + })); + } + + mediaSource.endOfStream(); + + return new Promise( (resolve, reject) => { + videoElement.onended = resolve; + videoElement.onerror = reject; + videoElement.play(); + }); + +}, "Buffer EncodedVideoChunks (VP9) one-by-one and play them with MSE"); + +// TODO(crbug.com/1144908): More exhaustive tests (multiple sourcebuffers, +// varying append patterns, invalid append patterns; eventually more codecs, +// out-of-order DTS, durations, etc.) + +</script> +</html> diff --git a/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/vp9.mp4 b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/vp9.mp4 Binary files differnew file mode 100644 index 0000000000..7553e5cae9 --- /dev/null +++ b/testing/web-platform/tests/media-source/mse-for-webcodecs/tentative/vp9.mp4 diff --git a/testing/web-platform/tests/media-source/webm/invalid-codec.webm b/testing/web-platform/tests/media-source/webm/invalid-codec.webm Binary files differnew file mode 100644 index 0000000000..f1c8bdd7ab --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/invalid-codec.webm diff --git a/testing/web-platform/tests/media-source/webm/test-a-128k-44100Hz-1ch-manifest.json b/testing/web-platform/tests/media-source/webm/test-a-128k-44100Hz-1ch-manifest.json new file mode 100644 index 0000000000..524da8149f --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-a-128k-44100Hz-1ch-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-a-128k-44100Hz-1ch.webm", + "type": "audio/webm;codecs=\"vorbis\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-a-128k-44100Hz-1ch.webm b/testing/web-platform/tests/media-source/webm/test-a-128k-44100Hz-1ch.webm Binary files differnew file mode 100644 index 0000000000..c5b064deb9 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-a-128k-44100Hz-1ch.webm diff --git a/testing/web-platform/tests/media-source/webm/test-a-192k-44100Hz-1ch-manifest.json b/testing/web-platform/tests/media-source/webm/test-a-192k-44100Hz-1ch-manifest.json new file mode 100644 index 0000000000..7f2fa1e8c3 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-a-192k-44100Hz-1ch-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-a-192k-44100Hz-1ch.webm", + "type": "audio/webm;codecs=\"vorbis\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-a-192k-44100Hz-1ch.webm b/testing/web-platform/tests/media-source/webm/test-a-192k-44100Hz-1ch.webm Binary files differnew file mode 100644 index 0000000000..53814d3bd6 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-a-192k-44100Hz-1ch.webm diff --git a/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..af9f07af15 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8,vorbis\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..8b705dbc89 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..f7ec86b3db --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8,vorbis\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..c5e010e3c7 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-384k-44100Hz-1ch-640x480-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..96a59db586 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8,vorbis\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..62c43288e6 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-448k-44100Hz-1ch-640x480-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..86723b34a8 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8,vorbis\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..93c31b6a97 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-av-640k-44100Hz-1ch-640x480-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-24fps-8kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-24fps-8kfr-manifest.json new file mode 100644 index 0000000000..00e103aca9 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-24fps-8kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-v-128k-320x240-24fps-8kfr.webm", + "type": "video/webm;codecs=\"vp8\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-24fps-8kfr.webm b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-24fps-8kfr.webm Binary files differnew file mode 100644 index 0000000000..189c472f99 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-24fps-8kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..fdeeb401d9 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-v-128k-320x240-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..18b2bafc3a --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-128k-320x240-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-v-128k-640x480-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-v-128k-640x480-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..4e30460667 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-128k-640x480-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-v-128k-640x480-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-v-128k-640x480-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-v-128k-640x480-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..75e38b0bfa --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-128k-640x480-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-v-256k-320x240-30fps-10kfr-manifest.json b/testing/web-platform/tests/media-source/webm/test-v-256k-320x240-30fps-10kfr-manifest.json new file mode 100644 index 0000000000..3470674bff --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-256k-320x240-30fps-10kfr-manifest.json @@ -0,0 +1,4 @@ +{ + "url": "webm/test-v-256k-320x240-30fps-10kfr.webm", + "type": "video/webm;codecs=\"vp8\"" +}
\ No newline at end of file diff --git a/testing/web-platform/tests/media-source/webm/test-v-256k-320x240-30fps-10kfr.webm b/testing/web-platform/tests/media-source/webm/test-v-256k-320x240-30fps-10kfr.webm Binary files differnew file mode 100644 index 0000000000..0250d26faf --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-v-256k-320x240-30fps-10kfr.webm diff --git a/testing/web-platform/tests/media-source/webm/test-vp8-vorbis-webvtt.webm b/testing/web-platform/tests/media-source/webm/test-vp8-vorbis-webvtt.webm Binary files differnew file mode 100644 index 0000000000..c626f86e33 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-vp8-vorbis-webvtt.webm diff --git a/testing/web-platform/tests/media-source/webm/test-vp9.webm b/testing/web-platform/tests/media-source/webm/test-vp9.webm Binary files differnew file mode 100644 index 0000000000..d63dfdaac7 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test-vp9.webm diff --git a/testing/web-platform/tests/media-source/webm/test.webm b/testing/web-platform/tests/media-source/webm/test.webm Binary files differnew file mode 100644 index 0000000000..3a601805d7 --- /dev/null +++ b/testing/web-platform/tests/media-source/webm/test.webm |