1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
11 SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
12 webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
15 XPCOMUtils.defineLazyServiceGetter(
18 "@mozilla.org/ospermissionrequest;1",
19 "nsIOSPermissionRequest"
22 export class WebRTCParent extends JSWindowActorParent {
24 // Media stream tracks end on unload, so call stopRecording() on them early
25 // *before* we go away, to ensure we're working with the right principal.
26 this.stopRecording(this.manager.outerWindowId);
27 lazy.webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext);
28 // Must clear activePerms here to prevent them from being read by laggard
29 // stopRecording() calls, which due to IPC, may come in *after* navigation.
30 // This is to prevent granting temporary grace periods to the wrong page.
31 lazy.webrtcUI.activePerms.delete(this.manager.outerWindowId);
35 return this.browsingContext.top.embedderElement;
38 receiveMessage(aMessage) {
39 switch (aMessage.name) {
40 case "rtcpeer:Request": {
41 let params = Object.freeze(
44 origin: this.manager.documentPrincipal.origin,
50 let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);
53 for (let blocker of blockers) {
55 let result = await blocker(params);
56 if (result == "deny") {
60 console.error(`error in PeerConnection blocker: ${err.message}`);
64 })().then(decision => {
67 lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
68 message = "rtcpeer:Allow";
70 lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
71 message = "rtcpeer:Deny";
74 this.sendAsyncMessage(message, {
75 callID: params.callID,
76 windowID: params.windowID,
81 case "rtcpeer:CancelRequest": {
82 let params = Object.freeze({
83 origin: this.manager.documentPrincipal.origin,
84 callID: aMessage.data,
86 lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
89 case "webrtc:Request": {
90 let data = aMessage.data;
92 // Record third party origins for telemetry.
93 let isThirdPartyOrigin =
94 this.manager.documentPrincipal.origin !=
95 this.manager.topWindowContext.documentPrincipal.origin;
96 data.isThirdPartyOrigin = isThirdPartyOrigin;
98 data.origin = this.manager.topWindowContext.documentPrincipal.origin;
100 let browser = this.getBrowser();
101 if (browser.fxrPermissionPrompt) {
102 // For Firefox Reality on Desktop, switch to a different mechanism to
103 // prompt the user since fewer permissions are available and since many
104 // UI dependencies are not available.
105 browser.fxrPermissionPrompt(data);
107 prompt(this, this.getBrowser(), data);
111 case "webrtc:StopRecording":
113 aMessage.data.windowID,
114 aMessage.data.mediaSource,
118 case "webrtc:CancelRequest": {
119 let browser = this.getBrowser();
120 // browser can be null when closing the window
122 removePrompt(browser, aMessage.data);
126 case "webrtc:UpdateIndicators": {
127 let { data } = aMessage;
128 data.documentURI = this.manager.documentURI?.spec;
131 data.principal = this.manager.topWindowContext.documentPrincipal;
133 lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
135 this.updateIndicators(data);
141 updateIndicators(aData) {
142 let browsingContext = this.browsingContext;
143 let state = lazy.webrtcUI.updateIndicators(browsingContext.top);
145 let browser = this.getBrowser();
150 state.browsingContext = browsingContext;
151 state.windowId = aData.windowId;
153 let tabbrowser = browser.ownerGlobal.gBrowser;
155 tabbrowser.updateBrowserSharing(browser, {
161 denyRequest(aRequest) {
162 this.sendAsyncMessage("webrtc:Deny", {
163 callID: aRequest.callID,
164 windowID: aRequest.windowID,
169 // Deny the request because the browser does not have access to the
170 // camera or microphone due to OS security restrictions. The user may
171 // have granted camera/microphone access to the site, but not have
172 // allowed the browser access in OS settings.
174 denyRequestNoPermission(aRequest) {
175 this.sendAsyncMessage("webrtc:Deny", {
176 callID: aRequest.callID,
177 windowID: aRequest.windowID,
178 noOSPermission: true,
183 // Check if we have permission to access the camera or screen-sharing and/or
184 // microphone at the OS level. Triggers a request to access the device if access
185 // is needed and the permission state has not yet been determined.
187 async checkOSPermission(camNeeded, micNeeded, scrNeeded) {
188 // Don't trigger OS permission requests for fake devices. Fake devices don't
189 // require OS permission and the dialogs are problematic in automated testing
190 // (where fake devices are used) because they require user interaction.
193 Services.prefs.getBoolPref("media.navigator.streams.fake", false)
199 if (camNeeded || micNeeded) {
200 lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
203 let camPermission = camStatus.value;
204 let camAccessible = await this.checkAndGetOSPermission(
206 lazy.OSPermissions.requestVideoCapturePermission
208 if (!camAccessible) {
213 let micPermission = micStatus.value;
214 let micAccessible = await this.checkAndGetOSPermission(
216 lazy.OSPermissions.requestAudioCapturePermission
218 if (!micAccessible) {
224 lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
225 if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
226 lazy.OSPermissions.maybeRequestScreenCapturePermission();
234 // Given a device's permission, return true if the device is accessible. If
235 // the device's permission is not yet determined, request access to the device.
236 // |requestPermissionFunc| must return a promise that resolves with true
237 // if the device is accessible and false otherwise.
239 async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
241 devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
242 devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
246 if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
247 let deviceAllowed = await requestPermissionFunc();
248 if (!deviceAllowed) {
255 stopRecording(aOuterWindowId, aMediaSource, aRawId) {
256 for (let { browsingContext, state } of lazy.webrtcUI._streams) {
257 if (browsingContext == this.browsingContext) {
258 let { principal } = state;
259 for (let { mediaSource, rawId } of state.devices) {
260 if (aRawId && (aRawId != rawId || aMediaSource != mediaSource)) {
263 // Deactivate this device (no aRawId means all devices).
264 this.deactivateDevicePerm(
276 * Add a device record to webrtcUI.activePerms, denoting a device as in use.
277 * Important to call for permission grace periods to work correctly.
279 activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
280 if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
281 lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
283 lazy.webrtcUI.activePerms
284 .get(this.manager.outerWindowId)
285 .set(aOuterWindowId + aMediaSource + aId, aMediaSource);
289 * Remove a device record from webrtcUI.activePerms, denoting a device as
290 * no longer in use by the site. Meaning: gUM requests for this device will
291 * no longer be implicitly granted through the webrtcUI.activePerms mechanism.
293 * However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
294 * grant is extended for an additional period of time through SitePermissions.
296 deactivateDevicePerm(
302 // If we don't have active permissions for the given window anymore don't
303 // set a grace period. This happens if there has been a user revoke and
304 // webrtcUI clears the permissions.
305 if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
308 let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
309 map.delete(aOuterWindowId + aMediaSource + aId);
311 // Add a permission grace period for camera and microphone only
313 (aMediaSource != "camera" && aMediaSource != "microphone") ||
314 !this.browsingContext.top.embedderElement
318 let gracePeriodMs = lazy.webrtcUI.deviceGracePeriodTimeoutMs;
319 if (gracePeriodMs > 0) {
320 // A grace period is extended (even past navigation) to this outer window
321 // + origin + deviceId only. This avoids re-prompting without the user
322 // having to persist permission to the site, in a common case of a web
323 // conference asking them for the camera in a lobby page, before
324 // navigating to the actual meeting room page. Does not survive tab close.
326 // Caution: since navigation causes deactivation, we may be in the middle
327 // of one. We must pass in a principal & URI for SitePermissions to use
328 // instead of browser.currentURI, because the latter may point to a new
329 // page already, and we must not leak permission to unrelated pages.
331 let permissionName = [aMediaSource, aId].join("^");
332 lazy.SitePermissions.setForPrincipal(
333 aPermissionPrincipal,
335 lazy.SitePermissions.ALLOW,
336 lazy.SitePermissions.SCOPE_TEMPORARY,
337 this.browsingContext.top.embedderElement,
344 * Checks if the principal has sufficient permissions
345 * to fulfill the given request. If the request can be
346 * fulfilled, a message is sent to the child
347 * signaling that WebRTC permissions were given and
348 * this function will return true.
350 checkRequestAllowed(aRequest, aPrincipal) {
351 if (!aRequest.secure) {
354 // Always prompt for screen sharing
355 if (aRequest.sharingScreen) {
364 hasInherentAudioConstraints,
365 hasInherentVideoConstraints,
369 if (audioOutputDevices?.length) {
370 // Prompt if a specific device is not requested, available and allowed.
371 let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
374 !lazy.SitePermissions.getForPrincipal(
376 ["speaker", device.id].join("^"),
378 ).state == lazy.SitePermissions.ALLOW
382 this.sendAsyncMessage("webrtc:Allow", {
385 devices: [device.deviceIndex],
390 let { perms } = Services;
392 perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
394 perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
397 // Don't use persistent permissions from the top-level principal
398 // if we're handling a potentially insecure third party
399 // through a wildcard ("*") allow attribute.
400 let limited = aRequest.secondOrigin;
402 let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
403 // We consider a camera or mic active if it is active or was active within a
404 // grace period of milliseconds ago.
405 const isAllowed = ({ mediaSource, rawId }, permissionID) =>
406 map?.get(windowID + mediaSource + rawId) ||
408 (lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
409 lazy.SitePermissions.ALLOW ||
410 lazy.SitePermissions.getForPrincipal(
412 [mediaSource, rawId].join("^"),
414 ).state == lazy.SitePermissions.ALLOW));
417 if (audioInputDevices.length) {
418 for (let device of audioInputDevices) {
419 if (isAllowed(device, "microphone")) {
423 if (hasInherentAudioConstraints) {
424 // Inherent constraints suggest site is looking for a specific mic
427 // Some sites don't look too hard at what they get, and spam gUM without
428 // adjusting what they ask for to match what they got last time. To keep
429 // users in charge and reduce prompts, ignore other constraints by
430 // returning the most-fit microphone a site already has access to.
437 if (videoInputDevices.length) {
438 for (let device of videoInputDevices) {
439 if (isAllowed(device, "camera")) {
443 if (hasInherentVideoConstraints) {
444 // Inherent constraints suggest site is looking for a specific camera
447 // Some sites don't look too hard at what they get, and spam gUM without
448 // adjusting what they ask for to match what they got last time. To keep
449 // users in charge and reduce prompts, ignore other constraints by
450 // returning the most-fit camera a site already has access to.
458 perms.addFromPrincipal(
464 devices.push(camera.deviceIndex);
465 this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
468 devices.push(microphone.deviceIndex);
469 this.activateDevicePerm(
471 microphone.mediaSource,
475 this.checkOSPermission(!!camera, !!microphone, false).then(
477 if (havePermission) {
478 this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
480 this.denyRequestNoPermission(aRequest);
488 function prompt(aActor, aBrowser, aRequest) {
499 Services.scriptSecurityManager.createContentPrincipalFromOrigin(
503 // For add-on principals, we immediately check for permission instead
504 // of waiting for the notification to focus. This allows for supporting
505 // cases such as browserAction popups where no prompt is shown.
506 if (principal.addonPolicy) {
508 let isBackground = false;
510 for (let view of principal.addonPolicy.extension.views) {
511 if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
514 if (view.viewType == "background" && view.xulBrowser == aBrowser) {
519 // Recording from background pages is considered too sensitive and will
522 aActor.denyRequest(aRequest);
526 // If the request comes from a popup, we don't want to show the prompt,
527 // but we do want to allow the request if the user previously gave permission.
529 if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
530 aActor.denyRequest(aRequest);
536 // If the user has already denied access once in this tab,
537 // deny again without even showing the notification icon.
538 for (const type of requestTypes) {
540 type == "AudioCapture" ? "microphone" : type.toLowerCase();
542 lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
543 .state == lazy.SitePermissions.BLOCK
545 aActor.denyRequest(aRequest);
550 let chromeDoc = aBrowser.ownerDocument;
551 const localization = new Localization(
552 ["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
556 /** @type {"Screen" | "Camera" | null} */
557 let reqVideoInput = null;
558 if (videoInputDevices.length) {
559 reqVideoInput = sharingScreen ? "Screen" : "Camera";
561 /** @type {"AudioCapture" | "Microphone" | null} */
562 let reqAudioInput = null;
563 if (audioInputDevices.length) {
564 reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
566 const reqAudioOutput = !!audioOutputDevices.length;
568 const stringId = getPromptMessageId(
572 !!aRequest.secondOrigin
576 if (principal.schemeIs("file")) {
577 message = localization.formatValueSync(stringId + "-with-file");
580 message = localization.formatValueSync(stringId, {
584 originToShow = lazy.webrtcUI.getHostOrExtensionName(principal.URI);
586 let notification; // Used by action callbacks.
587 const actionL10nIds = [{ id: "webrtc-action-allow" }];
589 let notificationSilencingEnabled = Services.prefs.getBoolPref(
590 "privacy.webrtc.allowSilencingNotifications"
593 const isNotNowLabelEnabled =
594 reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
595 let secondaryActions = [];
596 if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
597 // We want to free up the checkbox at the bottom of the permission
598 // panel for the notification silencing option, so we use a
599 // different configuration for the permissions panel when
600 // notification silencing is enabled.
602 let permissionName = reqAudioOutput ? "speaker" : "screen";
603 // When selecting speakers, we always offer 'Not now' instead of 'Block'.
604 // When selecting screens, we offer 'Not now' if and only if we have a
605 // (temporary) allow permission for some mic/cam device.
606 const id = isNotNowLabelEnabled
607 ? "webrtc-action-not-now"
608 : "webrtc-action-block";
609 actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
613 aActor.denyRequest(aRequest);
614 if (!isNotNowLabelEnabled) {
615 lazy.SitePermissions.setForPrincipal(
618 lazy.SitePermissions.BLOCK,
619 lazy.SitePermissions.SCOPE_TEMPORARY,
627 aActor.denyRequest(aRequest);
628 lazy.SitePermissions.setForPrincipal(
631 lazy.SitePermissions.BLOCK,
632 lazy.SitePermissions.SCOPE_PERSISTENT,
639 // We have a (temporary) allow permission for some device
640 // hence we offer a 'Not now' label instead of 'Block'.
641 const id = isNotNowLabelEnabled
642 ? "webrtc-action-not-now"
643 : "webrtc-action-block";
644 actionL10nIds.push({ id });
648 aActor.denyRequest(aRequest);
650 const isPersistent = aState?.checkboxChecked;
652 // Choosing 'Not now' will not set a block permission
653 // we just deny the request. This enables certain use cases
654 // where sites want to switch devices, but users back out of the permission request
655 // (See Bug 1609578).
656 // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
657 if (!isPersistent && isNotNowLabelEnabled) {
661 // Denying a camera / microphone prompt means we set a temporary or
662 // persistent permission block. There may still be active grace period
663 // permissions at this point. We need to remove them.
664 clearTemporaryGrants(
665 notification.browser,
666 reqVideoInput === "Camera",
670 const scope = isPersistent
671 ? lazy.SitePermissions.SCOPE_PERSISTENT
672 : lazy.SitePermissions.SCOPE_TEMPORARY;
674 lazy.SitePermissions.setForPrincipal(
677 lazy.SitePermissions.BLOCK,
683 lazy.SitePermissions.setForPrincipal(
685 sharingScreen ? "screen" : "camera",
686 lazy.SitePermissions.BLOCK,
696 // The formatMessagesSync method returns an array of results
697 // for each message that was requested, and for the ones with
698 // attributes, returns an attributes array with objects like:
699 // { name: "label", value: "somevalue" }
700 const [mainMessage, ...secondaryMessages] = localization
701 .formatMessagesSync(actionL10nIds)
703 msg.attributes.reduce(
704 (acc, { name, value }) => ({ ...acc, [name]: value }),
710 label: mainMessage.label,
711 accessKey: mainMessage.accesskey,
712 // The real callback will be set during the "showing" event. The
713 // empty function here is so that PopupNotifications.show doesn't
714 // reject the action.
718 for (let i = 0; i < secondaryActions.length; ++i) {
719 secondaryActions[i].label = secondaryMessages[i].label;
720 secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
727 eventCallback(aTopic, aNewBrowser, isCancel) {
728 if (aTopic == "swapping") {
732 let doc = this.browser.ownerDocument;
734 // Clean-up video streams of screensharing previews.
736 reqVideoInput !== "Screen" ||
737 aTopic == "dismissed" ||
740 let video = doc.getElementById("webRTC-previewVideo");
741 video.deviceId = null; // Abort previews still being started.
743 video.stream.getTracks().forEach(t => t.stop());
746 doc.getElementById("webRTC-preview").hidden = true;
748 let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
749 if (menupopup._commandEventListener) {
750 menupopup.removeEventListener(
752 menupopup._commandEventListener
754 menupopup._commandEventListener = null;
758 if (aTopic == "removed" && notification && isCancel) {
759 // The notification has been cancelled (e.g. due to entering
760 // full-screen). Also cancel the webRTC request.
761 aActor.denyRequest(aRequest);
764 audioOutputDevices.length > 1 &&
765 !notification.wasDismissed
767 // Focus the list on first show so that arrow keys select the speaker.
768 doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
771 if (aTopic != "showing") {
775 // If BLOCK has been set persistently in the permission manager or has
776 // been set on the tab, then it is handled synchronously before we add
778 // Handling of ALLOW is delayed until the popupshowing event,
779 // to avoid granting permissions automatically to background tabs.
780 if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
786 * Prepare the device selector for one kind of device.
787 * @param {Object[]} devices - available devices of this kind.
788 * @param {string} IDPrefix - indicating kind of device and so
789 * associated UI elements.
790 * @param {string[]} describedByIDs - an array to which might be
791 * appended ids of elements that describe the panel, for the caller to
792 * use in the aria-describedby attribute.
794 function listDevices(devices, IDPrefix, describedByIDs) {
795 let labelID = `${IDPrefix}-single-device-label`;
798 if (IDPrefix == "webRTC-selectSpeaker") {
799 list = doc.getElementById(`${IDPrefix}-richlistbox`);
802 itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
803 list = itemParent.parentNode; // menulist
805 while (itemParent.lastChild) {
806 itemParent.removeChild(itemParent.lastChild);
809 // Removing the child nodes of a menupopup doesn't clear the value
810 // attribute of its menulist. Similary for richlistbox state. This can
811 // have unfortunate side effects when the list is rebuilt with a
812 // different content, so we set the selectedIndex explicitly to reset
814 let defaultIndex = 0;
816 for (let device of devices) {
817 let item = addDeviceToList(list, device.name, device.deviceIndex);
818 if (IDPrefix == "webRTC-selectSpeaker") {
819 item.addEventListener("dblclick", event => {
820 // Allow the chosen speakers via
821 // .popup-notification-primary-button so that
822 // "security.notification_enable_delay" is checked.
823 event.target.closest("popupnotification").button.doCommand();
825 if (device.id == aRequest.audioOutputId) {
826 defaultIndex = device.deviceIndex;
830 list.selectedIndex = defaultIndex;
832 let label = doc.getElementById(labelID);
833 if (devices.length == 1) {
834 describedByIDs.push(`${IDPrefix}-icon`, labelID);
835 label.value = devices[0].name;
836 label.hidden = false;
844 let notificationElement = doc.getElementById(
845 "webRTC-shareDevices-notification"
848 function checkDisabledWindowMenuItem() {
849 let list = doc.getElementById("webRTC-selectWindow-menulist");
850 let item = list.selectedItem;
851 if (!item || item.hasAttribute("disabled")) {
852 notificationElement.setAttribute("invalidselection", "true");
854 notificationElement.removeAttribute("invalidselection");
858 function listScreenShareDevices(menupopup, devices) {
859 while (menupopup.lastChild) {
860 menupopup.removeChild(menupopup.lastChild);
863 // Removing the child nodes of the menupopup doesn't clear the value
864 // attribute of the menulist. This can have unfortunate side effects
865 // when the list is rebuilt with a different content, so we remove
866 // the value attribute and unset the selectedItem explicitly.
867 menupopup.parentNode.removeAttribute("value");
868 menupopup.parentNode.selectedItem = null;
870 // "Select a Window or Screen" is the default because we can't and don't
871 // want to pick a 'default' window to share (Full screen is "scary").
873 menupopup.parentNode,
874 localization.formatValueSync("webrtc-pick-window-or-screen"),
877 menupopup.appendChild(doc.createXULElement("menuseparator"));
879 let isPipeWireDetected = false;
881 // Build the list of 'devices'.
882 let monitorIndex = 1;
883 for (let i = 0; i < devices.length; ++i) {
884 let device = devices[i];
885 let type = device.mediaSource;
887 if (device.canRequestOsLevelPrompt) {
888 // When we share content by PipeWire add only one item to the device
889 // list. When it's selected PipeWire portal dialog is opened and
890 // user confirms actual window/screen sharing there.
891 // Don't mark it as scary as there's an extra confirmation step by
892 // PipeWire portal dialog.
894 isPipeWireDetected = true;
895 let item = addDeviceToList(
896 menupopup.parentNode,
897 localization.formatValueSync("webrtc-share-pipe-wire-portal"),
901 item.deviceId = device.rawId;
902 item.mediaSource = type;
904 // In this case the OS sharing dialog will be the only option and
905 // can be safely pre-selected.
906 menupopup.parentNode.selectedItem = item;
908 } else if (type == "screen") {
909 // Building screen list from available screens.
910 if (device.name == "Primary Monitor") {
911 name = localization.formatValueSync("webrtc-share-entire-screen");
913 name = localization.formatValueSync("webrtc-share-monitor", {
921 if (type == "application") {
922 // The application names returned by the platform are of the form:
923 // <window count>\x1e<application name>
924 const [count, appName] = name.split("\x1e");
925 name = localization.formatValueSync("webrtc-share-application", {
927 windowCount: parseInt(count),
931 let item = addDeviceToList(menupopup.parentNode, name, i, type);
932 item.deviceId = device.rawId;
933 item.mediaSource = type;
939 // Always re-select the "No <type>" item.
941 .getElementById("webRTC-selectWindow-menulist")
942 .removeAttribute("value");
943 doc.getElementById("webRTC-all-windows-shared").hidden = true;
945 menupopup._commandEventListener = event => {
946 checkDisabledWindowMenuItem();
947 let video = doc.getElementById("webRTC-previewVideo");
949 video.stream.getTracks().forEach(t => t.stop());
953 const { deviceId, mediaSource, scary } = event.target;
954 if (deviceId == undefined) {
955 doc.getElementById("webRTC-preview").hidden = true;
960 let warning = doc.getElementById("webRTC-previewWarning");
961 let warningBox = doc.getElementById("webRTC-previewWarningBox");
962 warningBox.hidden = !scary;
963 let chromeWin = doc.defaultView;
966 mediaSource == "screen"
967 ? "webrtc-share-screen-warning"
968 : "webrtc-share-browser-warning";
969 doc.l10n.setAttributes(warning, warnId);
971 const learnMore = doc.getElementById(
972 "webRTC-previewWarning-learnMore"
974 const baseURL = Services.urlFormatter.formatURLPref(
975 "app.support.baseURL"
977 learnMore.setAttribute("href", baseURL + "screenshare-safety");
978 doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");
980 // On Catalina, we don't want to blow our chance to show the
981 // OS-level helper prompt to enable screen recording if the user
982 // intends to reject anyway. OTOH showing it when they click Allow
983 // is too late. A happy middle is to show it when the user makes a
984 // choice in the picker. This already happens implicitly if the
985 // user chooses "Entire desktop", as a side-effect of our preview,
986 // we just need to also do it if they choose "Firefox". These are
987 // the lone two options when permission is absent on Catalina.
988 // Ironically, these are the two sources marked "scary" from a
989 // web-sharing perspective, which is why this code resides here.
990 // A restart doesn't appear to be necessary in spite of OS wording.
992 lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
993 if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
994 lazy.OSPermissions.maybeRequestScreenCapturePermission();
998 let perms = Services.perms;
999 let chromePrincipal =
1000 Services.scriptSecurityManager.getSystemPrincipal();
1001 perms.addFromPrincipal(
1003 "MediaManagerVideo",
1005 perms.EXPIRE_SESSION
1008 // We don't have access to any screen content besides our browser tabs
1009 // on Wayland, therefore there are no previews we can show.
1011 (!isPipeWireDetected || mediaSource == "browser") &&
1012 Services.prefs.getBoolPref(
1013 "media.getdisplaymedia.previews.enabled",
1017 video.deviceId = deviceId;
1019 video: { mediaSource, deviceId: { exact: deviceId } },
1021 chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
1023 if (video.deviceId != deviceId) {
1024 // The user has selected a different device or closed the panel
1025 // before getUserMedia finished.
1026 stream.getTracks().forEach(t => t.stop());
1029 video.srcObject = stream;
1030 video.stream = stream;
1031 doc.getElementById("webRTC-preview").hidden = false;
1032 video.onloadedmetadata = function () {
1038 err.name == "OverconstrainedError" &&
1039 err.constraint == "deviceId"
1041 // Window has disappeared since enumeration, which can happen.
1042 // No preview for you.
1046 `error in preview: ${err.message} ${err.constraint}`
1052 menupopup.addEventListener("command", menupopup._commandEventListener);
1055 function addDeviceToList(list, deviceName, deviceIndex, type) {
1056 let item = list.appendItem(deviceName, deviceIndex);
1057 item.setAttribute("tooltiptext", deviceName);
1059 item.setAttribute("devicetype", type);
1062 if (deviceIndex == "-1") {
1063 item.setAttribute("disabled", true);
1069 doc.getElementById("webRTC-selectCamera").hidden =
1070 reqVideoInput !== "Camera";
1071 doc.getElementById("webRTC-selectWindowOrScreen").hidden =
1072 reqVideoInput !== "Screen";
1073 doc.getElementById("webRTC-selectMicrophone").hidden =
1074 reqAudioInput !== "Microphone";
1075 doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
1077 let describedByIDs = ["webRTC-shareDevices-notification-description"];
1079 if (sharingScreen) {
1080 let windowMenupopup = doc.getElementById(
1081 "webRTC-selectWindow-menupopup"
1083 listScreenShareDevices(windowMenupopup, videoInputDevices);
1084 checkDisabledWindowMenuItem();
1086 listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
1087 notificationElement.removeAttribute("invalidselection");
1089 if (!sharingAudio) {
1092 "webRTC-selectMicrophone",
1096 listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);
1098 // PopupNotifications knows to clear the aria-describedby attribute
1099 // when hiding, so we don't have to worry about cleaning it up ourselves.
1100 chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
1102 describedByIDs.join(" ")
1105 this.mainAction.callback = async function (aState) {
1106 let remember = false;
1107 let silenceNotifications = false;
1109 if (notificationSilencingEnabled && sharingScreen) {
1110 silenceNotifications = aState && aState.checkboxChecked;
1112 remember = aState && aState.checkboxChecked;
1115 let allowedDevices = [];
1116 let perms = Services.perms;
1117 if (reqVideoInput) {
1118 let listId = sharingScreen
1119 ? "webRTC-selectWindow-menulist"
1120 : "webRTC-selectCamera-menulist";
1121 let videoDeviceIndex = doc.getElementById(listId).value;
1122 let allowVideoDevice = videoDeviceIndex != "-1";
1123 if (allowVideoDevice) {
1124 allowedDevices.push(videoDeviceIndex);
1125 // Session permission will be removed after use
1126 // (it's really one-shot, not for the entire session)
1127 perms.addFromPrincipal(
1129 "MediaManagerVideo",
1131 perms.EXPIRE_SESSION
1133 let { mediaSource, rawId } = videoInputDevices.find(
1134 ({ deviceIndex }) => deviceIndex == videoDeviceIndex
1136 aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1138 lazy.SitePermissions.setForPrincipal(
1141 lazy.SitePermissions.ALLOW
1147 if (reqAudioInput === "Microphone") {
1148 let audioDeviceIndex = doc.getElementById(
1149 "webRTC-selectMicrophone-menulist"
1151 let allowMic = audioDeviceIndex != "-1";
1153 allowedDevices.push(audioDeviceIndex);
1154 let { mediaSource, rawId } = audioInputDevices.find(
1155 ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1157 aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1159 lazy.SitePermissions.setForPrincipal(
1162 lazy.SitePermissions.ALLOW
1166 } else if (reqAudioInput === "AudioCapture") {
1167 // Only one device possible for audio capture.
1168 allowedDevices.push(0);
1171 if (reqAudioOutput) {
1172 let audioDeviceIndex = doc.getElementById(
1173 "webRTC-selectSpeaker-richlistbox"
1175 let allowSpeaker = audioDeviceIndex != "-1";
1177 allowedDevices.push(audioDeviceIndex);
1178 let { id } = audioOutputDevices.find(
1179 ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1181 lazy.SitePermissions.setForPrincipal(
1183 ["speaker", id].join("^"),
1184 lazy.SitePermissions.ALLOW
1189 if (!allowedDevices.length) {
1190 aActor.denyRequest(aRequest);
1194 const camNeeded = reqVideoInput === "Camera";
1195 const micNeeded = !!reqAudioInput;
1196 const scrNeeded = reqVideoInput === "Screen";
1197 const havePermission = await aActor.checkOSPermission(
1202 if (!havePermission) {
1203 aActor.denyRequestNoPermission(aRequest);
1207 aActor.sendAsyncMessage("webrtc:Allow", {
1208 callID: aRequest.callID,
1209 windowID: aRequest.windowID,
1210 devices: allowedDevices,
1211 suppressNotifications: silenceNotifications,
1215 // If we haven't handled the permission yet, we want to show the doorhanger.
1220 function shouldShowAlwaysRemember() {
1221 // Don't offer "always remember" action in PB mode
1222 if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
1226 // Don't offer "always remember" action in maybe unsafe permission
1228 if (aRequest.secondOrigin) {
1232 // Speaker grants are always remembered, so no checkbox is required.
1233 if (reqAudioOutput) {
1240 if (shouldShowAlwaysRemember()) {
1241 // Disable the permanent 'Allow' action if the connection isn't secure, or for
1242 // screen/audio sharing (because we can't guess which window the user wants to
1243 // share without prompting). Note that we never enter this block for private
1244 // browsing windows.
1246 if (sharingScreen) {
1247 reason = "webrtc-reason-for-no-permanent-allow-screen";
1248 } else if (sharingAudio) {
1249 reason = "webrtc-reason-for-no-permanent-allow-audio";
1250 } else if (!aRequest.secure) {
1251 reason = "webrtc-reason-for-no-permanent-allow-insecure";
1254 options.checkbox = {
1255 label: localization.formatValueSync("webrtc-remember-allow-checkbox"),
1256 checked: principal.isAddonOrExpandedAddonPrincipal,
1257 checkedState: reason
1259 disableMainAction: true,
1260 warningLabel: localization.formatValueSync(reason),
1266 // If the notification silencing feature is enabled and we're sharing a
1267 // screen, then the checkbox for the permission panel is what controls
1268 // notification silencing.
1269 if (notificationSilencingEnabled && sharingScreen) {
1270 options.checkbox = {
1271 label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
1274 disableMainAction: false,
1279 let anchorId = "webRTC-shareDevices-notification-icon";
1280 if (reqVideoInput === "Screen") {
1281 anchorId = "webRTC-shareScreen-notification-icon";
1282 } else if (!reqVideoInput) {
1283 if (reqAudioInput && !reqAudioOutput) {
1284 anchorId = "webRTC-shareMicrophone-notification-icon";
1285 } else if (!reqAudioInput && reqAudioOutput) {
1286 anchorId = "webRTC-shareSpeaker-notification-icon";
1290 if (aRequest.secondOrigin) {
1291 options.secondName = lazy.webrtcUI.getHostOrExtensionName(
1293 aRequest.secondOrigin
1297 notification = chromeDoc.defaultView.PopupNotifications.show(
1299 "webRTC-shareDevices",
1306 notification.callID = aRequest.callID;
1308 let schemeHistogram = Services.telemetry.getKeyedHistogramById(
1309 "PERMISSION_REQUEST_ORIGIN_SCHEME"
1311 let userInputHistogram = Services.telemetry.getKeyedHistogramById(
1312 "PERMISSION_REQUEST_HANDLING_USER_INPUT"
1315 let docURI = aRequest.documentURI;
1317 if (docURI.startsWith("https")) {
1319 } else if (docURI.startsWith("http")) {
1323 for (let requestType of requestTypes) {
1324 if (requestType == "AudioCapture") {
1325 requestType = "Microphone";
1327 requestType = requestType.toLowerCase();
1329 schemeHistogram.add(requestType, scheme);
1330 userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
1335 * @param {"Screen" | "Camera" | null} reqVideoInput
1336 * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
1337 * @param {boolean} reqAudioOutput
1338 * @param {boolean} delegation - Is the access delegated to a third party?
1339 * @returns {string} Localization message identifier
1341 function getPromptMessageId(
1347 switch (reqVideoInput) {
1349 switch (reqAudioInput) {
1352 ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
1353 : "webrtc-allow-share-camera-and-microphone";
1354 case "AudioCapture":
1356 ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
1357 : "webrtc-allow-share-camera-and-audio-capture";
1360 ? "webrtc-allow-share-camera-unsafe-delegation"
1361 : "webrtc-allow-share-camera";
1365 switch (reqAudioInput) {
1368 ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
1369 : "webrtc-allow-share-screen-and-microphone";
1370 case "AudioCapture":
1372 ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
1373 : "webrtc-allow-share-screen-and-audio-capture";
1376 ? "webrtc-allow-share-screen-unsafe-delegation"
1377 : "webrtc-allow-share-screen";
1381 switch (reqAudioInput) {
1384 ? "webrtc-allow-share-microphone-unsafe-delegation"
1385 : "webrtc-allow-share-microphone";
1386 case "AudioCapture":
1388 ? "webrtc-allow-share-audio-capture-unsafe-delegation"
1389 : "webrtc-allow-share-audio-capture";
1391 // This should be always true, if we've reached this far.
1392 if (reqAudioOutput) {
1394 ? "webrtc-allow-share-speaker-unsafe-delegation"
1395 : "webrtc-allow-share-speaker";
1403 * Checks whether we have a microphone/camera in use by checking the activePerms map
1404 * or if we have an allow permission for a microphone/camera in sitePermissions
1405 * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
1406 * @return true if one of the above conditions is met
1408 function allowedOrActiveCameraOrMicrophone(browser) {
1409 // Do we have an allow permission for cam/mic in the permissions manager?
1411 lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
1413 perm.state == lazy.SitePermissions.ALLOW &&
1414 (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
1418 // Return early, no need to check for active devices
1422 // Do we have an active device?
1424 // Find all windowIDs that belong to our browsing contexts
1425 browser.browsingContext
1426 .getAllBrowsingContextsInSubtree()
1427 // Only keep the outerWindowIds
1428 .map(bc => bc.currentWindowGlobal?.outerWindowId)
1429 .filter(id => id != null)
1430 // We have an active device if one of our windowIds has a non empty map in the activePerms map
1431 // that includes one device of type "camera" or "microphone"
1433 let map = lazy.webrtcUI.activePerms.get(id);
1435 // This windowId has no active device
1438 // Let's see if one of the devices is a camera or a microphone
1439 let types = [...map.values()];
1440 return types.includes("microphone") || types.includes("camera");
1445 function removePrompt(aBrowser, aCallId) {
1446 let chromeWin = aBrowser.ownerGlobal;
1447 let notification = chromeWin.PopupNotifications.getNotification(
1448 "webRTC-shareDevices",
1451 if (notification && notification.callID == aCallId) {
1452 notification.remove();
1457 * Clears temporary permission grants used for WebRTC device grace periods.
1458 * @param browser - Browser element to clear permissions for.
1459 * @param {boolean} clearCamera - Clear camera grants.
1460 * @param {boolean} clearMicrophone - Clear microphone grants.
1462 function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
1463 if (!clearCamera && !clearMicrophone) {
1464 // Nothing to clear.
1467 let perms = lazy.SitePermissions.getAllForBrowser(browser);
1470 let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
1471 // We only want to clear WebRTC grace periods. These are temporary, device
1472 // specifc (double-keyed) microphone or camera permissions.
1475 perm.state == lazy.SitePermissions.ALLOW &&
1476 perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
1477 ((clearCamera && id == "camera") ||
1478 (clearMicrophone && id == "microphone"))
1482 lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)