diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/mediacapture-streams | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/mediacapture-streams')
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 >=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 >=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> |