Bug 1712892 skip selectAudioOutput() prompt if the requested device has been previously allowed r=jib

Permission grants are persistent, supporting content use of
selectAudioOutput(previousDeviceId) to maintain the same output(s) on
subsequent same-origin page loads.

Permissions can be revoked from the site permissions panel.

Differential Revision: https://phabricator.services.mozilla.com/D162234
This commit is contained in:
Karl Tomlinson 2022-12-13 09:37:35 +00:00
Родитель 281ca8cf56
Коммит 7d1c6fc239
4 изменённых файлов: 110 добавлений и 30 удалений

Просмотреть файл

@ -380,8 +380,36 @@ class WebRTCParent extends JSWindowActorParent {
if (aRequest.sharingScreen) {
return false;
}
if (aRequest.audioOutputDevices?.length) {
return false;
let {
callID,
windowID,
audioInputDevices,
videoInputDevices,
audioOutputDevices,
hasInherentAudioConstraints,
hasInherentVideoConstraints,
audioOutputId,
} = aRequest;
if (audioOutputDevices?.length) {
// Prompt if a specific device is not requested, available and allowed.
let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
if (
!device ||
!lazy.SitePermissions.getForPrincipal(
aPrincipal,
["speaker", device.id].join("^"),
this.getBrowser()
).state == lazy.SitePermissions.ALLOW
) {
return false;
}
this.sendAsyncMessage("webrtc:Allow", {
callID,
windowID,
devices: [device.deviceIndex],
});
return true;
}
let { perms } = Services;
@ -400,15 +428,6 @@ class WebRTCParent extends JSWindowActorParent {
aRequest.secondOrigin;
let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
let {
callID,
windowID,
audioInputDevices,
videoInputDevices,
hasInherentAudioConstraints,
hasInherentVideoConstraints,
} = aRequest;
// We consider a camera or mic active if it is active or was active within a
// grace period of milliseconds ago.
const isAllowed = ({ mediaSource, rawId }, permissionID) =>
@ -1149,6 +1168,14 @@ function prompt(aActor, aBrowser, aRequest) {
let allowSpeaker = audioDeviceIndex != "-1";
if (allowSpeaker) {
allowedDevices.push(audioDeviceIndex);
let { id } = audioOutputDevices.find(
({ deviceIndex }) => deviceIndex == audioDeviceIndex
);
lazy.SitePermissions.setForPrincipal(
principal,
["speaker", id].join("^"),
lazy.SitePermissions.ALLOW
);
}
}

Просмотреть файл

@ -8,12 +8,18 @@ const permissionError =
"error: NotAllowedError: The request is not allowed " +
"by the user agent or the platform in the current context.";
async function requestAudioOutputExpectingPrompt() {
async function requestAudioOutput(options) {
await Promise.all([
promisePopupNotificationShown("webRTC-shareDevices"),
expectObserverCalled("getUserMedia:request"),
expectObserverCalled("recording-window-ended"),
promiseRequestAudioOutput(),
promiseRequestAudioOutput(options),
]);
}
async function requestAudioOutputExpectingPrompt(options) {
await Promise.all([
promisePopupNotificationShown("webRTC-shareDevices"),
requestAudioOutput(options),
]);
is(
@ -54,20 +60,24 @@ async function simulateAudioOutputRequest(options) {
);
}
async function allow() {
async function allowPrompt() {
const observerPromise = expectObserverCalled("getUserMedia:response:allow");
await promiseMessage("ok", () => {
PopupNotifications.panel.firstElementChild.button.click();
});
PopupNotifications.panel.firstElementChild.button.click();
await observerPromise;
}
async function allow() {
await Promise.all([promiseMessage("ok"), allowPrompt()]);
}
async function denyPrompt() {
const observerPromise = expectObserverCalled("getUserMedia:response:deny");
activateSecondaryAction(kActionDeny);
await observerPromise;
}
async function deny() {
const observerPromise = expectObserverCalled("getUserMedia:response:deny");
await promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
await observerPromise;
await Promise.all([promiseMessage(permissionError), denyPrompt()]);
}
async function escapePrompt() {
@ -82,13 +92,27 @@ async function escape() {
var gTests = [
{
desc: 'User clicks "Allow"',
desc: 'User clicks "Allow" and revokes',
run: async function checkAllow() {
await requestAudioOutputExpectingPrompt();
await allow();
info("selectAudioOutput() with no deviceId again should prompt again.");
await requestAudioOutputExpectingPrompt();
await allow();
info("selectAudioOutput() with same deviceId should not prompt again.");
await Promise.all([
expectObserverCalled("getUserMedia:response:allow"),
promiseMessage("ok"),
requestAudioOutput({ requestSameDevice: true }),
]);
await revokePermission("speaker", true);
info("Same deviceId should prompt again after revoked permission.");
await requestAudioOutputExpectingPrompt({ requestSameDevice: true });
await allow();
await revokePermission("speaker", true);
},
},
{
@ -106,6 +130,7 @@ var gTests = [
info("selectAudioOutput() after Esc should prompt again.");
await requestAudioOutputExpectingPrompt();
await allow();
await revokePermission("speaker", true);
},
},
{
@ -122,16 +147,39 @@ var gTests = [
{
desc: "Multi Device with deviceId",
run: async function checkMulti() {
const deviceCount = 4;
await Promise.all([
promisePopupNotificationShown("webRTC-shareDevices"),
simulateAudioOutputRequest({ deviceCount: 4, deviceId: "id 2" }),
simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
]);
const selectorList = document.getElementById(
`webRTC-selectSpeaker-menulist`
);
is(selectorList.selectedIndex, 2, "pre-selected index");
checkDeviceSelectors(["speaker"]);
await allowPrompt();
info("Expect same-device request allowed without prompt");
await Promise.all([
expectObserverCalled("getUserMedia:response:allow"),
simulateAudioOutputRequest({ deviceCount, deviceId: "id 2" }),
]);
info("Expect prompt for different-device request");
await Promise.all([
promisePopupNotificationShown("webRTC-shareDevices"),
simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
]);
await denyPrompt();
info("Expect prompt again for denied-device request");
await Promise.all([
promisePopupNotificationShown("webRTC-shareDevices"),
simulateAudioOutputRequest({ deviceCount, deviceId: "id 1" }),
]);
await escapePrompt();
await revokePermission("speaker", true);
},
},
{

Просмотреть файл

@ -74,10 +74,15 @@ async function requestDevice(aAudio, aVideo, aShare, aBadDevice = false) {
}
}
async function requestAudioOutput() {
let selectedAudioOutputId;
async function requestAudioOutput(options = {}) {
const audioOutputOptions = options.requestSameDevice && {
deviceId: selectedAudioOutputId,
};
SpecialPowers.wrap(document).notifyUserGestureActivation();
try {
await navigator.mediaDevices.selectAudioOutput();
({ deviceId: selectedAudioOutputId } =
await navigator.mediaDevices.selectAudioOutput(audioOutputOptions));
message("ok");
} catch (err) {
message("error: " + err);

Просмотреть файл

@ -705,12 +705,12 @@ async function promiseRequestDevice(
);
}
async function promiseRequestAudioOutput() {
async function promiseRequestAudioOutput(options) {
info("requesting audio output");
const bc = gBrowser.selectedBrowser;
return SpecialPowers.spawn(bc, [], async function() {
return SpecialPowers.spawn(bc, [options], async function(opts) {
const global = content.wrappedJSObject;
global.requestAudioOutput();
global.requestAudioOutput(Cu.cloneInto(opts, content));
});
}