Backed out 7 changesets (bug 1839993) for causing build bustages on DecoderTemplate...
[gecko.git] / browser / actors / WebRTCParent.sys.mjs
blob806fd4abcf898c5d21a59bc2a7902d1f23052e4f
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";
7 const lazy = {};
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",
13 });
15 XPCOMUtils.defineLazyServiceGetter(
16   lazy,
17   "OSPermissions",
18   "@mozilla.org/ospermissionrequest;1",
19   "nsIOSPermissionRequest"
22 export class WebRTCParent extends JSWindowActorParent {
23   didDestroy() {
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);
32   }
34   getBrowser() {
35     return this.browsingContext.top.embedderElement;
36   }
38   receiveMessage(aMessage) {
39     switch (aMessage.name) {
40       case "rtcpeer:Request": {
41         let params = Object.freeze(
42           Object.assign(
43             {
44               origin: this.manager.documentPrincipal.origin,
45             },
46             aMessage.data
47           )
48         );
50         let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);
52         (async function () {
53           for (let blocker of blockers) {
54             try {
55               let result = await blocker(params);
56               if (result == "deny") {
57                 return false;
58               }
59             } catch (err) {
60               console.error(`error in PeerConnection blocker: ${err.message}`);
61             }
62           }
63           return true;
64         })().then(decision => {
65           let message;
66           if (decision) {
67             lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
68             message = "rtcpeer:Allow";
69           } else {
70             lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
71             message = "rtcpeer:Deny";
72           }
74           this.sendAsyncMessage(message, {
75             callID: params.callID,
76             windowID: params.windowID,
77           });
78         });
79         break;
80       }
81       case "rtcpeer:CancelRequest": {
82         let params = Object.freeze({
83           origin: this.manager.documentPrincipal.origin,
84           callID: aMessage.data,
85         });
86         lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
87         break;
88       }
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);
106         } else {
107           prompt(this, this.getBrowser(), data);
108         }
109         break;
110       }
111       case "webrtc:StopRecording":
112         this.stopRecording(
113           aMessage.data.windowID,
114           aMessage.data.mediaSource,
115           aMessage.data.rawID
116         );
117         break;
118       case "webrtc:CancelRequest": {
119         let browser = this.getBrowser();
120         // browser can be null when closing the window
121         if (browser) {
122           removePrompt(browser, aMessage.data);
123         }
124         break;
125       }
126       case "webrtc:UpdateIndicators": {
127         let { data } = aMessage;
128         data.documentURI = this.manager.documentURI?.spec;
129         if (data.windowId) {
130           if (!data.remove) {
131             data.principal = this.manager.topWindowContext.documentPrincipal;
132           }
133           lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
134         }
135         this.updateIndicators(data);
136         break;
137       }
138     }
139   }
141   updateIndicators(aData) {
142     let browsingContext = this.browsingContext;
143     let state = lazy.webrtcUI.updateIndicators(browsingContext.top);
145     let browser = this.getBrowser();
146     if (!browser) {
147       return;
148     }
150     state.browsingContext = browsingContext;
151     state.windowId = aData.windowId;
153     let tabbrowser = browser.ownerGlobal.gBrowser;
154     if (tabbrowser) {
155       tabbrowser.updateBrowserSharing(browser, {
156         webRTC: state,
157       });
158     }
159   }
161   denyRequest(aRequest) {
162     this.sendAsyncMessage("webrtc:Deny", {
163       callID: aRequest.callID,
164       windowID: aRequest.windowID,
165     });
166   }
168   //
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.
173   //
174   denyRequestNoPermission(aRequest) {
175     this.sendAsyncMessage("webrtc:Deny", {
176       callID: aRequest.callID,
177       windowID: aRequest.windowID,
178       noOSPermission: true,
179     });
180   }
182   //
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.
186   //
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.
191     if (
192       !scrNeeded &&
193       Services.prefs.getBoolPref("media.navigator.streams.fake", false)
194     ) {
195       return true;
196     }
197     let camStatus = {},
198       micStatus = {};
199     if (camNeeded || micNeeded) {
200       lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
201     }
202     if (camNeeded) {
203       let camPermission = camStatus.value;
204       let camAccessible = await this.checkAndGetOSPermission(
205         camPermission,
206         lazy.OSPermissions.requestVideoCapturePermission
207       );
208       if (!camAccessible) {
209         return false;
210       }
211     }
212     if (micNeeded) {
213       let micPermission = micStatus.value;
214       let micAccessible = await this.checkAndGetOSPermission(
215         micPermission,
216         lazy.OSPermissions.requestAudioCapturePermission
217       );
218       if (!micAccessible) {
219         return false;
220       }
221     }
222     let scrStatus = {};
223     if (scrNeeded) {
224       lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
225       if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
226         lazy.OSPermissions.maybeRequestScreenCapturePermission();
227         return false;
228       }
229     }
230     return true;
231   }
233   //
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.
238   //
239   async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
240     if (
241       devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
242       devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
243     ) {
244       return false;
245     }
246     if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
247       let deviceAllowed = await requestPermissionFunc();
248       if (!deviceAllowed) {
249         return false;
250       }
251     }
252     return true;
253   }
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)) {
261             continue;
262           }
263           // Deactivate this device (no aRawId means all devices).
264           this.deactivateDevicePerm(
265             aOuterWindowId,
266             mediaSource,
267             rawId,
268             principal
269           );
270         }
271       }
272     }
273   }
275   /**
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.
278    */
279   activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
280     if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
281       lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
282     }
283     lazy.webrtcUI.activePerms
284       .get(this.manager.outerWindowId)
285       .set(aOuterWindowId + aMediaSource + aId, aMediaSource);
286   }
288   /**
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.
292    *
293    * However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
294    * grant is extended for an additional period of time through SitePermissions.
295    */
296   deactivateDevicePerm(
297     aOuterWindowId,
298     aMediaSource,
299     aId,
300     aPermissionPrincipal
301   ) {
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)) {
306       return;
307     }
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
312     if (
313       (aMediaSource != "camera" && aMediaSource != "microphone") ||
314       !this.browsingContext.top.embedderElement
315     ) {
316       return;
317     }
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.
325       //
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.
330       //
331       let permissionName = [aMediaSource, aId].join("^");
332       lazy.SitePermissions.setForPrincipal(
333         aPermissionPrincipal,
334         permissionName,
335         lazy.SitePermissions.ALLOW,
336         lazy.SitePermissions.SCOPE_TEMPORARY,
337         this.browsingContext.top.embedderElement,
338         gracePeriodMs
339       );
340     }
341   }
343   /**
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.
349    */
350   checkRequestAllowed(aRequest, aPrincipal) {
351     if (!aRequest.secure) {
352       return false;
353     }
354     // Always prompt for screen sharing
355     if (aRequest.sharingScreen) {
356       return false;
357     }
358     let {
359       callID,
360       windowID,
361       audioInputDevices,
362       videoInputDevices,
363       audioOutputDevices,
364       hasInherentAudioConstraints,
365       hasInherentVideoConstraints,
366       audioOutputId,
367     } = aRequest;
369     if (audioOutputDevices?.length) {
370       // Prompt if a specific device is not requested, available and allowed.
371       let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
372       if (
373         !device ||
374         !lazy.SitePermissions.getForPrincipal(
375           aPrincipal,
376           ["speaker", device.id].join("^"),
377           this.getBrowser()
378         ).state == lazy.SitePermissions.ALLOW
379       ) {
380         return false;
381       }
382       this.sendAsyncMessage("webrtc:Allow", {
383         callID,
384         windowID,
385         devices: [device.deviceIndex],
386       });
387       return true;
388     }
390     let { perms } = Services;
391     if (
392       perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
393     ) {
394       perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
395     }
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) ||
407       (!limited &&
408         (lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
409           lazy.SitePermissions.ALLOW ||
410           lazy.SitePermissions.getForPrincipal(
411             aPrincipal,
412             [mediaSource, rawId].join("^"),
413             this.getBrowser()
414           ).state == lazy.SitePermissions.ALLOW));
416     let microphone;
417     if (audioInputDevices.length) {
418       for (let device of audioInputDevices) {
419         if (isAllowed(device, "microphone")) {
420           microphone = device;
421           break;
422         }
423         if (hasInherentAudioConstraints) {
424           // Inherent constraints suggest site is looking for a specific mic
425           break;
426         }
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.
431       }
432       if (!microphone) {
433         return false;
434       }
435     }
436     let camera;
437     if (videoInputDevices.length) {
438       for (let device of videoInputDevices) {
439         if (isAllowed(device, "camera")) {
440           camera = device;
441           break;
442         }
443         if (hasInherentVideoConstraints) {
444           // Inherent constraints suggest site is looking for a specific camera
445           break;
446         }
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.
451       }
452       if (!camera) {
453         return false;
454       }
455     }
456     let devices = [];
457     if (camera) {
458       perms.addFromPrincipal(
459         aPrincipal,
460         "MediaManagerVideo",
461         perms.ALLOW_ACTION,
462         perms.EXPIRE_SESSION
463       );
464       devices.push(camera.deviceIndex);
465       this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
466     }
467     if (microphone) {
468       devices.push(microphone.deviceIndex);
469       this.activateDevicePerm(
470         windowID,
471         microphone.mediaSource,
472         microphone.rawId
473       );
474     }
475     this.checkOSPermission(!!camera, !!microphone, false).then(
476       havePermission => {
477         if (havePermission) {
478           this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
479         } else {
480           this.denyRequestNoPermission(aRequest);
481         }
482       }
483     );
484     return true;
485   }
488 function prompt(aActor, aBrowser, aRequest) {
489   let {
490     audioInputDevices,
491     videoInputDevices,
492     audioOutputDevices,
493     sharingScreen,
494     sharingAudio,
495     requestTypes,
496   } = aRequest;
498   let principal =
499     Services.scriptSecurityManager.createContentPrincipalFromOrigin(
500       aRequest.origin
501     );
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) {
507     let isPopup = false;
508     let isBackground = false;
510     for (let view of principal.addonPolicy.extension.views) {
511       if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
512         isPopup = true;
513       }
514       if (view.viewType == "background" && view.xulBrowser == aBrowser) {
515         isBackground = true;
516       }
517     }
519     // Recording from background pages is considered too sensitive and will
520     // always be denied.
521     if (isBackground) {
522       aActor.denyRequest(aRequest);
523       return;
524     }
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.
528     if (isPopup) {
529       if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
530         aActor.denyRequest(aRequest);
531       }
532       return;
533     }
534   }
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) {
539     const permissionID =
540       type == "AudioCapture" ? "microphone" : type.toLowerCase();
541     if (
542       lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
543         .state == lazy.SitePermissions.BLOCK
544     ) {
545       aActor.denyRequest(aRequest);
546       return;
547     }
548   }
550   let chromeDoc = aBrowser.ownerDocument;
551   const localization = new Localization(
552     ["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
553     true
554   );
556   /** @type {"Screen" | "Camera" | null} */
557   let reqVideoInput = null;
558   if (videoInputDevices.length) {
559     reqVideoInput = sharingScreen ? "Screen" : "Camera";
560   }
561   /** @type {"AudioCapture" | "Microphone" | null} */
562   let reqAudioInput = null;
563   if (audioInputDevices.length) {
564     reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
565   }
566   const reqAudioOutput = !!audioOutputDevices.length;
568   const stringId = getPromptMessageId(
569     reqVideoInput,
570     reqAudioInput,
571     reqAudioOutput,
572     !!aRequest.secondOrigin
573   );
574   let message;
575   let originToShow;
576   if (principal.schemeIs("file")) {
577     message = localization.formatValueSync(stringId + "-with-file");
578     originToShow = null;
579   } else {
580     message = localization.formatValueSync(stringId, {
581       origin: "<>",
582       thirdParty: "{}",
583     });
584     originToShow = lazy.webrtcUI.getHostOrExtensionName(principal.URI);
585   }
586   let notification; // Used by action callbacks.
587   const actionL10nIds = [{ id: "webrtc-action-allow" }];
589   let notificationSilencingEnabled = Services.prefs.getBoolPref(
590     "privacy.webrtc.allowSilencingNotifications"
591   );
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" });
610     secondaryActions = [
611       {
612         callback() {
613           aActor.denyRequest(aRequest);
614           if (!isNotNowLabelEnabled) {
615             lazy.SitePermissions.setForPrincipal(
616               principal,
617               permissionName,
618               lazy.SitePermissions.BLOCK,
619               lazy.SitePermissions.SCOPE_TEMPORARY,
620               notification.browser
621             );
622           }
623         },
624       },
625       {
626         callback() {
627           aActor.denyRequest(aRequest);
628           lazy.SitePermissions.setForPrincipal(
629             principal,
630             permissionName,
631             lazy.SitePermissions.BLOCK,
632             lazy.SitePermissions.SCOPE_PERSISTENT,
633             notification.browser
634           );
635         },
636       },
637     ];
638   } else {
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 });
645     secondaryActions = [
646       {
647         callback(aState) {
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) {
658             return;
659           }
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",
667             !!reqAudioInput
668           );
670           const scope = isPersistent
671             ? lazy.SitePermissions.SCOPE_PERSISTENT
672             : lazy.SitePermissions.SCOPE_TEMPORARY;
673           if (reqAudioInput) {
674             lazy.SitePermissions.setForPrincipal(
675               principal,
676               "microphone",
677               lazy.SitePermissions.BLOCK,
678               scope,
679               notification.browser
680             );
681           }
682           if (reqVideoInput) {
683             lazy.SitePermissions.setForPrincipal(
684               principal,
685               sharingScreen ? "screen" : "camera",
686               lazy.SitePermissions.BLOCK,
687               scope,
688               notification.browser
689             );
690           }
691         },
692       },
693     ];
694   }
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)
702     .map(msg =>
703       msg.attributes.reduce(
704         (acc, { name, value }) => ({ ...acc, [name]: value }),
705         {}
706       )
707     );
709   const mainAction = {
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.
715     callback() {},
716   };
718   for (let i = 0; i < secondaryActions.length; ++i) {
719     secondaryActions[i].label = secondaryMessages[i].label;
720     secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
721   }
723   let options = {
724     name: originToShow,
725     persistent: true,
726     hideClose: true,
727     eventCallback(aTopic, aNewBrowser, isCancel) {
728       if (aTopic == "swapping") {
729         return true;
730       }
732       let doc = this.browser.ownerDocument;
734       // Clean-up video streams of screensharing previews.
735       if (
736         reqVideoInput !== "Screen" ||
737         aTopic == "dismissed" ||
738         aTopic == "removed"
739       ) {
740         let video = doc.getElementById("webRTC-previewVideo");
741         video.deviceId = null; // Abort previews still being started.
742         if (video.stream) {
743           video.stream.getTracks().forEach(t => t.stop());
744           video.stream = null;
745           video.src = null;
746           doc.getElementById("webRTC-preview").hidden = true;
747         }
748         let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
749         if (menupopup._commandEventListener) {
750           menupopup.removeEventListener(
751             "command",
752             menupopup._commandEventListener
753           );
754           menupopup._commandEventListener = null;
755         }
756       }
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);
762       } else if (
763         aTopic == "shown" &&
764         audioOutputDevices.length > 1 &&
765         !notification.wasDismissed
766       ) {
767         // Focus the list on first show so that arrow keys select the speaker.
768         doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
769       }
771       if (aTopic != "showing") {
772         return false;
773       }
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
777       // the notification.
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)) {
781         this.remove();
782         return true;
783       }
785       /**
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.
793        */
794       function listDevices(devices, IDPrefix, describedByIDs) {
795         let labelID = `${IDPrefix}-single-device-label`;
796         let list;
797         let itemParent;
798         if (IDPrefix == "webRTC-selectSpeaker") {
799           list = doc.getElementById(`${IDPrefix}-richlistbox`);
800           itemParent = list;
801         } else {
802           itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
803           list = itemParent.parentNode; // menulist
804         }
805         while (itemParent.lastChild) {
806           itemParent.removeChild(itemParent.lastChild);
807         }
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
813         // state.
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();
824             });
825             if (device.id == aRequest.audioOutputId) {
826               defaultIndex = device.deviceIndex;
827             }
828           }
829         }
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;
837           list.hidden = true;
838         } else {
839           label.hidden = true;
840           list.hidden = false;
841         }
842       }
844       let notificationElement = doc.getElementById(
845         "webRTC-shareDevices-notification"
846       );
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");
853         } else {
854           notificationElement.removeAttribute("invalidselection");
855         }
856       }
858       function listScreenShareDevices(menupopup, devices) {
859         while (menupopup.lastChild) {
860           menupopup.removeChild(menupopup.lastChild);
861         }
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").
872         addDeviceToList(
873           menupopup.parentNode,
874           localization.formatValueSync("webrtc-pick-window-or-screen"),
875           "-1"
876         );
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;
886           let name;
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"),
898               i,
899               type
900             );
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;
907             continue;
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");
912             } else {
913               name = localization.formatValueSync("webrtc-share-monitor", {
914                 monitorIndex,
915               });
916               ++monitorIndex;
917             }
918           } else {
919             name = device.name;
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", {
926                 appName,
927                 windowCount: parseInt(count),
928               });
929             }
930           }
931           let item = addDeviceToList(menupopup.parentNode, name, i, type);
932           item.deviceId = device.rawId;
933           item.mediaSource = type;
934           if (device.scary) {
935             item.scary = true;
936           }
937         }
939         // Always re-select the "No <type>" item.
940         doc
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");
948           if (video.stream) {
949             video.stream.getTracks().forEach(t => t.stop());
950             video.stream = null;
951           }
953           const { deviceId, mediaSource, scary } = event.target;
954           if (deviceId == undefined) {
955             doc.getElementById("webRTC-preview").hidden = true;
956             video.src = null;
957             return;
958           }
960           let warning = doc.getElementById("webRTC-previewWarning");
961           let warningBox = doc.getElementById("webRTC-previewWarningBox");
962           warningBox.hidden = !scary;
963           let chromeWin = doc.defaultView;
964           if (scary) {
965             const warnId =
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"
973             );
974             const baseURL = Services.urlFormatter.formatURLPref(
975               "app.support.baseURL"
976             );
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.
991             let scrStatus = {};
992             lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
993             if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
994               lazy.OSPermissions.maybeRequestScreenCapturePermission();
995             }
996           }
998           let perms = Services.perms;
999           let chromePrincipal =
1000             Services.scriptSecurityManager.getSystemPrincipal();
1001           perms.addFromPrincipal(
1002             chromePrincipal,
1003             "MediaManagerVideo",
1004             perms.ALLOW_ACTION,
1005             perms.EXPIRE_SESSION
1006           );
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.
1010           if (
1011             (!isPipeWireDetected || mediaSource == "browser") &&
1012             Services.prefs.getBoolPref(
1013               "media.getdisplaymedia.previews.enabled",
1014               true
1015             )
1016           ) {
1017             video.deviceId = deviceId;
1018             let constraints = {
1019               video: { mediaSource, deviceId: { exact: deviceId } },
1020             };
1021             chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
1022               stream => {
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());
1027                   return;
1028                 }
1029                 video.srcObject = stream;
1030                 video.stream = stream;
1031                 doc.getElementById("webRTC-preview").hidden = false;
1032                 video.onloadedmetadata = function () {
1033                   video.play();
1034                 };
1035               },
1036               err => {
1037                 if (
1038                   err.name == "OverconstrainedError" &&
1039                   err.constraint == "deviceId"
1040                 ) {
1041                   // Window has disappeared since enumeration, which can happen.
1042                   // No preview for you.
1043                   return;
1044                 }
1045                 console.error(
1046                   `error in preview: ${err.message} ${err.constraint}`
1047                 );
1048               }
1049             );
1050           }
1051         };
1052         menupopup.addEventListener("command", menupopup._commandEventListener);
1053       }
1055       function addDeviceToList(list, deviceName, deviceIndex, type) {
1056         let item = list.appendItem(deviceName, deviceIndex);
1057         item.setAttribute("tooltiptext", deviceName);
1058         if (type) {
1059           item.setAttribute("devicetype", type);
1060         }
1062         if (deviceIndex == "-1") {
1063           item.setAttribute("disabled", true);
1064         }
1066         return item;
1067       }
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"
1082         );
1083         listScreenShareDevices(windowMenupopup, videoInputDevices);
1084         checkDisabledWindowMenuItem();
1085       } else {
1086         listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
1087         notificationElement.removeAttribute("invalidselection");
1088       }
1089       if (!sharingAudio) {
1090         listDevices(
1091           audioInputDevices,
1092           "webRTC-selectMicrophone",
1093           describedByIDs
1094         );
1095       }
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(
1101         "aria-describedby",
1102         describedByIDs.join(" ")
1103       );
1105       this.mainAction.callback = async function (aState) {
1106         let remember = false;
1107         let silenceNotifications = false;
1109         if (notificationSilencingEnabled && sharingScreen) {
1110           silenceNotifications = aState && aState.checkboxChecked;
1111         } else {
1112           remember = aState && aState.checkboxChecked;
1113         }
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(
1128               principal,
1129               "MediaManagerVideo",
1130               perms.ALLOW_ACTION,
1131               perms.EXPIRE_SESSION
1132             );
1133             let { mediaSource, rawId } = videoInputDevices.find(
1134               ({ deviceIndex }) => deviceIndex == videoDeviceIndex
1135             );
1136             aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1137             if (remember) {
1138               lazy.SitePermissions.setForPrincipal(
1139                 principal,
1140                 "camera",
1141                 lazy.SitePermissions.ALLOW
1142               );
1143             }
1144           }
1145         }
1147         if (reqAudioInput === "Microphone") {
1148           let audioDeviceIndex = doc.getElementById(
1149             "webRTC-selectMicrophone-menulist"
1150           ).value;
1151           let allowMic = audioDeviceIndex != "-1";
1152           if (allowMic) {
1153             allowedDevices.push(audioDeviceIndex);
1154             let { mediaSource, rawId } = audioInputDevices.find(
1155               ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1156             );
1157             aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1158             if (remember) {
1159               lazy.SitePermissions.setForPrincipal(
1160                 principal,
1161                 "microphone",
1162                 lazy.SitePermissions.ALLOW
1163               );
1164             }
1165           }
1166         } else if (reqAudioInput === "AudioCapture") {
1167           // Only one device possible for audio capture.
1168           allowedDevices.push(0);
1169         }
1171         if (reqAudioOutput) {
1172           let audioDeviceIndex = doc.getElementById(
1173             "webRTC-selectSpeaker-richlistbox"
1174           ).value;
1175           let allowSpeaker = audioDeviceIndex != "-1";
1176           if (allowSpeaker) {
1177             allowedDevices.push(audioDeviceIndex);
1178             let { id } = audioOutputDevices.find(
1179               ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1180             );
1181             lazy.SitePermissions.setForPrincipal(
1182               principal,
1183               ["speaker", id].join("^"),
1184               lazy.SitePermissions.ALLOW
1185             );
1186           }
1187         }
1189         if (!allowedDevices.length) {
1190           aActor.denyRequest(aRequest);
1191           return;
1192         }
1194         const camNeeded = reqVideoInput === "Camera";
1195         const micNeeded = !!reqAudioInput;
1196         const scrNeeded = reqVideoInput === "Screen";
1197         const havePermission = await aActor.checkOSPermission(
1198           camNeeded,
1199           micNeeded,
1200           scrNeeded
1201         );
1202         if (!havePermission) {
1203           aActor.denyRequestNoPermission(aRequest);
1204           return;
1205         }
1207         aActor.sendAsyncMessage("webrtc:Allow", {
1208           callID: aRequest.callID,
1209           windowID: aRequest.windowID,
1210           devices: allowedDevices,
1211           suppressNotifications: silenceNotifications,
1212         });
1213       };
1215       // If we haven't handled the permission yet, we want to show the doorhanger.
1216       return false;
1217     },
1218   };
1220   function shouldShowAlwaysRemember() {
1221     // Don't offer "always remember" action in PB mode
1222     if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
1223       return false;
1224     }
1226     // Don't offer "always remember" action in maybe unsafe permission
1227     // delegation
1228     if (aRequest.secondOrigin) {
1229       return false;
1230     }
1232     // Speaker grants are always remembered, so no checkbox is required.
1233     if (reqAudioOutput) {
1234       return false;
1235     }
1237     return true;
1238   }
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.
1245     let reason = "";
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";
1252     }
1254     options.checkbox = {
1255       label: localization.formatValueSync("webrtc-remember-allow-checkbox"),
1256       checked: principal.isAddonOrExpandedAddonPrincipal,
1257       checkedState: reason
1258         ? {
1259             disableMainAction: true,
1260             warningLabel: localization.formatValueSync(reason),
1261           }
1262         : undefined,
1263     };
1264   }
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"),
1272       checked: false,
1273       checkedState: {
1274         disableMainAction: false,
1275       },
1276     };
1277   }
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";
1287     }
1288   }
1290   if (aRequest.secondOrigin) {
1291     options.secondName = lazy.webrtcUI.getHostOrExtensionName(
1292       null,
1293       aRequest.secondOrigin
1294     );
1295   }
1297   notification = chromeDoc.defaultView.PopupNotifications.show(
1298     aBrowser,
1299     "webRTC-shareDevices",
1300     message,
1301     anchorId,
1302     mainAction,
1303     secondaryActions,
1304     options
1305   );
1306   notification.callID = aRequest.callID;
1308   let schemeHistogram = Services.telemetry.getKeyedHistogramById(
1309     "PERMISSION_REQUEST_ORIGIN_SCHEME"
1310   );
1311   let userInputHistogram = Services.telemetry.getKeyedHistogramById(
1312     "PERMISSION_REQUEST_HANDLING_USER_INPUT"
1313   );
1315   let docURI = aRequest.documentURI;
1316   let scheme = 0;
1317   if (docURI.startsWith("https")) {
1318     scheme = 2;
1319   } else if (docURI.startsWith("http")) {
1320     scheme = 1;
1321   }
1323   for (let requestType of requestTypes) {
1324     if (requestType == "AudioCapture") {
1325       requestType = "Microphone";
1326     }
1327     requestType = requestType.toLowerCase();
1329     schemeHistogram.add(requestType, scheme);
1330     userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
1331   }
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
1340  */
1341 function getPromptMessageId(
1342   reqVideoInput,
1343   reqAudioInput,
1344   reqAudioOutput,
1345   delegation
1346 ) {
1347   switch (reqVideoInput) {
1348     case "Camera":
1349       switch (reqAudioInput) {
1350         case "Microphone":
1351           return delegation
1352             ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
1353             : "webrtc-allow-share-camera-and-microphone";
1354         case "AudioCapture":
1355           return delegation
1356             ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
1357             : "webrtc-allow-share-camera-and-audio-capture";
1358         default:
1359           return delegation
1360             ? "webrtc-allow-share-camera-unsafe-delegation"
1361             : "webrtc-allow-share-camera";
1362       }
1364     case "Screen":
1365       switch (reqAudioInput) {
1366         case "Microphone":
1367           return delegation
1368             ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
1369             : "webrtc-allow-share-screen-and-microphone";
1370         case "AudioCapture":
1371           return delegation
1372             ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
1373             : "webrtc-allow-share-screen-and-audio-capture";
1374         default:
1375           return delegation
1376             ? "webrtc-allow-share-screen-unsafe-delegation"
1377             : "webrtc-allow-share-screen";
1378       }
1380     default:
1381       switch (reqAudioInput) {
1382         case "Microphone":
1383           return delegation
1384             ? "webrtc-allow-share-microphone-unsafe-delegation"
1385             : "webrtc-allow-share-microphone";
1386         case "AudioCapture":
1387           return delegation
1388             ? "webrtc-allow-share-audio-capture-unsafe-delegation"
1389             : "webrtc-allow-share-audio-capture";
1390         default:
1391           // This should be always true, if we've reached this far.
1392           if (reqAudioOutput) {
1393             return delegation
1394               ? "webrtc-allow-share-speaker-unsafe-delegation"
1395               : "webrtc-allow-share-speaker";
1396           }
1397           return undefined;
1398       }
1399   }
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
1407  */
1408 function allowedOrActiveCameraOrMicrophone(browser) {
1409   // Do we have an allow permission for cam/mic in the permissions manager?
1410   if (
1411     lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
1412       return (
1413         perm.state == lazy.SitePermissions.ALLOW &&
1414         (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
1415       );
1416     })
1417   ) {
1418     // Return early, no need to check for active devices
1419     return true;
1420   }
1422   // Do we have an active device?
1423   return (
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"
1432       .some(id => {
1433         let map = lazy.webrtcUI.activePerms.get(id);
1434         if (!map) {
1435           // This windowId has no active device
1436           return false;
1437         }
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");
1441       })
1442   );
1445 function removePrompt(aBrowser, aCallId) {
1446   let chromeWin = aBrowser.ownerGlobal;
1447   let notification = chromeWin.PopupNotifications.getNotification(
1448     "webRTC-shareDevices",
1449     aBrowser
1450   );
1451   if (notification && notification.callID == aCallId) {
1452     notification.remove();
1453   }
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.
1461  */
1462 function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
1463   if (!clearCamera && !clearMicrophone) {
1464     // Nothing to clear.
1465     return;
1466   }
1467   let perms = lazy.SitePermissions.getAllForBrowser(browser);
1468   perms
1469     .filter(perm => {
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.
1473       return (
1474         key &&
1475         perm.state == lazy.SitePermissions.ALLOW &&
1476         perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
1477         ((clearCamera && id == "camera") ||
1478           (clearMicrophone && id == "microphone"))
1479       );
1480     })
1481     .forEach(perm =>
1482       lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
1483     );