summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/mediacapture-streams
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/mediacapture-streams
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/mediacapture-streams')
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-api.https.html22
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-deny.https.html35
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-empty-option-param.https.html34
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-impossible-constraint.https.html37
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-invalid-facing-mode.https.html31
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-non-applicable-constraint.https.html79
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-optional-constraint.https.html32
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-required-constraint-with-ideal-value.https.html33
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-trivial-constraint.https.html32
-rw-r--r--testing/web-platform/tests/mediacapture-streams/GUM-unknownkey-option-param.https.html31
-rw-r--r--testing/web-platform/tests/mediacapture-streams/META.yml5
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-SecureContext.html19
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-after-discard.https.html64
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html30
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html.headers1
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html30
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html.headers1
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-per-origin-ids.sub.https.html87
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-persistent-permission.https.html38
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-returned-objects.https.html60
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices.https.html113
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-getSupportedConstraints.https.html49
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaDevices-getUserMedia.https.html134
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-firstframe.https.html106
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html84
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-srcObject.https.html476
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-add-audio-track.https.html42
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-audio-only.https.html32
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-clone.https.html98
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-default-feature-policy.https.html84
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-finished-add.https.html35
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-gettrackid.https.html29
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-id.https.html30
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-idl.https.html77
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-removetrack.https.html140
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-supported-by-feature-policy.html15
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStream-video-only.https.html32
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-audio-is-silence.https.html59
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-video-is-black.https.html126
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html124
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-end-manual.https.html54
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html154
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getSettings.https.html235
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-id.https.html27
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html31
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html31
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-init.https.html39
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer-video.https.html26
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer.https.html50
-rw-r--r--testing/web-platform/tests/mediacapture-streams/MediaStreamTrackEvent-constructor.https.html42
-rw-r--r--testing/web-platform/tests/mediacapture-streams/crashtests/enumerateDevices-after-discard-1.https.html18
-rw-r--r--testing/web-platform/tests/mediacapture-streams/enumerateDevices-with-navigation.https.html77
-rw-r--r--testing/web-platform/tests/mediacapture-streams/historical.https.html33
-rw-r--r--testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js50
-rw-r--r--testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html2
-rw-r--r--testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html.headers1
-rw-r--r--testing/web-platform/tests/mediacapture-streams/iframe-enumerate-nogum.html2
-rw-r--r--testing/web-platform/tests/mediacapture-streams/iframe-enumerate.html2
-rw-r--r--testing/web-platform/tests/mediacapture-streams/message-enumerateddevices-nogum.js6
-rw-r--r--testing/web-platform/tests/mediacapture-streams/message-enumerateddevices.js8
-rw-r--r--testing/web-platform/tests/mediacapture-streams/overconstrained_error.https.html29
-rw-r--r--testing/web-platform/tests/mediacapture-streams/parallel-capture-requests.https.html57
-rw-r--r--testing/web-platform/tests/mediacapture-streams/permission-helper.js24
-rw-r--r--testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer-video.html27
-rw-r--r--testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer.html21
65 files changed, 3632 insertions, 0 deletions
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-api.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-api.https.html
new file mode 100644
index 0000000000..1483170176
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-api.https.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia: test that getUserMedia is present</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermedia">
+<meta name='assert' content='Check that the getUserMedia() method is present.'/>
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks for the presence of the
+<code>navigator.mediaDevices.getUserMedia</code> method.</p>
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(function () {
+ assert_true(undefined !== navigator.mediaDevices && undefined !== navigator.mediaDevices.getUserMedia, "navigator.mediaDevices.getUserMedia exists");
+}, "mediaDevices.getUserMedia() is present on navigator");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-deny.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-deny.https.html
new file mode 100644
index 0000000000..2042b038b6
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-deny.https.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+<head>
+ <title>getUserMedia() triggers error callback when auth is denied</title>
+ <link rel="author" title="Dr. A. Gouaillard" href="mailto:agouaillard@gmail.com"/>
+ <link rel="help" href="https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia">
+</head>
+<body>
+ <p class="instructions">When prompted, <strong>please deny</strong> access to
+ the video stream.</p>
+ <h1 class="instructions">Description</h1>
+ <p class="instructions">This test checks that the error callback is triggered
+ when user denies access to the video stream.</p>
+ <div id='log'></div>
+<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=permission-helper.js></script>
+ <script>
+promise_test(async () => {
+ try {
+ await setMediaPermission('denied', ['camera']);
+ await navigator.mediaDevices.getUserMedia({video: true})
+ } catch (error) {
+ assert_throws_dom("NotAllowedError", () => { throw error });
+ assert_false('constraintName' in error,
+ "constraintName attribute not set as expected");
+ return;
+ };
+ assert_unreached("The success callback should not be triggered since access is to be denied");
+}, "Tests that the error callback is triggered when permission is denied");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-empty-option-param.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-empty-option-param.https.html
new file mode 100644
index 0000000000..5c6b3785a3
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-empty-option-param.https.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia({}) rejects with TypeError</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia">
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that getUserMedia with no value in the
+options parameter raises a TypeError exception.</p>
+
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+promise_test(async () => {
+ try {
+ // Race a settled promise to check that the returned promise is already
+ // rejected.
+ await Promise.race([navigator.mediaDevices.getUserMedia({}),
+ Promise.resolve()]);
+ } catch (error) {
+ assert_throws_js(TypeError, () => { throw error });
+ assert_false('constraintName' in error,
+ "constraintName attribute not set as expected");
+ return;
+ }
+ assert_unreached("should have returned an already-rejected promise.");
+}, "Tests that getUserMedia is rejected with a TypeError when used with an empty options parameter");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-impossible-constraint.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-impossible-constraint.https.html
new file mode 100644
index 0000000000..c65b2860a2
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-impossible-constraint.https.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html>
+<head>
+<title>Trivial mandatory constraint in getUserMedia</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-NavigatorUserMedia-getUserMedia-void-MediaStreamConstraints-constraints-NavigatorUserMediaSuccessCallback-successCallback-NavigatorUserMediaErrorCallback-errorCallback">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that setting an impossible mandatory
+constraint (width &gt;=1G) in getUserMedia works</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ // Note - integer conversion is weird for +inf and numbers > 2^32, so we
+ // use a number less than 2^32 for testing.
+ try {
+ await navigator.mediaDevices.getUserMedia({video: {width: {min:100000000}}});
+ assert_unreached("a Video stream of width 100M cannot be created");
+
+ } catch (error) {
+ assert_equals(error.name, "OverconstrainedError", "An impossible constraint triggers a OverconstrainedError");
+ assert_equals(error.constraint, "width", "The name of the not satisfied error is given in error.constraint");
+ }
+}, "Tests that setting an impossible constraint in getUserMedia fails");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-invalid-facing-mode.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-invalid-facing-mode.https.html
new file mode 100644
index 0000000000..8da6c9754e
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-invalid-facing-mode.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html>
+<head>
+<title>Invalid facingMode in getUserMedia</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#def-constraint-facingMode">
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that trying to set an empty facingMode
+ value in getUserMedia results in an OverconstrainedError.
+</p>
+
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ try {
+ await navigator.mediaDevices.getUserMedia({video: {facingMode: {exact: ''}}});
+ assert_unreached("The empty string is not a valid facingMode");
+ } catch (error) {
+ assert_equals(error.name, "OverconstrainedError");
+ assert_equals(error.constraint, "facingMode");
+ };
+}, "Tests that setting an invalid facingMode constraint in getUserMedia fails");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-non-applicable-constraint.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-non-applicable-constraint.https.html
new file mode 100644
index 0000000000..e9fd6665dc
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-non-applicable-constraint.https.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<title>non-applicable constraint in getUserMedia</title>
+<link rel="author" title="Intel" href="http://www.intel.com"/>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#methods-5">
+<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=permission-helper.js></script>
+
+<p class="instructions">When prompted, accept to share your audio and video stream.</p>
+
+<script>
+
+let video_only_valid_constraints = {
+ width: {min: 0},
+ height: {min: 0},
+ frameRate: {min: 0},
+ aspectRatio: {min: 0},
+ facingMode: {ideal: 'environment'},
+ resizeMode: {ideal: 'none'}
+}
+
+let video_only_invalid_constraints = {
+ width: {min: 100000000},
+ height: {min: 100000000},
+ frameRate: {min: 100000000},
+ aspectRatio: {min: 100000000},
+ facingMode: {exact: 'invalid'},
+ resizeMode: {exact: 'invalid'}
+}
+
+let audio_only_valid_constraints = {
+ volume: {min: 0},
+ sampleRate: {min: 0},
+ sampleSize: {min: 0},
+ echoCancellation: {ideal: true},
+ autoGainControl: {ideal: true},
+ noiseSuppression: {ideal: true},
+ voiceIsolation: {ideal: true},
+ latency: {min: 0},
+ channelCount: {min: 0}
+}
+
+let audio_only_invalid_constraints = {
+ volume: {min: 2},
+ sampleRate: {min: 100000000},
+ sampleSize: {min: 100000000},
+ echoCancellation: {exact: true},
+ autoGainControl: {exact: true},
+ noiseSuppression: {exact: true},
+ voiceIsolation: {exact: true},
+ latency: {max: 0},
+ channelCount: {max: 0}
+}
+
+promise_test(async () => {
+ // Both permissions are needed at some point, asking for both at once
+ await setMediaPermission();
+ let stream = await navigator.mediaDevices.getUserMedia({audio: video_only_valid_constraints})
+ assert_equals(stream.getAudioTracks().length, 1, "the media stream has exactly one audio track");
+}, 'Test that setting video-only valid constraints inside of "audio" is simply ignored');
+
+promise_test(async () => {
+ let stream = await navigator.mediaDevices.getUserMedia({audio: video_only_invalid_constraints})
+ assert_equals(stream.getAudioTracks().length, 1, "the media stream has exactly one audio track");
+}, 'Test that setting video-only invalid constraints inside of "audio" is simply ignored');
+
+promise_test(async () => {
+ let stream = await navigator.mediaDevices.getUserMedia({video: audio_only_valid_constraints})
+ assert_equals(stream.getVideoTracks().length, 1, "the media stream has exactly one video track");
+}, 'Test that setting audio-only valid constraints inside of "video" is simply ignored');
+
+promise_test(async () => {
+ let stream = await navigator.mediaDevices.getUserMedia({video: audio_only_invalid_constraints})
+ assert_equals(stream.getVideoTracks().length, 1, "the media stream has exactly one video track");
+}, 'Test that setting audio-only invalid constraints inside of "video" is simply ignored');
+
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-optional-constraint.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-optional-constraint.https.html
new file mode 100644
index 0000000000..e8892a5916
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-optional-constraint.https.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+<head>
+<title>Optional constraint recognized as optional in getUserMedia</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-NavigatorUserMedia-getUserMedia-void-MediaStreamConstraints-constraints-NavigatorUserMediaSuccessCallback-successCallback-NavigatorUserMediaErrorCallback-errorCallback">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that setting an optional constraint in
+getUserMedia is handled as optional</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({video: {advanced: [{width: {min:1024, max: 800}}]}});
+ assert_equals(stream.getVideoTracks().length, 1, "the media stream has exactly one video track");
+ } catch (error) {
+ assert_unreached("an optional constraint can't stop us from obtaining a video stream");
+ }
+}, "Tests that setting an optional constraint in getUserMedia is handled as optional");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-required-constraint-with-ideal-value.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-required-constraint-with-ideal-value.https.html
new file mode 100644
index 0000000000..7f234c5c74
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-required-constraint-with-ideal-value.https.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+<head>
+<title>Ideal value in required constraint in getUserMedia</title>
+<link rel="author" title="Intel" href="http://www.intel.com"/>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#dfn-fitness-distance">
+<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=permission-helper.js></script>
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that setting a required constraint
+with an ideal value in getUserMedia works</p>
+<div id='log'></div>
+<script>
+promise_test(async t => {
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: {width: {ideal: 320, min: 160}}});
+ assert_equals(stream.getVideoTracks().length, 1, "the media stream has exactly one video track");
+ assert_equals(stream.getVideoTracks()[0].getSettings().width, 320, 'ideal width is selected for getUserMedia() video tracks');
+ const video = document.createElement('video');
+ video.srcObject = stream;
+ await video.play();
+ assert_equals(video.videoWidth, 320, 'video width equals to track width');
+ stream.getVideoTracks()[0].stop();
+}, "Tests that setting a required constraint with an ideal value in getUserMedia works");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-trivial-constraint.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-trivial-constraint.https.html
new file mode 100644
index 0000000000..66dd3a23c8
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-trivial-constraint.https.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+<head>
+<title>Trivial mandatory constraint in getUserMedia</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-NavigatorUserMedia-getUserMedia-void-MediaStreamConstraints-constraints-NavigatorUserMediaSuccessCallback-successCallback-NavigatorUserMediaErrorCallback-errorCallback">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that setting a trivial mandatory
+constraint (width &gt;=0) in getUserMedia works</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission();
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({video: {width: {min:0}}})
+ assert_equals(stream.getVideoTracks().length, 1, "the media stream has exactly one video track");
+ } catch (error) {
+ assert_unreached("a Video stream of minimally zero width can always be created");
+ }
+}, "Tests that setting a trivial mandatory constraint in getUserMedia works");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/GUM-unknownkey-option-param.https.html b/testing/web-platform/tests/mediacapture-streams/GUM-unknownkey-option-param.https.html
new file mode 100644
index 0000000000..edd727d5e3
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/GUM-unknownkey-option-param.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia({doesnotexist:true}) rejects with TypeError</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-NavigatorUserMedia-getUserMedia-void-MediaStreamConstraints-constraints-NavigatorUserMediaSuccessCallback-successCallback-NavigatorUserMediaErrorCallback-errorCallback">
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that getUserMedia with an unknown value
+in the constraints parameter rejects with a TypeError.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ try {
+ await navigator.mediaDevices.getUserMedia({doesnotexist:true})
+ assert_unreached("This should never be triggered since the constraints parameter only contains an unrecognized constraint");
+ } catch (error) {
+ assert_equals(error.name, "TypeError", "TypeError returned as expected");
+ assert_equals(error.constraintName, undefined, "constraintName attribute not set as expected");
+ }
+}, "Tests that getUserMedia is rejected with a TypeError when used with an unknown constraint");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/META.yml b/testing/web-platform/tests/mediacapture-streams/META.yml
new file mode 100644
index 0000000000..97363cf368
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/META.yml
@@ -0,0 +1,5 @@
+spec: https://w3c.github.io/mediacapture-main/
+suggested_reviewers:
+ - alvestrand
+ - youennf
+ - jan-ivar
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-SecureContext.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-SecureContext.html
new file mode 100644
index 0000000000..bada628176
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-SecureContext.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+<title>MediaDevices and SecureContext</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+</head>
+<body>
+<script>
+test(function() {
+ assert_false(window.isSecureContext, "This test must be run in a non secure context");
+ assert_false('MediaDevices' in window, "MediaDevices is not exposed");
+ assert_false('MediaDeviceInfo' in window, "MediaDeviceInfo is not exposed");
+ assert_false('getUserMedia' in navigator, "getUserMedia is not exposed");
+ assert_false('mediaDevices' in navigator, "mediaDevices is not exposed");
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-after-discard.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-after-discard.https.html
new file mode 100644
index 0000000000..ef4f492964
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-after-discard.https.html
@@ -0,0 +1,64 @@
+<!doctype html>
+<title>Test promises from MediaDevices methods in a discarded browsing
+ context</title>
+<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=permission-helper.js></script>
+<body></body>
+<script>
+let devices;
+let child_DOMException;
+setup(() => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ devices = frame.contentWindow.navigator.mediaDevices;
+ child_DOMException = frame.contentWindow.DOMException;
+ frame.remove();
+});
+
+// https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia
+// If the current settings object's responsible document is NOT fully active,
+// return a promise rejected with a DOMException object whose name attribute
+// has the value "InvalidStateError".
+promise_test(async () => {
+ await setMediaPermission("granted", ["microphone"]);
+ // `catch()` is used rather than static Promise methods because microtasks
+ // for `PromiseResolve()` do not run when Promises in inactive Documents are
+ // involved. Whether microtasks for `catch()` run depends on the realm of
+ // the handler rather than the realm of the Promise.
+ // See https://github.com/whatwg/html/issues/5319.
+ let promise_already_rejected = false;
+ let rejected_reason;
+ devices.getUserMedia({audio:true}).catch(reason => {
+ promise_already_rejected = true;
+ rejected_reason = reason;
+ });
+ // Race a settled promise to check that the returned promise is already
+ // rejected.
+ await Promise.reject().catch(() => {
+ assert_true(promise_already_rejected,
+ 'should have returned an already-rejected promise.');
+ assert_throws_dom('InvalidStateError', child_DOMException,
+ () => { throw rejected_reason });
+ });
+}, 'getUserMedia() in a discarded browsing context');
+
+// https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices
+// https://w3c.github.io/mediacapture-main/#device-enumeration-can-proceed
+// Device enumeration can proceed steps return false when device enumeration
+// can be exposed is true and the document is not fully active.
+promise_test(async () => {
+ let promise_state = 'pending';
+ // `then()` is used to avoid methods that apply `PromiseResolve()` to
+ // Promises from inactive realms, which would lead to microtasks that don't
+ // run.
+ devices.enumerateDevices().then(() => promise_state = 'resolved',
+ () => promise_state = 'rejected');
+ // Enumerate in the parent document to provide enough time to check that the
+ // Promise from the inactive document does not settle.
+ await navigator.mediaDevices.enumerateDevices();
+ assert_equals(promise_state, 'pending', 'Promise state');
+}, 'enumerateDevices() in a discarded browsing context');
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html
new file mode 100644
index 0000000000..9e8b7a6f36
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+<head>
+<title>enumerateDevices: test enumerateDevices should not expose camera devices if they are not allowed by Permissions-Policy header</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices">
+<meta name='assert' content='Check that the enumerateDevices() method should not expose camera devices when blocked by Permissions-Policy.'/>
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks for the absence of camera in
+<code>navigator.mediaDevices.enumerateDevices()</code> method when blocked by the Permissions-Policy header.</p>
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+promise_test(async () => {
+ assert_not_equals(navigator.mediaDevices.enumerateDevices, undefined, "navigator.mediaDevices.enumerateDevices exists");
+ const deviceList = await navigator.mediaDevices.enumerateDevices();
+ for (const mediaInfo of deviceList) {
+ assert_not_equals(mediaInfo.deviceId, undefined, "mediaInfo's deviceId should exist.");
+ assert_not_equals(mediaInfo.kind, undefined, "mediaInfo's kind should exist.");
+ assert_not_equals(mediaInfo.label, undefined, "mediaInfo's label should exist.");
+ assert_not_equals(mediaInfo.groupId, undefined, "mediaInfo's groupId should exist.");
+ assert_in_array(mediaInfo.kind, ["audioinput", "audiooutput"]);
+ }
+}, "Camera is not exposed in mediaDevices.enumerateDevices() when blocked by Permissions-Policy header");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html.headers b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html.headers
new file mode 100644
index 0000000000..6fcbae1419
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-camera.https.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: camera=()
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html
new file mode 100644
index 0000000000..d45fd740e8
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+<head>
+<title>enumerateDevices: test enumerateDevices should not expose microphone devices if they are not allowed by Permissions-Policy header</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices">
+<meta name='assert' content='Check that the enumerateDevices() method should not expose microphone devices when blocked by Permissions-Policy header.'/>
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks for the absence of microphone in
+<code>navigator.mediaDevices.enumerateDevices()</code> method when blocked by Permissions-Policy header.</p>
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+promise_test(async () => {
+ assert_not_equals(navigator.mediaDevices.enumerateDevices, undefined, "navigator.mediaDevices.enumerateDevices exists");
+ const deviceList = await navigator.mediaDevices.enumerateDevices();
+ for (const mediaInfo of deviceList) {
+ assert_not_equals(mediaInfo.deviceId, undefined, "mediaInfo's deviceId should exist.");
+ assert_not_equals(mediaInfo.kind, undefined, "mediaInfo's kind should exist.");
+ assert_not_equals(mediaInfo.label, undefined, "mediaInfo's label should exist.");
+ assert_not_equals(mediaInfo.groupId, undefined, "mediaInfo's groupId should exist.");
+ assert_in_array(mediaInfo.kind, ["videoinput", "audiooutput"]);
+ }
+}, "Microphone is not exposed in mediaDevices.enumerateDevices() when blocked by Permissions-Policy");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html.headers b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html.headers
new file mode 100644
index 0000000000..ae65ea5c73
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-not-allowed-mic.https.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: microphone=()
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-per-origin-ids.sub.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-per-origin-ids.sub.https.html
new file mode 100644
index 0000000000..714355f5c1
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-per-origin-ids.sub.https.html
@@ -0,0 +1,87 @@
+<!doctype html>
+<html>
+<head>
+<title>enumerateDevices rotates deviceId across origins and after cookies get cleared</title>
+<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=permission-helper.js></script>
+</head>
+<body>
+ <iframe allow="camera 'src';microphone 'src'" id=same src="/mediacapture-streams/iframe-enumerate.html"></iframe>
+<iframe allow="camera 'src';microphone 'src'" id=cross src="https://{{hosts[][www1]}}:{{ports[https][0]}}/mediacapture-streams/iframe-enumerate.html"></iframe>
+<script>
+
+ let deviceList;
+
+ promise_test(async t => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({audio : true, video: true});
+ stream.getTracks().forEach(t => t.stop());
+ deviceList = await navigator.mediaDevices.enumerateDevices();
+ const msgWatcher = new EventWatcher(t, window, ['message']);
+ frames[0].postMessage('run', '*')
+ const e = await msgWatcher.wait_for('message');
+ const iframeDevices = e.data.devices;
+ assert_equals(deviceList.length, iframeDevices.length, "Same number of devices detected same-origin");
+ for (const device of deviceList) {
+ // Look for the same device in the iframe based on deviceId
+ // "default" can be used across several kinds, so it needs an additional check
+ // but we limit that check to "default" to detect re-use of deviceId across kinds
+ const sameDevice = iframeDevices.find(d => d.deviceId === device.deviceId && (device.deviceId !== "default" || d.kind === device.kind));
+ assert_true(!!sameDevice, "deviceIds stay the same when loaded in same origin");
+ assert_equals(sameDevice.label, device.label, "labels matches when deviceId matches");
+ assert_equals(sameDevice.kind, device.kind, "kind matches when deviceId matches");
+ // The group identifier MUST be uniquely generated for each document.
+ assert_not_equals(sameDevice.groupId, device.groupId, "groupId is specific to a document");
+ }
+ // setting a cookie as a way to detect if cookie clearing gets done
+ document.cookie = "test=true";
+ window.localStorage.touched = true;
+ }, "enumerateDevices has stable deviceIds across same-origin iframe");
+
+ promise_test(async t => {
+ const msgWatcher = new EventWatcher(t, window, ['message']);
+ frames[1].postMessage('run', '*')
+ const e = await msgWatcher.wait_for('message');
+ const iframeDevices = e.data.devices;
+ assert_equals(deviceList.length, iframeDevices.length, "Same number of devices detected cross-origin");
+ for (const device of deviceList) {
+ // An identifier can be reused across origins as long as
+ // it is not tied to the user and can be guessed by other means
+ // In practice, "default" is what is used today, so we hardcode it
+ // to be able to detect the general case of non-shared deviceIds
+ if (device.deviceId !== "default") {
+ const sameDevice = iframeDevices.find(d => d.deviceId === device.deviceId);
+ assert_false(!!sameDevice, "deviceIds are not shared across origin");
+ }
+ assert_false(!!iframeDevices.find(d => d.groupId === device.groupId), "groupId is specific to a document");
+ }
+ }, "enumerateDevices rotates deviceId across different-origin iframe");
+
+ promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("allow", "camera 'src';microphone 'src'");
+ iframe.src = "/mediacapture-streams/iframe-enumerate-cleared.html";
+ document.body.appendChild(iframe);
+ const loadWatcher = new EventWatcher(t, iframe, ['load']);
+ await loadWatcher.wait_for('load');
+ assert_implements_optional(document.cookie === "", "Clear-Site-Data not enabled, can't test clearing deviceId");
+
+ const msgWatcher = new EventWatcher(t, window, ['message']);
+ frames[2].postMessage('run', '*')
+ const e = await msgWatcher.wait_for('message');
+ const iframeDevices = e.data.devices;
+ assert_equals(deviceList.length, iframeDevices.length, "Same number of devices detected after clearing cookies");
+ for (const device of deviceList) {
+ const sameDevice = iframeDevices.find(d => d.deviceId === device.deviceId);
+ assert_false(!!sameDevice, "deviceIds are not kept after clearing site data");
+ assert_false(!!iframeDevices.find(d => d.groupId === device.groupId), "groupId is specific to a document");
+ }
+
+ }, "enumerateDevices rotates deviceId after clearing site data");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-persistent-permission.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-persistent-permission.https.html
new file mode 100644
index 0000000000..ec12762522
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-persistent-permission.https.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<html>
+<head>
+<title>enumerateDevices depends only on capture state, not permission state</title>
+<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=permission-helper.js></script>
+</head>
+<body>
+
+<script>
+ promise_test(async t => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({audio : true, video: true});
+ stream.getTracks().forEach(t => t.stop());
+ // the page loaded below hasn't had capture enabled
+ // so enumerateDevices should not list detailed info yet
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("allow", "microphone;camera");
+ iframe.src = "/mediacapture-streams/iframe-enumerate-nogum.html";
+ document.body.appendChild(iframe);
+ const loadWatcher = new EventWatcher(t, iframe, ['load']);
+ await loadWatcher.wait_for('load');
+ const msgWatcher = new EventWatcher(t, window, ['message']);
+ frames[0].postMessage('run', '*')
+ const e = await msgWatcher.wait_for('message');
+ const iframeDevices = e.data.devices;
+ const kinds = iframeDevices.map(({kind}) => kind);
+ assert_equals(kinds.length, new Set(kinds).size, "At most one of a kind prior to capture");
+ for (const device of iframeDevices) {
+ assert_equals(device.deviceId, "", "deviceId pre-capture is empty");
+ assert_equals(device.label, "", "label pre-capture is empty");
+ assert_equals(device.groupId, "", "groupId pre-capture is empty");
+ }
+ });
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-returned-objects.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-returned-objects.https.html
new file mode 100644
index 0000000000..6e6dbaf3bb
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices-returned-objects.https.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<html>
+<head>
+<title>enumerateDevices is returning new MediaDeviceInfo objects every time</title>
+<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=permission-helper.js></script>
+</head>
+<body>
+<script>
+function doTest(callGetUserMedia, testName)
+{
+ promise_test(async () => {
+ if (callGetUserMedia) {
+ await setMediaPermission();
+ await navigator.mediaDevices.getUserMedia({audio : true, video: true});
+ }
+
+ const deviceList1 = await navigator.mediaDevices.enumerateDevices();
+ const deviceList2 = await navigator.mediaDevices.enumerateDevices();
+
+ assert_equals(deviceList1.length, deviceList2.length);
+ for (let i = 0; i < deviceList1.length; i++) {
+ const device1 = deviceList1[i];
+ const device2 = deviceList2[i];
+ assert_not_equals(device1, device2);
+ assert_equals(device1.deviceId, device2.deviceId, "deviceId");
+ assert_equals(device1.kind, device2.kind, "kind");
+ if (!callGetUserMedia) {
+ /* For camera and microphone devices,
+ if the browsing context did not capture (i.e. getUserMedia() was not called or never resolved successfully),
+ the MediaDeviceInfo object will contain a valid value for kind
+ but empty strings for deviceId, label, and groupId. */
+ assert_equals(device1.deviceId, "", "deviceId is empty before capture");
+ assert_equals(device1.groupId, "", "groupId is empty before capture");
+ assert_equals(device1.label, "", "label is empty before capture");
+ assert_in_array(device1.kind, ["audioinput", "audiooutput", "videoinput"],
+ "kind is set to a valid value before capture");
+ }
+ }
+ /* Additionally, at most one device of each kind
+ will be listed in enumerateDevices() result. */
+ // FIXME: ensure browsers are tested as if they had multiple devices of at least one kind -
+ // this probably needs https://w3c.github.io/mediacapture-automation/ support
+ if (!callGetUserMedia) {
+ const deviceKinds = deviceList1.map(d => d.kind);
+ for (let kind of deviceKinds) {
+ assert_equals(deviceKinds.filter(x => x===kind).length, 1, "At most 1 " + kind + " prior to capture");
+ }
+ }
+ }, testName);
+}
+
+doTest(false, "enumerateDevices exposes mostly empty objects ahead of successful getUserMedia call");
+doTest(true, "enumerateDevices exposes expected objects after successful getUserMedia call");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices.https.html
new file mode 100644
index 0000000000..88c07048a1
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices.https.html
@@ -0,0 +1,113 @@
+<!doctype html>
+<html>
+<head>
+<title>enumerateDevices: test that enumerateDevices is present</title>
+<link rel="author" title="Dr Alex Gouaillard" href="mailto:agouaillard@gmail.com"/>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#enumerating-devices">
+<meta name='assert' content='Check that the enumerateDevices() method is present.'/>
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks for the presence of the
+<code>navigator.mediaDevices.enumerateDevices()</code> method.</p>
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+"use strict";
+
+//NOTE ALEX: for completion, a test for ondevicechange event is missing.
+
+promise_test(async () => {
+ assert_not_equals(navigator.mediaDevices.enumerateDevices, undefined, "navigator.mediaDevices.enumerateDevices exists");
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ for (const {kind, deviceId, label, groupId} of devices) {
+ assert_in_array(kind, ["videoinput", "audioinput", "audiooutput"]);
+ assert_equals(deviceId, "", "deviceId should be empty string if getUserMedia was never called successfully.");
+ assert_equals(label, "", "label should be empty string if getUserMedia was never called successfully.");
+ assert_equals(groupId, "", "groupId should be empty string if getUserMedia was never called successfully.");
+ }
+ assert_less_than_equal(devices.filter(({kind}) => kind == "audioinput").length,
+ 1, "there should be zero or one audio input device.");
+ assert_less_than_equal(devices.filter(({kind}) => kind == "videoinput").length,
+ 1, "there should be zero or one video input device.");
+ assert_equals(devices.filter(({kind}) => kind == "audiooutput").length,
+ 0, "there should be no audio output devices.");
+ assert_less_than_equal(devices.length, 2,
+ "there should be no more than two devices.");
+ if (devices.length > 1) {
+ assert_equals(devices[0].kind, "audioinput", "audioinput is first");
+ assert_equals(devices[1].kind, "videoinput", "videoinput is second");
+ }
+}, "mediaDevices.enumerateDevices() is present and working - before capture");
+
+promise_test(async t => {
+ await setMediaPermission("granted");
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+ stream.getTracks()[0].stop();
+
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const kinds = ["audioinput", "videoinput"];
+ for (const {kind, deviceId} of devices) {
+ assert_in_array(kind, kinds, "camera doesn't expose audiooutput");
+ assert_equals(typeof deviceId, "string", "deviceId is a string.");
+ switch (kind) {
+ case "videoinput":
+ assert_greater_than(deviceId.length, 0, "video deviceId should not be empty.");
+ break;
+ case "audioinput":
+ assert_equals(deviceId.length, 0, "audio deviceId should be empty.");
+ break;
+ }
+ }
+}, "mediaDevices.enumerateDevices() is working - after video capture");
+
+// This test is designed to come after its video counterpart directly above
+promise_test(async t => {
+ const devices1 = await navigator.mediaDevices.enumerateDevices();
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ stream.getTracks()[0].stop();
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ assert_equals(devices.filter(({kind}) => kind == "videoinput").length,
+ devices1.filter(({kind}) => kind == "videoinput").length,
+ "same number of (previously exposed) videoinput devices");
+ assert_greater_than_equal(devices.filter(d => d.kind == "audioinput").length,
+ devices1.filter(d => d.kind == "audioinput").length,
+ "same number or more audioinput devices");
+ const order = ["audioinput", "videoinput", "audiooutput"];
+ for (const {kind, deviceId} of devices) {
+ assert_in_array(kind, order, "expected kind");
+ assert_equals(typeof deviceId, "string", "deviceId is a string.");
+ switch (kind) {
+ case "videoinput":
+ assert_greater_than(deviceId.length, 0, "video deviceId should not be empty.");
+ break;
+ case "audioinput":
+ assert_greater_than(deviceId.length, 0, "audio deviceId should not be empty.");
+ break;
+ }
+ }
+ const kinds = devices.map(({kind}) => kind);
+ const correct = [...kinds].sort((a, b) => order.indexOf(a) - order.indexOf(b));
+ assert_equals(JSON.stringify(kinds), JSON.stringify(correct), "correct order");
+}, "mediaDevices.enumerateDevices() is working - after video then audio capture");
+
+promise_test(async () => {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ for (const mediaInfo of devices) {
+ if (mediaInfo.kind == "audioinput" || mediaInfo.kind == "videoinput") {
+ assert_true("InputDeviceInfo" in window, "InputDeviceInfo exists");
+ assert_true(mediaInfo instanceof InputDeviceInfo);
+ } else if (mediaInfo.kind == "audiooutput") {
+ assert_true(mediaInfo instanceof MediaDeviceInfo);
+ } else {
+ assert_unreached("mediaInfo.kind should be one of 'audioinput', 'videoinput', or 'audiooutput'.")
+ }
+ }
+}, "InputDeviceInfo is supported");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-getSupportedConstraints.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-getSupportedConstraints.https.html
new file mode 100644
index 0000000000..7d374b5336
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-getSupportedConstraints.https.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<html>
+<head>
+<title>Test navigator.mediaDevices.getSupportedConstraints()</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#enumerating-devices">
+<meta name='assert' content='Test the getSupportedConstraints() method.'/>
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks for the presence of the
+<code>navigator.mediaDevices.getSupportedConstraints()</code> method.</p>
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+test(() => {
+ assert_inherits(navigator.mediaDevices, "getSupportedConstraints");
+ assert_equals(typeof navigator.mediaDevices.getSupportedConstraints, "function");
+}, "navigator.mediaDevices.getSupportedConstraints exists");
+
+{
+ const properties = [
+ "width",
+ "height",
+ "aspectRatio",
+ "frameRate",
+ "facingMode",
+ "resizeMode",
+ "sampleRate",
+ "sampleSize",
+ "echoCancellation",
+ "autoGainControl",
+ "noiseSuppression",
+ "voiceIsolation",
+ "latency",
+ "channelCount",
+ "deviceId",
+ "groupId"];
+ properties.forEach(property => {
+ test(()=>{
+ const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
+ assert_true(supportedConstraints[property]);
+ }, property + " is supported");
+ });
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaDevices-getUserMedia.https.html b/testing/web-platform/tests/mediacapture-streams/MediaDevices-getUserMedia.https.html
new file mode 100644
index 0000000000..9376f52897
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-getUserMedia.https.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia: test that mediaDevices.getUserMedia is present</title>
+<link rel="author" title="Dr Alex Gouaillard" href="mailto:agouaillard@gmail.com"/>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#mediadevices-interface-extensions">
+<meta name='assert' content='Check that the mediaDevices.getUserMedia() method is present.'/>
+</head>
+<body>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks for the presence of the
+<code>navigator.mediaDevices.getUserMedia</code> method.</p>
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+test(function () {
+ assert_not_equals(navigator.mediaDevices.getUserMedia, undefined, "navigator.mediaDevices.getUserMedia exists.");
+ // TODO: do some stuff with it
+ assert_not_equals(navigator.mediaDevices.getSupportedConstraints, undefined, "navigator.mediaDevices.getSupportedConstraints exists.");
+ var list = navigator.mediaDevices.getSupportedConstraints();
+ // TODO: we are supposed to check that all values returned can be used in a constraint ....
+ // NOTE: the current list of attributes that may or may not be here
+ // ... FF for example has many no tin that list, should we fail if an attribute is present but not listed in the specs?
+ // list.width
+ // list.height
+ // list.aspectRatio
+ // list.frameRate
+ // list.facingMode
+ // list.volume
+ // list.sampleRate
+ // list.sampleSize
+ // list.echoCancellation
+ // list.latency
+ // list.channelCount
+ // list.deviceId
+ // list.groupId
+ }, "mediaDevices.getUserMedia() is present on navigator");
+
+promise_test(async t => {
+ // Both permissions are needed at some point, asking both at once
+ await setMediaPermission();
+ // A successful camera gUM call is needed to expose camera information
+ const afterGum = await navigator.mediaDevices.getUserMedia({video: true});
+ afterGum.getTracks()[0].stop();
+
+ assert_true(navigator.mediaDevices.getSupportedConstraints()["groupId"],
+ "groupId should be supported");
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ for (const device of devices) {
+ await navigator.mediaDevices.getUserMedia(
+ {video: {groupId: {exact: device.groupId}}}).then(stream => {
+ const found_device = devices.find(({deviceId}) =>
+ deviceId == stream.getTracks()[0].getSettings().deviceId);
+ assert_not_equals(found_device, undefined);
+ assert_equals(found_device.kind, "videoinput");
+ assert_equals(found_device.groupId, device.groupId);
+ stream.getTracks().forEach(t => t.stop());
+ }, error => {
+ assert_equals(error.name, "OverconstrainedError");
+ assert_equals(error.constraint, "groupId");
+ const found_device = devices.find(element =>
+ element.kind == "videoinput" && element.groupId == device.groupId);
+ assert_equals(found_device, undefined);
+ });
+ }
+}, 'groupId is correctly supported by getUserMedia() for video devices');
+
+promise_test(async t => {
+ // A successful microphone gUM call is needed to expose microphone information
+ const afterGum = await navigator.mediaDevices.getUserMedia({audio: true});
+ afterGum.getTracks()[0].stop();
+
+ assert_true(navigator.mediaDevices.getSupportedConstraints()["groupId"],
+ "groupId should be supported");
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ for (const device of devices) {
+ await navigator.mediaDevices.getUserMedia(
+ {audio: {groupId: {exact: device.groupId}}}).then(stream => {
+ const found_device = devices.find(({deviceId}) =>
+ deviceId == stream.getTracks()[0].getSettings().deviceId);
+ assert_not_equals(found_device, undefined);
+ assert_equals(found_device.kind, "audioinput");
+ assert_equals(found_device.groupId, device.groupId);
+ stream.getTracks().forEach(t => t.stop());
+ }, error => {
+ assert_equals(error.name, "OverconstrainedError");
+ assert_equals(error.constraint, "groupId");
+ const found_device = devices.find(element =>
+ element.kind == "audioinput" && element.groupId == device.groupId);
+ assert_equals(found_device, undefined);
+ });
+ }
+}, 'groupId is correctly supported by getUserMedia() for audio devices');
+
+promise_test(async t => {
+ assert_true(navigator.mediaDevices.getSupportedConstraints()["resizeMode"],
+ "resizeMode should be supported");
+ const stream = await navigator.mediaDevices.getUserMedia(
+ { video: {resizeMode: {exact: 'none'}}});
+ const [track] = stream.getVideoTracks();
+ t.add_cleanup(() => track.stop());
+ assert_equals(track.getSettings().resizeMode, 'none');
+}, 'getUserMedia() supports setting none as resizeMode.');
+
+promise_test(async t => {
+ assert_true(navigator.mediaDevices.getSupportedConstraints()["resizeMode"],
+ "resizeMode should be supported");
+ const stream = await navigator.mediaDevices.getUserMedia(
+ { video: {resizeMode: {exact: 'crop-and-scale'}}});
+ const [track] = stream.getVideoTracks();
+ t.add_cleanup(() => track.stop());
+ assert_equals(track.getSettings().resizeMode, 'crop-and-scale');
+}, 'getUserMedia() supports setting crop-and-scale as resizeMode.');
+
+promise_test(async t => {
+ assert_true(navigator.mediaDevices.getSupportedConstraints()["resizeMode"],
+ "resizeMode should be supported");
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(
+ { video: {resizeMode: {exact: 'INVALID'}}});
+ t.add_cleanup(() => stream.getVideoTracks()[0].stop());
+ t.unreached_func('getUserMedia() should fail with invalid resizeMode')();
+ } catch (e) {
+ assert_equals(e.name, 'OverconstrainedError');
+ assert_equals(e.constraint, 'resizeMode');
+ }
+}, 'getUserMedia() fails with exact invalid resizeMode.');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-firstframe.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-firstframe.https.html
new file mode 100644
index 0000000000..36641b58c9
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-firstframe.https.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<html>
+<head>
+<title>Assigning a MediaStream to a media element and not playing it results in rendering a first frame</title>
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that a HTMLMediaElement with an
+assigned MediaStream with a video track fires the appropriate events to reach
+the "canplay" event and readyState HAVE_ENOUGH_DATA even when not playing or
+autoplaying.</p>
+<video id="vid"></video>
+<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=permission-helper.js></script>
+<script>
+'use strict';
+const vid = document.getElementById("vid");
+
+promise_test(async t => {
+ const wait = ms => new Promise(r => t.step_timeout(r, ms));
+ const timeout = (promise, time, msg) => Promise.race([
+ promise,
+ wait(time).then(() => Promise.reject(new Error(msg)))
+ ]);
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+ vid.srcObject = stream;
+
+ await timeout(new Promise(r => vid.oncanplay = r), 8000, "canplay timeout");
+ assert_equals(vid.readyState, vid.HAVE_ENOUGH_DATA,
+ "readyState is HAVE_ENOUGH_DATA after \"canplay\"");
+}, "Tests that loading a MediaStream in a media element eventually results in \"canplay\" even when not playing or autoplaying");
+
+promise_test(async t => {
+ const wait = ms => new Promise(r => t.step_timeout(r, ms));
+ const timeout = (promise, time, msg) => Promise.race([
+ promise,
+ wait(time).then(() => Promise.reject(new Error(msg)))
+ ]);
+ const unexpected = e => assert_unreached(`Got unexpected event ${e.type}`);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.ondurationchange = null;
+ stream.getTracks().forEach(track => track.stop())
+ });
+ vid.srcObject = stream;
+
+ vid.onloadstart = unexpected;
+ vid.ondurationchange = unexpected;
+ vid.onresize = unexpected;
+ vid.onloadedmetadata = unexpected;
+ vid.onloadeddata = unexpected;
+ vid.oncanplay = unexpected;
+ vid.oncanplaythrough = unexpected;
+
+ await timeout(new Promise(r => vid.onloadstart = r), 8000,
+ "loadstart timeout");
+ vid.onloadstart = unexpected;
+
+ await timeout(new Promise(r => vid.ondurationchange = r), 8000,
+ "durationchange timeout");
+ vid.ondurationchange = unexpected;
+ assert_equals(vid.duration, Infinity, "duration changes to Infinity");
+
+ await timeout(new Promise(r => vid.onresize = r), 8000,
+ "resize timeout");
+ vid.onresize = unexpected;
+ assert_not_equals(vid.videoWidth, 0,
+ "videoWidth is something after \"resize\"");
+ assert_not_equals(vid.videoHeight, 0,
+ "videoHeight is something after \"resize\"");
+
+ await timeout(new Promise(r => vid.onloadedmetadata = r), 8000,
+ "loadedmetadata timeout");
+ vid.onloadedmetadata = unexpected;
+ assert_greater_than_equal(vid.readyState, vid.HAVE_METADATA,
+ "readyState is at least HAVE_METADATA after \"loadedmetadata\"");
+
+ await timeout(new Promise(r => vid.onloadeddata = r), 8000,
+ "loadeddata timeout");
+ vid.onloadeddata = unexpected;
+ assert_equals(vid.readyState, vid.HAVE_ENOUGH_DATA,
+ "readyState is HAVE_ENOUGH_DATA after \"loadeddata\" since there's no buffering");
+
+ await timeout(new Promise(r => vid.oncanplay = r), 8000, "canplay timeout");
+ vid.oncanplay = unexpected;
+ assert_equals(vid.readyState, vid.HAVE_ENOUGH_DATA,
+ "readyState is HAVE_ENOUGH_DATA after \"canplay\" since there's no buffering");
+
+ await timeout(new Promise(r => vid.oncanplaythrough = r), 8000,
+ "canplaythrough timeout");
+ vid.oncanplaythrough = unexpected;
+ assert_equals(vid.readyState, vid.HAVE_ENOUGH_DATA,
+ "readyState is HAVE_ENOUGH_DATA after \"canplaythrough\"");
+
+ // Crank the event loop to see whether any more events are fired.
+ await wait(100);
+}, "Tests that loading a MediaStream in a media element sees all the expected (deterministic) events even when not playing or autoplaying");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html
new file mode 100644
index 0000000000..52a13af272
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-preload-none.https.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test that the HTMLMediaElement preload 'none' attribute value is ignored for MediaStream used as srcObject and MediaStream object URLs used as src.</title>
+ <link rel="author" title="Matthew Wolenetz" href="mailto:wolenetz@chromium.org"/>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="permission-helper.js"></script>
+ </head>
+ <body>
+ <p class="instructions">When prompted, accept to share your audio and video streams.</p>
+ <p class="instructions">This test checks that the HTMLMediaElement preload 'none' attribute value is ignored for MediaStream used as srcObject and MediaStream object URLs used as src.</p>
+ <div id=log></div>
+
+ <audio preload="none"></audio>
+ <video preload="none"></video>
+
+ <script>
+ async function testPreloadNone(mediaElement, stream)
+ {
+ let rejectSuspendedPromise, rejectErrorPromise, resolveDataLoadedPromise;
+ const suspended = new Promise((r, rej) => {
+ rejectSuspendedPromise = rej;
+ });
+ const errored = new Promise((r, rej) => {
+ rejectErrorPromise = rej;
+ });
+ const loaded = new Promise(resolve => {
+ resolveDataLoadedPromise = resolve;
+ });
+
+ // The optional deferred load steps (for preload none) for MediaStream resources should be skipped.
+ mediaElement.addEventListener("suspend", () => {
+ rejectSuspendedPromise("'suspend' should not be fired.")
+ });
+ mediaElement.addEventListener("error", () => {
+ rejectErrorPromise("'error' should not be fired, code=" + mediaElement.error.code);
+ });
+
+ mediaElement.addEventListener("loadeddata", () => {
+ assert_equals(mediaElement.networkState, mediaElement.NETWORK_LOADING);
+ resolveDataLoadedPromise();
+ });
+
+ mediaElement.srcObject = stream;
+ assert_equals(mediaElement.networkState, mediaElement.NETWORK_NO_SOURCE); // Resource selection is active.
+ try {
+ await Promise.race([suspended, errored, loaded]);
+ } catch (msg) {
+ assert_unreached(msg);
+ }
+2 }
+
+ promise_test(async () =>
+ {
+ const aud = document.querySelector("audio");
+ // camera is needed for the next test, asking for both at once
+ await setMediaPermission();
+ let stream;
+ try {
+ stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ } catch (e) {
+ assert_unreached("getUserMedia error callback was invoked.");
+ }
+ await testPreloadNone(aud, stream);
+ }, "Test that preload 'none' is ignored for MediaStream object URL used as srcObject for audio");
+
+ promise_test(async () =>
+ {
+ const vid = document.querySelector("video");
+ let stream;
+ try {
+ stream = await navigator.mediaDevices.getUserMedia({video:true});
+ } catch (e) {
+ assert_unreached("getUserMedia error callback was invoked.")
+ }
+ await testPreloadNone(vid, stream);
+
+ }, "Test that preload 'none' is ignored for MediaStream used as srcObject for video");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-srcObject.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-srcObject.https.html
new file mode 100644
index 0000000000..be64123058
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-MediaElement-srcObject.https.html
@@ -0,0 +1,476 @@
+<!doctype html>
+<html>
+<head>
+<title>Assigning mediastream to a video element</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermedia">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the MediaStream object returned by
+the success callback in getUserMedia can be properly assigned to a video element
+via the <code>srcObject</code> attribute.</p>
+
+<audio id="aud"></audio>
+<video id="vid"></video>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+'use strict';
+const vid = document.getElementById("vid");
+
+function queueTask(f) {
+ window.onmessage = f;
+ window.postMessage("hi");
+}
+
+promise_test(async t => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+}, "Tests that a MediaStream can be assigned to a video element with srcObject");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ assert_true(!vid.seeking, "A MediaStream is not seekable");
+ assert_equals(vid.seekable.length, 0, "A MediaStream is not seekable");
+}, "Tests that a MediaStream assigned to a video element is not seekable");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ assert_equals(vid.readyState, vid.HAVE_NOTHING,
+ "readyState is HAVE_NOTHING initially");
+ await new Promise(r => vid.onloadeddata = r);
+ assert_equals(vid.readyState, vid.HAVE_ENOUGH_DATA,
+ "Upon having loaded a media stream, the UA sets readyState to HAVE_ENOUGH_DATA");
+}, "Tests that a MediaStream assigned to a video element is in readyState HAVE_NOTHING initially");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ assert_equals(vid.duration, NaN,
+ "A MediaStream does not have any duration initially.");
+ await new Promise(r => vid.ondurationchange = r);
+ assert_equals(vid.duration, Infinity,
+ "A loaded MediaStream does not have a pre-defined duration.");
+
+ vid.play();
+ await new Promise(r => vid.ontimeupdate = r);
+ for (const t of stream.getTracks()) {
+ t.stop();
+ }
+
+ await new Promise(r => vid.ondurationchange = r);
+ assert_equals(vid.duration, vid.currentTime,
+ "After ending playback, duration gets set to currentTime");
+}, "Tests that a MediaStream assigned to a video element has expected duration");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+
+ vid.preload = "metadata";
+ vid.srcObject = stream;
+
+ assert_equals(vid.buffered.length, 0,
+ "A MediaStream cannot be preloaded. Therefore, there are no buffered timeranges");
+ assert_equals(vid.preload, "none", "preload must always be none");
+ vid.preload = "auto";
+ assert_equals(vid.preload, "none", "Setting preload must be ignored");
+
+ await new Promise(r => vid.onloadeddata = r);
+ assert_equals(vid.buffered.length, 0,
+ "A MediaStream cannot be preloaded. Therefore, there are no buffered timeranges");
+
+ vid.srcObject = null;
+
+ assert_equals(vid.preload, "metadata",
+ "The preload attribute returns the value it had before using a MediaStream");
+}, "Tests that a video element with a MediaStream assigned is not preloaded");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+
+ vid.defaultPlaybackRate = 0.3;
+ vid.playbackRate = 0.3;
+ vid.onratechange = t.unreached_func("ratechange event must not be fired");
+ vid.srcObject = stream;
+
+ assert_equals(vid.defaultPlaybackRate, 1, "playback rate is always 1");
+ vid.defaultPlaybackRate = 0.5;
+ assert_equals(vid.defaultPlaybackRate, 1,
+ "Setting defaultPlaybackRate must be ignored");
+
+ assert_equals(vid.playbackRate, 1, "playback rate is always 1");
+ vid.playbackRate = 0.5;
+ assert_equals(vid.playbackRate, 1, "Setting playbackRate must be ignored");
+
+ vid.srcObject = null;
+ assert_equals(vid.defaultPlaybackRate, 0.3,
+ "The defaultPlaybackRate attribute returns the value it had before using a MediaStream");
+ assert_equals(vid.playbackRate, 0.3,
+ "The playbackRate attribute is set to the value of the defaultPlaybackRate attribute when unsetting srcObject");
+
+ // Check that there's no ratechange event
+ await new Promise(r => t.step_timeout(r, 100));
+}, "Tests that a video element with a MediaStream assigned ignores playbackRate attributes (defaultPlaybackRate is identical)");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+
+ vid.defaultPlaybackRate = 0.3;
+ vid.playbackRate = 0.4;
+ vid.onratechange = t.unreached_func("ratechange event must not be fired");
+ vid.srcObject = stream;
+
+ assert_equals(vid.defaultPlaybackRate, 1, "playback rate is always 1");
+ vid.defaultPlaybackRate = 0.5;
+ assert_equals(vid.defaultPlaybackRate, 1,
+ "Setting defaultPlaybackRate must be ignored");
+
+ assert_equals(vid.playbackRate, 1, "playback rate is always 1");
+ vid.playbackRate = 0.5;
+ assert_equals(vid.playbackRate, 1, "Setting playbackRate must be ignored");
+
+ vid.srcObject = null;
+ assert_equals(vid.defaultPlaybackRate, 0.3,
+ "The defaultPlaybackRate attribute returns the value it had before using a MediaStream");
+ assert_equals(vid.playbackRate, 0.3,
+ "The playbackRate attribute is set to the value of the defaultPlaybackRate attribute when unsetting srcObject (and fires ratechange)");
+ await new Promise(r => vid.onratechange = r);
+}, "Tests that a video element with a MediaStream assigned ignores playbackRate attributes (defaultPlaybackRate is different)");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+ await new Promise(r => vid.oncanplay = r);
+ vid.play();
+ await new Promise(r => vid.ontimeupdate = r);
+ assert_greater_than(vid.currentTime, 0,
+ "currentTime is greater than 0 after first timeupdate");
+
+ assert_equals(vid.played.length, 1,
+ "A MediaStream's timeline always consists of a single range");
+ assert_equals(vid.played.start(0), 0,
+ "A MediaStream's timeline always starts at zero");
+ assert_equals(vid.played.end(0), vid.currentTime,
+ "A MediaStream's end MUST return the last known currentTime");
+
+ const time = vid.currentTime;
+ vid.currentTime = 0;
+ assert_equals(vid.currentTime, time,
+ "The UA MUST ignore attempts to set the currentTime attribute");
+}, "Tests that a media element with an assigned MediaStream reports the played attribute as expected");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ assert_equals(vid.currentTime, 0, "The initial value is 0");
+ vid.currentTime = 42;
+ assert_equals(vid.currentTime, 0,
+ "The UA MUST ignore attempts to set the currentTime attribute (default playback start position)");
+
+ await new Promise(r => vid.onloadeddata = r);
+ assert_equals(vid.currentTime, 0, "The initial value is 0");
+ vid.currentTime = 42;
+ assert_equals(vid.currentTime, 0,
+ "The UA MUST ignore attempts to set the currentTime attribute (official playback position)");
+
+ vid.play();
+ await new Promise(r => vid.ontimeupdate = r);
+ assert_greater_than(vid.currentTime, 0,
+ "currentTime is greater than 0 after first timeupdate");
+
+ const lastTime = vid.currentTime;
+ vid.currentTime = 0;
+ assert_equals(vid.currentTime, lastTime,
+ "The UA MUST ignore attempts to set the currentTime attribute (restart)");
+
+ for(const t of stream.getTracks()) {
+ t.stop();
+ }
+ await new Promise(r => vid.onended = r);
+ assert_greater_than_equal(vid.currentTime, lastTime,
+ "currentTime advanced after stopping");
+}, "Tests that a media element with an assigned MediaStream reports the currentTime attribute as expected");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ await new Promise(r => t.step_timeout(r, 500));
+
+ vid.play();
+ await new Promise(r => vid.ontimeupdate = r);
+ assert_between_exclusive(vid.currentTime, 0, 0.5,
+ "currentTime starts at 0 and has progressed at first timeupdate");
+}, "Tests that a media element with an assigned MediaStream starts its timeline at 0 regardless of when the MediaStream was created");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ vid.play();
+ await new Promise(r => vid.ontimeupdate = r);
+
+ vid.pause();
+ const pauseCurrentTime = vid.currentTime;
+
+ await new Promise(r => vid.onpause = r);
+ vid.ontimeupdate = () => assert_unreached("No timeupdate while paused");
+
+ await new Promise(r => t.step_timeout(r, 500));
+ assert_equals(vid.currentTime, pauseCurrentTime,
+ "currentTime does not change while paused");
+
+ vid.play();
+
+ await new Promise(r => vid.ontimeupdate = r);
+ assert_between_exclusive(vid.currentTime - pauseCurrentTime, 0, 0.5,
+ "currentTime does not skip ahead after pause");
+}, "Tests that a media element with an assigned MediaStream does not advance currentTime while paused");
+
+promise_test(async t => {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ const stream = canvas.captureStream();
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ vid.srcObject = stream;
+
+ vid.ontimeupdate = () =>
+ assert_unreached("No timeupdate until potentially playing");
+
+ vid.play();
+
+ await new Promise(r => t.step_timeout(r, 1000));
+ assert_equals(vid.readyState, vid.HAVE_NOTHING,
+ "Video dimensions not known yet");
+
+ const start = performance.now();
+ ctx.fillStyle = "green";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Wait for, and check, potentially playing
+ await new Promise(r => vid.oncanplay = r);
+ const canplayDuration = (performance.now() - start) / 1000;
+ // "canplay" was just dispatched from a task queued when the element became
+ // potentially playing. currentTime may not have progressed more than the time
+ // it took from becoming potentially playing to starting the
+ // canplay-dispatching task. Though the media clock and the js clock may be
+ // different, so we take double this duration, or 100ms, whichever is greater,
+ // as a safety margin.
+ const margin = Math.max(0.1, canplayDuration * 2);
+ assert_between_inclusive(vid.currentTime, 0, margin,
+ "currentTime has not advanced more than twice it took to dispatch canplay");
+ assert_false(vid.paused, "Media element is not paused");
+ assert_false(vid.ended, "Media element is not ended");
+ assert_equals(vid.error, null,
+ "Media element playback has not stopped due to errors");
+ assert_greater_than(vid.readyState, vid.HAVE_CURRENT_DATA,
+ "Media element playback is not blocked");
+ // Unclear how to check for "paused for user interaction" and "paused for
+ // in-band content".
+
+ await new Promise(r => vid.ontimeupdate = r);
+ assert_between_exclusive(vid.currentTime, 0, 1,
+ "currentTime advances while potentially playing");
+}, "Tests that a media element with an assigned MediaStream does not start advancing currentTime until potentially playing");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ vid.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ assert_equals(vid.loop, false, "loop is false by default");
+ vid.srcObject = stream;
+
+ vid.loop = true;
+ assert_equals(vid.loop, true,
+ "loop can be changed when assigned a MediaStream");
+
+ await new Promise(r => vid.onloadeddata = r);
+ vid.loop = false;
+ assert_equals(vid.loop, false,
+ "loop can be changed when having loaded a MediaStream");
+
+ vid.play();
+ await new Promise(r => vid.ontimeupdate = r);
+ vid.loop = true;
+ assert_equals(vid.loop, true,
+ "loop can be changed when playing a MediaStream");
+
+ for(const t of stream.getTracks()) {
+ t.stop();
+ }
+ // If loop is ignored, we get "ended",
+ // otherwise the media element sets currentTime to 0 without ending.
+ await new Promise(r => vid.onended = r);
+}, "Tests that the loop attribute has no effect on a media element with an assigned MediaStream");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => { vid.srcObject = null; });
+ vid.srcObject = stream;
+
+ await vid.play();
+
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+
+ assert_false(stream.active, "MediaStream becomes inactive with only ended tracks");
+ assert_false(vid.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (sync)");
+
+ await Promise.resolve();
+ assert_false(vid.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (microtask)");
+
+ let ended = false;
+ vid.onended = () => ended = true;
+ await new Promise(r => queueTask(r));
+
+ assert_true(vid.ended, "HTMLMediaElement becomes ended asynchronously when its MediaStream provider becomes inactive");
+ assert_true(ended, "HTMLMediaElement fires the ended event asynchronously when its MediaStream provider becomes inactive");
+}, "Tests that a media element with an assigned MediaStream ends when the MediaStream becomes inactive through tracks ending");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ t.add_cleanup(() => {
+ aud.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ aud.srcObject = stream;
+
+ await aud.play();
+
+ for (const track of stream.getAudioTracks()) {
+ track.stop();
+ }
+
+ assert_true(stream.active, "MediaStream is still active with a live video track");
+ assert_false(aud.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (sync)");
+
+ await Promise.resolve();
+ assert_false(aud.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (microtask)");
+
+ let ended = false;
+ aud.onended = () => ended = true;
+ await new Promise(r => queueTask(r));
+
+ assert_true(aud.ended, "HTMLAudioElement becomes ended asynchronously when its MediaStream provider becomes inaudible");
+ assert_true(ended, "HTMLAudioElement fires the ended event asynchronously when its MediaStream provider becomes inaudible");
+}, "Tests that an audio element with an assigned MediaStream ends when the MediaStream becomes inaudible through audio tracks ending");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => { vid.srcObject = null; });
+ vid.srcObject = stream;
+
+ await vid.play();
+
+ for (const track of stream.getTracks()) {
+ stream.removeTrack(track);
+ }
+
+ assert_false(stream.active, "MediaStream becomes inactive with no tracks");
+ assert_false(vid.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (sync)");
+
+ await Promise.resolve();
+ assert_false(vid.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (microtask)");
+
+ let ended = false;
+ vid.onended = () => ended = true;
+ await new Promise(r => queueTask(r));
+
+ assert_true(vid.ended, "HTMLMediaElement becomes ended asynchronously when its MediaStream provider becomes inactive");
+ assert_true(ended, "HTMLMediaElement fires the ended event asynchronously when its MediaStream provider becomes inactive");
+}, "Tests that a media element with an assigned MediaStream ends when the MediaStream becomes inactive through track removal");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ t.add_cleanup(() => {
+ aud.srcObject = null;
+ stream.getTracks().forEach(track => track.stop());
+ });
+ aud.srcObject = stream;
+
+ await aud.play();
+
+ for (const track of stream.getAudioTracks()) {
+ stream.removeTrack(track);
+ }
+
+ assert_true(stream.active, "MediaStream is still active with a live video track");
+ assert_false(aud.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (sync)");
+
+ await Promise.resolve();
+ assert_false(aud.ended, "HTMLMediaElement reports ended the next time the event loop reaches step 1 (microtask)");
+
+ let ended = false;
+ aud.onended = () => ended = true;
+ await new Promise(r => queueTask(r));
+
+ assert_true(aud.ended, "HTMLAudioElement becomes ended asynchronously when its MediaStream provider becomes inaudible");
+ assert_true(ended, "HTMLAudioElement fires the ended event asynchronously when its MediaStream provider becomes inaudible");
+}, "Tests that an audio element with an assigned MediaStream ends when the MediaStream becomes inaudible through track removal");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-add-audio-track.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-add-audio-track.https.html
new file mode 100644
index 0000000000..880941b3ba
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-add-audio-track.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+<head>
+<title>Adding a track to a MediaStream</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrackList-add-void-MediaStreamTrack-track">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#event-mediastream-addtrack">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your audio stream, then your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that adding a track to a MediaStream works as expected.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async t => {
+ await setMediaPermission();
+ const audio = await navigator.mediaDevices.getUserMedia({audio: true});
+ const video = await navigator.mediaDevices.getUserMedia({video: true});
+ assert_equals(video.getAudioTracks().length, 0, "video mediastream starts with no audio track");
+ video.addTrack(audio.getAudioTracks()[0]);
+ assert_equals(video.getAudioTracks().length, 1, "video mediastream has now one audio track");
+ video.addTrack(audio.getAudioTracks()[0]);
+ // If track is already in stream's track set, then abort these steps.
+ assert_equals(video.getAudioTracks().length, 1, "video mediastream still has one audio track");
+
+ audio.onaddtrack = t.step_func(function () {
+ assert_unreached("onaddtrack is not fired when the script directly modified the track of a mediastream");
+ });
+
+ assert_equals(audio.getVideoTracks().length, 0, "audio mediastream starts with no video track");
+ audio.addTrack(video.getVideoTracks()[0]);
+ assert_equals(audio.getVideoTracks().length, 1, "audio mediastream now has one video track");
+}, "Tests that adding a track to a MediaStream works as expected");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-audio-only.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-audio-only.https.html
new file mode 100644
index 0000000000..033a7cc76e
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-audio-only.https.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia({audio:true}) creates a stream with at least an audio track</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-NavigatorUserMedia-getUserMedia-void-MediaStreamConstraints-constraints-NavigatorUserMediaSuccessCallback-successCallback-NavigatorUserMediaErrorCallback-errorCallback">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrack-kind">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your audio stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the MediaStream object returned by
+the success callback in getUserMedia has exactly one audio track.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ assert_true(stream instanceof MediaStream, "getUserMedia success callback comes with a MediaStream object");
+ assert_equals(stream.getAudioTracks().length, 1, "the media stream has exactly one audio track");
+ assert_equals(stream.getAudioTracks()[0].kind, "audio", "getAudioTracks() returns a sequence of tracks whose kind is 'audio'");
+ assert_equals(stream.getVideoTracks().length, 0, "the media stream has zero video track");
+}, "Tests that a MediaStream with exactly one audio track is returned");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-clone.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-clone.https.html
new file mode 100644
index 0000000000..0fe6f3498b
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-clone.https.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<html>
+<head>
+<title>MediaStream and MediaStreamTrack clone()</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#dom-mediastream-clone">
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-clone">
+</head>
+<body>
+<p class="instructions">When prompted, accept to give permission to use your audio and video devices.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that cloning MediaStreams and MediaStreamTracks works as expected.</p>
+<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=permission-helper.js></script>
+<script>
+
+promise_test(async t => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+ assert_equals(stream.getAudioTracks().length, 1);
+ assert_equals(stream.getVideoTracks().length, 1);
+
+ const clone1 = stream.clone();
+ assert_equals(clone1.getAudioTracks().length, 1);
+ assert_equals(clone1.getVideoTracks().length, 1);
+ assert_not_equals(stream.getAudioTracks()[0].id, clone1.getAudioTracks()[0].id);
+ assert_not_equals(stream.getVideoTracks()[0].id, clone1.getVideoTracks()[0].id);
+
+ stream.getTracks().forEach(track => track.stop());
+ assert_false(stream.active);
+ assert_equals(stream.getAudioTracks()[0].readyState, "ended");
+ assert_equals(stream.getVideoTracks()[0].readyState, "ended");
+ assert_true(clone1.active);
+ assert_equals(clone1.getAudioTracks()[0].readyState, "live");
+ assert_equals(clone1.getVideoTracks()[0].readyState, "live");
+
+ clone1.getAudioTracks()[0].stop();
+ assert_true(clone1.active);
+ assert_equals(clone1.getAudioTracks()[0].readyState, "ended");
+ assert_equals(clone1.getVideoTracks()[0].readyState, "live");
+
+ const clone2 = clone1.clone();
+ assert_true(clone2.active);
+ assert_equals(clone2.getAudioTracks()[0].readyState, "ended");
+ assert_equals(clone2.getVideoTracks()[0].readyState, "live");
+
+ clone1.getVideoTracks()[0].stop();
+ clone2.getVideoTracks()[0].stop();
+
+ const clone3 = clone2.clone();
+ assert_false(clone3.active);
+ assert_equals(clone3.getAudioTracks()[0].readyState, "ended");
+ assert_equals(clone3.getVideoTracks()[0].readyState, "ended");
+ assert_not_equals(clone1.getAudioTracks()[0].id, clone2.getAudioTracks()[0].id);
+ assert_not_equals(clone1.getVideoTracks()[0].id, clone2.getVideoTracks()[0].id);
+ assert_not_equals(clone2.getAudioTracks()[0].id, clone3.getAudioTracks()[0].id);
+ assert_not_equals(clone2.getVideoTracks()[0].id, clone3.getVideoTracks()[0].id);
+ assert_not_equals(clone1.getAudioTracks()[0].id, clone3.getAudioTracks()[0].id);
+ assert_not_equals(clone1.getVideoTracks()[0].id, clone3.getVideoTracks()[0].id);
+}, "Tests that cloning MediaStream objects works as expected");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+ assert_equals(stream.getAudioTracks().length, 1);
+ assert_equals(stream.getVideoTracks().length, 1);
+ assert_equals(stream.getAudioTracks()[0].readyState, "live");
+ assert_equals(stream.getVideoTracks()[0].readyState, "live");
+ assert_true(stream.active);
+
+ const audio_clone = stream.getAudioTracks()[0].clone();
+ const video_clone = stream.getVideoTracks()[0].clone();
+ assert_equals(audio_clone.readyState, "live");
+ assert_equals(video_clone.readyState, "live");
+ assert_not_equals(stream.getAudioTracks()[0].id, audio_clone.id);
+ assert_not_equals(stream.getVideoTracks()[0].id, video_clone.id);
+
+ stream.getTracks().forEach(track => track.stop());
+ assert_false(stream.active);
+ assert_equals(stream.getAudioTracks()[0].readyState, "ended");
+ assert_equals(stream.getVideoTracks()[0].readyState, "ended");
+ assert_equals(audio_clone.readyState, "live");
+ assert_equals(video_clone.readyState, "live");
+
+ stream.addTrack(audio_clone);
+ stream.addTrack(video_clone);
+ assert_true(stream.active);
+
+ stream.getTracks().forEach(track => track.stop());
+ assert_false(stream.active);
+ assert_equals(audio_clone.readyState, "ended");
+ assert_equals(video_clone.readyState, "ended");
+}, "Tests that cloning MediaStreamTrack objects works as expected");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-default-feature-policy.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-default-feature-policy.https.html
new file mode 100644
index 0000000000..b81404bf33
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-default-feature-policy.https.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<body>
+<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=permission-helper.js></script>
+ <script src=/common/get-host-info.sub.js></script>
+ <script src=/feature-policy/resources/featurepolicy.js></script>
+ <script>
+ 'use strict';
+ async function gUM({audio, video}) {
+ let stream;
+ if (!page_loaded_in_iframe()) {
+ await setMediaPermission();
+ }
+ try {
+ stream = await navigator.mediaDevices.getUserMedia({audio, video});
+ // getUserMedia must guarantee the number of tracks requested or fail.
+ if ((audio && stream.getAudioTracks().length == 0) ||
+ (video && stream.getVideoTracks().length == 0)) {
+ throw {name: `All requested devices must be present with ` +
+ `audio ${audio} and video ${video}, or fail`};
+ }
+ } finally {
+ if (stream) {
+ stream.getTracks().forEach(track => track.stop());
+ }
+ }
+ }
+
+ async function must_disallow_gUM({audio, video}) {
+ try {
+ await gUM({audio, video});
+ } catch (e) {
+ if (e.name == 'NotAllowedError') {
+ return;
+ }
+ throw e;
+ }
+ throw {name: `audio ${audio} and video ${video} constraints must not be ` +
+ `allowed.`};
+ }
+
+ const cross_domain = get_host_info().HTTPS_REMOTE_ORIGIN;
+ run_all_fp_tests_allow_self(
+ cross_domain,
+ 'microphone',
+ 'NotAllowedError',
+ async () => {
+ await gUM({audio: true});
+ if (window.location.href.includes(cross_domain)) {
+ await must_disallow_gUM({video: true});
+ await must_disallow_gUM({audio: true, video: true});
+ }
+ }
+ );
+
+ run_all_fp_tests_allow_self(
+ cross_domain,
+ 'camera',
+ 'NotAllowedError',
+ async () => {
+ await gUM({video: true});
+ if (window.location.href.includes(cross_domain)) {
+ await must_disallow_gUM({audio: true});
+ await must_disallow_gUM({audio: true, video: true});
+ }
+ }
+ );
+
+ run_all_fp_tests_allow_self(
+ cross_domain,
+ 'camera;microphone',
+ 'NotAllowedError',
+ async () => {
+ await gUM({audio: true, video: true});
+ await gUM({audio: true});
+ await gUM({video: true});
+ }
+ );
+ </script>
+</body>
+
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-finished-add.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-finished-add.https.html
new file mode 100644
index 0000000000..797db0444e
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-finished-add.https.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+<head>
+<title>Adding a track to an inactive MediaStream</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#widl-MediaStream-addTrack-void-MediaStreamTrack-track">
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#widl-MediaStreamTrack-stop-void">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your audio stream, then
+your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that adding a track to an inactive
+MediaStream is allowed.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission();
+ const audio = await navigator.mediaDevices.getUserMedia({audio:true});
+ const video = await navigator.mediaDevices.getUserMedia({video:true});
+ audio.getAudioTracks()[0].stop();
+ assert_false(audio.active, "audio stream is inactive after stopping its only audio track");
+ assert_true(video.active, "video stream is active");
+ audio.addTrack(video.getVideoTracks()[0]);
+ audio.removeTrack(audio.getAudioTracks()[0]);
+}, "Tests that adding a track to an inactive MediaStream is allowed");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-gettrackid.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-gettrackid.https.html
new file mode 100644
index 0000000000..9a4aef9782
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-gettrackid.https.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+<head>
+<title>Retrieving a track from a MediaStream</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStream-getTrackById-MediaStreamTrack-DOMString-trackId">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that MediaStream.getTrackById behaves as expected</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ var track = stream.getVideoTracks()[0];
+ assert_equals(track, stream.getTrackById(track.id), "getTrackById returns track of given id");
+ assert_equals(stream.getTrackById(track.id + "foo"), null, "getTrackById of inexistant id returns null");
+}, "Tests that MediaStream.getTrackById works as expected");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-id.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-id.https.html
new file mode 100644
index 0000000000..3c4fe0529b
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-id.https.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia() creates a stream with a proper id</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStream-id">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the MediaStream object returned by
+the success callback in getUserMedia has a correct id.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+const allowedCharacters = /^[\u0021\u0023-\u0027\u002A-\u002B\u002D-\u002E\u0030-\u0039\u0041-\u005A\u005E-\u007E]*$/;
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video:true});
+ assert_equals(stream.id.length, 36, "the media stream id has 36 characters");
+ assert_regexp_match(stream.id, allowedCharacters, "the media stream id uses the set of allowed characters");
+}, "Tests that a MediaStream with a correct id is returned");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-idl.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-idl.https.html
new file mode 100644
index 0000000000..49c6bc07ca
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-idl.https.html
@@ -0,0 +1,77 @@
+<!doctype html>
+<html>
+<head>
+<title>MediaStream constructor algorithm</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#idl-def-MediaStream">
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#widl-MediaStream-id">
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#mediastream">
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#event-mediastreamtrack-ended">
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#widl-MediaStreamTrack-stop-void">
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#widl-MediaStreamTrack-clone-MediaStreamTrack">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video and audio stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the MediaStream constructor
+follows the algorithm set in the spec.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+ promise_test(async () => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio:true})
+ let stream1 = new MediaStream();
+ assert_not_equals(stream.id, stream1.id, "Two different MediaStreams have different ids");
+ let stream2 = new MediaStream(stream);
+ assert_not_equals(stream.id, stream2.id, "A MediaStream constructed from another has a different id");
+ let audioTrack1 = stream.getAudioTracks()[0];
+ let videoTrack = stream.getVideoTracks()[0];
+ assert_equals(audioTrack1, stream2.getAudioTracks()[0], "A MediaStream constructed from another shares the same audio track");
+ assert_equals(videoTrack, stream2.getVideoTracks()[0], "A MediaStream constructed from another shares the same video track");
+ let stream4 = new MediaStream([audioTrack1]);
+ assert_equals(stream4.getTrackById(audioTrack1.id), audioTrack1, "a non-ended track gets added via the MediaStream constructor");
+
+ let audioTrack2 = audioTrack1.clone();
+ audioTrack2.addEventListener("ended", () => {
+ throw new Error("ended event should not be fired by MediaStreamTrack.stop().")
+ });
+ audioTrack2.stop();
+ assert_equals(audioTrack2.readyState, "ended", "a stopped track is marked ended synchronously");
+
+ let stream3 = new MediaStream([audioTrack2, videoTrack]);
+ assert_equals(stream3.getTrackById(audioTrack2.id), audioTrack2, "an ended track gets added via the MediaStream constructor");
+ assert_equals(stream3.getTrackById(videoTrack.id), videoTrack, "a non-ended track gets added via the MediaStream constructor even if the previous track was ended");
+
+ let stream5 = new MediaStream([audioTrack2]);
+ assert_equals(stream5.getTrackById(audioTrack2.id), audioTrack2, "an ended track gets added via the MediaStream constructor");
+ assert_false(stream5.active, "a MediaStream created using the MediaStream() constructor whose arguments are lists of MediaStreamTrack objects that are all ended, the MediaStream object MUST be created with its active attribute set to false");
+
+ audioTrack1.stop();
+ assert_equals(audioTrack1.readyState, "ended",
+ "Stopping audioTrack1 marks it ended synchronously");
+
+ videoTrack.stop();
+ assert_equals(videoTrack.readyState, "ended",
+ "Stopping videoTrack marks it ended synchronously");
+
+ assert_false(stream.active,
+ "The original MediaStream is marked inactive synchronously");
+ assert_false(stream1.active,
+ "MediaStream 1 is marked inactive synchronously");
+ assert_false(stream2.active,
+ "MediaStream 2 is marked inactive synchronously");
+ assert_false(stream3.active,
+ "MediaStream 3 is marked inactive synchronously");
+ assert_false(stream4.active,
+ "MediaStream 4 is marked inactive synchronously");
+
+}, "Tests that a MediaStream constructor follows the algorithm set in the spec");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-removetrack.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-removetrack.https.html
new file mode 100644
index 0000000000..6c9b9b02e3
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-removetrack.https.html
@@ -0,0 +1,140 @@
+<!doctype html>
+<html>
+<head>
+<title>Removing a track from a MediaStream</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrackList-remove-void-MediaStreamTrack-track">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#event-mediastream-removetrack">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your audio stream, then your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that removinging a track from a MediaStream works as expected.</p>
+<video id="video" height="120" width="160" autoplay muted></video>
+<audio id="audio" autoplay muted></audio>
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+
+promise_test(async t => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+ const tracks = stream.getTracks();
+ t.add_cleanup(() => tracks.forEach(track => track.stop()));
+ const stream2 = await navigator.mediaDevices.getUserMedia({audio: true});
+ tracks.push(...stream2.getTracks());
+
+ stream.onremovetrack = stream2.onremovetrack = t.step_func(() =>
+ assert_unreached("onremovetrack is not triggered by script itself"));
+
+ assert_equals(stream.getTracks().length, 2, "mediastream starts with 2 tracks");
+ stream.removeTrack(stream.getVideoTracks()[0]);
+ assert_equals(stream.getTracks().length, 1, "mediastream has 1 track left");
+ stream.removeTrack(stream.getAudioTracks()[0]);
+ assert_equals(stream.getTracks().length, 0, "mediastream has no tracks left");
+ stream.removeTrack(stream2.getTracks()[0]); // should not throw
+
+ // Allow time to verify no events fire.
+ await new Promise(r => t.step_timeout(r, 1));
+
+}, "Tests that a removal from a MediaStream works as expected");
+
+async function doesEventFire(t, target, name, ms = 1) {
+ const cookie = {};
+ const value = await Promise.race([
+ new Promise(r => target.addEventListener(name, r, {once: true})),
+ new Promise(r => t.step_timeout(r, ms)).then(() => cookie)
+ ]);
+ return value !== cookie;
+}
+
+const doEventsFire = (t, target1, target2, name, ms = 1) => Promise.all([
+ doesEventFire(t, target1, "ended", ms),
+ doesEventFire(t, target2, "ended", ms)
+]);
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+ const tracks = stream.getTracks();
+
+ audio.srcObject = video.srcObject = stream;
+
+ t.add_cleanup(() => {
+ for (const track of tracks) {
+ track.stop();
+ }
+ audio.srcObject = video.srcObject = null;
+ });
+
+ await Promise.all([
+ new Promise(r => audio.onloadedmetadata = r),
+ new Promise(r => video.onloadedmetadata = r)
+ ]);
+
+ assert_equals(audio.ended, false, "audio element starts out not ended");
+ assert_equals(video.ended, false, "video element starts out not ended");
+
+ stream.removeTrack(stream.getVideoTracks()[0]);
+ {
+ const [audioDidEnd, videoDidEnd] = await doEventsFire(t, audio, video, "ended");
+ assert_equals(audio.ended, false, "audio element unaffected");
+ assert_equals(audioDidEnd, false, "no audio ended event should fire yet");
+ assert_equals(video.ended, false, "video element keeps going with audio track");
+ assert_equals(videoDidEnd, false, "no video ended event should fire yet");
+ }
+ stream.removeTrack(stream.getAudioTracks()[0]);
+ {
+ const [audioDidEnd, videoDidEnd] = await doEventsFire(t, audio, video, "ended");
+ assert_equals(audio.ended, true, "audio element ended because no more audio tracks");
+ assert_equals(audioDidEnd, true, "go audio ended event");
+ assert_equals(video.ended, true, "video element ended because no more tracks");
+ assert_equals(videoDidEnd, true, "got video ended event");
+ }
+}, "Test that removal from a MediaStream fires ended on media elements (video first)");
+
+promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
+ const tracks = stream.getTracks();
+
+ audio.srcObject = video.srcObject = stream;
+
+ t.add_cleanup(() => {
+ for (const track of tracks) {
+ track.stop();
+ }
+ audio.srcObject = video.srcObject = null;
+ });
+
+ await Promise.all([
+ new Promise(r => audio.onloadedmetadata = r),
+ new Promise(r => video.onloadedmetadata = r)
+ ]);
+
+ assert_equals(audio.ended, false, "audio element starts out not ended");
+ assert_equals(video.ended, false, "video element starts out not ended");
+
+ stream.removeTrack(stream.getAudioTracks()[0]);
+ {
+ const [audioDidEnd, videoDidEnd] = await doEventsFire(t, audio, video, "ended");
+ assert_equals(audio.ended, true, "audio element ended because no more audio tracks");
+ assert_equals(audioDidEnd, true, "got audio ended event");
+ assert_equals(video.ended, false, "video element keeps going with video track");
+ assert_equals(videoDidEnd, false, "no video ended event should fire yet");
+ }
+ stream.removeTrack(stream.getVideoTracks()[0]);
+ {
+ const [audioDidEnd, videoDidEnd] = await doEventsFire(t, audio, video, "ended");
+ assert_equals(audio.ended, true, "audio element remains ended from before");
+ assert_equals(audioDidEnd, false, "no second audio ended event should fire");
+ assert_equals(video.ended, true, "video element ended because no more tracks");
+ assert_equals(videoDidEnd, true, "got video ended event");
+ }
+}, "Test that removal from a MediaStream fires ended on media elements (audio first)");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-supported-by-feature-policy.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-supported-by-feature-policy.html
new file mode 100644
index 0000000000..63f565926a
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-supported-by-feature-policy.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Test that camera and microphone are advertised in the feature list</title>
+<link rel="help" href="https://w3c.github.io/webappsec-feature-policy/#dom-featurepolicy-features">
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#feature-policy-integration">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_in_array('camera', document.featurePolicy.features());
+}, 'document.featurePolicy.features should advertise camera.');
+
+test(() => {
+ assert_in_array('microphone', document.featurePolicy.features());
+}, 'document.featurePolicy.features should advertise microphone.');
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStream-video-only.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStream-video-only.https.html
new file mode 100644
index 0000000000..6e0a6cc9cf
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStream-video-only.https.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia({video:true}) creates a stream with one video track</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-NavigatorUserMedia-getUserMedia-void-MediaStreamConstraints-constraints-NavigatorUserMediaSuccessCallback-successCallback-NavigatorUserMediaErrorCallback-errorCallback">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrack-kind">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the MediaStream object returned by
+the success callback in getUserMedia has exactly one video track and no audio.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ assert_true(stream instanceof MediaStream, "getUserMedia success callback comes with a MediaStream object");
+ assert_equals(stream.getAudioTracks().length, 0, "the media stream has zero audio track");
+ assert_equals(stream.getVideoTracks().length, 1, "the media stream has exactly one video track");
+ assert_equals(stream.getVideoTracks()[0].kind, "video", "getAudioTracks() returns a sequence of tracks whose kind is 'video'");
+}, "Tests that a MediaStream with at least one video track is returned");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-audio-is-silence.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-audio-is-silence.https.html
new file mode 100644
index 0000000000..4ad2340ed5
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-audio-is-silence.https.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<html>
+<head>
+<title>A disabled audio track is rendered as silence</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#introduction">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#mediastreams-as-media-elements">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your audio stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that a disabled audio track in a
+MediaStream is rendered as silence. It relies on the
+<a href="https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html">
+Web Audio API</a>.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+const aud = document.getElementById("aud");
+promise_test(async t => {
+ await setMediaPermission("granted", ["microphone"]);
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ var ctx = new AudioContext();
+ var streamSource = ctx.createMediaStreamSource(stream);
+ var silenceDetector = ctx.createScriptProcessor(1024);
+ var count = 10;
+ let resolveAudioProcessPromise;
+ const audioProcessed = new Promise(res => resolveAudioProcessPromise = res)
+
+ silenceDetector.onaudioprocess = function (e) {
+ var buffer1 = e.inputBuffer.getChannelData(0);
+ var buffer2 = e.inputBuffer.getChannelData(1);
+ var out = e.outputBuffer.getChannelData(0);
+ out = new Float32Array(buffer1);
+ for (var i = 0; i < buffer1.length; i++) {
+ assert_equals(buffer1[i], 0, "Audio buffer entry #" + i + " in channel 0 is silent");
+ }
+ for (var i = 0; i < buffer2.length; i++) {
+ assert_equals(buffer2[i], 0, "Audio buffer entry #" + i + " in channel 1 is silent");
+ }
+ count--;
+ if (count === 0) {
+ silenceDetector.onaudioprocess = null;
+ resolveAudioProcessPromise();
+ }
+ };
+ stream.getAudioTracks()[0].enabled = false;
+
+ streamSource.connect(silenceDetector);
+ silenceDetector.connect(ctx.destination);
+}, "Tests that a disabled audio track in a MediaStream is rendered as silence");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-video-is-black.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-video-is-black.https.html
new file mode 100644
index 0000000000..9215f931f0
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-MediaElement-disabled-video-is-black.https.html
@@ -0,0 +1,126 @@
+<!doctype html>
+<html>
+<head>
+<title>A disabled video track is rendered as blackness</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#introduction">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#mediastreams-as-media-elements">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that a disabled video track in a
+MediaStream is handled correctly by HTMLVideoElement.</p>
+<video id="vid"></video>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+
+function assertVideoElementIsBlack(vid) {
+ const cv = document.createElement("canvas");
+ cv.width = vid.videoWidth;
+ cv.height = vid.videoHeight;
+ const ctx = cv.getContext("2d");
+ ctx.drawImage(vid,0,0);
+ const imageData = ctx.getImageData(0, 0, cv.width, cv.height);
+ for (let i = 0; i < imageData.data.length / 4; ++i) {
+ assert_equals(imageData.data[i * 4], 0, "No red component in pixel #" + i);
+ assert_equals(imageData.data[i * 4 + 1], 0, "No green component in pixel #" + i);
+ assert_equals(imageData.data[i * 4 + 2], 0, "No blue component in pixel #" + i);
+ }
+}
+
+promise_test(async t => {
+ const vid = document.getElementById("vid");
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ vid.srcObject = null;
+ });
+
+ stream.getTracks()[0].enabled = false;
+ vid.srcObject = stream;
+ vid.play();
+ await new Promise(r => vid.onloadeddata = r);
+ assertVideoElementIsBlack(vid);
+}, "Tests that a disabled video track in a MediaStream is rendered as blackness");
+
+promise_test(async t => {
+ const vid = document.getElementById("vid");
+ const cv = document.createElement("canvas");
+ const ctx = cv.getContext("2d");
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, cv.width, cv.height);
+ const stream = cv.captureStream();
+ t.add_cleanup(() => {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ vid.srcObject = null;
+ });
+
+ stream.getTracks()[0].enabled = false;
+ vid.srcObject = stream;
+ vid.play();
+ await new Promise(r => vid.onloadeddata = r);
+
+ assert_equals(vid.videoWidth, cv.width);
+ assert_equals(vid.videoHeight, cv.height);
+}, "Test that a video element rendering a disabled video track reports correct intrinsic dimensions");
+
+promise_test(async t => {
+ const vid = document.getElementById("vid");
+ const cv = document.createElement("canvas");
+ const originalWidth = cv.width;
+ const originalHeight = cv.height;
+
+ const vid2 = document.createElement("video");
+ const ctx = cv.getContext("2d");
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, cv.width, cv.height);
+ const stream = cv.captureStream();
+ const stream2 = stream.clone();
+ t.add_cleanup(() => {
+ for (let track of [...stream.getTracks(), ...stream2.getTracks()]) {
+ track.stop();
+ }
+ vid.srcObject = null;
+ vid2.srcObject = null;
+ cv.width = originalWidth;
+ cv.height = originalHeight;
+ });
+
+ stream.getTracks()[0].enabled = false;
+
+ vid.srcObject = stream;
+ vid2.srcObject = stream2;
+ vid.play();
+ vid2.play();
+ await Promise.all([
+ new Promise(r => vid.onresize = r),
+ new Promise(r => vid2.onresize = r),
+ ]);
+
+ // Test "flow" of the disabled track by checking for "resize" events on a
+ // video element rendering that track. Any delivered frames must be black.
+ let resized = 0;
+ vid.addEventListener("resize", t.step_func(() => assertVideoElementIsBlack(vid)));
+
+ for (let i = 0; i < 2; ++i) {
+ cv.width = cv.width / 2;
+ cv.height = cv.height / 2;
+ ctx.fillRect(0, 0, cv.width, cv.height);
+ await new Promise(r => vid2.onresize = r);
+ }
+}, "Test that frames are black for a disabled video track with an enabled clone");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html
new file mode 100644
index 0000000000..da27b3c357
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html
@@ -0,0 +1,124 @@
+<!doctype html>
+<title>MediaStreamTrack applyConstraints</title>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<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=permission-helper.js></script>
+<script>
+ 'use strict'
+
+ // https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-applyconstraints
+
+ promise_test(async t => {
+ await setMediaPermission("granted", ["camera"]);
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(t.step_func(stream => {
+ return stream.getVideoTracks()[0].applyConstraints(
+ { groupId: { exact: "INVALID" } }).then(
+ t.unreached_func('Accepted invalid groupID'),
+ t.step_func(e => {
+ assert_equals(e.name, 'OverconstrainedError');
+ assert_equals(e.constraint, 'groupId');
+ }));
+ }));
+ }, 'applyConstraints rejects invalid groupID');
+
+ promise_test(async t => {
+ let long_string_groupId = "2".padStart(501);
+ await setMediaPermission("granted", ["camera"]);
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(t.step_func(stream => {
+ return stream.getVideoTracks()[0].applyConstraints(
+ { groupId: { ideal: long_string_groupId } }).then(
+ t.unreached_func('Accepted ideal long string groupID'),
+ t.step_func(e => {
+ assert_equals(e.name, 'OverconstrainedError');
+ }));
+ }));
+ }, 'applyConstraints rejects long string ideal groupID');
+
+ promise_test(async t => {
+ let long_string_groupId = "2".padStart(501);
+ await setMediaPermission("granted", ["camera"]);
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(t.step_func(stream => {
+ return stream.getVideoTracks()[0].applyConstraints(
+ { groupId: { exact: long_string_groupId } }).then(
+ t.unreached_func('Accepted exact long string groupID'),
+ t.step_func(e => {
+ assert_equals(e.name, 'OverconstrainedError');
+ }));
+ }));
+ }, 'applyConstraints rejects long string groupID');
+
+ promise_test(async t => {
+ await setMediaPermission("granted", ["camera"]);
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(t.step_func(stream => {
+ return stream.getVideoTracks()[0].applyConstraints(
+ { mandatory: { groupId: "INVALID" }, groupId: { exact: "INVALID" } }).then(
+ t.unreached_func('Accepted exact long string groupID'),
+ t.step_func(e => {
+ assert_equals(e.name, 'OverconstrainedError');
+ }));
+ }));
+ }, 'applyConstraints rejects using both mandatory and specific constraints');
+
+ promise_test(t => {
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(t.step_func(stream => {
+ var track = stream.getVideoTracks()[0];
+ var groupId = track.getSettings().groupId;
+ return track.applyConstraints({ groupId: "INVALID" }).then(
+ t.step_func(() => {
+ assert_equals(track.getSettings().groupId, groupId);
+ }));
+ }));
+ }, 'applyConstraints accepts invalid ideal groupID, does not change setting');
+
+ promise_test(t => {
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(t.step_func(stream => {
+ var track = stream.getVideoTracks()[0];
+ var groupId = track.getSettings().groupId;
+ return navigator.mediaDevices.enumerateDevices().then(devices => {
+ var anotherDevice = devices.find(device => {
+ return device.kind == "videoinput" && device.groupId != groupId;
+ });
+ if (anotherDevice !== undefined) {
+ return track.applyConstraints(
+ { groupId: { exact: anotherDevice.groupId } }).then(
+ t.unreached_func(),
+ t.step_func(e => {
+ assert_equals(e.name, 'OverconstrainedError');
+ assert_equals(e.constraint, 'groupId');
+ }));
+ }
+ });
+ }));
+ }, 'applyConstraints rejects attempt to switch device using groupId');
+
+ promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+ const [track] = stream.getVideoTracks();
+ t.add_cleanup(() => track.stop());
+ try {
+ await track.applyConstraints({ resizeMode: { exact: "INVALID" } });
+ t.unreached_func('applyConstraints() must fail with invalid resizeMode')();
+ } catch (e) {
+ assert_equals(e.name, 'OverconstrainedError');
+ assert_equals(e.constraint, 'resizeMode');
+ }
+ }, 'applyConstraints rejects invalid resizeMode');
+
+ promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+ const [track] = stream.getVideoTracks();
+ t.add_cleanup(() => track.stop());
+ const resizeMode = track.getSettings().resizeMode;
+ await track.applyConstraints({ resizeMode: "INVALID" });
+ assert_equals(track.getSettings().resizeMode, resizeMode);
+ }, 'applyConstraints accepts invalid ideal resizeMode, does not change setting');
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-end-manual.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-end-manual.https.html
new file mode 100644
index 0000000000..58e484d484
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-end-manual.https.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<html>
+<head>
+<title>Test that mediastreamtrack are properly ended</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://w3c.github.io/mediacapture-main/getusermedia.html#mediastreamtrack">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video and audio
+stream, and then revoke that permission.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the video and audio tracks of
+MediaStream object returned by the success callback in getUserMedia are
+correctly set into inactive state when permission is revoked.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+'use strict';
+promise_test(async t => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({
+ audio: true,
+ video: true,
+ });
+
+ const vidTrack = stream.getVideoTracks()[0];
+ assert_equals(vidTrack.readyState, "live",
+ "The video track object is in live state");
+ const vidEnded = new Promise(r => vidTrack.onended = r);
+ const audTrack = stream.getAudioTracks()[0];
+ assert_equals(audTrack.readyState, "live",
+ "The audio track object is in live state");
+ const audEnded = new Promise(r => audTrack.onended = r);
+
+ await Promise.race([vidEnded, audEnded]);
+ assert_equals(stream.getTracks().filter(t => t.readyState == "ended").length,
+ 1, "Only one track is ended after first track's ended event");
+ assert_equals(stream.getTracks().filter(t => t.readyState == "live").length,
+ 1, "One track is still live after first track's ended event");
+ assert_true(stream.active, "MediaStream is still active");
+
+ await Promise.all([vidEnded, audEnded]);
+ assert_equals(vidTrack.readyState, "ended", "Video track ended as expected");
+ assert_equals(audTrack.readyState, "ended", "Audio track ended as expected");
+ assert_false(stream.active, "MediaStream has become inactive as expected");
+}, "Tests that MediaStreamTracks end properly on permission revocation");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html
new file mode 100644
index 0000000000..7d600c0e1b
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html
@@ -0,0 +1,154 @@
+<!doctype html>
+<title>MediaStreamTrack and InputDeviceInfo GetCapabilities</title>
+<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=permission-helper.js></script>
+<script>
+
+const audioProperties = [
+ {name: "sampleRate", type: "number"},
+ {name: "sampleSize", type: "number"},
+ {name: "echoCancellation", type: "boolean"},
+ {name: "autoGainControl", type: "boolean"},
+ {name: "noiseSuppression", type: "boolean"},
+ {name: "voiceIsolation", type: "boolean"},
+ {name: "latency", type: "number"},
+ {name: "channelCount", type: "number"},
+ {name: "deviceId", type: "string"},
+ {name: "groupId", type: "string"}
+];
+
+const videoProperties = [
+ {name: "width", type: "number"},
+ {name: "height", type: "number"},
+ {name: "aspectRatio", type: "number"},
+ {name: "frameRate", type: "number"},
+ {name: "facingMode", type: "enum-any", validValues: ["user", "environment", "left", "right"]},
+ {name: "resizeMode", type: "enum-all", validValues: ["none", "crop-and-scale"]},
+ {name: "deviceId", type: "string"},
+ {name: "groupId", type: "string"},
+];
+
+function verifyBooleanCapability(capability) {
+ assert_less_than_equal(capability.length, 2);
+ capability.forEach(c => assert_equals(typeof c, "boolean"));
+}
+
+function verifyNumberCapability(capability) {
+ assert_equals(typeof capability, "object");
+ assert_equals(Object.keys(capability).length, 2);
+ assert_true(capability.hasOwnProperty('min'));
+ assert_true(capability.hasOwnProperty('max'));
+ assert_less_than_equal(capability.min, capability.max);
+}
+
+// Verify that any value provided by an enum capability is in the set of valid
+// values.
+function verifyEnumAnyCapability(capability, enumMembers) {
+ capability.forEach(c => {
+ assert_equals(typeof c, "string");
+ assert_in_array(c, enumMembers);
+ });
+}
+
+// Verify that all required values are supported by a capability.
+function verifyEnumAllCapability(capability, enumMembers, testNamePrefix) {
+ enumMembers.forEach(member => {
+ test(() => {
+ assert_in_array(member, capability);
+ }, testNamePrefix + " Value: " + member);
+ });
+}
+
+function testCapabilities(capabilities, property, testNamePrefix) {
+ let testName = testNamePrefix + " " + property.name;
+ test(() => {
+ assert_true(capabilities.hasOwnProperty(property.name));
+ }, testName + " property present.");
+
+ const capability = capabilities[property.name];
+ testName += " properly supported.";
+ if (property.type == "string") {
+ test(() => {
+ assert_equals(typeof capability, "string");
+ }, testName);
+ }
+
+ if (property.type == "boolean") {
+ test(() => {
+ verifyBooleanCapability(capability);
+ }, testName);
+ }
+
+ if (property.type == "number") {
+ test(() => {
+ verifyNumberCapability(capability);
+ }, testName);
+ }
+
+ if (property.type.startsWith("enum")) {
+ test(() => {
+ verifyEnumAnyCapability(capability, property.validValues);
+ }, testName);
+
+ if (property.type == "enum-all") {
+ verifyEnumAllCapability(capability, property.validValues, testName);
+ }
+ }
+}
+
+{
+ audioProperties.forEach((property, i) => {
+ promise_test(async t => {
+ if (i === 0) await setMediaPermission("granted", ["microphone"]);
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ t.add_cleanup(() => stream.getAudioTracks()[0].stop());
+ const audioCapabilities = stream.getAudioTracks()[0].getCapabilities();
+ testCapabilities(audioCapabilities, property, "Audio track getCapabilities()");
+ }, "Setup audio MediaStreamTrack getCapabilities() test for " + property.name);
+ });
+
+ videoProperties.forEach(property => {
+ promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ t.add_cleanup(() => stream.getVideoTracks()[0].stop());
+ const audioCapabilities = stream.getVideoTracks()[0].getCapabilities();
+ testCapabilities(audioCapabilities, property, "Video track getCapabilities()");
+ }, "Setup video MediaStreamTrack getCapabilities() test for " + property.name);
+ });
+}
+
+{
+ audioProperties.forEach(property => {
+ promise_test(async t => {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ for (const device of devices) {
+ // Test only one device.
+ if (device.kind == "audioinput") {
+ assert_inherits(device, "getCapabilities");
+ const capabilities = device.getCapabilities();
+ testCapabilities(capabilities, property, "Audio device getCapabilities()");
+ break;
+ }
+ }
+ }, "Setup audio InputDeviceInfo getCapabilities() test for " + property.name);
+ });
+
+ videoProperties.forEach(property => {
+ promise_test(async t => {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ for (const device of devices) {
+ // Test only one device.
+ if (device.kind == "videoinput") {
+ assert_inherits(device, "getCapabilities");
+ const capabilities = device.getCapabilities();
+ testCapabilities(capabilities, property, "Video device getCapabilities()");
+ break;
+ }
+ }
+ }, "Setup video InputDeviceInfo getCapabilities() test for " + property.name);
+ });
+}
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getSettings.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getSettings.https.html
new file mode 100644
index 0000000000..3bae50c346
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getSettings.https.html
@@ -0,0 +1,235 @@
+<!doctype html>
+<title>MediaStreamTrack GetSettings</title>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<meta name=timeout content=long>
+<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=permission-helper.js></script>
+<script>
+ 'use strict'
+
+ // https://w3c.github.io/mediacapture-main/archives/20170605/getusermedia.html
+
+ async function createTrackAndGetSettings(t, kind) {
+ const constraints = {};
+ constraints[kind] = true;
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ assert_equals(stream.getTracks().length, 1);
+ t.add_cleanup(() => stream.getTracks()[0].stop());
+ return stream.getTracks()[0].getSettings();
+ }
+
+ promise_test(async t => {
+ await setMediaPermission("granted", ["camera"]);
+ const mediaStream1 = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: false,
+ });
+ t.add_cleanup(() => mediaStream1.getVideoTracks()[0].stop());
+ const settings1 = mediaStream1.getVideoTracks()[0].getSettings();
+
+ const mediaStream2 = await navigator.mediaDevices.getUserMedia({
+ video: {
+ deviceId: {exact: settings1.deviceId},
+ },
+ audio: false
+ });
+ t.add_cleanup(() => mediaStream2.getVideoTracks()[0].stop());
+ const settings2 = mediaStream2.getVideoTracks()[0].getSettings();
+
+ assert_equals(settings1.deviceId, settings2.deviceId);
+ }, 'A device can be opened twice and have the same device ID');
+
+ promise_test(async t => {
+ const mediaStream1 = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: false,
+ });
+ t.add_cleanup(() => mediaStream1.getVideoTracks()[0].stop());
+ const settings1 = mediaStream1.getVideoTracks()[0].getSettings();
+
+ const mediaStream2 = await navigator.mediaDevices.getUserMedia({
+ video: {
+ deviceId: {exact: settings1.deviceId},
+ width: {
+ ideal: settings1.width / 2,
+ },
+ },
+ audio: false
+ });
+ t.add_cleanup(() => mediaStream2.getVideoTracks()[0].stop());
+ const settings2 = mediaStream2.getVideoTracks()[0].getSettings();
+
+ assert_equals(settings1.deviceId, settings2.deviceId);
+ assert_between_inclusive(settings2.width, settings1.width / 2, settings1.width);
+ }, 'A device can be opened twice with different resolutions requested');
+
+ promise_test(async t => {
+ // getUserMedia needs to be called before deviceIds and groupIds are exposed
+ const afterGum = await navigator.mediaDevices.getUserMedia({
+ video: true, audio: true
+ });
+ afterGum.getTracks().forEach(track => track.stop());
+
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const inputDevices = devices.filter(({kind}) => kind != "audiooutput");
+ assert_greater_than(inputDevices.length, 1, "have at least 2 test devices");
+ for (const {kind, deviceId, groupId} of inputDevices) {
+ const type = {videoinput: "video", audioinput: "audio"}[kind];
+ const stream = await navigator.mediaDevices.getUserMedia({
+ [type]: {deviceId: {exact: deviceId}}
+ });
+ const [track] = stream.getTracks();
+ const settings = track.getSettings();
+ track.stop();
+ assert_true(settings.groupId == groupId, "device groupId");
+ assert_greater_than(settings.groupId.length, 0, "groupId is not empty");
+ }
+ }, 'groupId is correctly reported by getSettings() for all input devices');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.deviceId), "string",
+ "deviceId should exist and it should be a string.");
+ }, 'deviceId is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.groupId), "string",
+ "groupId should exist and it should be a string.");
+ }, 'groupId is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.sampleRate), "number",
+ "sampleRate should exist and it should be a number.");
+ assert_greater_than(settings.sampleRate, 0);
+ }, 'sampleRate is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.sampleSize), "number",
+ "sampleSize should exist and it should be a number.");
+ assert_greater_than(settings.sampleSize, 0);
+ }, 'sampleSize is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.echoCancellation), "boolean",
+ "echoCancellation should exist and it should be a boolean.");
+ }, 'echoCancellation is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.autoGainControl), "boolean",
+ "autoGainControl should exist and it should be a boolean.");
+ }, 'autoGainControl is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.noiseSuppression), "boolean",
+ "noiseSuppression should exist and it should be a boolean.");
+ }, 'noiseSuppression is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.voiceIsolation), "boolean",
+ "voiceIsolation should exist and it should be a boolean.");
+ }, 'voiceIsolation is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.latency), "number",
+ "latency should exist and it should be a number.");
+ assert_greater_than_equal(settings.latency,0);
+ }, 'latency is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "audio");
+ assert_equals(typeof(settings.channelCount), "number",
+ "channelCount should exist and it should be a number.");
+ assert_greater_than(settings.channelCount, 0);
+ }, 'channelCount is reported by getSettings() for getUserMedia() audio tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.deviceId), "string",
+ "deviceId should exist and it should be a string.");
+ }, 'deviceId is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.groupId), "string",
+ "groupId should exist and it should be a string.");
+ }, 'groupId is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.width), "number",
+ "width should exist and it should be a number.");
+ assert_true(Number.isInteger(settings.width), "width should be an integer.");
+ assert_greater_than_equal(settings.width, 0);;
+ }, 'width is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.height), "number",
+ "height should exist and it should be a number.");
+ assert_true(Number.isInteger(settings.height), "height should be an integer.");
+ assert_greater_than_equal(settings.height, 0);
+ }, 'height is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.aspectRatio), "number",
+ "aspectRatio should exist and it should be a number.");
+ assert_greater_than_equal(settings.aspectRatio, 0);
+ }, 'aspectRatio is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.frameRate), "number",
+ "frameRate should exist and it should be a number.");
+ assert_greater_than_equal(settings.frameRate, 0);
+ }, 'frameRate is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ // facingMode not treated as mandatory because not all platforms provide
+ // this information.
+ if (settings.facingMode) {
+ assert_equals(typeof(settings.facingMode), "string",
+ "If facingMode is provided it should be a string.");
+ assert_in_array(settings.facingMode,
+ ['user', 'environment', 'left', 'right']);
+ }
+ }, 'facingMode is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const settings = await createTrackAndGetSettings(t, "video");
+ assert_equals(typeof(settings.resizeMode), "string",
+ "resizeMode should exist and it should be a string.");
+ assert_in_array(settings.resizeMode, ['none', 'crop-and-scale']);
+ }, 'resizeMode is reported by getSettings() for getUserMedia() video tracks');
+
+ promise_test(async t => {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video : true});
+ const audioTrack = stream.getAudioTracks()[0];
+ const videoTrack = stream.getVideoTracks()[0];
+
+ const audioDeviceId = audioTrack.getSettings().deviceId;
+ const videoDeviceId = videoTrack.getSettings().deviceId;
+ const audioGroupId = audioTrack.getSettings().groupId;
+ const videoGroupId = videoTrack.getSettings().groupId;
+
+ audioTrack.stop();
+ videoTrack.stop();
+
+ assert_equals(audioTrack.getSettings().deviceId, audioDeviceId, "audio track deviceId");
+ assert_equals(videoTrack.getSettings().deviceId, videoDeviceId, "video track deviceId");
+ assert_equals(audioTrack.getSettings().groupId, audioGroupId, "audio track groupId");
+ assert_equals(videoTrack.getSettings().groupId, videoGroupId, "video track groupId");
+ }, 'Stopped tracks should expose deviceId/groupId');
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-id.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-id.https.html
new file mode 100644
index 0000000000..a9b4b99f87
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-id.https.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+<head>
+<title>Distinct id for distinct mediastream tracks</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrack-id">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your audio and video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that distinct mediastream tracks have distinct ids.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true})
+ assert_not_equals(stream.getVideoTracks()[0], stream.getAudioTracks()[0].id, "audio and video tracks have distinct ids");
+}, "Tests that distinct mediastream tracks have distinct ids ");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html
new file mode 100644
index 0000000000..3f132e10db
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>MediaStreamTrack transfer to iframe</title>
+<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>
+promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ await test_driver.bless('getDisplayMedia');
+ const stream = await navigator.mediaDevices.getDisplayMedia({audio:true, video: true});
+ const track = stream.getAudioTracks()[0];
+ const result = new Promise((resolve, reject) => {
+ window.onmessage = (e) => {
+ if (e.data.result === 'Failure') {
+ reject('Failed: ' + e.data.error);
+ } else {
+ resolve();
+ }
+ };
+ });
+ iframe.addEventListener("load", () => {
+ assert_not_equals(track.readyState, "ended");
+ iframe.contentWindow.postMessage(track);
+ assert_equals(track.readyState, "ended");
+ });
+ iframe.src = "support/iframe-MediaStreamTrack-transfer.html";
+ document.body.appendChild(iframe);
+ return result;
+});
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html
new file mode 100644
index 0000000000..2215954669
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-iframe-transfer.https.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>MediaStreamTrack transfer to iframe</title>
+<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>
+promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ await test_driver.bless('getDisplayMedia');
+ const stream = await navigator.mediaDevices.getDisplayMedia({video: true});
+ const track = stream.getVideoTracks()[0];
+ const iframeLoaded = new Promise((resolve) => {iframe.onload = resolve});
+
+ iframe.src = "support/iframe-MediaStreamTrack-transfer.html";
+ document.body.appendChild(iframe);
+
+ await iframeLoaded;
+
+ const nextMessage = new Promise((resolve) => {
+ window.onmessage = resolve
+ });
+
+ assert_not_equals(track.readyState, "ended");
+ iframe.contentWindow.postMessage(track);
+ assert_equals(track.readyState, "ended");
+
+ const message = await nextMessage;
+ assert_not_equals(message.data.result, 'Failure', 'Failed: ' + message.data.error);
+});
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-init.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-init.https.html
new file mode 100644
index 0000000000..54ebf049e8
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-init.https.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<html>
+<head>
+<title>getUserMedia({video:true}) creates a stream with a properly initialized video track</title>
+<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamTrack">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#life-cycle-and-media-flow">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrack-kind">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrack-enabled">
+<link rel="help" href="http://dev.w3.org/2011/webrtc/editor/getusermedia.html#widl-MediaStreamTrack-readyState">
+</head>
+<body>
+<p class="instructions">When prompted, accept to share your video stream.</p>
+<h1 class="instructions">Description</h1>
+<p class="instructions">This test checks that the video track of MediaStream
+object returned by the success callback in getUserMedia is correctly initialized.</p>
+
+<div id='log'></div>
+<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=permission-helper.js></script>
+<script>
+promise_test(async () => {
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const videoTracks = stream.getVideoTracks();
+ assert_equals(videoTracks.length, 1, "There is exactly one video track in the media stream");
+ track = videoTracks[0];
+ assert_equals(track.readyState, "live", "The track object is in live state");
+ assert_equals(track.kind, "video", "The track object is of video kind");
+ // Not clear that this is required by the spec,
+ // see https://www.w3.org/Bugs/Public/show_bug.cgi?id=22212
+ assert_true(track.enabled, "The track object is enabed");
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer-video.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer-video.https.html
new file mode 100644
index 0000000000..f38768a472
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer-video.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<title>MediaStreamTrack transfer to iframe</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+promise_test(async () => {
+ const iframe = document.createElement("iframe");
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const track = stream.getVideoTracks()[0];
+ const result = new Promise((resolve, reject) => {
+ window.onmessage = (e) => {
+ if (e.data.result === 'Failure') {
+ reject('Failed: ' + e.data.error);
+ } else {
+ resolve();
+ }
+ };
+ });
+ iframe.addEventListener("load", () => {
+ iframe.contentWindow.postMessage(track);
+ });
+ iframe.src = "support/iframe-MediaStreamTrack-transfer-video.html";
+ document.body.appendChild(iframe);
+ return result;
+});
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer.https.html
new file mode 100644
index 0000000000..e110b4b372
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-transfer.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<title>MediaStreamTrack transfer to Worker</title>
+<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=permission-helper.js></script>
+<script id="workerCode" type="javascript/worker">
+self.onmessage = (e) => {
+ try {
+ if(e.data instanceof MediaStreamTrack) {
+ self.postMessage({result: 'Success'});
+ return;
+ } else {
+ self.postMessage({
+ result: 'Failure',
+ error: `${e.data} is not a MediaStreamTrack`
+ });
+ }
+ } catch (error) {
+ self.postMessage({
+ result: 'Failure',
+ error
+ });
+ }
+}
+</script>
+<script>
+promise_test(async () => {
+ const workerBlob = new Blob([document.querySelector('#workerCode').textContent],
+ {type: "text/javascript"});
+ const workerUrl = window.URL.createObjectURL(workerBlob);
+ const worker = new Worker(workerUrl);
+ window.URL.revokeObjectURL(workerUrl);
+ await setMediaPermission("granted", ["camera"]);
+ const stream = await navigator.mediaDevices.getDisplayMedia({video: true});
+ const track = stream.getVideoTracks()[0];
+ const result = new Promise((resolve, reject) => {
+ worker.onmessage = (e) => {
+ if (e.data.result === 'Failure') {
+ reject('Failed: ' + e.data.error);
+ } else {
+ resolve();
+ }
+ };
+ });
+ worker.postMessage(track, [track]);
+ return result;
+});
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrackEvent-constructor.https.html b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrackEvent-constructor.https.html
new file mode 100644
index 0000000000..4946cd71d8
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrackEvent-constructor.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<title>MediaStreamTrackEvent constructor</title>
+<link rel="help" href="https://w3c.github.io/mediacapture-main/#mediastreamtrackevent">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {
+ assert_equals(MediaStreamTrackEvent.length, 2);
+ assert_throws_js(TypeError, function() {
+ new MediaStreamTrackEvent("type");
+ });
+ assert_throws_js(TypeError, function() {
+ new MediaStreamTrackEvent("type", null);
+ });
+ assert_throws_js(TypeError, function() {
+ new MediaStreamTrackEvent("type", undefined);
+ });
+}, "The eventInitDict argument is required");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ new MediaStreamTrackEvent("type", {});
+ });
+ assert_throws_js(TypeError, function() {
+ new MediaStreamTrackEvent("type", { track: null });
+ });
+ assert_throws_js(TypeError, function() {
+ new MediaStreamTrackEvent("type", { track: undefined });
+ });
+}, "The eventInitDict's track member is required.");
+
+test(function() {
+ // a MediaStreamTrack instance is needed to test, any instance will do.
+ var context = new AudioContext();
+ var dest = context.createMediaStreamDestination();
+ var track = dest.stream.getTracks()[0];
+ assert_true(track instanceof MediaStreamTrack);
+ var event = new MediaStreamTrackEvent("type", { track: track });
+ assert_equals(event.type, "type");
+ assert_equals(event.track, track);
+}, "The MediaStreamTrackEvent instance's track attribute is set.");
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/crashtests/enumerateDevices-after-discard-1.https.html b/testing/web-platform/tests/mediacapture-streams/crashtests/enumerateDevices-after-discard-1.https.html
new file mode 100644
index 0000000000..d1f4bab145
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/crashtests/enumerateDevices-after-discard-1.https.html
@@ -0,0 +1,18 @@
+<html class="test-wait">
+<head>
+ <title>
+ Test enumerateDevices() calls either side of browsing context discard
+ </title>
+</head>
+<script>
+ const frame = document.createElement('frame');
+ document.documentElement.appendChild(frame);
+ const devices = frame.contentWindow.navigator.mediaDevices;
+ devices.enumerateDevices();
+ frame.remove();
+ devices.enumerateDevices();
+ // Wait long enough to expect the async enumerateDevices() code to complete.
+ navigator.mediaDevices.enumerateDevices().then(
+ () => document.documentElement.removeAttribute("class"));
+</script>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/enumerateDevices-with-navigation.https.html b/testing/web-platform/tests/mediacapture-streams/enumerateDevices-with-navigation.https.html
new file mode 100644
index 0000000000..d951c1cdff
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/enumerateDevices-with-navigation.https.html
@@ -0,0 +1,77 @@
+<!doctype html>
+<title>enumerateDevices() with navigation</title>
+<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>
+<body></body>
+<script>
+'use strict';
+const blank_url = '/common/blank.html';
+const search2 = '?2';
+
+function promise_new_task(t) {
+ return new Promise(resolve => t.step_timeout(resolve, 0));
+}
+function promise_event(target, name) {
+ return new Promise(resolve => target[`on${name}`] = resolve);
+}
+
+promise_test(async t => {
+ // Gecko persists only toplevel documents, so load documents in a toplevel.
+ await test_driver.bless('window.open()');
+ const proxy = window.open(blank_url);
+ t.add_cleanup(() => proxy.close());
+ await promise_event(proxy, 'pageshow');
+ const devices = proxy.navigator.mediaDevices;
+ // Use another task so that another load creates a new session history entry.
+ await promise_new_task(t);
+
+ proxy.location = blank_url + search2;
+ await promise_event(proxy, 'pagehide');
+ // Use another task to ensure the first subdocument is no longer fully
+ // active and proxy refers to the realm of the second document.
+ await promise_new_task(t);
+ assert_equals(proxy.location.search, search2, 'navigated search');
+ // Enumerate from the inactive first Window.
+ const promise_enumerate = devices.enumerateDevices();
+ // `then()` is used rather than static Promise methods because microtasks
+ // for `PromiseResolve()` do not run when Promises from inactive realms are
+ // involved. Whether microtasks for `then()` run depends on the realm of
+ // the handler rather than the realm of the Promise.
+ // Don't use `finally()`, because it uses `PromiseResolve()` and so
+ // microtasks don't run.
+ // See https://github.com/whatwg/html/issues/5319.
+ let promise_state = 'pending';
+ promise_enumerate.then(() => promise_state = 'resolved',
+ () => promise_state = 'rejected');
+ // Enumerate in the active second Window to provide enough time to check
+ // that the Promise from the inactive Window does not settle.
+ await proxy.navigator.mediaDevices.enumerateDevices();
+
+ proxy.history.back();
+ await promise_event(proxy, 'pagehide');
+ // enumerateDevices() Promise resolution is triggered only in parallel
+ // steps, so manipulation of the Promise (if the first document was
+ // persisted) would occur through a queued task, which would run after
+ // the pagehide event is dispatched and so after the associated
+ // microtask that runs the following assert.
+ // https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-for-spec-authors
+ assert_equals(promise_state, 'pending', 'Promise state while inactive');
+ // If the first document is restored, then that will occur immediately after
+ // pagehide (and associated microtasks), before the next global task is run.
+ // https://html.spec.whatwg.org/multipage/history.html#traverse-the-history-by-a-delta
+ await promise_new_task(t);
+ if (proxy.navigator.mediaDevices == devices) {
+ // The first document was persisted and restored.
+ assert_equals(proxy.location.search, '', 'history search');
+ await promise_enumerate;
+ } else {
+ // The first document was not restored, but gets re-fetched.
+ await t.step_wait(() => proxy.location.search == '', 'navigation');
+ assert_not_equals(proxy.navigator.mediaDevices, devices, 'new realm')
+ await proxy.navigator.mediaDevices.enumerateDevices();
+ assert_equals(promise_state, 'pending', 'Promise state after discard');
+ }
+}, 'enumerateDevices with navigation');
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/historical.https.html b/testing/web-platform/tests/mediacapture-streams/historical.https.html
new file mode 100644
index 0000000000..84326cec0a
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/historical.https.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Historical Media Capture and Streams features</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ assert_false("webkitMediaStream" in window);
+}, "webkitMediaStream interface should not exist");
+
+test(function() {
+ assert_false("webkitGetUserMedia" in navigator);
+}, "navigator.webkitGetUserMedia should not exist");
+
+test(function() {
+ assert_false("mozGetUserMedia" in navigator);
+}, "navigator.mozGetUserMedia should not exist");
+
+test(() => {
+ const mediaStream = new MediaStream();
+ assert_throws_js(TypeError, () => URL.createObjectURL(mediaStream));
+}, "Passing MediaStream to URL.createObjectURL() should throw");
+
+test(() => {
+ const mediaStream = new MediaStream();
+ assert_false("onactive" in mediaStream);
+}, "MediaStream.onactive should not exist");
+
+test(() => {
+ const mediaStream = new MediaStream();
+ assert_false("oninactive" in mediaStream);
+}, "MediaStream.oninactive should not exist");
+</script>
diff --git a/testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js b/testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js
new file mode 100644
index 0000000000..d8b7bc8d7a
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js
@@ -0,0 +1,50 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+// https://w3c.github.io/mediacapture-main/
+
+idl_test(
+ ['mediacapture-streams'],
+ ['webidl', 'dom', 'html', 'permissions'],
+ async idl_array => {
+ const inputDevices = [];
+ const outputDevices = [];
+ try {
+ const list = await navigator.mediaDevices.enumerateDevices();
+ for (const device of list) {
+ if (device.kind in self) {
+ continue;
+ }
+ assert_in_array(device.kind, ['audioinput', 'videoinput', 'audiooutput']);
+ self[device.kind] = device;
+ if (device.kind.endsWith('input')) {
+ inputDevices.push(device.kind);
+ } else {
+ outputDevices.push(device.kind);
+ }
+ }
+ } catch (e) {}
+
+ try {
+ self.stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ self.track = stream.getTracks()[0];
+ self.trackEvent = new MediaStreamTrackEvent("type", {
+ track: track,
+ });
+ } catch (e) {}
+
+ idl_array.add_objects({
+ InputDeviceInfo: inputDevices,
+ MediaStream: ['stream', 'new MediaStream()'],
+ Navigator: ['navigator'],
+ MediaDevices: ['navigator.mediaDevices'],
+ MediaDeviceInfo: outputDevices,
+ MediaStreamTrack: ['track'],
+ MediaStreamTrackEvent: ['trackEvent'],
+ OverconstrainedError: ['new OverconstrainedError("constraint")'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html
new file mode 100644
index 0000000000..27dd046ac5
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="message-enumerateddevices.js"></script>
diff --git a/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html.headers b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html.headers
new file mode 100644
index 0000000000..ac4ddd8aba
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-cleared.html.headers
@@ -0,0 +1 @@
+Clear-Site-Data: "cookies"
diff --git a/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-nogum.html b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-nogum.html
new file mode 100644
index 0000000000..394575ed48
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate-nogum.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="message-enumerateddevices-nogum.js"></script>
diff --git a/testing/web-platform/tests/mediacapture-streams/iframe-enumerate.html b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate.html
new file mode 100644
index 0000000000..27dd046ac5
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/iframe-enumerate.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script src="message-enumerateddevices.js"></script>
diff --git a/testing/web-platform/tests/mediacapture-streams/message-enumerateddevices-nogum.js b/testing/web-platform/tests/mediacapture-streams/message-enumerateddevices-nogum.js
new file mode 100644
index 0000000000..3a503e0ce5
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/message-enumerateddevices-nogum.js
@@ -0,0 +1,6 @@
+onmessage = async e => {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ e.source.postMessage({
+ devices: devices.map(d => d.toJSON())
+ }, '*');
+}
diff --git a/testing/web-platform/tests/mediacapture-streams/message-enumerateddevices.js b/testing/web-platform/tests/mediacapture-streams/message-enumerateddevices.js
new file mode 100644
index 0000000000..4541636b4b
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/message-enumerateddevices.js
@@ -0,0 +1,8 @@
+onmessage = async e => {
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ stream.getTracks().forEach(t => t.stop());
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ e.source.postMessage({
+ devices: devices.map(d => d.toJSON())
+ }, '*');
+}
diff --git a/testing/web-platform/tests/mediacapture-streams/overconstrained_error.https.html b/testing/web-platform/tests/mediacapture-streams/overconstrained_error.https.html
new file mode 100644
index 0000000000..abaff3b773
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/overconstrained_error.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+</head>
+<body>
+<script>
+
+promise_test(async t => {
+ try {
+ stream = await navigator.mediaDevices.getUserMedia(
+ {video: {width: {exact: 639}, resizeMode: {exact: "none"}}});
+ t.add_cleanup(()=>stream.getVideoTracks()[0].stop());
+ t.step(() => assert_unreached('applyConstraints should have failed'));
+ } catch(e) {
+ assert_true(e instanceof DOMException);
+ assert_equals(e.name, 'OverconstrainedError');
+ assert_equals(e.constraint, 'width');
+ }
+}, 'Error of OverconstrainedError type inherit from DOMException');
+
+promise_test(async t => {
+ assert_true(new OverconstrainedError("constraint") instanceof DOMException);
+}, 'OverconstrainedError class inherits from DOMException');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/parallel-capture-requests.https.html b/testing/web-platform/tests/mediacapture-streams/parallel-capture-requests.https.html
new file mode 100644
index 0000000000..301515d1bd
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/parallel-capture-requests.https.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<html>
+<head>
+<title>Parallel capture requests</title>
+</head>
+<body>
+<button id="button">User gesture</button>
+<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>
+async function getDisplayMedia(constraints) {
+ const p = new Promise(r => button.onclick = r);
+ await test_driver.click(button);
+ await p;
+ return navigator.mediaDevices.getDisplayMedia(constraints);
+}
+
+promise_test(function() {
+ const getUserMediaPromise =
+ navigator.mediaDevices.getUserMedia({audio: true, video:true});
+ const getDisplayMediaPromise =
+ getDisplayMedia({video: true, audio: true});
+ return Promise.all([getUserMediaPromise, getDisplayMediaPromise])
+ .then(function(s) {
+ assert_greater_than_equal(s[0].getTracks().length, 1);
+ assert_less_than_equal(s[0].getTracks().length, 2);
+ assert_equals(s[0].getVideoTracks().length, 1);
+ assert_less_than_equal(s[0].getAudioTracks().length, 1);
+ assert_greater_than_equal(s[1].getTracks().length, 1);
+ assert_less_than_equal(s[1].getTracks().length, 2);
+ assert_equals(s[1].getVideoTracks().length, 1);
+ assert_less_than_equal(s[1].getAudioTracks().length, 1);
+ });
+}, 'getDisplayMedia() and parallel getUserMedia()');
+
+promise_test(function() {
+ const getDisplayMediaPromise =
+ getDisplayMedia({video: true, audio: true});
+ const getUserMediaPromise =
+ navigator.mediaDevices.getUserMedia({audio: true, video:true});
+ return Promise.all([getDisplayMediaPromise, getUserMediaPromise])
+ .then(function(s) {
+ assert_greater_than_equal(s[0].getTracks().length, 1);
+ assert_less_than_equal(s[0].getTracks().length, 2);
+ assert_equals(s[0].getVideoTracks().length, 1);
+ assert_less_than_equal(s[0].getAudioTracks().length, 1);
+ assert_greater_than_equal(s[1].getTracks().length, 1);
+ assert_less_than_equal(s[1].getTracks().length, 2);
+ assert_equals(s[1].getVideoTracks().length, 1);
+ assert_less_than_equal(s[1].getAudioTracks().length, 1);
+ });
+}, 'getUserMedia() and parallel getDisplayMedia()');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/permission-helper.js b/testing/web-platform/tests/mediacapture-streams/permission-helper.js
new file mode 100644
index 0000000000..0a237f7d43
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/permission-helper.js
@@ -0,0 +1,24 @@
+// Set permissions for camera and microphone using Web Driver
+// Status can be one of "granted" or "denied"
+// Scope take values from permission names
+async function setMediaPermission(status="granted", scope=["camera", "microphone"]) {
+ try {
+ for (let s of scope) {
+ await test_driver.set_permission({ name: s }, status);
+ }
+ } catch (e) {
+ const noSetPermissionSupport = typeof e === "string" && e.match(/set_permission not implemented/);
+ if (!(noSetPermissionSupport ||
+ (e instanceof Error && e.message.match("unimplemented")) )) {
+ throw e;
+ }
+ // Web Driver not implemented action
+ // FF: https://bugzilla.mozilla.org/show_bug.cgi?id=1524074
+
+ // with current WPT runners, will default to granted state for FF and Safari
+ // throw if status!="granted" to invalidate test results
+ if (status === "denied") {
+ assert_implements_optional(!noSetPermissionSupport, "Unable to set permission to denied for this test");
+ }
+ }
+}
diff --git a/testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer-video.html b/testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer-video.html
new file mode 100644
index 0000000000..9f37ba0ffa
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer-video.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>iframe</title>
+ <script>
+ function onMsg(e) {
+ if(e.data instanceof MediaStreamTrack) {
+ const track = e.data;
+ video = document.getElementById("myvideo");
+ video.srcObject = new MediaStream ([track]);
+ video.play();
+
+ parent.postMessage({result: 'Success'});
+ } else {
+ parent.postMessage({
+ result: 'Failure',
+ error: `${e.data} is not a MediaStreamTrack`
+ });
+ }
+ }
+ window.addEventListener("message", onMsg);
+ </script>
+ </head>
+ <body>
+ <video id="myvideo"></video>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer.html b/testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer.html
new file mode 100644
index 0000000000..e8d6aac647
--- /dev/null
+++ b/testing/web-platform/tests/mediacapture-streams/support/iframe-MediaStreamTrack-transfer.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>iframe</title>
+ <script>
+ function onMsg(e) {
+ if(e.data instanceof MediaStreamTrack) {
+ parent.postMessage({result: 'Success'});
+ } else {
+ parent.postMessage({
+ result: 'Failure',
+ error: `${e.data} is not a MediaStreamTrack`
+ });
+ }
+ }
+ window.addEventListener("message", onMsg);
+ </script>
+ </head>
+ <body>
+ </body>
+</html>