summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/mediacapture-record
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/mediacapture-record')
-rw-r--r--testing/web-platform/tests/mediacapture-record/BlobEvent-constructor.html38
-rw-r--r--testing/web-platform/tests/mediacapture-record/META.yml3
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-bitrate.https.html230
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html128
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-creation.https.html59
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-destroy-script-execution.html79
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-detached-context.html26
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-disabled-tracks.https.html56
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-error.html62
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html108
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html205
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html89
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection-no-sink.https.html47
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html86
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-start.html25
-rw-r--r--testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html151
-rw-r--r--testing/web-platform/tests/mediacapture-record/idlharness.window.js40
-rw-r--r--testing/web-platform/tests/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html74
-rw-r--r--testing/web-platform/tests/mediacapture-record/support/MediaRecorder-iframe.html20
-rw-r--r--testing/web-platform/tests/mediacapture-record/utils/peerconnection.js141
-rw-r--r--testing/web-platform/tests/mediacapture-record/utils/sources.js75
21 files changed, 1742 insertions, 0 deletions
diff --git a/testing/web-platform/tests/mediacapture-record/BlobEvent-constructor.html b/testing/web-platform/tests/mediacapture-record/BlobEvent-constructor.html
new file mode 100644
index 0000000000..66dc3404d7
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/BlobEvent-constructor.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<title>BlobEvent constructor</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#blob-event">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {
+ assert_equals(BlobEvent.length, 2);
+ assert_throws_js(TypeError, function() {
+ new BlobEvent("type");
+ });
+ assert_throws_js(TypeError, function() {
+ new BlobEvent("type", null);
+ });
+ assert_throws_js(TypeError, function() {
+ new BlobEvent("type", undefined);
+ });
+}, "The BlobEventInit dictionary is required");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ new BlobEvent("type", {});
+ });
+ assert_throws_js(TypeError, function() {
+ new BlobEvent("type", { data: null });
+ });
+ assert_throws_js(TypeError, function() {
+ new BlobEvent("type", { data: undefined });
+ });
+}, "The BlobEventInit dictionary's data member is required.");
+
+test(function() {
+ var blob = new Blob();
+ var event = new BlobEvent("type", { data: blob });
+ assert_equals(event.type, "type");
+ assert_equals(event.data, blob);
+}, "The BlobEvent instance's data attribute is set.");
+</script>
diff --git a/testing/web-platform/tests/mediacapture-record/META.yml b/testing/web-platform/tests/mediacapture-record/META.yml
new file mode 100644
index 0000000000..d59e5e3084
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/mediacapture-record/
+suggested_reviewers:
+ - yellowdoge
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-bitrate.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-bitrate.https.html
new file mode 100644
index 0000000000..d89f739033
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-bitrate.https.html
@@ -0,0 +1,230 @@
+<!doctype html>
+<html>
+<head>
+<title>MediaRecorder {audio|video}bitsPerSecond attributes</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../mediacapture-streams/permission-helper.js"></script>
+</head>
+<script>
+
+/*
+ * The bitrate handling is difficult to test, given that the spec uses text such
+ * as: "values the User Agent deems reasonable" and "such that the sum of
+ * videoBitsPerSecond and audioBitsPerSecond is close to the value of recorder’s
+ * [[ConstrainedBitsPerSecond]] slot". For cases like that this test tries to
+ * use values that are reasonable for the tested track types. Should a UA vendor
+ * see a need to update this to fit their definition of reasonable, they should
+ * feel free to do so, doing their best to avoid regressing existing compliant
+ * implementations.
+ */
+
+async function getStream(t, constraints) {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ const tracks = stream.getTracks();
+ t.add_cleanup(() => tracks.forEach(tr => tr.stop()));
+ return stream;
+}
+
+function getAudioStream(t) {
+ return getStream(t, {audio: true});
+}
+
+function getVideoStream(t) {
+ return getStream(t, {video: true});
+}
+
+function getAudioVideoStream(t) {
+ return getStream(t, {audio: true, video: true});
+}
+
+const AUDIO_BITRATE = 1e5; // 100kbps
+const VIDEO_BITRATE = 1e6; // 1Mbps
+const LOW_TOTAL_BITRATE = 5e5; // 500kbps
+const HIGH_TOTAL_BITRATE = 2e6; // 2Mbps
+const BITRATE_EPSILON = 1e5; // 100kbps
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t));
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+}, "Passing no bitrate config results in defaults");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ bitsPerSecond: 0,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+ assert_approx_equals(rec.audioBitsPerSecond + rec.videoBitsPerSecond, 0,
+ BITRATE_EPSILON);
+}, "Passing bitsPerSecond:0 results in targets close to 0");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ audioBitsPerSecond: 0,
+ });
+ assert_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+}, "Passing only audioBitsPerSecond:0 results in 0 for audio, default for video");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ videoBitsPerSecond: 0,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_equals(rec.videoBitsPerSecond, 0);
+}, "Passing only videoBitsPerSecond:0 results in 0 for video, default for audio");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ bitsPerSecond: 0,
+ audioBitsPerSecond: AUDIO_BITRATE,
+ videoBitsPerSecond: VIDEO_BITRATE,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+ assert_approx_equals(rec.audioBitsPerSecond + rec.videoBitsPerSecond, 0,
+ BITRATE_EPSILON);
+}, "Passing bitsPerSecond:0 overrides audio/video-specific values");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ bitsPerSecond: HIGH_TOTAL_BITRATE,
+ audioBitsPerSecond: 0,
+ videoBitsPerSecond: 0,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+ assert_approx_equals(rec.audioBitsPerSecond + rec.videoBitsPerSecond,
+ HIGH_TOTAL_BITRATE, BITRATE_EPSILON);
+}, "Passing bitsPerSecond overrides audio/video zero values");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ bitsPerSecond: HIGH_TOTAL_BITRATE,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+ assert_approx_equals(rec.audioBitsPerSecond + rec.videoBitsPerSecond,
+ HIGH_TOTAL_BITRATE, BITRATE_EPSILON);
+}, "Passing bitsPerSecond sets audio/video bitrate values");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ audioBitsPerSecond: AUDIO_BITRATE,
+ });
+ assert_equals(rec.audioBitsPerSecond, AUDIO_BITRATE);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+}, "Passing only audioBitsPerSecond results in default for video");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ videoBitsPerSecond: VIDEO_BITRATE,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_equals(rec.videoBitsPerSecond, VIDEO_BITRATE);
+}, "Passing only videoBitsPerSecond results in default for audio");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioStream(t), {
+ videoBitsPerSecond: VIDEO_BITRATE,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_equals(rec.videoBitsPerSecond, VIDEO_BITRATE);
+}, "Passing videoBitsPerSecond for audio-only stream still results in something for video");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getVideoStream(t), {
+ audioBitsPerSecond: AUDIO_BITRATE,
+ });
+ assert_equals(rec.audioBitsPerSecond, AUDIO_BITRATE);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+}, "Passing audioBitsPerSecond for video-only stream still results in something for audio");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioStream(t), {
+ bitsPerSecond: HIGH_TOTAL_BITRATE,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+}, "Passing bitsPerSecond for audio-only stream still results in something for video");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getVideoStream(t), {
+ bitsPerSecond: HIGH_TOTAL_BITRATE,
+ });
+ assert_not_equals(rec.audioBitsPerSecond, 0);
+ assert_not_equals(rec.videoBitsPerSecond, 0);
+}, "Passing bitsPerSecond for video-only stream still results in something for audio");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t));
+ t.add_cleanup(() => rec.stop());
+ const abps = rec.audioBitsPerSecond;
+ const vbps = rec.videoBitsPerSecond;
+ rec.start();
+ assert_equals(rec.audioBitsPerSecond, abps);
+ assert_equals(rec.videoBitsPerSecond, vbps);
+}, "Selected default track bitrates are not changed by start()");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ audioBitsPerSecond: AUDIO_BITRATE,
+ videoBitsPerSecond: VIDEO_BITRATE,
+ });
+ t.add_cleanup(() => rec.stop());
+ const abps = rec.audioBitsPerSecond;
+ const vbps = rec.videoBitsPerSecond;
+ rec.start();
+ assert_equals(rec.audioBitsPerSecond, abps);
+ assert_equals(rec.videoBitsPerSecond, vbps);
+}, "Passed-in track bitrates are not changed by start()");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioVideoStream(t), {
+ bitsPerSecond: HIGH_TOTAL_BITRATE,
+ });
+ t.add_cleanup(() => rec.stop());
+ const abps = rec.audioBitsPerSecond;
+ const vbps = rec.videoBitsPerSecond;
+ rec.start();
+ assert_equals(rec.audioBitsPerSecond, abps);
+ assert_equals(rec.videoBitsPerSecond, vbps);
+}, "Passing bitsPerSecond for audio/video stream does not change track bitrates in start()");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getAudioStream(t), {
+ bitsPerSecond: LOW_TOTAL_BITRATE,
+ });
+ t.add_cleanup(() => rec.stop());
+ const abps = rec.audioBitsPerSecond;
+ const vbps = rec.videoBitsPerSecond;
+ rec.start();
+ assert_approx_equals(rec.audioBitsPerSecond, LOW_TOTAL_BITRATE,
+ BITRATE_EPSILON);
+ assert_equals(rec.videoBitsPerSecond, 0);
+ assert_not_equals(rec.audioBitsPerSecond, abps);
+ assert_not_equals(rec.videoBitsPerSecond, vbps);
+}, "Passing bitsPerSecond for audio stream sets video track bitrate to 0 in start()");
+
+promise_test(async t => {
+ const rec = new MediaRecorder(await getVideoStream(t), {
+ bitsPerSecond: HIGH_TOTAL_BITRATE,
+ });
+ t.add_cleanup(() => rec.stop());
+ const abps = rec.audioBitsPerSecond;
+ const vbps = rec.videoBitsPerSecond;
+ rec.start();
+ assert_equals(rec.audioBitsPerSecond, 0);
+ assert_approx_equals(rec.videoBitsPerSecond, HIGH_TOTAL_BITRATE,
+ BITRATE_EPSILON);
+ assert_not_equals(rec.audioBitsPerSecond, abps);
+ assert_not_equals(rec.videoBitsPerSecond, vbps);
+}, "Passing bitsPerSecond for video stream sets audio track bitrate to 0 in start()");
+</script>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html
new file mode 100644
index 0000000000..187015f42e
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html
@@ -0,0 +1,128 @@
+<!doctype html>
+<html>
+<meta name="timeout" content="long">
+
+<head>
+ <title>MediaRecorder canvas media source</title>
+ <link rel="help"
+ href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="../mediacapture-streams/permission-helper.js"></script>
+</head>
+
+<body>
+ <canvas id="canvas"></canvas>
+ <script>
+
+async_test(test => {
+ const CANVAS_WIDTH = 256;
+ const CANVAS_HEIGHT = 144;
+
+ // Empty video frames from this resolution consistently have ~750 bytes in my
+ // tests, while valid video frames usually contain 7-8KB. A threshold of
+ // 1.5KB consistently fails when video frames are empty but passes when video
+ // frames are non-empty.
+ const THRESHOLD_FOR_EMPTY_FRAMES = 1500;
+
+ const CAMERA_CONSTRAINTS = {
+ video: {
+ width: { ideal: CANVAS_WIDTH },
+ height: { ideal: CANVAS_HEIGHT }
+ }
+ };
+
+ function useUserMedia(constraints) {
+ let activeStream = null;
+
+ function startCamera() {
+ return navigator.mediaDevices.getUserMedia(constraints).then(
+ (stream) => {
+ activeStream = stream;
+ return stream;
+ }
+ );
+ }
+
+ function stopCamera() {
+ activeStream?.getTracks().forEach((track) => track.stop());
+ }
+
+ return { startCamera, stopCamera };
+ }
+
+ function useMediaRecorder(stream, frameSizeCallback) {
+ const mediaRecorder = new MediaRecorder(
+ stream,
+ {}
+ );
+
+ mediaRecorder.ondataavailable = event => {
+ const {size} = event.data;
+ frameSizeCallback(size);
+
+ if (mediaRecorder.state !== "inactive") {
+ mediaRecorder.stop();
+ }
+ };
+
+ mediaRecorder.start(1000);
+ }
+
+ const canvas = document.querySelector("canvas");
+ const ctx = canvas.getContext("2d", {
+ alpha: false,
+ });
+
+ canvas.width = CANVAS_WIDTH;
+ canvas.height = CANVAS_HEIGHT;
+
+ const {startCamera, stopCamera} = useUserMedia(CAMERA_CONSTRAINTS);
+ startCamera().then(async stream => {
+ const videoTrack = stream.getVideoTracks()[0];
+ const { readable: readableStream } = new MediaStreamTrackProcessor({
+ track: videoTrack
+ });
+
+ const composedTrackGenerator = new MediaStreamTrackGenerator({
+ kind: "video"
+ });
+ const sink = composedTrackGenerator.writable;
+
+ ctx.fillStyle = "#333";
+ ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
+
+ const transformer = new TransformStream({
+ async transform(cameraFrame, controller) {
+ if (cameraFrame && cameraFrame?.codedWidth > 0) {
+ const leftPos = (CANVAS_WIDTH - cameraFrame.displayWidth) / 2;
+ const topPos = (CANVAS_HEIGHT - cameraFrame.displayHeight) / 2;
+
+ ctx.drawImage(cameraFrame, leftPos, topPos);
+
+ const newFrame = new VideoFrame(canvas, {
+ timestamp: cameraFrame.timestamp
+ });
+ cameraFrame.close();
+ controller.enqueue(newFrame);
+ }
+ }
+ });
+
+ readableStream.pipeThrough(transformer).pipeTo(sink);
+
+ const compositedMediaStream = new MediaStream([composedTrackGenerator]);
+
+ useMediaRecorder(compositedMediaStream, test.step_func_done(size => {
+ assert_greater_than(size, THRESHOLD_FOR_EMPTY_FRAMES);
+ stopCamera();
+ }));
+ });
+}, "MediaRecorder returns frames containing video content");
+
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-creation.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-creation.https.html
new file mode 100644
index 0000000000..d2190c3ee5
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-creation.https.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Creation</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+</head>
+<script>
+ // This test verifies that MediaRecorder can be created with different Media
+ // Stream Track combinations: 1 Video Track only, 1 Audio Track only and finally
+ // a Media Stream with both a Video and an Audio Track. Note that recording is
+ // _not_ started in this test, see MediaRecorder-audio-video.html for it.
+
+ function makeAsyncTest(constraints, verifyStream, message) {
+ async_test(function(test) {
+
+ const gotStream = test.step_func(function(stream) {
+ verifyStream(stream);
+
+ var recorder = new MediaRecorder(stream);
+ assert_equals(recorder.state, "inactive");
+ assert_not_equals(recorder.videoBitsPerSecond, 0);
+ assert_not_equals(recorder.audioBitsPerSecond, 0);
+ test.done();
+ });
+
+ const onError = test.unreached_func('Error creating MediaStream.');
+ setMediaPermission().then(() => navigator.mediaDevices.getUserMedia(constraints)).then(gotStream, onError);
+ }, message);
+ }
+
+ function verifyVideoOnlyStream(stream) {
+ assert_equals(stream.getAudioTracks().length, 0);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_equals(stream.getVideoTracks()[0].readyState, 'live');
+ }
+ function verifyAudioOnlyStream(stream) {
+ assert_equals(stream.getAudioTracks().length, 1);
+ assert_equals(stream.getVideoTracks().length, 0);
+ assert_equals(stream.getAudioTracks()[0].readyState, 'live');
+ }
+ function verifyAudioVideoStream(stream) {
+ assert_equals(stream.getAudioTracks().length, 1);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_equals(stream.getVideoTracks()[0].readyState, 'live');
+ assert_equals(stream.getAudioTracks()[0].readyState, 'live');
+ }
+
+ // Note: webkitGetUserMedia() must be called with at least video or audio true.
+ makeAsyncTest({video:true}, verifyVideoOnlyStream, 'Video-only MediaRecorder');
+ makeAsyncTest({audio:true}, verifyAudioOnlyStream, 'Audio-only MediaRecorder');
+ makeAsyncTest({audio:true, video:true}, verifyAudioVideoStream, 'Video+Audio MediaRecorder');
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-destroy-script-execution.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-destroy-script-execution.html
new file mode 100644
index 0000000000..3e9add3c61
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-destroy-script-execution.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<title>MediaRecorder destroy script execution context</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<iframe src="support/MediaRecorder-iframe.html" id="subFrame-start" name="subFrameStart"></iframe>
+<iframe src="support/MediaRecorder-iframe.html" id="subFrame-stop" name="subFrameStop"></iframe>
+<iframe src="support/MediaRecorder-iframe.html" id="subFrame-allTrackEnded" name="subFrameAllTrackEnded"></iframe>
+<iframe src="support/MediaRecorder-iframe.html" id="subFrame-audioBitrateMode" name="subFrameAudioBitrateMode"></iframe>
+<script>
+ var iframeForCallingStart = document.getElementById('subFrame-start');
+ var iframeForCallingStop = document.getElementById('subFrame-stop');
+ var iframeForAllTrackEnded = document.getElementById('subFrame-allTrackEnded');
+ var iframeForAudioBitrateMode = document.getElementById('subFrame-audioBitrateMode');
+
+ var testForCallingStart = async_test('MediaRecorder will throw when start() is called and the script execution context is going away');
+ var testForCallingStop = async_test('MediaRecorder will not fire the stop event when stop() is called and the script execution context is going away');
+ var testForAllTrackEnded = async_test('MediaRecorder will not fire the stop event when all tracks are ended and the script execution context is going away');
+ var testForAudioBitrateMode = async_test('MediaRecorder will not crash on accessing audioBitrateMode when the script execution context is going away');
+
+
+ iframeForCallingStart.onload = function(e) {
+ let testWindow = subFrameStart.window;
+ testWindow.prepareForTest(testForCallingStart);
+ let exceptionCtor = testWindow.DOMException;
+ const recorder = subFrameStart.window.recorder;
+ iframeForCallingStart.remove();
+ testForCallingStart.step(function() {
+ assert_throws_dom('NotSupportedError', exceptionCtor, () => recorder.start(),
+ "MediaRecorder.start() should throw");
+ });;
+ testForCallingStart.done();
+ };
+
+ iframeForCallingStop.onload = function(e) {
+ subFrameStop.window.prepareForTest(testForCallingStop);
+ const recorder = subFrameStop.window.recorder;
+ recorder.ondataavailable = testForCallingStop.step_func(blobEvent => {
+ iframeForCallingStop.remove();
+ testForCallingStop.step_timeout(testForCallingStop.step_func_done(), 0);
+ });
+ recorder.onstop = testForCallingStop.unreached_func('Unexpected stop event');
+ recorder.start();
+ testForCallingStop.step(function() {
+ assert_equals(recorder.state, 'recording', 'MediaRecorder has been started successfully');
+ });
+ subFrameStop.window.control.addVideoFrame();
+ recorder.stop();
+ };
+
+ iframeForAllTrackEnded.onload = function(e) {
+ subFrameAllTrackEnded.window.prepareForTest(testForAllTrackEnded);
+ const recorder = subFrameAllTrackEnded.window.recorder;
+ recorder.ondataavailable = testForAllTrackEnded.step_func(blobEvent => {
+ iframeForAllTrackEnded.remove();
+ testForAllTrackEnded.step_timeout(testForAllTrackEnded.step_func_done(), 0);
+ });
+ recorder.onstop = testForAllTrackEnded.unreached_func('Unexpected stop event');
+ recorder.start();
+ testForAllTrackEnded.step(function() {
+ assert_equals(recorder.state, 'recording', 'MediaRecorder has been started successfully');
+ });
+ subFrameAllTrackEnded.window.control.addVideoFrame();
+ subFrameAllTrackEnded.window.video.getVideoTracks()[0].stop();
+ };
+
+ iframeForAudioBitrateMode.onload = testForAudioBitrateMode.step_func(function(e) {
+ subFrameAudioBitrateMode.window.prepareForTest(testForAudioBitrateMode);
+ const recorder = subFrameAudioBitrateMode.window.recorder;
+ iframeForAudioBitrateMode.remove();
+ recorder.audioBitrateMode;
+ testForAudioBitrateMode.done();
+ });
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-detached-context.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-detached-context.html
new file mode 100644
index 0000000000..f8a8699ad9
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-detached-context.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Detached Context</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+ async_test(t => {
+ const frame = document.body.appendChild(document.createElement('iframe'));
+ const recorderFunc = frame.contentWindow.MediaRecorder;
+ frame.remove();
+
+ try {
+ new recorderFunc(new MediaStream);
+ } catch (err) {
+ assert_equals(err.name, 'NotAllowedError');
+ t.done();
+ }
+ assert_unreached('MediaRecorder should have failed');
+ }, 'MediaRecorder creation with detached context');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-disabled-tracks.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-disabled-tracks.https.html
new file mode 100644
index 0000000000..ea64673264
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-disabled-tracks.https.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Disabled Tracks</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+<script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src=/resources/testdriver.js></script>
+<script src=/resources/testdriver-vendor.js></script>
+<script src='../mediacapture-streams/permission-helper.js'></script>
+</head>
+<script>
+
+ // This test verifies that MediaStream with disabled tracks can be correctly
+ // recorded. See crbug.com/878255 for more context.
+
+ [ ["video-only", {video: true, audio: false}],
+ ["audio-only", {video: false, audio: true}],
+ ["audio-video", {video: true, audio: true}]]
+ .forEach( function(args) {
+ async_test(function(test) {
+ let recorder;
+ const recorderOnDataAvailable = test.step_func(function(event) {
+ if (recorder.state != "recording")
+ return;
+
+ recorder.onstop = recorderOnStopExpected;
+ recorder.stop();
+ });
+
+ const recorderOnStopExpected = test.step_func_done();
+ const recorderOnStopUnexpected = test.unreached_func('Recording stopped.');
+ const recorderOnError = test.unreached_func('Recording error.');
+
+ const gotStream = test.step_func(function(stream) {
+ for (track of stream.getTracks())
+ track.enabled = false;
+
+ recorder = new MediaRecorder(stream);
+
+ assert_equals(recorder.state, "inactive");
+ recorder.ondataavailable = recorderOnDataAvailable;
+ recorder.onstop = recorderOnStopUnexpected;
+ recorder.onerror = recorderOnError;
+ recorder.start();
+
+ assert_equals(recorder.state, "recording");
+ recorder.requestData();
+ });
+
+ const onError = test.unreached_func('Error creating MediaStream.');
+ setMediaPermission().then(() => navigator.mediaDevices.getUserMedia(args[1])).then(gotStream, onError);
+ }, args[0]);
+ });
+
+</script>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-error.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-error.html
new file mode 100644
index 0000000000..54e83ecac7
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-error.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Error</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="utils/sources.js"></script>
+</head>
+<body>
+<script>
+ async_test(t => {
+ const {stream: video, control} = createVideoStream(t);
+ const {stream: audio} = createAudioStream(t);
+ const recorder = new MediaRecorder(video);
+ recorder.onerror = t.step_func(mediaRecorderErrorEvent => {
+ assert_true(mediaRecorderErrorEvent instanceof MediaRecorderErrorEvent, 'the type of event should be MediaRecorderErrorEvent');
+ assert_equals(mediaRecorderErrorEvent.error.name, 'InvalidModificationError', 'the type of error should be InvalidModificationError when track has been added or removed');
+ assert_true(mediaRecorderErrorEvent.isTrusted, 'isTrusted should be true when the event is created by C++');
+ assert_equals(recorder.state, "inactive", "MediaRecorder has been stopped after adding a track to stream");
+ t.done();
+ });
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ video.addTrack(audio.getAudioTracks()[0]);
+ control.addVideoFrame();
+ t.step_timeout(() => {
+ assert_unreached("error event is not fired after 2 seconds");
+ }, 2000);
+ }, "MediaRecorder will stop recording when any of track is added and error event will be fired");
+
+ async_test(t => {
+ const {stream: video, control} = createVideoStream(t);
+ const recorder = new MediaRecorder(video);
+ recorder.onerror = t.step_func(mediaRecorderErrorEvent => {
+ assert_true(mediaRecorderErrorEvent instanceof MediaRecorderErrorEvent, 'the type of event should be MediaRecorderErrorEvent');
+ assert_equals(mediaRecorderErrorEvent.error.name, 'InvalidModificationError', 'the type of error should be InvalidModificationError when track has been added or removed');
+ assert_true(mediaRecorderErrorEvent.isTrusted, 'isTrusted should be true when the event is created by C++');
+ assert_equals(recorder.state, "inactive", "MediaRecorder has been stopped after removing a track from stream");
+ t.done();
+ });
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ video.removeTrack(video.getVideoTracks()[0]);
+ control.addVideoFrame();
+ t.step_timeout(() => {
+ assert_unreached("error event is not fired after 2 seconds");
+ }, 2000);
+ }, "MediaRecorder will stop recording when any of track is removed and error event will be fired");
+
+ test(t => {
+ const {stream: video} = createVideoStream(t);
+ const recorder = new MediaRecorder(video);
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ assert_throws_dom("InvalidStateError", function() {
+ recorder.start();
+ });
+ }, "MediaRecorder cannot start recording when MediaRecorder' state is not inactive and an InvalidStateError should be thrown");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html
new file mode 100644
index 0000000000..0a377991ba
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html
@@ -0,0 +1,108 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder events and exceptions</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="utils/sources.js"></script>
+</head>
+<body>
+<script>
+
+ // This test exercises the MediaRecorder API event sequence:
+ // onStart -> onPause -> onResume -> onDataAvailable -> onStop
+ // verifying the |state| and a few exceptions that are supposed to be thrown
+ // when doing the wrong thing.
+
+ async_test(test => {
+
+ recorderOnUnexpectedEvent = test.step_func(() => {
+ assert_unreached('Unexpected event.');
+ });
+
+ recorderOnDataAvailable = test.step_func(event => {
+ assert_equals(recorder.state, "inactive");
+ assert_not_equals(event.data.size, 0, 'We should get a Blob with data');
+ });
+
+ recorderOnStop = test.step_func(function() {
+ assert_equals(recorder.state, "inactive");
+ recorder.onstop = recorderOnUnexpectedEvent;
+ recorder.stop();
+ assert_equals(recorder.state, "inactive", "stop() is idempotent");
+ assert_throws_dom("InvalidStateError", function() { recorder.pause() },
+ "recorder cannot be pause()ed in |inactive| state");
+ assert_throws_dom("InvalidStateError", function() { recorder.resume() },
+ "recorder cannot be resume()d in |inactive| state");
+ assert_throws_dom("InvalidStateError", function() { recorder.requestData() },
+ "cannot requestData() if recorder is in |inactive| state");
+ test.done();
+ });
+
+ recorderOnResume = test.step_func(function() {
+ assert_equals(recorder.state, "recording");
+ recorder.onresume = recorderOnUnexpectedEvent;
+ recorder.onstop = recorderOnStop;
+ recorder.stop();
+ });
+
+ recorderOnPause = test.step_func(function() {
+ assert_equals(recorder.state, "paused");
+ recorder.onpause = recorderOnUnexpectedEvent;
+ recorder.onresume = recorderOnResume;
+ recorder.resume();
+ });
+
+ recorderOnStart = test.step_func(function() {
+ assert_equals(recorder.state, "recording");
+ recorder.onstart = recorderOnUnexpectedEvent;
+ recorder.onpause = recorderOnPause;
+ recorder.pause();
+ });
+
+ const {stream, control} = createVideoStream(test);
+ assert_equals(stream.getAudioTracks().length, 0);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_equals(stream.getVideoTracks()[0].readyState, 'live');
+
+ assert_throws_dom("NotSupportedError",
+ function() {
+ new MediaRecorder(
+ new MediaStream(), {mimeType : "video/invalid"});
+ },
+ "recorder should throw() with unsupported mimeType");
+ const recorder = new MediaRecorder(new MediaStream());
+ assert_equals(recorder.state, "inactive");
+
+ recorder.stop();
+ assert_equals(recorder.state, "inactive", "stop() is idempotent");
+ assert_throws_dom("InvalidStateError", function(){recorder.pause()},
+ "recorder cannot be pause()ed in |inactive| state");
+ assert_throws_dom("InvalidStateError", function(){recorder.resume()},
+ "recorder cannot be resume()d in |inactive| state");
+ assert_throws_dom("InvalidStateError", function(){recorder.requestData()},
+ "cannot requestData() if recorder is in |inactive| state");
+
+ assert_throws_dom("NotSupportedError",
+ function() {
+ recorder.start();
+ },
+ "recorder should throw() when starting with inactive stream");
+
+ recorder.stream.addTrack(stream.getTracks()[0]);
+
+ control.addVideoFrame();
+
+ recorder.onstop = recorderOnUnexpectedEvent;
+ recorder.onpause = recorderOnUnexpectedEvent;
+ recorder.onresume = recorderOnUnexpectedEvent;
+ recorder.onerror = recorderOnUnexpectedEvent;
+ recorder.ondataavailable = recorderOnDataAvailable;
+ recorder.onstart = recorderOnStart;
+
+ recorder.start();
+ assert_equals(recorder.state, "recording");
+ });
+
+</script>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html
new file mode 100644
index 0000000000..07721abfd4
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-mimetype.html
@@ -0,0 +1,205 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder mimeType</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="utils/sources.js"></script>
+</head>
+<body>
+<script>
+test(t => {
+ const recorder = new MediaRecorder(createAudioStream(t).stream);
+ assert_equals(recorder.mimeType, "",
+ "MediaRecorder has no default mimeType");
+}, "MediaRecorder sets no default mimeType in the constructor for audio");
+
+test(t => {
+ const recorder = new MediaRecorder(createVideoStream(t).stream);
+ assert_equals(recorder.mimeType, "",
+ "MediaRecorder has no default mimeType");
+}, "MediaRecorder sets no default mimeType in the constructor for video");
+
+test(t => {
+ const recorder = new MediaRecorder(createAudioVideoStream(t).stream);
+ assert_equals(recorder.mimeType, "",
+ "MediaRecorder has no default mimeType");
+}, "MediaRecorder sets no default mimeType in the constructor for audio/video");
+
+test(t => {
+ assert_throws_dom("NotSupportedError",
+ () => new MediaRecorder(new MediaStream(), {mimeType: "audio/banana"}));
+}, "MediaRecorder invalid audio mimeType throws");
+
+test(t => {
+ assert_false(MediaRecorder.isTypeSupported("audio/banana"));
+}, "MediaRecorder invalid audio mimeType is unsupported");
+
+test(t => {
+ assert_throws_dom("NotSupportedError",
+ () => new MediaRecorder(new MediaStream(), {mimeType: "video/pineapple"}));
+}, "MediaRecorder invalid video mimeType throws");
+
+test(t => {
+ assert_false(MediaRecorder.isTypeSupported("video/pineapple"));
+}, "MediaRecorder invalid video mimeType is unsupported");
+
+// New MIME types could be added to this list as needed.
+for (const mimeType of [
+ 'audio/mp4',
+ 'video/mp4',
+ 'audio/ogg',
+ 'audio/ogg; codecs="vorbis"',
+ 'audio/ogg; codecs="opus"',
+ 'audio/webm',
+ 'audio/webm; codecs="vorbis"',
+ 'audio/webm; codecs="opus"',
+ 'video/webm',
+ 'video/webm; codecs="vp8"',
+ 'video/webm; codecs="vp8, vorbis"',
+ 'video/webm; codecs="vp8, opus"',
+ 'video/webm; codecs="vp9"',
+ 'video/webm; codecs="vp9, vorbis"',
+ 'video/webm; codecs="vp9, opus"',
+ 'video/webm; codecs="av1"',
+ 'video/webm; codecs="av1, opus"',
+]) {
+ if (MediaRecorder.isTypeSupported(mimeType)) {
+ test(t => {
+ const recorder = new MediaRecorder(new MediaStream(), {mimeType});
+ assert_equals(recorder.mimeType, mimeType, "Supported mimeType is set");
+ }, `Supported mimeType ${mimeType} is set immediately after constructing`);
+ } else {
+ test(t => {
+ assert_throws_dom("NotSupportedError",
+ () => new MediaRecorder(new MediaStream(), {mimeType}));
+ }, `Unsupported mimeType ${mimeType} throws`);
+ }
+}
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioStream(t).stream);
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ assert_not_equals(recorder.mimeType, "");
+}, "MediaRecorder sets a nonempty mimeType on 'onstart' for audio");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingVideoStream(t).stream);
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ assert_not_equals(recorder.mimeType, "");
+}, "MediaRecorder sets a nonempty mimeType on 'onstart' for video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioVideoStream(t).stream);
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ assert_not_equals(recorder.mimeType, "");
+}, "MediaRecorder sets a nonempty mimeType on 'onstart' for audio/video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioStream(t).stream);
+ recorder.start();
+ assert_equals(recorder.mimeType, "");
+}, "MediaRecorder mimeType is not set before 'onstart' for audio");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingVideoStream(t).stream);
+ recorder.start();
+ assert_equals(recorder.mimeType, "");
+}, "MediaRecorder mimeType is not set before 'onstart' for video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioVideoStream(t).stream);
+ recorder.start();
+ assert_equals(recorder.mimeType, "");
+}, "MediaRecorder mimeType is not set before 'onstart' for audio/video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioStream(t).stream);
+ const onstartPromise = new Promise(resolve => {
+ recorder.onstart = () => {
+ recorder.onstart = () => t.step_func(() => {
+ assert_not_reached("MediaRecorder doesn't fire 'onstart' twice");
+ });
+ resolve();
+ }
+ });
+ recorder.start();
+ await onstartPromise;
+ await new Promise(r => t.step_timeout(r, 1000));
+}, "MediaRecorder doesn't fire 'onstart' multiple times for audio");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingVideoStream(t).stream);
+ const onstartPromise = new Promise(resolve => {
+ recorder.onstart = () => {
+ recorder.onstart = () => t.step_func(() => {
+ assert_not_reached("MediaRecorder doesn't fire 'onstart' twice");
+ });
+ resolve();
+ }
+ });
+ recorder.start();
+ await onstartPromise;
+ await new Promise(r => t.step_timeout(r, 1000));
+}, "MediaRecorder doesn't fire 'onstart' multiple times for video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioVideoStream(t).stream);
+ const onstartPromise = new Promise(resolve => {
+ recorder.onstart = () => {
+ recorder.onstart = () => t.step_func(() => {
+ assert_not_reached("MediaRecorder doesn't fire 'onstart' twice");
+ });
+ resolve();
+ }
+ });
+ recorder.start();
+ await onstartPromise;
+ await new Promise(r => t.step_timeout(r, 1000));
+}, "MediaRecorder doesn't fire 'onstart' multiple times for audio/video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioStream(t).stream);
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ assert_regexp_match(recorder.mimeType, /^audio\//,
+ "mimeType has an expected media type");
+ assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
+ "mimeType has a container subtype");
+ assert_regexp_match(
+ recorder.mimeType, /^[a-z]+\/[a-z]+;[ ]*codecs=[^,]+$/,
+ "mimeType has one codec a");
+}, "MediaRecorder formats mimeType well after 'start' for audio");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingVideoStream(t).stream);
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ assert_regexp_match(recorder.mimeType, /^video\//,
+ "mimeType has an expected media type");
+ assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
+ "mimeType has a container subtype");
+ assert_regexp_match(
+ recorder.mimeType, /^[a-z]+\/[a-z]+;[ ]*codecs=[^,]+$/,
+ "mimeType has one codec a");
+}, "MediaRecorder formats mimeType well after 'start' for video");
+
+promise_test(async t => {
+ const recorder = new MediaRecorder(createFlowingAudioVideoStream(t).stream);
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ assert_regexp_match(recorder.mimeType, /^video\//,
+ "mimeType has an expected media type");
+ assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
+ "mimeType has a container subtype");
+ assert_regexp_match(
+ recorder.mimeType, /^[a-z]+\/[a-z]+;[ ]*codecs=[^,]+,[^,]+$/,
+ "mimeType has two codecs");
+}, "MediaRecorder formats mimeType well after 'start' for audio/video");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html
new file mode 100644
index 0000000000..a1495dcb0c
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-pause-resume.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Pause and Resume</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="utils/sources.js"></script>
+</head>
+<body>
+<script>
+ function recordEvents(target, events) {
+ let arr = [];
+ for (let ev of events) {
+ target.addEventListener(ev, _ => arr.push(ev));
+ }
+ return arr;
+ }
+
+ promise_test(async t => {
+ const {stream: video, control} = createVideoStream(t);
+ control.addVideoFrame();
+ const recorder = new MediaRecorder(video);
+ const events = recordEvents(recorder,
+ ["start", "stop", "dataavailable", "pause", "resume", "error"]);
+
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ await new Promise(r => recorder.onstart = r);
+
+ recorder.pause();
+ assert_equals(recorder.state, "paused", "MediaRecorder should be paused immediately following pause()");
+
+ // A second call to pause should be idempotent
+ recorder.pause();
+ assert_equals(recorder.state, "paused", "MediaRecorder should be paused immediately following pause()");
+
+ let event = await new Promise(r => recorder.onpause = r);
+ assert_equals(event.type, "pause", "the event type should be pause");
+ assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
+
+ recorder.resume();
+ assert_equals(recorder.state, "recording", "MediaRecorder state should be recording immediately following resume() call");
+
+ // A second call to resume should be idempotent
+ recorder.resume();
+ assert_equals(recorder.state, "recording", "MediaRecorder state should be recording immediately following resume() call");
+
+ event = await new Promise(r => recorder.onresume = r);
+ assert_equals(event.type, "resume", "the event type should be resume");
+ assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
+
+ recorder.stop();
+ await new Promise(r => recorder.onstop = r);
+
+ assert_array_equals(events, ["start", "pause", "resume", "dataavailable", "stop"],
+ "Should have gotten expected events");
+ }, "MediaRecorder handles pause() and resume() calls appropriately in state and events");
+
+ promise_test(async () => {
+ let video = createVideoStream();
+ let recorder = new MediaRecorder(video);
+ let events = recordEvents(recorder,
+ ["start", "stop", "dataavailable", "pause", "resume", "error"]);
+
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ await new Promise(r => recorder.onstart = r);
+
+ recorder.pause();
+ assert_equals(recorder.state, "paused", "MediaRecorder should be paused immediately following pause()");
+ let event = await new Promise(r => recorder.onpause = r);
+ assert_equals(event.type, "pause", "the event type should be pause");
+ assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
+
+ recorder.stop();
+ assert_equals(recorder.state, "inactive", "MediaRecorder should be inactive after being stopped");
+ await new Promise(r => recorder.onstop = r);
+
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ await new Promise(r => recorder.onstart = r);
+
+ assert_array_equals(events, ["start", "pause", "dataavailable", "stop", "start"],
+ "Should have gotten expected events");
+ }, "MediaRecorder handles stop() in paused state appropriately");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection-no-sink.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection-no-sink.https.html
new file mode 100644
index 0000000000..106ad06059
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection-no-sink.https.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<html>
+<meta name="timeout" content="long">
+
+<head>
+ <title>MediaRecorder peer connection</title>
+ <link rel="help"
+ href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="../mediacapture-streams/permission-helper.js"></script>
+ <script src="utils/peerconnection.js"></script>
+</head>
+
+<body>
+ <script>
+
+promise_setup(async () => {
+ const t = {add_cleanup: add_completion_callback};
+ const [, pc, stream] = await startConnection(t, true, true);
+ const [audio] = stream.getAudioTracks();
+ const [video] = stream.getVideoTracks();
+
+ for (const kinds of [{ audio }, { video }, { audio, video }]) {
+ const tag = `${JSON.stringify(kinds)}`;
+ const stream = new MediaStream([kinds.audio, kinds.video].filter(n => n));
+
+ promise_test(async t => {
+ const recorder = new MediaRecorder(stream);
+ recorder.start(200);
+ let combinedSize = 0;
+ // Wait for a small amount of data to appear. Kept small for mobile tests
+ while (combinedSize < 2000) {
+ const {data} = await new Promise(r => recorder.ondataavailable = r);
+ combinedSize += data.size;
+ }
+ recorder.stop();
+ }, `MediaRecorder records from PeerConnection without sinks, ${tag}`);
+ }
+});
+
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html
new file mode 100644
index 0000000000..86c9d4f4a2
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-peerconnection.https.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<html>
+<meta name="timeout" content="long">
+
+<head>
+ <title>MediaRecorder peer connection</title>
+ <link rel="help"
+ href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="../mediacapture-streams/permission-helper.js"></script>
+ <script src="utils/peerconnection.js"></script>
+</head>
+
+<body>
+ <video id="remote" autoplay width="240" />
+ <script>
+
+promise_setup(async () => {
+ const t = {add_cleanup: add_completion_callback};
+ const [, pc, stream] = await startConnection(t, true, true);
+ const [audio] = stream.getAudioTracks();
+ const [video] = stream.getVideoTracks();
+
+ // Needed for the tests to get exercised in Chrome (bug)
+ document.getElementById('remote').srcObject = stream;
+
+ for (const {kinds, mimeType} of [
+ { kinds: { video }, mimeType: "" },
+ { kinds: { audio }, mimeType: "" },
+ { kinds: { video, audio }, mimeType: "" },
+ { kinds: { audio }, mimeType: "audio/webm;codecs=opus" },
+ { kinds: { video }, mimeType: "video/webm;codecs=vp8" },
+ { kinds: { video, audio }, mimeType: "video/webm;codecs=vp8,opus" },
+ { kinds: { video }, mimeType: "video/webm;codecs=vp9" },
+ { kinds: { video, audio }, mimeType: "video/webm;codecs=vp9,opus" }
+ ]) {
+ const tag = `${JSON.stringify(kinds)} mimeType "${mimeType}"`;
+ const stream = new MediaStream([kinds.audio, kinds.video].filter(n => n));
+
+ // Spec doesn't mandate codecs, so if not supported, test failure instead.
+ if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
+ promise_test(async t => {
+ assert_throws_dom('NotSupportedError',
+ () => new MediaRecorder(stream, { mimeType }));
+ }, `MediaRecorder constructor throws on no support, ${tag}`);
+ continue;
+ }
+
+ promise_test(async t => {
+ const recorder = new MediaRecorder(stream, { mimeType });
+ recorder.start(200);
+ await new Promise(r => recorder.onstart = r);
+ let combinedSize = 0;
+ // Wait for a small amount of data to appear. Kept small for mobile tests
+ while (combinedSize < 2000) {
+ const {data} = await new Promise(r => recorder.ondataavailable = r);
+ combinedSize += data.size;
+ }
+ recorder.stop();
+ }, `PeerConnection MediaRecorder receives data after onstart, ${tag}`);
+
+ promise_test(async t => {
+ const clone = stream.clone();
+ const recorder = new MediaRecorder(clone, { mimeType });
+ recorder.start();
+ await new Promise(r => recorder.onstart = r);
+ await waitForReceivedFramesOrPackets(t, pc, kinds.audio, kinds.video, 10);
+ for (const track of clone.getTracks()) {
+ track.stop();
+ }
+ // As the tracks ended, expect data from the recorder.
+ await Promise.all([
+ new Promise(r => recorder.onstop = r),
+ new Promise(r => recorder.ondataavailable = r)
+ ]);
+ }, `PeerConnection MediaRecorder gets ondata on stopping tracks, ${tag}`);
+ }
+});
+
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-start.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-start.html
new file mode 100644
index 0000000000..ef2fe69719
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-start.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Start</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-start">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<canvas id="canvas" width="200" height="200">
+</canvas>
+<script>
+ function createVideoStream() {
+ canvas.getContext('2d');
+ return canvas.captureStream();
+ }
+
+ test(t => {
+ const mimeType = [ 'audio/aac', 'audio/ogg', 'audio/webm' ].find(MediaRecorder.isTypeSupported);
+ const mediaRecorder = new MediaRecorder(createVideoStream(), {mimeType});
+ assert_throws_dom("NotSupportedError", () => mediaRecorder.start());
+ }, "MediaRecorder cannot record the stream using the current configuration");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html b/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html
new file mode 100644
index 0000000000..73eb2999ad
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/MediaRecorder-stop.html
@@ -0,0 +1,151 @@
+<!doctype html>
+<html>
+<head>
+ <title>MediaRecorder Stop</title>
+ <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="utils/sources.js"></script>
+</head>
+<body>
+<script>
+ function recordEvents(target, events) {
+ let arr = [];
+ for (let ev of events) {
+ target.addEventListener(ev, _ => arr.push(ev));
+ }
+ return arr;
+ }
+
+ // This function is used to check that elements of |actual| is a sub
+ // sequence in the |expected| sequence.
+ function assertSequenceIn(actual, expected) {
+ let i = 0;
+ for (event of actual) {
+ const j = expected.slice(i).indexOf(event);
+ assert_greater_than_equal(
+ j, 0, "Sequence element " + event + " is not included in " +
+ expected.slice(i));
+ i = j;
+ }
+ return true;
+ }
+
+ promise_test(async t => {
+ const {stream: video} = createVideoStream(t);
+ const recorder = new MediaRecorder(video);
+ const events = recordEvents(recorder,
+ ["start", "stop", "dataavailable", "pause", "resume", "error"]);
+ assert_equals(video.getVideoTracks().length, 1, "video mediastream starts with one track");
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ video.getVideoTracks()[0].stop();
+ assert_equals(recorder.state, "recording", "MediaRecorder state should be recording immediately following last track ending");
+ const event = await new Promise(r => recorder.onstop = r);
+
+ assert_equals(event.type, "stop", "the event type should be stop");
+ assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
+ assert_equals(recorder.state, "inactive", "MediaRecorder is inactive after stop event");
+
+ // As the test is written, it's not guaranteed that
+ // onstart/ondataavailable is invoked, but it's fine if they are.
+ // The stop element is guaranteed to be in events when we get here.
+ assertSequenceIn(events, ["start", "dataavailable", "stop"]);
+ }, "MediaRecorder will stop recording and fire a stop event when all tracks are ended");
+
+ promise_test(async t => {
+ const {stream: video} = createVideoStream(t);
+ const recorder = new MediaRecorder(video);
+ const events = recordEvents(recorder,
+ ["start", "stop", "dataavailable", "pause", "resume", "error"]);
+ recorder.start();
+ assert_equals(recorder.state, "recording", "MediaRecorder has been started successfully");
+ recorder.stop();
+ assert_equals(recorder.state, "inactive", "MediaRecorder state should be inactive immediately following stop() call");
+
+ const event = await new Promise (r => recorder.onstop = r);
+ assert_equals(event.type, "stop", "the event type should be stop");
+ assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
+ assert_equals(recorder.state, "inactive", "MediaRecorder is inactive after stop event");
+
+ // As the test is written, it's not guaranteed that
+ // onstart/ondataavailable is invoked, but it's fine if they are.
+ // The stop element is guaranteed to be in events when we get here.
+ assertSequenceIn(events, ["start", "dataavailable", "stop"]);
+ }, "MediaRecorder will stop recording and fire a stop event when stop() is called");
+
+ promise_test(async t => {
+ const recorder = new MediaRecorder(createVideoStream(t).stream);
+ recorder.stop();
+ await Promise.race([
+ new Promise((_, reject) => recorder.onstop =
+ _ => reject(new Error("onstop should never have been called"))),
+ new Promise(r => t.step_timeout(r, 0))]);
+ }, "MediaRecorder will not fire an exception when stopped after creation");
+
+ promise_test(async t => {
+ const recorder = new MediaRecorder(createVideoStream(t).stream);
+ recorder.start();
+ recorder.stop();
+ const event = await new Promise(r => recorder.onstop = r);
+ recorder.stop();
+ await Promise.race([
+ new Promise((_, reject) => recorder.onstop =
+ _ => reject(new Error("onstop should never have been called"))),
+ new Promise(r => t.step_timeout(r, 0))]);
+ }, "MediaRecorder will not fire an exception when stopped after having just been stopped");
+
+ promise_test(async t => {
+ const {stream} = createVideoStream(t);
+ const recorder = new MediaRecorder(stream);
+ recorder.start();
+ stream.getVideoTracks()[0].stop();
+ const event = await new Promise(r => recorder.onstop = r);
+ recorder.stop();
+ await Promise.race([
+ new Promise((_, reject) => recorder.onstop =
+ _ => reject(new Error("onstop should never have been called"))),
+ new Promise(r => t.step_timeout(r, 0))]);
+ }, "MediaRecorder will not fire an exception when stopped after having just been spontaneously stopped");
+
+ promise_test(async t => {
+ const {stream} = createAudioVideoStream(t);
+ const recorder = new MediaRecorder(stream);
+ const events = [];
+ const startPromise = new Promise(resolve => recorder.onstart = resolve);
+ const stopPromise = new Promise(resolve => recorder.onstop = resolve);
+
+ startPromise.then(() => events.push("start"));
+ stopPromise.then(() => events.push("stop"));
+
+ recorder.start();
+ recorder.stop();
+
+ await stopPromise;
+ assert_array_equals(events, ["start", "stop"]);
+ }, "MediaRecorder will fire start event even if stopped synchronously");
+
+ promise_test(async t => {
+ const {stream} = createAudioVideoStream(t);
+ const recorder = new MediaRecorder(stream);
+ const events = [];
+ const startPromise = new Promise(resolve => recorder.onstart = resolve);
+ const stopPromise = new Promise(resolve => recorder.onstop = resolve);
+ const errorPromise = new Promise(resolve => recorder.onerror = resolve);
+ const dataPromise = new Promise(resolve => recorder.ondataavailable = resolve);
+
+ startPromise.then(() => events.push("start"));
+ stopPromise.then(() => events.push("stop"));
+ errorPromise.then(() => events.push("error"));
+ dataPromise.then(() => events.push("data"));
+
+ recorder.start();
+ stream.removeTrack(stream.getAudioTracks()[0]);
+
+ await stopPromise;
+ assert_array_equals(events, ["start", "error", "data", "stop"]);
+ }, "MediaRecorder will fire start event even if a track is removed synchronously");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/idlharness.window.js b/testing/web-platform/tests/mediacapture-record/idlharness.window.js
new file mode 100644
index 0000000000..99e884530c
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/idlharness.window.js
@@ -0,0 +1,40 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/mediacapture-record/
+
+idl_test(
+ ['mediastream-recording'],
+ ['mediacapture-streams', 'FileAPI', 'html', 'dom', 'webidl'],
+ idl_array => {
+ // Ignored errors will be surfaced in idlharness.js's test_object below.
+ let recorder, blob, error;
+ try {
+ const canvas = document.createElement('canvas');
+ document.body.appendChild(canvas);
+ const context = canvas.getContext("2d");
+ context.fillStyle = "red";
+ context.fillRect(0, 0, 10, 10);
+ const stream = canvas.captureStream();
+ recorder = new MediaRecorder(stream);
+ } catch(e) {}
+ idl_array.add_objects({ MediaRecorder: [recorder] });
+
+ try {
+ blob = new BlobEvent("type", {
+ data: new Blob(),
+ timecode: performance.now(),
+ });
+ } catch(e) {}
+ idl_array.add_objects({ BlobEvent: [blob] });
+
+ try {
+ error = new MediaRecorderErrorEvent("type", {
+ error: new DOMException,
+ });
+ } catch(e) {}
+ idl_array.add_objects({ MediaRecorderErrorEvent: [error] });
+ }
+);
diff --git a/testing/web-platform/tests/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html b/testing/web-platform/tests/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html
new file mode 100644
index 0000000000..ceeae2eade
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<html>
+
+<head>
+ <title>MediaRecorder peer connection</title>
+ <link rel="help"
+ href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="../../mediacapture-streams/permission-helper.js"></script>
+ <script src="../utils/peerconnection.js"></script>
+</head>
+
+<body>
+ <video id="remote" autoplay width="240" />
+ <script>
+
+[{kind: "video", audio: false, codecPreference: "VP8", codecRegex: /.*vp8.*/},
+ {kind: "audio/video", audio: true, codecPreference: "VP8", codecRegex: /.*vp8.*/},
+ {kind: "video", audio: false, codecPreference: "VP9", codecRegex: /.*vp9.*/},
+ {kind: "audio/video", audio: true, codecPreference: "VP9", codecRegex: /.*vp9.*/}]
+ .forEach(args => {
+ promise_test(async t => {
+ const [localPc, remotePc, stream] = await startConnection(
+ t, args.audio, /*video=*/true, args.codecPreference);
+
+ // Needed for the tests to get exercised in Chrome (bug)
+ document.getElementById('remote').srcObject = stream;
+
+ const recorder = new MediaRecorder(stream); // Passthrough.
+ const onstartPromise = new Promise(resolve => {
+ recorder.onstart = t.step_func(() => {
+ assert_regexp_match(
+ recorder.mimeType, args.codecRegex,
+ "mimeType is matching " + args.codecPreference +
+ " in case of passthrough.");
+ resolve();
+ });
+ });
+ recorder.start();
+ await(onstartPromise);
+ }, "PeerConnection passthrough MediaRecorder receives " +
+ args.codecPreference + " after onstart with a " + args.kind +
+ " stream.");
+ });
+
+promise_test(async t => {
+ const [localPc, remotePc, stream, transceivers] = await startConnection(
+ t, /*audio=*/false, /*video=*/true, /*videoCodecPreference=*/"VP8");
+
+ // Needed for the tests to get exercised in Chrome (bug)
+ document.getElementById('remote').srcObject = stream;
+
+ const recorder = new MediaRecorder(stream); // Possibly passthrough.
+ recorder.start();
+ await waitForReceivedFramesOrPackets(t, remotePc, false, true, 10);
+
+ // Switch codec to VP9; we expect onerror to not be invoked.
+ recorder.onerror = t.step_func(() => assert_unreached(
+ "MediaRecorder should be prepared to handle codec switches"));
+ setTransceiverCodecPreference(transceivers.video, "VP9");
+ await Promise.all([
+ exchangeOfferAnswer(localPc, remotePc),
+ waitForReceivedCodec(t, remotePc, "VP9")
+ ]);
+}, "PeerConnection passthrough MediaRecorder should be prepared to handle " +
+ "the codec switching from VP8 to VP9");
+
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/support/MediaRecorder-iframe.html b/testing/web-platform/tests/mediacapture-record/support/MediaRecorder-iframe.html
new file mode 100644
index 0000000000..df60c4e8e1
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/support/MediaRecorder-iframe.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Start a MediaRecorder</title>
+<html>
+<body>
+<script src="../utils/sources.js"></script>
+<script>
+ var context;
+ var recorder;
+ var video;
+ var control;
+
+ function prepareForTest(test) {
+ const obj = createVideoStream(test);
+ video = obj.stream;
+ control = obj.control;
+ recorder = new MediaRecorder(video);
+ }
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-record/utils/peerconnection.js b/testing/web-platform/tests/mediacapture-record/utils/peerconnection.js
new file mode 100644
index 0000000000..26a925abf0
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/utils/peerconnection.js
@@ -0,0 +1,141 @@
+/**
+ * @fileoverview Utility functions for tests utilizing PeerConnections
+ */
+
+/**
+ * Exchanges offers and answers between two peer connections.
+ *
+ * pc1's offer is set as local description in pc1 and
+ * remote description in pc2. After that, pc2's answer
+ * is set as it's local description and remote description in pc1.
+ *
+ * @param {!RTCPeerConnection} pc1 The first peer connection.
+ * @param {!RTCPeerConnection} pc2 The second peer connection.
+ */
+async function exchangeOfferAnswer(pc1, pc2) {
+ await pc1.setLocalDescription(await pc1.createOffer());
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription(await pc2.createAnswer());
+ await pc1.setRemoteDescription(pc2.localDescription);
+}
+
+/**
+ * Sets the specified codec preference if it's included in the transceiver's
+ * list of supported codecs.
+ * @param {!RTCRtpTransceiver} transceiver The RTP transceiver.
+ * @param {string} codecPreference The codec preference.
+ */
+function setTransceiverCodecPreference(transceiver, codecPreference) {
+ for (const codec of RTCRtpSender.getCapabilities('video').codecs) {
+ if (codec.mimeType.includes(codecPreference)) {
+ transceiver.setCodecPreferences([codec]);
+ return;
+ }
+ }
+}
+
+/**
+ * Starts a connection between two peer connections, using a audio and/or video
+ * stream.
+ * @param {*} t Test instance.
+ * @param {boolean} audio True if audio should be used.
+ * @param {boolean} video True if video should be used.
+ * @param {string} [videoCodecPreference] String containing the codec preference.
+ * @returns an array with the two connected peer connections, the remote stream,
+ * and an object containing transceivers by kind.
+ */
+async function startConnection(t, audio, video, videoCodecPreference) {
+ const scope = [];
+ if (audio) scope.push("microphone");
+ if (video) scope.push("camera");
+ await setMediaPermission("granted", scope);
+ const stream = await navigator.mediaDevices.getUserMedia({audio, video});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ const pc1 = new RTCPeerConnection();
+ t.add_cleanup(() => pc1.close());
+ const pc2 = new RTCPeerConnection();
+ t.add_cleanup(() => pc2.close());
+ const transceivers = {};
+ for (const track of stream.getTracks()) {
+ const transceiver = pc1.addTransceiver(track, {streams: [stream]});
+ transceivers[track.kind] = transceiver;
+ if (videoCodecPreference && track.kind == 'video') {
+ setTransceiverCodecPreference(transceiver, videoCodecPreference);
+ }
+ }
+ for (const [local, remote] of [[pc1, pc2], [pc2, pc1]]) {
+ local.addEventListener('icecandidate', ({candidate}) => {
+ if (!candidate || remote.signalingState == 'closed') return;
+ remote.addIceCandidate(candidate);
+ });
+ }
+ const haveTrackEvent = new Promise(r => pc2.ontrack = r);
+ await exchangeOfferAnswer(pc1, pc2);
+ const {streams} = await haveTrackEvent;
+ return [pc1, pc2, streams[0], transceivers];
+}
+
+/**
+ * Given a peer connection, return after at least numFramesOrPackets
+ * frames (video) or packets (audio) have been received.
+ * @param {*} t Test instance.
+ * @param {!RTCPeerConnection} pc The peer connection.
+ * @param {boolean} lookForAudio True if audio packets should be waited for.
+ * @param {boolean} lookForVideo True if video packets should be waited for.
+ * @param {int} numFramesOrPackets Number of frames (video) and packets (audio)
+ * to wait for.
+ */
+async function waitForReceivedFramesOrPackets(
+ t, pc, lookForAudio, lookForVideo, numFramesOrPackets) {
+ let initialAudioPackets = 0;
+ let initialVideoFrames = 0;
+ while (lookForAudio || lookForVideo) {
+ const report = await pc.getStats();
+ for (const stats of report.values()) {
+ if (stats.type == 'inbound-rtp') {
+ if (lookForAudio && stats.kind == 'audio') {
+ if (!initialAudioPackets) {
+ initialAudioPackets = stats.packetsReceived;
+ } else if (stats.packetsReceived > initialAudioPackets +
+ numFramesOrPackets) {
+ lookForAudio = false;
+ }
+ }
+ if (lookForVideo && stats.kind == 'video') {
+ if (!initialVideoFrames) {
+ initialVideoFrames = stats.framesDecoded;
+ } else if (stats.framesDecoded > initialVideoFrames +
+ numFramesOrPackets) {
+ lookForVideo = false;
+ }
+ }
+ }
+ }
+ await new Promise(r => t.step_timeout(r, 100));
+ }
+}
+
+/**
+ * Given a peer connection, return after one of its inbound RTP connections
+ * includes use of the specified codec.
+ * @param {*} t Test instance.
+ * @param {!RTCPeerConnection} pc The peer connection.
+ * @param {string} codecToLookFor The waited-for codec.
+ */
+async function waitForReceivedCodec(t, pc, codecToLookFor) {
+ let currentCodecId;
+ for (;;) {
+ const report = await pc.getStats();
+ for (const stats of report.values()) {
+ if (stats.type == 'inbound-rtp' && stats.kind == 'video') {
+ if (stats.codecId) {
+ if (report.get(stats.codecId).mimeType.toLowerCase()
+ .includes(codecToLookFor.toLowerCase())) {
+ return;
+ }
+ }
+ }
+ }
+ await new Promise(r => t.step_timeout(r, 100));
+ }
+}
diff --git a/testing/web-platform/tests/mediacapture-record/utils/sources.js b/testing/web-platform/tests/mediacapture-record/utils/sources.js
new file mode 100644
index 0000000000..44947272d6
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-record/utils/sources.js
@@ -0,0 +1,75 @@
+function createAudioStream(t) {
+ const ac = new AudioContext();
+ const { stream } = ac.createMediaStreamDestination();
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => {
+ ac.close();
+ track.stop();
+ });
+ return { stream };
+}
+
+function createFlowingAudioStream(t) {
+ const ac = new AudioContext();
+ const dest = ac.createMediaStreamDestination();
+ const osc = ac.createOscillator();
+ osc.connect(dest);
+ osc.start();
+ const [track] = dest.stream.getTracks();
+ t.add_cleanup(() => {
+ ac.close();
+ track.stop();
+ });
+ return { stream: dest.stream };
+}
+
+function createVideoStream(t) {
+ const canvas = document.createElement("canvas");
+ canvas.id = "canvas";
+ document.body.appendChild(canvas);
+ const ctx = canvas.getContext("2d");
+ const stream = canvas.captureStream();
+ const [track] = stream.getTracks();
+ t.add_cleanup(() => {
+ document.body.removeChild(canvas);
+ track.stop();
+ });
+ const addVideoFrame = () => {
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ };
+ return { stream, control: { addVideoFrame } };
+}
+
+function createFlowingVideoStream(t) {
+ const { stream } = createVideoStream(t);
+ const [track] = stream.getTracks();
+ const canvas = document.getElementById("canvas");
+ const ctx = canvas.getContext("2d");
+ ctx.fillStyle = "green";
+ requestAnimationFrame(function draw() {
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ if (track.readyState == "live") {
+ requestAnimationFrame(draw);
+ }
+ });
+ return { stream };
+}
+
+function createAudioVideoStream(t) {
+ const { stream: audio } = createAudioStream(t);
+ const { stream: video, control } = createVideoStream(t);
+ return {
+ stream: new MediaStream([...audio.getTracks(), ...video.getTracks()]),
+ control,
+ };
+}
+
+function createFlowingAudioVideoStream(t) {
+ return {
+ stream: new MediaStream([
+ ...createFlowingAudioStream(t).stream.getTracks(),
+ ...createFlowingVideoStream(t).stream.getTracks(),
+ ]),
+ };
+}