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
574 const message = localization.formatValueSync(stringId, {
579 let notification; // Used by action callbacks.
580 const actionL10nIds = [{ id: "webrtc-action-allow" }];
582 let notificationSilencingEnabled = Services.prefs.getBoolPref(
583 "privacy.webrtc.allowSilencingNotifications"
586 const isNotNowLabelEnabled =
587 reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
588 let secondaryActions = [];
589 if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
590 // We want to free up the checkbox at the bottom of the permission
591 // panel for the notification silencing option, so we use a
592 // different configuration for the permissions panel when
593 // notification silencing is enabled.
595 let permissionName = reqAudioOutput ? "speaker" : "screen";
596 // When selecting speakers, we always offer 'Not now' instead of 'Block'.
597 // When selecting screens, we offer 'Not now' if and only if we have a
598 // (temporary) allow permission for some mic/cam device.
599 const id = isNotNowLabelEnabled
600 ? "webrtc-action-not-now"
601 : "webrtc-action-block";
602 actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
606 aActor.denyRequest(aRequest);
607 if (!isNotNowLabelEnabled) {
608 lazy.SitePermissions.setForPrincipal(
611 lazy.SitePermissions.BLOCK,
612 lazy.SitePermissions.SCOPE_TEMPORARY,
620 aActor.denyRequest(aRequest);
621 lazy.SitePermissions.setForPrincipal(
624 lazy.SitePermissions.BLOCK,
625 lazy.SitePermissions.SCOPE_PERSISTENT,
632 // We have a (temporary) allow permission for some device
633 // hence we offer a 'Not now' label instead of 'Block'.
634 const id = isNotNowLabelEnabled
635 ? "webrtc-action-not-now"
636 : "webrtc-action-block";
637 actionL10nIds.push({ id });
641 aActor.denyRequest(aRequest);
643 const isPersistent = aState?.checkboxChecked;
645 // Choosing 'Not now' will not set a block permission
646 // we just deny the request. This enables certain use cases
647 // where sites want to switch devices, but users back out of the permission request
648 // (See Bug 1609578).
649 // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
650 if (!isPersistent && isNotNowLabelEnabled) {
654 // Denying a camera / microphone prompt means we set a temporary or
655 // persistent permission block. There may still be active grace period
656 // permissions at this point. We need to remove them.
657 clearTemporaryGrants(
658 notification.browser,
659 reqVideoInput === "Camera",
663 const scope = isPersistent
664 ? lazy.SitePermissions.SCOPE_PERSISTENT
665 : lazy.SitePermissions.SCOPE_TEMPORARY;
667 lazy.SitePermissions.setForPrincipal(
670 lazy.SitePermissions.BLOCK,
676 lazy.SitePermissions.setForPrincipal(
678 sharingScreen ? "screen" : "camera",
679 lazy.SitePermissions.BLOCK,
689 // The formatMessagesSync method returns an array of results
690 // for each message that was requested, and for the ones with
691 // attributes, returns an attributes array with objects like:
692 // { name: "label", value: "somevalue" }
693 const [mainMessage, ...secondaryMessages] = localization
694 .formatMessagesSync(actionL10nIds)
696 msg.attributes.reduce(
697 (acc, { name, value }) => ({ ...acc, [name]: value }),
703 label: mainMessage.label,
704 accessKey: mainMessage.accesskey,
705 // The real callback will be set during the "showing" event. The
706 // empty function here is so that PopupNotifications.show doesn't
707 // reject the action.
711 for (let i = 0; i < secondaryActions.length; ++i) {
712 secondaryActions[i].label = secondaryMessages[i].label;
713 secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
717 name: lazy.webrtcUI.getHostOrExtensionName(principal.URI),
720 eventCallback(aTopic, aNewBrowser, isCancel) {
721 if (aTopic == "swapping") {
725 let doc = this.browser.ownerDocument;
727 // Clean-up video streams of screensharing previews.
729 reqVideoInput !== "Screen" ||
730 aTopic == "dismissed" ||
733 let video = doc.getElementById("webRTC-previewVideo");
734 video.deviceId = null; // Abort previews still being started.
736 video.stream.getTracks().forEach(t => t.stop());
739 doc.getElementById("webRTC-preview").hidden = true;
741 let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
742 if (menupopup._commandEventListener) {
743 menupopup.removeEventListener(
745 menupopup._commandEventListener
747 menupopup._commandEventListener = null;
751 if (aTopic == "removed" && notification && isCancel) {
752 // The notification has been cancelled (e.g. due to entering
753 // full-screen). Also cancel the webRTC request.
754 aActor.denyRequest(aRequest);
757 audioOutputDevices.length > 1 &&
758 !notification.wasDismissed
760 // Focus the list on first show so that arrow keys select the speaker.
761 doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
764 if (aTopic != "showing") {
768 // If BLOCK has been set persistently in the permission manager or has
769 // been set on the tab, then it is handled synchronously before we add
771 // Handling of ALLOW is delayed until the popupshowing event,
772 // to avoid granting permissions automatically to background tabs.
773 if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
779 * Prepare the device selector for one kind of device.
780 * @param {Object[]} devices - available devices of this kind.
781 * @param {string} IDPrefix - indicating kind of device and so
782 * associated UI elements.
783 * @param {string[]} describedByIDs - an array to which might be
784 * appended ids of elements that describe the panel, for the caller to
785 * use in the aria-describedby attribute.
787 function listDevices(devices, IDPrefix, describedByIDs) {
788 let labelID = `${IDPrefix}-single-device-label`;
791 if (IDPrefix == "webRTC-selectSpeaker") {
792 list = doc.getElementById(`${IDPrefix}-richlistbox`);
795 itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
796 list = itemParent.parentNode; // menulist
798 while (itemParent.lastChild) {
799 itemParent.removeChild(itemParent.lastChild);
802 // Removing the child nodes of a menupopup doesn't clear the value
803 // attribute of its menulist. Similary for richlistbox state. This can
804 // have unfortunate side effects when the list is rebuilt with a
805 // different content, so we set the selectedIndex explicitly to reset
807 let defaultIndex = 0;
809 for (let device of devices) {
810 let item = addDeviceToList(list, device.name, device.deviceIndex);
811 if (IDPrefix == "webRTC-selectSpeaker") {
812 item.addEventListener("dblclick", event => {
813 // Allow the chosen speakers via
814 // .popup-notification-primary-button so that
815 // "security.notification_enable_delay" is checked.
816 event.target.closest("popupnotification").button.doCommand();
818 if (device.id == aRequest.audioOutputId) {
819 defaultIndex = device.deviceIndex;
823 list.selectedIndex = defaultIndex;
825 let label = doc.getElementById(labelID);
826 if (devices.length == 1) {
827 describedByIDs.push(`${IDPrefix}-icon`, labelID);
828 label.value = devices[0].name;
829 label.hidden = false;
837 let notificationElement = doc.getElementById(
838 "webRTC-shareDevices-notification"
841 function checkDisabledWindowMenuItem() {
842 let list = doc.getElementById("webRTC-selectWindow-menulist");
843 let item = list.selectedItem;
844 if (!item || item.hasAttribute("disabled")) {
845 notificationElement.setAttribute("invalidselection", "true");
847 notificationElement.removeAttribute("invalidselection");
851 function listScreenShareDevices(menupopup, devices) {
852 while (menupopup.lastChild) {
853 menupopup.removeChild(menupopup.lastChild);
856 // Removing the child nodes of the menupopup doesn't clear the value
857 // attribute of the menulist. This can have unfortunate side effects
858 // when the list is rebuilt with a different content, so we remove
859 // the value attribute and unset the selectedItem explicitly.
860 menupopup.parentNode.removeAttribute("value");
861 menupopup.parentNode.selectedItem = null;
863 // "Select a Window or Screen" is the default because we can't and don't
864 // want to pick a 'default' window to share (Full screen is "scary").
866 menupopup.parentNode,
867 localization.formatValueSync("webrtc-pick-window-or-screen"),
870 menupopup.appendChild(doc.createXULElement("menuseparator"));
872 let isPipeWireDetected = false;
874 // Build the list of 'devices'.
875 let monitorIndex = 1;
876 for (let i = 0; i < devices.length; ++i) {
877 let device = devices[i];
878 let type = device.mediaSource;
880 if (device.canRequestOsLevelPrompt) {
881 // When we share content by PipeWire add only one item to the device
882 // list. When it's selected PipeWire portal dialog is opened and
883 // user confirms actual window/screen sharing there.
884 // Don't mark it as scary as there's an extra confirmation step by
885 // PipeWire portal dialog.
887 isPipeWireDetected = true;
888 let item = addDeviceToList(
889 menupopup.parentNode,
890 localization.formatValueSync("webrtc-share-pipe-wire-portal"),
894 item.deviceId = device.rawId;
895 item.mediaSource = type;
897 // In this case the OS sharing dialog will be the only option and
898 // can be safely pre-selected.
899 menupopup.parentNode.selectedItem = item;
901 } else if (type == "screen") {
902 // Building screen list from available screens.
903 if (device.name == "Primary Monitor") {
904 name = localization.formatValueSync("webrtc-share-entire-screen");
906 name = localization.formatValueSync("webrtc-share-monitor", {
914 if (type == "application") {
915 // The application names returned by the platform are of the form:
916 // <window count>\x1e<application name>
917 const [count, appName] = name.split("\x1e");
918 name = localization.formatValueSync("webrtc-share-application", {
920 windowCount: parseInt(count),
924 let item = addDeviceToList(menupopup.parentNode, name, i, type);
925 item.deviceId = device.rawId;
926 item.mediaSource = type;
932 // Always re-select the "No <type>" item.
934 .getElementById("webRTC-selectWindow-menulist")
935 .removeAttribute("value");
936 doc.getElementById("webRTC-all-windows-shared").hidden = true;
938 menupopup._commandEventListener = event => {
939 checkDisabledWindowMenuItem();
940 let video = doc.getElementById("webRTC-previewVideo");
942 video.stream.getTracks().forEach(t => t.stop());
946 const { deviceId, mediaSource, scary } = event.target;
947 if (deviceId == undefined) {
948 doc.getElementById("webRTC-preview").hidden = true;
953 let warning = doc.getElementById("webRTC-previewWarning");
954 let warningBox = doc.getElementById("webRTC-previewWarningBox");
955 warningBox.hidden = !scary;
956 let chromeWin = doc.defaultView;
959 mediaSource == "screen"
960 ? "webrtc-share-screen-warning"
961 : "webrtc-share-browser-warning";
962 doc.l10n.setAttributes(warning, warnId);
964 const learnMore = doc.getElementById(
965 "webRTC-previewWarning-learnMore"
967 const baseURL = Services.urlFormatter.formatURLPref(
968 "app.support.baseURL"
970 learnMore.setAttribute("href", baseURL + "screenshare-safety");
971 doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");
973 // On Catalina, we don't want to blow our chance to show the
974 // OS-level helper prompt to enable screen recording if the user
975 // intends to reject anyway. OTOH showing it when they click Allow
976 // is too late. A happy middle is to show it when the user makes a
977 // choice in the picker. This already happens implicitly if the
978 // user chooses "Entire desktop", as a side-effect of our preview,
979 // we just need to also do it if they choose "Firefox". These are
980 // the lone two options when permission is absent on Catalina.
981 // Ironically, these are the two sources marked "scary" from a
982 // web-sharing perspective, which is why this code resides here.
983 // A restart doesn't appear to be necessary in spite of OS wording.
985 lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
986 if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
987 lazy.OSPermissions.maybeRequestScreenCapturePermission();
991 let perms = Services.perms;
992 let chromePrincipal =
993 Services.scriptSecurityManager.getSystemPrincipal();
994 perms.addFromPrincipal(
1001 // We don't have access to any screen content besides our browser tabs
1002 // on Wayland, therefore there are no previews we can show.
1004 (!isPipeWireDetected || mediaSource == "browser") &&
1005 Services.prefs.getBoolPref(
1006 "media.getdisplaymedia.previews.enabled",
1010 video.deviceId = deviceId;
1012 video: { mediaSource, deviceId: { exact: deviceId } },
1014 chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
1016 if (video.deviceId != deviceId) {
1017 // The user has selected a different device or closed the panel
1018 // before getUserMedia finished.
1019 stream.getTracks().forEach(t => t.stop());
1022 video.srcObject = stream;
1023 video.stream = stream;
1024 doc.getElementById("webRTC-preview").hidden = false;
1025 video.onloadedmetadata = function (e) {
1031 err.name == "OverconstrainedError" &&
1032 err.constraint == "deviceId"
1034 // Window has disappeared since enumeration, which can happen.
1035 // No preview for you.
1039 `error in preview: ${err.message} ${err.constraint}`
1045 menupopup.addEventListener("command", menupopup._commandEventListener);
1048 function addDeviceToList(list, deviceName, deviceIndex, type) {
1049 let item = list.appendItem(deviceName, deviceIndex);
1050 item.setAttribute("tooltiptext", deviceName);
1052 item.setAttribute("devicetype", type);
1055 if (deviceIndex == "-1") {
1056 item.setAttribute("disabled", true);
1062 doc.getElementById("webRTC-selectCamera").hidden =
1063 reqVideoInput !== "Camera";
1064 doc.getElementById("webRTC-selectWindowOrScreen").hidden =
1065 reqVideoInput !== "Screen";
1066 doc.getElementById("webRTC-selectMicrophone").hidden =
1067 reqAudioInput !== "Microphone";
1068 doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
1070 let describedByIDs = ["webRTC-shareDevices-notification-description"];
1072 if (sharingScreen) {
1073 let windowMenupopup = doc.getElementById(
1074 "webRTC-selectWindow-menupopup"
1076 listScreenShareDevices(windowMenupopup, videoInputDevices);
1077 checkDisabledWindowMenuItem();
1079 listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
1080 notificationElement.removeAttribute("invalidselection");
1082 if (!sharingAudio) {
1085 "webRTC-selectMicrophone",
1089 listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);
1091 // PopupNotifications knows to clear the aria-describedby attribute
1092 // when hiding, so we don't have to worry about cleaning it up ourselves.
1093 chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
1095 describedByIDs.join(" ")
1098 this.mainAction.callback = async function (aState) {
1099 let remember = false;
1100 let silenceNotifications = false;
1102 if (notificationSilencingEnabled && sharingScreen) {
1103 silenceNotifications = aState && aState.checkboxChecked;
1105 remember = aState && aState.checkboxChecked;
1108 let allowedDevices = [];
1109 let perms = Services.perms;
1110 if (reqVideoInput) {
1111 let listId = sharingScreen
1112 ? "webRTC-selectWindow-menulist"
1113 : "webRTC-selectCamera-menulist";
1114 let videoDeviceIndex = doc.getElementById(listId).value;
1115 let allowVideoDevice = videoDeviceIndex != "-1";
1116 if (allowVideoDevice) {
1117 allowedDevices.push(videoDeviceIndex);
1118 // Session permission will be removed after use
1119 // (it's really one-shot, not for the entire session)
1120 perms.addFromPrincipal(
1122 "MediaManagerVideo",
1124 perms.EXPIRE_SESSION
1126 let { mediaSource, rawId } = videoInputDevices.find(
1127 ({ deviceIndex }) => deviceIndex == videoDeviceIndex
1129 aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1131 lazy.SitePermissions.setForPrincipal(
1134 lazy.SitePermissions.ALLOW
1140 if (reqAudioInput === "Microphone") {
1141 let audioDeviceIndex = doc.getElementById(
1142 "webRTC-selectMicrophone-menulist"
1144 let allowMic = audioDeviceIndex != "-1";
1146 allowedDevices.push(audioDeviceIndex);
1147 let { mediaSource, rawId } = audioInputDevices.find(
1148 ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1150 aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1152 lazy.SitePermissions.setForPrincipal(
1155 lazy.SitePermissions.ALLOW
1159 } else if (reqAudioInput === "AudioCapture") {
1160 // Only one device possible for audio capture.
1161 allowedDevices.push(0);
1164 if (reqAudioOutput) {
1165 let audioDeviceIndex = doc.getElementById(
1166 "webRTC-selectSpeaker-richlistbox"
1168 let allowSpeaker = audioDeviceIndex != "-1";
1170 allowedDevices.push(audioDeviceIndex);
1171 let { id } = audioOutputDevices.find(
1172 ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1174 lazy.SitePermissions.setForPrincipal(
1176 ["speaker", id].join("^"),
1177 lazy.SitePermissions.ALLOW
1182 if (!allowedDevices.length) {
1183 aActor.denyRequest(aRequest);
1187 const camNeeded = reqVideoInput === "Camera";
1188 const micNeeded = !!reqAudioInput;
1189 const scrNeeded = reqVideoInput === "Screen";
1190 const havePermission = await aActor.checkOSPermission(
1195 if (!havePermission) {
1196 aActor.denyRequestNoPermission(aRequest);
1200 aActor.sendAsyncMessage("webrtc:Allow", {
1201 callID: aRequest.callID,
1202 windowID: aRequest.windowID,
1203 devices: allowedDevices,
1204 suppressNotifications: silenceNotifications,
1208 // If we haven't handled the permission yet, we want to show the doorhanger.
1213 function shouldShowAlwaysRemember() {
1214 // Don't offer "always remember" action in PB mode
1215 if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
1219 // Don't offer "always remember" action in maybe unsafe permission
1221 if (aRequest.secondOrigin) {
1225 // Speaker grants are always remembered, so no checkbox is required.
1226 if (reqAudioOutput) {
1233 if (shouldShowAlwaysRemember()) {
1234 // Disable the permanent 'Allow' action if the connection isn't secure, or for
1235 // screen/audio sharing (because we can't guess which window the user wants to
1236 // share without prompting). Note that we never enter this block for private
1237 // browsing windows.
1239 if (sharingScreen) {
1240 reason = "webrtc-reason-for-no-permanent-allow-screen";
1241 } else if (sharingAudio) {
1242 reason = "webrtc-reason-for-no-permanent-allow-audio";
1243 } else if (!aRequest.secure) {
1244 reason = "webrtc-reason-for-no-permanent-allow-insecure";
1247 options.checkbox = {
1248 label: localization.formatValueSync("webrtc-remember-allow-checkbox"),
1249 checked: principal.isAddonOrExpandedAddonPrincipal,
1250 checkedState: reason
1252 disableMainAction: true,
1253 warningLabel: localization.formatValueSync(reason),
1259 // If the notification silencing feature is enabled and we're sharing a
1260 // screen, then the checkbox for the permission panel is what controls
1261 // notification silencing.
1262 if (notificationSilencingEnabled && sharingScreen) {
1263 options.checkbox = {
1264 label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
1267 disableMainAction: false,
1272 let anchorId = "webRTC-shareDevices-notification-icon";
1273 if (reqVideoInput === "Screen") {
1274 anchorId = "webRTC-shareScreen-notification-icon";
1275 } else if (!reqVideoInput) {
1276 if (reqAudioInput && !reqAudioOutput) {
1277 anchorId = "webRTC-shareMicrophone-notification-icon";
1278 } else if (!reqAudioInput && reqAudioOutput) {
1279 anchorId = "webRTC-shareSpeaker-notification-icon";
1283 if (aRequest.secondOrigin) {
1284 options.secondName = lazy.webrtcUI.getHostOrExtensionName(
1286 aRequest.secondOrigin
1290 notification = chromeDoc.defaultView.PopupNotifications.show(
1292 "webRTC-shareDevices",
1299 notification.callID = aRequest.callID;
1301 let schemeHistogram = Services.telemetry.getKeyedHistogramById(
1302 "PERMISSION_REQUEST_ORIGIN_SCHEME"
1304 let userInputHistogram = Services.telemetry.getKeyedHistogramById(
1305 "PERMISSION_REQUEST_HANDLING_USER_INPUT"
1308 let docURI = aRequest.documentURI;
1310 if (docURI.startsWith("https")) {
1312 } else if (docURI.startsWith("http")) {
1316 for (let requestType of requestTypes) {
1317 if (requestType == "AudioCapture") {
1318 requestType = "Microphone";
1320 requestType = requestType.toLowerCase();
1322 schemeHistogram.add(requestType, scheme);
1323 userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
1328 * @param {"Screen" | "Camera" | null} reqVideoInput
1329 * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
1330 * @param {boolean} reqAudioOutput
1331 * @param {boolean} delegation - Is the access delegated to a third party?
1332 * @returns {string} Localization message identifier
1334 function getPromptMessageId(
1340 switch (reqVideoInput) {
1342 switch (reqAudioInput) {
1345 ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
1346 : "webrtc-allow-share-camera-and-microphone";
1347 case "AudioCapture":
1349 ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
1350 : "webrtc-allow-share-camera-and-audio-capture";
1353 ? "webrtc-allow-share-camera-unsafe-delegation"
1354 : "webrtc-allow-share-camera";
1358 switch (reqAudioInput) {
1361 ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
1362 : "webrtc-allow-share-screen-and-microphone";
1363 case "AudioCapture":
1365 ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
1366 : "webrtc-allow-share-screen-and-audio-capture";
1369 ? "webrtc-allow-share-screen-unsafe-delegation"
1370 : "webrtc-allow-share-screen";
1374 switch (reqAudioInput) {
1377 ? "webrtc-allow-share-microphone-unsafe-delegation"
1378 : "webrtc-allow-share-microphone";
1379 case "AudioCapture":
1381 ? "webrtc-allow-share-audio-capture-unsafe-delegation"
1382 : "webrtc-allow-share-audio-capture";
1384 // This should be always true, if we've reached this far.
1385 if (reqAudioOutput) {
1387 ? "webrtc-allow-share-speaker-unsafe-delegation"
1388 : "webrtc-allow-share-speaker";
1396 * Checks whether we have a microphone/camera in use by checking the activePerms map
1397 * or if we have an allow permission for a microphone/camera in sitePermissions
1398 * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
1399 * @return true if one of the above conditions is met
1401 function allowedOrActiveCameraOrMicrophone(browser) {
1402 // Do we have an allow permission for cam/mic in the permissions manager?
1404 lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
1406 perm.state == lazy.SitePermissions.ALLOW &&
1407 (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
1411 // Return early, no need to check for active devices
1415 // Do we have an active device?
1417 // Find all windowIDs that belong to our browsing contexts
1418 browser.browsingContext
1419 .getAllBrowsingContextsInSubtree()
1420 // Only keep the outerWindowIds
1421 .map(bc => bc.currentWindowGlobal?.outerWindowId)
1422 .filter(id => id != null)
1423 // We have an active device if one of our windowIds has a non empty map in the activePerms map
1424 // that includes one device of type "camera" or "microphone"
1426 let map = lazy.webrtcUI.activePerms.get(id);
1428 // This windowId has no active device
1431 // Let's see if one of the devices is a camera or a microphone
1432 let types = [...map.values()];
1433 return types.includes("microphone") || types.includes("camera");
1438 function removePrompt(aBrowser, aCallId) {
1439 let chromeWin = aBrowser.ownerGlobal;
1440 let notification = chromeWin.PopupNotifications.getNotification(
1441 "webRTC-shareDevices",
1444 if (notification && notification.callID == aCallId) {
1445 notification.remove();
1450 * Clears temporary permission grants used for WebRTC device grace periods.
1451 * @param browser - Browser element to clear permissions for.
1452 * @param {boolean} clearCamera - Clear camera grants.
1453 * @param {boolean} clearMicrophone - Clear microphone grants.
1455 function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
1456 if (!clearCamera && !clearMicrophone) {
1457 // Nothing to clear.
1460 let perms = lazy.SitePermissions.getAllForBrowser(browser);
1463 let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
1464 // We only want to clear WebRTC grace periods. These are temporary, device
1465 // specifc (double-keyed) microphone or camera permissions.
1468 perm.state == lazy.SitePermissions.ALLOW &&
1469 perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
1470 ((clearCamera && id == "camera") ||
1471 (clearMicrophone && id == "microphone"))
1475 lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)