diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/audio-output | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/audio-output')
12 files changed, 531 insertions, 0 deletions
diff --git a/testing/web-platform/tests/audio-output/META.yml b/testing/web-platform/tests/audio-output/META.yml new file mode 100644 index 0000000000..b6a7d4d062 --- /dev/null +++ b/testing/web-platform/tests/audio-output/META.yml @@ -0,0 +1,4 @@ +spec: https://w3c.github.io/mediacapture-output/ +suggested_reviewers: + - guidou + - jan-ivar diff --git a/testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html b/testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html new file mode 100644 index 0000000000..4e5652e68f --- /dev/null +++ b/testing/web-platform/tests/audio-output/enumerateDevices-permissions-policy.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<head> + <title> + Test permissions policy on enumerateDevices() after getUserMedia() + </title> + <link rel="help" href="https://w3c.github.io/mediacapture-output/#privacy-obtaining-consent"> + <meta charset=utf-8> +<body> + <p class="instructions">If prompted, <strong>please allow</strong> access to + a microphone device.</p> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/permissions-policy/resources/permissions-policy.js"></script> + <script> +'use strict'; + +promise_test(async () => { + const frame = document.createElement('iframe'); + frame.allow = "speaker-selection 'none'" + const promise_load = new Promise(r => frame.onload = r); + document.body.appendChild(frame); + await promise_load; + + const fDevices = frame.contentWindow.navigator.mediaDevices; + await fDevices.getUserMedia({ audio: true }); + const list = await fDevices.enumerateDevices(); + const outputDevicesList = list.filter(({kind}) => kind == "audiooutput"); + assert_equals(outputDevicesList.length, 0, "number of output devices."); +}, "permissions policy on enumerateDevices() after getUserMedia()"); + </script> +</body> diff --git a/testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html b/testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html new file mode 100644 index 0000000000..c1a825592a --- /dev/null +++ b/testing/web-platform/tests/audio-output/enumerateDevices-with-selectAudioOutput.https.html @@ -0,0 +1,48 @@ +<!doctype html> +<head> +<title>Test effect of selectAudioOutput() on audiooutput devices from enumerateDevices()</title> +<link rel="help" href="https://w3c.github.io/mediacapture-output/#dom-mediadevices-selectaudiooutput"> +</head> +<body> + <p class="instructions">If prompted, <strong>please allow</strong> access to + an audio output device.</p> +</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> +'use strict'; + +promise_test(async () => { + const devices = await navigator.mediaDevices.enumerateDevices(); + const outputDevices = devices.filter(info => info.kind == "audiooutput"); + assert_equals(outputDevices.length, 0, "number of audiooutput devices."); +}, "enumerateDevices() returns no audiooutput devices before permission grant"); + +let selected; + +promise_test(async t => { + await test_driver.bless('transient activation for selectAudioOutput()'); + selected = await navigator.mediaDevices.selectAudioOutput(); + assert_true(selected instanceof MediaDeviceInfo, + "resolves with a MediaDeviceInfo."); + assert_equals(selected.kind, "audiooutput", "selected.kind"); + assert_greater_than(selected.deviceId.length, 0, "selected.deviceId.length"); + assert_greater_than(selected.groupId.length, 0, "selected.groupId.length"); + assert_not_equals(selected.label, undefined, "selected.label"); +}, "selectAudioOutput()"); + +promise_test(async () => { + // "Once a device is exposed after a call to selectAudioOutput, it MUST be + // listed by enumerateDevices() for the current browsing context." + const devices = await navigator.mediaDevices.enumerateDevices(); + const outputDevices = devices.filter(info => info.kind == "audiooutput"); + assert_equals(outputDevices.length, 1, "number of audiooutput devices."); + assert_not_equals(selected, undefined); + const [info] = outputDevices; + assert_equals(info.deviceId, selected.deviceId, "deviceId exposed"); + assert_equals(info.groupId, selected.groupId, "groupId exposed"); + assert_equals(info.label, selected.label, "label exposed"); +}, "enumerateDevices() after selectAudioOutput()"); +</script> diff --git a/testing/web-platform/tests/audio-output/idlharness.https.window.js b/testing/web-platform/tests/audio-output/idlharness.https.window.js new file mode 100644 index 0000000000..d7cdbd0768 --- /dev/null +++ b/testing/web-platform/tests/audio-output/idlharness.https.window.js @@ -0,0 +1,20 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +// https://w3c.github.io/mediacapture-output/ + +'use strict'; + +idl_test( + ['audio-output'], + ['mediacapture-streams', 'html', 'dom'], + idl_array => { + self.audio = document.createElement('audio'); + self.video = document.createElement('video'); + idl_array.add_objects({ + HTMLAudioElement: ['audio'], + HTMLVideoElement: ['video'], + MediaDevices: ['navigator.mediaDevices'], + }); + } +); diff --git a/testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html b/testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html new file mode 100644 index 0000000000..97db5f81e2 --- /dev/null +++ b/testing/web-platform/tests/audio-output/removeTrack-after-setSinkId.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test source track removal after setSinkId does not crash</title> +</head> +<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="/common/gc.js"></script> +<script> +'use strict'; +// This could be a crashtest, except that testdriver.bless() is not functional +// in crashtests. promise_test() is more elegant than class="test-wait" anyway. +promise_test(async t => { + await test_driver.bless('transient activation'); + const {deviceId, label} = await navigator.mediaDevices.selectAudioOutput(); + const audio = new Audio(); + await audio.setSinkId(deviceId); + audio.srcObject = new AudioContext().createMediaStreamDestination().stream; + audio.play(); + await new Promise(r => t.step_timeout(r, 0)); + audio.srcObject.removeTrack(audio.srcObject.getTracks()[0]); + await garbageCollect(); +}); +</script> +</html> diff --git a/testing/web-platform/tests/audio-output/secure-context.html b/testing/web-platform/tests/audio-output/secure-context.html new file mode 100644 index 0000000000..7b4dd229a6 --- /dev/null +++ b/testing/web-platform/tests/audio-output/secure-context.html @@ -0,0 +1,23 @@ +<!doctype html> +<html> +<head> +<title>setSinkId and sinkId are SecureContext</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +</head> +<body> +<script> +let audio; +setup(() => { + assert_false(window.isSecureContext, + "This test must be run in a non secure context"); + audio = document.createElement('audio'); +}); +for (const prop of ['sinkId', 'setSinkId']) { + test(() => { + assert_false(prop in audio); + }, `${prop} exposure`); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html b/testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html new file mode 100644 index 0000000000..41917bb1a6 --- /dev/null +++ b/testing/web-platform/tests/audio-output/selectAudioOutput-permissions-policy.https.sub.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<body> + <script src="/resources/testharness.js" + type="text/javascript{{GET[in-iframe]}}"></script> + <script src="/resources/testharnessreport.js" + type="text/javascript{{GET[in-iframe]}}"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/permissions-policy/resources/permissions-policy.js"></script> + <script> + 'use strict'; + + async function testSelectAudioOutput() { + await test_driver.bless('transient activation for selectAudioOutput()'); + const selected = await navigator.mediaDevices.selectAudioOutput(); + + let devices; + try { + devices = await navigator.mediaDevices.enumerateDevices(); + } catch (e) { + // Throw a unique error to avoid risk of false-pass if e should match + // an expected error from selectAudioOutput(). + throw Error(`enumerateDevices() failed with ${e}`); + } + const selected_devices = + devices.filter(info => info.deviceId == selected.deviceId); + // Don't use assert_not_equals() because testharness.js cannot be + // loaded in the iframe with testdriver.js. + if (selected_devices.length != 1) { + throw Error('Device count matching selected is ' + + selected_devices.length); + } + } + + if (page_loaded_in_iframe()) { + test_driver.set_test_context(window.parent); + } + const cross_domain = get_host_info().HTTPS_REMOTE_ORIGIN; + run_all_fp_tests_allow_self( + cross_domain, + 'speaker-selection', + 'NotAllowedError', + testSelectAudioOutput + ); + </script> +</body> diff --git a/testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html b/testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html new file mode 100644 index 0000000000..9f014476d2 --- /dev/null +++ b/testing/web-platform/tests/audio-output/selectAudioOutput-sans-user-activation.https.html @@ -0,0 +1,18 @@ +<!doctype html> +<head> +<title>Test selectAudioOutput() without user activation</title> +<link rel="help" href="https://w3c.github.io/mediacapture-output/#dom-mediadevices-selectaudiooutput"> +</head> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +'use strict'; +promise_test(t => { + const p = navigator.mediaDevices.selectAudioOutput(); + // Race a settled promise to check that the returned promise is already + // rejected. + return promise_rejects_dom( + t, 'InvalidStateError', Promise.race([p, Promise.resolve()]), + 'selectAudioOutput should have returned an already-rejected promise.'); +}, `selectAudioOutput() before user activation`); +</script> diff --git a/testing/web-platform/tests/audio-output/setSinkId-manual.https.html b/testing/web-platform/tests/audio-output/setSinkId-manual.https.html new file mode 100644 index 0000000000..a083cdf092 --- /dev/null +++ b/testing/web-platform/tests/audio-output/setSinkId-manual.https.html @@ -0,0 +1,132 @@ +<!doctype html> +<html> +<head> +<title>Test setSinkId behavior with permissions / device changes</title> +<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/> +<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid"> +<meta name="timeout" content="long"> + +</head> +<body> +<h1 class="instructions">Description</h1> +<p class="instructions">This test checks that <code>setSinkId</code> follows the algorithm, this includes manually checking the proper rendering on new output devices.</p> +<p class="instructions">When prompted to access microphones, please accept as this is the only current way to get permissions for associated output devices.</p> +<p class="instructions">For each authorized output device, check that selecting it makes the audio beat rendered on the corresponding device.</p> +<p>Available but unauthorized devices (only those for which we can gain permission can be selected):</p> +<ul id="available"></ul> +<p>Authorized devices:</p> +<ul id="authorized"></ul> +<audio controls id="beat" src="/media/sound_5.mp3" loop></audio> + +<div id='log'></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +const is_output = d => d.kind === "audiooutput"; +const by_id = (id) => d => d.groupId === id; +const is_input = d => d.kind === "audioinput"; +const audio = document.getElementById("beat"); +const available = document.getElementById("available"); +const authorized = document.getElementById("authorized"); + +let outputList; + +const selectDeviceTester = (t) => (e) => { + const groupId = e.target.dataset["groupid"]; + const device = outputList.find(by_id(groupId)); + if (audio.paused) audio.play(); + promise_test(pt => audio.setSinkId(device.deviceId).then(() => { + assert_equals(device.deviceId, audio.sinkId); + + const pass = document.createElement("button"); + const fail = document.createElement("button"); + + const result = (bool) => () => { + assert_true(bool, "Sound rendered on right device"); + fail.remove(); + pass.remove(); + audio.pause(); + e.target.checked = false; + e.target.disabled = true; + t.done(); + }; + + pass.style.backgroundColor = "#0f0"; + pass.textContent = "\u2713 Sound plays on " + device.label; + pass.addEventListener("click", result(true)); + + fail.style.backgroundColor = "#f00"; + fail.textContent = "\u274C Sound doesn't play on " + device.label; + fail.addEventListener("click", result(true)); + + const container = e.target.parentNode.parentNode; + container.appendChild(pass); + container.appendChild(fail); + }), "setSinkId for " + device.label + " resolves"); +}; + +const addAuthorizedDevice = (groupId) => { + const device = outputList.find(by_id(groupId)); + const async_t = async_test("Selecting output device " + device.label + " makes the audio rendered on the proper device"); + const li = document.createElement("li"); + const label = document.createElement("label"); + const input = document.createElement("input"); + input.type = "radio"; + input.name = "device"; + input.dataset["groupid"] = device.groupId; + input.addEventListener("change", selectDeviceTester(async_t)); + const span = document.createElement("span"); + span.textContent = device.label; + label.appendChild(input); + label.appendChild(span); + li.appendChild(label); + authorized.appendChild(li); +}; + +const authorizeDeviceTester = (t) => (e) => { + const groupId = e.target.dataset["groupid"]; + navigator.mediaDevices.getUserMedia({audio: {groupId}}) + .then( () => { + addAuthorizedDevice(groupId); + t.done(); + }); +}; + +promise_test(gum => + navigator.mediaDevices.getUserMedia({audio: true}).then( + () => { + promise_test(t => + navigator.mediaDevices.enumerateDevices().then(list => { + assert_not_equals(list.find(is_output), undefined, "media device list includes at least one audio output device"); + outputList = list.filter(is_output); + outputList.forEach(d => { + const li = document.createElement("li"); + assert_not_equals(d.label, "", "Audio Output Device Labels are available after permission grant"); + li.textContent = d.label; + // Check permission + promise_test(perm => navigator.permissions.query({name: "speaker", deviceId: d.deviceId}).then(({state}) => { + if (state === "granted") { + addAuthorizedDevice(d.groupId); + } else if (state === "prompt") { + const inp = list.find(inp => inp.kind === "audioinput" && inp.groupId === d.groupId); + if (inp || true) { + const async_t = async_test("Authorizing output devices via permission requests for microphones works"); + const button = document.createElement("button"); + button.textContent = "Authorize access"; + button.dataset["groupid"] = d.groupId; + button.addEventListener("click", async_t.step_func_done(authorizeDeviceTester(async_t))); + li.appendChild(button); + } + available.appendChild(li); + } + }, () => { + // if we can't query the permission, we assume it's granted :/ + addAuthorizedDevice(d.groupId); + }) + , "Query permission to use " + d.label); + }); + }), "List media devices"); + }), "Authorize mike access"); +</script> diff --git a/testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html b/testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html new file mode 100644 index 0000000000..8e26fc44a8 --- /dev/null +++ b/testing/web-platform/tests/audio-output/setSinkId-permissions-policy.https.sub.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<body> + <script src="/resources/testharness.js" + type="text/javascript{{GET[in-iframe]}}"></script> + <script src="/resources/testharnessreport.js" + type="text/javascript{{GET[in-iframe]}}"></script> + <script src="/common/get-host-info.sub.js"></script> + <script src="/permissions-policy/resources/permissions-policy.js"></script> + <script> + 'use strict'; + + async function testSetSinkId() { + const audio = new Audio(); + const p = audio.setSinkId(''); + // Race a settled promise to check that the returned promise is already + // settled. + return Promise.race([ + p, + Promise.reject(Error('setSinkId() promise not already settled')), + ]); + } + + const cross_domain = get_host_info().HTTPS_REMOTE_ORIGIN; + run_all_fp_tests_allow_self( + cross_domain, + 'speaker-selection', + 'NotAllowedError', + testSetSinkId + ); + </script> +</body> diff --git a/testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html b/testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html new file mode 100644 index 0000000000..dbe32e2606 --- /dev/null +++ b/testing/web-platform/tests/audio-output/setSinkId-with-selectAudioOutput.https.html @@ -0,0 +1,85 @@ +<!doctype html> +<head> +<title>Test setSinkId() before and after selectAudioOutput()</title> +<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid"> +</head> +<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> +"use strict"; + +let deviceId; +promise_test(async t => { + await test_driver.bless('transient activation for selectAudioOutput()'); + ({deviceId} = await navigator.mediaDevices.selectAudioOutput()); + + const audio = new Audio(); + const p1 = audio.setSinkId(deviceId); + assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged"); + let r = await p1; + assert_equals(r, undefined, "setSinkId resolves with undefined"); + assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId"); + + r = await audio.setSinkId(deviceId); + assert_equals(r, undefined, "resetting sinkid on same current value should always work"); + + r = await audio.setSinkId(""); + assert_equals(r, undefined, "resetting sinkid on default audio output should always work"); +}, "setSinkId() after selectAudioOutput()"); + +const src = "/media/movie_5.ogv"; + +promise_test(async t => { + assert_not_equals(deviceId, undefined, "selectAudioOutput() resolved"); + const video = document.createElement('video'); + try { + document.body.appendChild(video); + video.src = src; + await new Promise(r => video.onloadeddata = r); + await video.setSinkId(deviceId); + assert_equals(video.sinkId, deviceId, "sinkId after setSinkId()"); + } finally { + video.remove(); + } +}, "setSinkId() on video after loadeddata"); + +promise_test(async t => { + assert_not_equals(deviceId, undefined, "selectAudioOutput() resolved"); + const video = document.createElement('video'); + try { + video.src = src; + video.autoplay = true; + video.loop = true; + await new Promise(r => video.onplay = r); + await video.setSinkId(deviceId); + assert_equals(video.sinkId, deviceId, "sinkId after setSinkId()"); + } finally { + video.pause(); + } +}, "setSinkId() on video after play"); + +// Use the same sinkId in another same-origin top-level browsing context. +// "the identifier MUST be the same in documents of the same origin in +// top-level browsing contexts." +// https://w3c.github.io/mediacapture-main/#dom-mediadeviceinfo-deviceid +promise_test(async t => { + assert_not_equals(deviceId, undefined, "selectAudioOutput() resolved"); + const proxy = window.open('/common/blank.html'); + t.add_cleanup(() => proxy.close()); + await new Promise(r => proxy.onload = r); + const pAudio = new proxy.Audio(); + await promise_rejects_dom(t, "NotFoundError", proxy.DOMException, + pAudio.setSinkId(deviceId), + "before selectAudioOutput()"); + await test_driver.bless('transient activation for selectAudioOutput()', + null, proxy); + const { deviceId: pDeviceId } = + await proxy.navigator.mediaDevices.selectAudioOutput({deviceId}); + assert_equals(pDeviceId, deviceId, + "deviceIds should be same in each browsing context"); + await pAudio.setSinkId(deviceId); + assert_equals(pAudio.sinkId, deviceId, "sinkId after setSinkId()"); +}, "setSinkId() with deviceID from another window"); +</script> diff --git a/testing/web-platform/tests/audio-output/setSinkId.https.html b/testing/web-platform/tests/audio-output/setSinkId.https.html new file mode 100644 index 0000000000..be65f0ac81 --- /dev/null +++ b/testing/web-platform/tests/audio-output/setSinkId.https.html @@ -0,0 +1,62 @@ +<!doctype html> +<head> +<title>Test setSinkId behavior </title> +<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/> +<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid"> +</head> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/resources/testdriver.js></script> +<script src=/resources/testdriver-vendor.js></script> +<script src='../mediacapture-streams/permission-helper.js'></script> +<script> +"use strict"; + +const audio = new Audio(); + +promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work"); + +promise_test(t => promise_rejects_dom(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")), + "setSinkId fails with NotFoundError on made up deviceid"); + +promise_test(async t => { + // Attempt to expose some audiooutput devices. + await setMediaPermission("granted", ["microphone"]); + await navigator.mediaDevices.getUserMedia({ audio: true }); + const list = await navigator.mediaDevices.enumerateDevices(); + assert_greater_than(list.length, 0, + "media device list includes at least one device"); + const audioInputList = list.filter(({kind}) => kind == "audioinput"); + const outputDevicesList = list.filter(({kind}) => kind == "audiooutput"); + // List of exposed microphone groupIds + const exposedGroupIds = new Set(audioInputList.map(device => device.groupId)); + + for (const { deviceId, groupId } of outputDevicesList) { + assert_true(exposedGroupIds.has(groupId), + "audiooutput device groupId must match an exposed microphone"); + assert_greater_than(deviceId.length, 0, "deviceId.length"); + + const p1 = audio.setSinkId(deviceId); + assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged"); + let r; + try { + r = await p1; + } catch (e) { + // "If sinkId is not the empty string, and the application would not be + // permitted to play audio through the device identified by sinkId if it + // weren't the current user agent default device, reject p with a new + // DOMException whose name is NotAllowedError and abort these + // substeps." + assert_equals(e.name, "NotAllowedError", "Non-permitted devices are failing with NotAllowed error"); + continue; + } + assert_equals(r, undefined, "setSinkId resolves with undefined"); + assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId"); + r = await audio.setSinkId(deviceId); + assert_equals(r, undefined, "resetting sinkid on same current value should always work"); + r = await audio.setSinkId(""); + assert_equals(r, undefined, "resetting sinkid on default audio output should always work"); + } +}, "setSinkId() with output device IDs exposed by getUserMedia() should either reject with NotAllowedError or resolve"); + +</script> |