diff options
Diffstat (limited to 'testing/web-platform/tests/mediacapture-record')
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(), + ]), + }; +} |