Bug 1712898 add a basic prompt for selectAudioOutput() r=jib,flod,pbz

This contains the code paths that are similar to the prompt for getUserMedia().

Permanent permissions and grace period are not included here and their model
may differ from that of getUserMedia().
See https://bugzilla.mozilla.org/show_bug.cgi?id=1712892

Differential Revision: https://phabricator.services.mozilla.com/D115994
This commit is contained in:
Karl Tomlinson 2021-06-09 04:01:38 +00:00
parent b52cf6bbe2
commit 78defe09b9
7 changed files with 100 additions and 33 deletions

View File

@ -266,22 +266,22 @@ function handleGUMRequest(aSubject, aTopic, aData) {
GlobalMuteListener.init();
let constraints = aSubject.getConstraints();
let secure = aSubject.isSecure;
let isHandlingUserInput = aSubject.isHandlingUserInput;
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
prompt(
aSubject.type,
contentWindow,
aSubject.windowID,
aSubject.callID,
constraints,
aSubject.devices,
secure,
isHandlingUserInput
aSubject.isSecure,
aSubject.isHandlingUserInput
);
}
function prompt(
aRequestType,
aContentWindow,
aWindowID,
aCallID,
@ -292,6 +292,7 @@ function prompt(
) {
let audioInputDevices = [];
let videoInputDevices = [];
let audioOutputDevices = [];
let devices = [];
// MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
@ -303,18 +304,19 @@ function prompt(
audio && typeof audio != "boolean" && audio.mediaSource != "microphone";
for (let device of aDevices) {
device = device.QueryInterface(Ci.nsIMediaDevice);
let deviceObject = {
name: device.rawName, // unfiltered device name to show to the user
deviceIndex: devices.length,
id: device.rawId,
mediaSource: device.mediaSource,
};
switch (device.type) {
case "audioinput":
// Check that if we got a microphone, we have not requested an audio
// capture, and if we have requested an audio capture, we are not
// getting a microphone instead.
if (audio && (device.mediaSource == "microphone") != sharingAudio) {
audioInputDevices.push({
name: device.rawName, // unfiltered device name to show to the user
deviceIndex: devices.length,
id: device.rawId,
mediaSource: device.mediaSource,
});
audioInputDevices.push(deviceObject);
devices.push(device);
}
break;
@ -322,12 +324,6 @@ function prompt(
// Verify that if we got a camera, we haven't requested a screen share,
// or that if we requested a screen share we aren't getting a camera.
if (video && (device.mediaSource == "camera") != sharingScreen) {
let deviceObject = {
name: device.rawName, // unfiltered device name to show to the user
deviceIndex: devices.length,
id: device.rawId,
mediaSource: device.mediaSource,
};
if (device.scary) {
deviceObject.scary = true;
}
@ -335,6 +331,12 @@ function prompt(
devices.push(device);
}
break;
case "audiooutput":
if (aRequestType == "selectaudiooutput") {
audioOutputDevices.push(deviceObject);
devices.push(device);
}
break;
}
}
@ -345,6 +347,9 @@ function prompt(
if (audioInputDevices.length) {
requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
}
if (audioOutputDevices.length) {
requestTypes.push("Speaker");
}
if (!requestTypes.length) {
// Device enumeration is done ahead of handleGUMRequest, so we're not
@ -401,6 +406,7 @@ function prompt(
sharingAudio,
audioInputDevices,
videoInputDevices,
audioOutputDevices,
};
let actor = getActorForWindow(aContentWindow);

View File

@ -389,7 +389,12 @@ class WebRTCParent extends JSWindowActorParent {
return false;
}
let { audioInputDevices, videoInputDevices, sharingScreen } = aRequest;
let {
audioInputDevices,
videoInputDevices,
audioOutputDevices,
sharingScreen,
} = aRequest;
let micAllowed =
SitePermissions.getForPrincipal(aPrincipal, "microphone").state ==
@ -471,7 +476,8 @@ class WebRTCParent extends JSWindowActorParent {
}
if (
(!audioInputDevices.length || micAllowed || activeMic) &&
(!videoInputDevices.length || camAllowed || activeCamera)
(!videoInputDevices.length || camAllowed || activeCamera) &&
!audioOutputDevices.length
) {
let allowedDevices = [];
if (videoInputDevices.length) {
@ -522,6 +528,7 @@ function prompt(aActor, aBrowser, aRequest) {
let {
audioInputDevices,
videoInputDevices,
audioOutputDevices,
sharingScreen,
sharingAudio,
requestTypes,
@ -566,19 +573,16 @@ function prompt(aActor, aBrowser, aRequest) {
// If the user has already denied access once in this tab,
// deny again without even showing the notification icon.
if (
(audioInputDevices.length &&
SitePermissions.getForPrincipal(principal, "microphone", aBrowser)
.state == SitePermissions.BLOCK) ||
(videoInputDevices.length &&
SitePermissions.getForPrincipal(
principal,
sharingScreen ? "screen" : "camera",
aBrowser
).state == SitePermissions.BLOCK)
) {
aActor.denyRequest(aRequest);
return;
for (const type of requestTypes) {
const permissionID =
type == "AudioCapture" ? "microphone" : type.toLowerCase();
if (
SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
.state == SitePermissions.BLOCK
) {
aActor.denyRequest(aRequest);
return;
}
}
// Tell the browser to refresh the identity block display in case there
@ -606,6 +610,7 @@ function prompt(aActor, aBrowser, aRequest) {
"getUserMedia.shareMicrophoneUnsafeDelegations2.message",
"getUserMedia.shareScreenUnsafeDelegation2.message",
"getUserMedia.shareAudioCaptureUnsafeDelegation2.message",
"selectAudioOutput.shareSpeakerUnsafeDelegation.message",
// Combinations of the above request types last.
"getUserMedia.shareCameraAndMicrophoneUnsafeDelegation2.message",
"getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation2.message",
@ -619,6 +624,7 @@ function prompt(aActor, aBrowser, aRequest) {
"getUserMedia.shareMicrophone3.message",
"getUserMedia.shareScreen4.message",
"getUserMedia.shareAudioCapture3.message",
"selectAudioOutput.shareSpeaker.message",
// Combinations of the above request types last.
"getUserMedia.shareCameraAndMicrophone3.message",
"getUserMedia.shareCameraAndAudioCapture3.message",
@ -1102,12 +1108,18 @@ function prompt(aActor, aBrowser, aRequest) {
!sharingScreen || !videoInputDevices.length;
doc.getElementById("webRTC-selectMicrophone").hidden =
!audioInputDevices.length || sharingAudio;
doc.getElementById(
"webRTC-selectSpeaker"
).hidden = !audioOutputDevices.length;
let camMenupopup = doc.getElementById("webRTC-selectCamera-menupopup");
let windowMenupopup = doc.getElementById("webRTC-selectWindow-menupopup");
let micMenupopup = doc.getElementById(
"webRTC-selectMicrophone-menupopup"
);
let speakerMenupopup = doc.getElementById(
"webRTC-selectSpeaker-menupopup"
);
let describedByIDs = ["webRTC-shareDevices-notification-description"];
let describedBySuffix = "icon";
@ -1133,6 +1145,13 @@ function prompt(aActor, aBrowser, aRequest) {
}
}
let labelID = "webRTC-selectSpeaker-single-device-label";
listDevices(speakerMenupopup, audioOutputDevices, labelID);
if (audioInputDevices.length == 1) {
describedByIDs.push("webRTC-selectSpeaker-icon");
describedByIDs.push(labelID);
}
// PopupNotifications knows to clear the aria-describedby attribute
// when hiding, so we don't have to worry about cleaning it up ourselves.
chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
@ -1207,6 +1226,15 @@ function prompt(aActor, aBrowser, aRequest) {
allowedDevices.push(0);
}
}
if (audioOutputDevices.length) {
let audioDeviceIndex = doc.getElementById(
"webRTC-selectSpeaker-menulist"
).value;
let allowSpeaker = audioDeviceIndex != "-1";
if (allowSpeaker) {
allowedDevices.push(audioDeviceIndex);
}
}
if (!allowedDevices.length) {
aActor.denyRequest(aRequest);
@ -1257,6 +1285,12 @@ function prompt(aActor, aBrowser, aRequest) {
return false;
}
// "Always allow this speaker" not yet supported for
// selectAudioOutput(). Bug 1712892
if (audioOutputDevices.length) {
return false;
}
return true;
}

View File

@ -54,6 +54,16 @@
<label id="webRTC-selectMicrophone-single-device-label" class="webRTC-selectDevice-label"></label>
</html:div>
</popupnotificationcontent>
<popupnotificationcontent id="webRTC-selectSpeaker" orient="vertical">
<html:div class="webRTC-selectDevice-selector-container">
<xul:image id="webRTC-selectSpeaker-icon" data-l10n-id="popup-select-speaker-icon" class="webRTC-selectDevice-icon"/>
<menulist id="webRTC-selectSpeaker-menulist" aria-labelledby="webRTC-selectSpeaker-icon" size="large">
<menupopup id="webRTC-selectSpeaker-menupopup"/>
</menulist>
<label id="webRTC-selectSpeaker-single-device-label" class="webRTC-selectDevice-label"></label>
</html:div>
</popupnotificationcontent>
</popupnotification>
<popupnotification id="servicesInstall-notification" hidden="true">

View File

@ -846,13 +846,13 @@ async function reloadAndAssertClosedStreams() {
*/
function checkDeviceSelectors(aExpectedTypes, aWindow = window) {
for (const type of aExpectedTypes) {
if (!["microphone", "camera", "screen"].includes(type)) {
if (!["microphone", "camera", "screen", "speaker"].includes(type)) {
throw new Error(`Bad device type name ${type}`);
}
}
let document = aWindow.document;
for (let type of ["Microphone", "Camera"]) {
for (let type of ["Microphone", "Camera", "Speaker"]) {
let selector = document.getElementById(`webRTC-select${type}`);
if (!aExpectedTypes.includes(type.toLowerCase())) {
ok(selector.hidden, `${type} selector hidden`);

View File

@ -388,6 +388,8 @@ popup-select-microphone-device =
.accesskey = M
popup-select-microphone-icon =
.tooltiptext = Microphone
popup-select-speaker-icon =
.tooltiptext = Speakers
popup-all-windows-shared = All visible windows on your screen will be shared.
popup-screen-sharing-block =

View File

@ -709,6 +709,11 @@ getUserMedia.shareCameraAndAudioCapture3.message = Allow %S to use your camera a
getUserMedia.shareScreenAndMicrophone4.message = Allow %S to use your microphone and see your screen?
getUserMedia.shareScreenAndAudioCapture4.message = Allow %S to listen to this tabs audio and see your screen?
getUserMedia.shareAudioCapture3.message = Allow %S to listen to this tabs audio?
# LOCALIZATION NOTE (selectAudioOutput.shareSpeaker.message):
# "Speakers" is used in a general sense that might include headphones or
# another audio output connection.
# %S is the website origin (e.g. www.mozilla.org)
selectAudioOutput.shareSpeaker.message = Allow %S to use other speakers?
# LOCALIZATION NOTE (getUserMedia.shareCameraUnsafeDelegation2.message,
# getUserMedia.shareMicrophoneUnsafeDelegation2.message,
@ -726,6 +731,12 @@ getUserMedia.shareCameraAndMicrophoneUnsafeDelegation2.message = Allow %1$S to g
getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation2.message = Allow %1$S to give %2$S access to your camera and listen to this tabs audio?
getUserMedia.shareScreenAndMicrophoneUnsafeDelegation2.message = Allow %1$S to give %2$S access to your microphone and see your screen?
getUserMedia.shareScreenAndAudioCaptureUnsafeDelegation2.message = Allow %1$S to give %2$S permission to listen to this tabs audio and see your screen?
# LOCALIZATION NOTE ():
# "Speakers" is used in a general sense that might include headphones or
# another audio output connection.
# %1$S is the first party origin.
# %2$S is the third party origin.
selectAudioOutput.shareSpeakerUnsafeDelegation.message = Allow %1$S to give %2$S access to other speakers?
# LOCALIZATION NOTE (getUserMedia.shareScreenWarning.message): NB: inserted via innerHTML, so please don't use <, > or & in this string.
getUserMedia.shareScreenWarning2.message = Only share screens with sites you trust. Sharing can allow deceptive sites to browse as you and steal your private data.

View File

@ -799,6 +799,10 @@ popupnotificationcontent {
list-style-image: url("chrome://browser/skin/notification-icons/microphone.svg");
}
#webRTC-selectSpeaker-icon {
list-style-image: url("chrome://global/skin/media/audio.svg");
}
.popup-notification-body :is(description, label, checkbox, input) {
margin-inline: 0;
}