diff --git a/browser/actors/WebRTCParent.jsm b/browser/actors/WebRTCParent.jsm index 11782c26135814c09040b60d482433035852ce4a..c7e9331d2ec710b3b2293ab76e5f059e0558f0a7 100644 --- a/browser/actors/WebRTCParent.jsm +++ b/browser/actors/WebRTCParent.jsm @@ -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 + ); } } diff --git a/browser/base/content/test/webrtc/browser_devices_select_audio_output.js b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js index 9b6f3538909545c9f921aafff53387e871289804..ceafc3269a71775c94e801f27daf464b607e07f7 100644 --- a/browser/base/content/test/webrtc/browser_devices_select_audio_output.js +++ b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js @@ -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,22 +60,26 @@ 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 deny() { +async function allow() { + await Promise.all([promiseMessage("ok"), allowPrompt()]); +} + +async function denyPrompt() { const observerPromise = expectObserverCalled("getUserMedia:response:deny"); - await promiseMessage(permissionError, () => { - activateSecondaryAction(kActionDeny); - }); + activateSecondaryAction(kActionDeny); await observerPromise; } +async function deny() { + await Promise.all([promiseMessage(permissionError), denyPrompt()]); +} + async function escapePrompt() { const observerPromise = expectObserverCalled("getUserMedia:response:deny"); EventUtils.synthesizeKey("KEY_Escape"); @@ -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); }, }, { diff --git a/browser/base/content/test/webrtc/get_user_media.html b/browser/base/content/test/webrtc/get_user_media.html index 3692373fa472e547c41350ad8d4388cc2b146289..844c6428ccd319d2486d6c7fa27d785a22014f2b 100644 --- a/browser/base/content/test/webrtc/get_user_media.html +++ b/browser/base/content/test/webrtc/get_user_media.html @@ -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); diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js index 456ee77a67b19314d10600f24d361f89b7df8939..484af268ec41e3caf87f8b249d56a648b071a3dc 100644 --- a/browser/base/content/test/webrtc/head.js +++ b/browser/base/content/test/webrtc/head.js @@ -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)); }); }