Bug 1958963 Expose all speaker devices, irrespective of groupId, upon microphone access. r=karlt
Relaxes exposure criteria for speakers in the navigator.mediaDevices.enumerateDevices() API, to match the recent spec change in w3c/mediacapture-output#150 and other implementations for web compatibility. The old behavior was to only expose speakers whose groupId is the same as that of any microphone, once the document is using a microphone. The new behavior is to expose all speakers once the document is using a microphone. The old behavior is retained for RFPTarget::MediaDevices, to avoid any new exposure there. Also fixes an assumption in WPT test MediaDevices-enumerateDevices-per-origin-ids.sub.https.html broken by this patch about the total number of devices exposed after gUM in an iframe vs the top frame. Differential Revision: https://phabricator.services.mozilla.com/D246001
This commit is contained in:
@@ -264,7 +264,8 @@ RefPtr<MediaDeviceSetRefCnt> MediaDevices::FilterExposedDevices(
|
|||||||
bool dropSpeakers =
|
bool dropSpeakers =
|
||||||
!Preferences::GetBool("media.setsinkid.enabled") ||
|
!Preferences::GetBool("media.setsinkid.enabled") ||
|
||||||
!FeaturePolicyUtils::IsFeatureAllowed(doc, u"speaker-selection"_ns);
|
!FeaturePolicyUtils::IsFeatureAllowed(doc, u"speaker-selection"_ns);
|
||||||
|
bool shouldResistFingerprinting =
|
||||||
|
window->AsGlobal()->ShouldResistFingerprinting(RFPTarget::MediaDevices);
|
||||||
bool legacy = IsLegacyMode(window);
|
bool legacy = IsLegacyMode(window);
|
||||||
bool outputIsDefault = true; // First output is the default.
|
bool outputIsDefault = true; // First output is the default.
|
||||||
bool haveDefaultOutput = false;
|
bool haveDefaultOutput = false;
|
||||||
@@ -293,8 +294,10 @@ RefPtr<MediaDeviceSetRefCnt> MediaDevices::FilterExposedDevices(
|
|||||||
case MediaDeviceKind::Audiooutput:
|
case MediaDeviceKind::Audiooutput:
|
||||||
if (dropSpeakers ||
|
if (dropSpeakers ||
|
||||||
(!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) &&
|
(!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) &&
|
||||||
// Assumes aDevices order has microphones before speakers.
|
(!mCanExposeMicrophoneInfo ||
|
||||||
!exposedMicrophoneGroupIds.Contains(device->mRawGroupID))) {
|
(shouldResistFingerprinting &&
|
||||||
|
// Assumes aDevices order has microphones before speakers.
|
||||||
|
!exposedMicrophoneGroupIds.Contains(device->mRawGroupID))))) {
|
||||||
outputIsDefault = false;
|
outputIsDefault = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,5 +161,6 @@ async function testLegacyEnumerateDevices() {
|
|||||||
["media.video_loopback_dev", "none"]
|
["media.video_loopback_dev", "none"]
|
||||||
);
|
);
|
||||||
devices = await navigator.mediaDevices.enumerateDevices();
|
devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
devices = devices.filter(({ kind }) => kind != "audiooutput");
|
||||||
is(devices.length, 0, "No devices");
|
is(devices.length, 0, "No devices");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ runTest(async () => {
|
|||||||
["media.audio_loopback_dev", "none"],
|
["media.audio_loopback_dev", "none"],
|
||||||
["media.video_loopback_dev", "none"]);
|
["media.video_loopback_dev", "none"]);
|
||||||
devices = await navigator.mediaDevices.enumerateDevices();
|
devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
devices = devices.filter(({kind}) => kind != "audiooutput");
|
||||||
is(devices.length, 0, "No devices");
|
is(devices.length, 0, "No devices");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,16 +8,20 @@
|
|||||||
<script>
|
<script>
|
||||||
/* global SimpleTest SpecialPowers */
|
/* global SimpleTest SpecialPowers */
|
||||||
|
|
||||||
async function testEnumerateDevices(expectDevices) {
|
async function testEnumerateDevices(header,
|
||||||
let devices = await navigator.mediaDevices.enumerateDevices();
|
expectedCameras,
|
||||||
if (!expectDevices) {
|
expectedMicrophones,
|
||||||
SimpleTest.is(devices.length, 0, "testEnumerateDevices: No devices");
|
expectedSpeakers) {
|
||||||
return;
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const cams = devices.filter(({kind}) => kind == "videoinput");
|
||||||
|
const mics = devices.filter(({kind}) => kind == "audioinput");
|
||||||
|
const spkr = devices.filter(({kind}) => kind == "audiooutput");
|
||||||
|
|
||||||
|
SimpleTest.is(cams.length, expectedCameras, `${header}: cameras`);
|
||||||
|
SimpleTest.is(mics.length, expectedMicrophones, `${header}: microphones`);
|
||||||
|
if (expectedSpeakers !== undefined) {
|
||||||
|
SimpleTest.is(spkr.length, expectedSpeakers, `${header}: speakers`);
|
||||||
}
|
}
|
||||||
let cams = devices.filter((device) => device.kind == "videoinput");
|
|
||||||
let mics = devices.filter((device) => device.kind == "audioinput");
|
|
||||||
SimpleTest.ok((cams.length == 1) && (mics.length == 1),
|
|
||||||
"testEnumerateDevices: a microphone and a camera");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testGetUserMedia(expectDevices) {
|
async function testGetUserMedia(expectDevices) {
|
||||||
@@ -63,14 +67,14 @@ async function testGetUserMedia(expectDevices) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testDevices() {
|
async function testPreGum() {
|
||||||
await SpecialPowers.pushPrefEnv({
|
await SpecialPowers.pushPrefEnv({
|
||||||
set: [
|
set: [
|
||||||
["privacy.resistFingerprinting", true],
|
["privacy.resistFingerprinting", true],
|
||||||
["media.navigator.streams.fake", true]
|
["media.navigator.streams.fake", true]
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
await testEnumerateDevices(true); // should list a microphone and a camera
|
await testEnumerateDevices("testPreGum w/resist", 1, 1, 0);
|
||||||
await testGetUserMedia(true); // should get audio and video streams
|
await testGetUserMedia(true); // should get audio and video streams
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,13 +88,13 @@ async function testNoDevices() {
|
|||||||
["media.video_loopback_dev", "bar"]
|
["media.video_loopback_dev", "bar"]
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
await testEnumerateDevices(false); // should list nothing
|
await testEnumerateDevices("testNoDevices wo/resist", 0, 0);
|
||||||
await SpecialPowers.pushPrefEnv({
|
await SpecialPowers.pushPrefEnv({
|
||||||
set: [
|
set: [
|
||||||
["privacy.resistFingerprinting", true]
|
["privacy.resistFingerprinting", true]
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
await testEnumerateDevices(true); // should list a microphone and a camera
|
await testEnumerateDevices("testNoDevices w/resist", 1, 1, 1);
|
||||||
await testGetUserMedia(false); // should reject with NotAllowedError
|
await testGetUserMedia(false); // should reject with NotAllowedError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +106,7 @@ createHTML({
|
|||||||
runTest(async () => {
|
runTest(async () => {
|
||||||
// Make sure enumerateDevices and getUserMedia work when
|
// Make sure enumerateDevices and getUserMedia work when
|
||||||
// privacy.resistFingerprinting is true.
|
// privacy.resistFingerprinting is true.
|
||||||
await testDevices();
|
await testPreGum();
|
||||||
|
|
||||||
// Test that absence of devices can't be detected.
|
// Test that absence of devices can't be detected.
|
||||||
await testNoDevices();
|
await testNoDevices();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ createHTML({ title: "Test group id of MediaDeviceInfo", bug: "1213453" });
|
|||||||
|
|
||||||
async function getDefaultDevices() {
|
async function getDefaultDevices() {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
is(devices.length, 2, "Two fake devices found.");
|
|
||||||
|
|
||||||
devices.forEach(d => isnot(d.groupId, "", "GroupId is included in every device"));
|
devices.forEach(d => isnot(d.groupId, "", "GroupId is included in every device"));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,29 @@
|
|||||||
[audiocontext-sinkid-constructor.https.html]
|
[audiocontext-sinkid-constructor.https.html]
|
||||||
expected:
|
expected:
|
||||||
if (os == "android") and fission: [ERROR, TIMEOUT]
|
if (os == "android") and fission: [ERROR, TIMEOUT]
|
||||||
|
if (os == "mac"): [ERROR, OK]
|
||||||
ERROR
|
ERROR
|
||||||
|
|
||||||
|
[Setting sinkId to the empty string at construction should succeed.]
|
||||||
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected: FAIL
|
||||||
|
|
||||||
|
[Setting sinkId with a valid device identifier at construction should succeed.]
|
||||||
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected: FAIL
|
||||||
|
|
||||||
|
[Setting sinkId with an AudioSinkOptions at construction should succeed.]
|
||||||
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected: FAIL
|
||||||
|
|
||||||
|
[Invalid sinkId argument with a wrong type should throw an appropriate exception.]
|
||||||
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected: FAIL
|
||||||
|
|
||||||
|
[Invalid sinkId argument with a wrong ID should dispatch an onerror event.]
|
||||||
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected: FAIL
|
||||||
|
|
||||||
|
[Invalid sinkId argument with a wrong type should throw an appropriate exception.]
|
||||||
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected: FAIL
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
[audiocontext-sinkid-setsinkid.https.html]
|
[audiocontext-sinkid-setsinkid.https.html]
|
||||||
expected:
|
expected:
|
||||||
if (os == "android") and fission: [ERROR, TIMEOUT]
|
if (os == "android") and fission: [ERROR, TIMEOUT]
|
||||||
|
if (os == "mac"): [ERROR, OK]
|
||||||
ERROR
|
ERROR
|
||||||
|
|
||||||
[setSinkId() with a valid device identifier should succeeded.]
|
[setSinkId() with a valid device identifier should succeeded.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[setSinkId() with the same sink ID should resolve immediately.]
|
[setSinkId() with the same sink ID should resolve immediately.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[setSinkId() with the same AudioSinkOptions.type value should resolve immediately.]
|
[setSinkId() with the same AudioSinkOptions.type value should resolve immediately.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[setSinkId() should fail with TypeError on an invalid AudioSinkOptions.type value.]
|
[setSinkId() should fail with TypeError on an invalid AudioSinkOptions.type value.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[setSinkId() should fail with NotFoundError on an invalid device identifier.]
|
[setSinkId() should fail with NotFoundError on an invalid device identifier.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[setSinkId() should fail with InvalidStateError when calling from astopped AudioContext]
|
[setSinkId() should fail with InvalidStateError when calling from astopped AudioContext]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[setSinkId() should fail with InvalidStateError when calling from adetached document]
|
[setSinkId() should fail with InvalidStateError when calling from adetached document]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[pending setSinkId() should be rejected with InvalidStateError whenAudioContext is closed]
|
[pending setSinkId() should be rejected with InvalidStateError whenAudioContext is closed]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
[audiocontext-sinkid-state-change.https.html]
|
[audiocontext-sinkid-state-change.https.html]
|
||||||
expected:
|
expected:
|
||||||
if (os == "android") and fission: [ERROR, TIMEOUT]
|
if (os == "android") and fission: [ERROR, TIMEOUT]
|
||||||
|
if (os == "mac"): [ERROR, OK]
|
||||||
ERROR
|
ERROR
|
||||||
[Calling setSinkId() on a suspended AudioContext should fire only sink change events.]
|
[Calling setSinkId() on a suspended AudioContext should fire only sink change events.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, FAIL]
|
||||||
|
NOTRUN
|
||||||
|
|
||||||
[Calling setSinkId() on a running AudioContext should fire both state and sink change events.]
|
[Calling setSinkId() on a running AudioContext should fire both state and sink change events.]
|
||||||
expected: NOTRUN
|
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1942786
|
||||||
|
expected:
|
||||||
|
if (os == "mac"): [NOTRUN, TIMEOUT]
|
||||||
|
NOTRUN
|
||||||
|
|||||||
@@ -26,14 +26,8 @@ promise_test(async t => {
|
|||||||
const list = await navigator.mediaDevices.enumerateDevices();
|
const list = await navigator.mediaDevices.enumerateDevices();
|
||||||
assert_greater_than(list.length, 0,
|
assert_greater_than(list.length, 0,
|
||||||
"media device list includes at least one device");
|
"media device list includes at least one device");
|
||||||
const audioInputList = list.filter(({kind}) => kind == "audioinput");
|
|
||||||
const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
|
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) {
|
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");
|
assert_greater_than(deviceId.length, 0, "deviceId.length");
|
||||||
|
|
||||||
const p1 = audio.setSinkId(deviceId);
|
const p1 = audio.setSinkId(deviceId);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
<script src=permission-helper.js></script>
|
<script src=permission-helper.js></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<iframe allow="camera 'src';microphone 'src'" id=same src="/mediacapture-streams/iframe-enumerate.html"></iframe>
|
<iframe allow="camera 'src';microphone 'src';speaker-selection '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>
|
<iframe allow="camera 'src';microphone 'src';speaker-selection 'src'" id=cross src="https://{{hosts[][www1]}}:{{ports[https][0]}}/mediacapture-streams/iframe-enumerate.html"></iframe>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
let deviceList;
|
let deviceList;
|
||||||
|
|||||||
Reference in New Issue
Block a user