tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

webrtcUI.sys.mjs (36210B)


      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/. */
      4 
      5 import { EventEmitter } from "resource:///modules/syncedtabs/EventEmitter.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      8 
      9 const lazy = {};
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     12  SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
     13 });
     14 ChromeUtils.defineLazyGetter(
     15  lazy,
     16  "syncL10n",
     17  () => new Localization(["browser/webrtcIndicator.ftl"], true)
     18 );
     19 ChromeUtils.defineLazyGetter(
     20  lazy,
     21  "listFormat",
     22  () => new Services.intl.ListFormat(undefined)
     23 );
     24 
     25 const SHARING_L10NID_BY_TYPE = new Map([
     26  [
     27    "Camera",
     28    [
     29      "webrtc-indicator-menuitem-sharing-camera-with",
     30      "webrtc-indicator-menuitem-sharing-camera-with-n-tabs",
     31    ],
     32  ],
     33  [
     34    "Microphone",
     35    [
     36      "webrtc-indicator-menuitem-sharing-microphone-with",
     37      "webrtc-indicator-menuitem-sharing-microphone-with-n-tabs",
     38    ],
     39  ],
     40  [
     41    "Application",
     42    [
     43      "webrtc-indicator-menuitem-sharing-application-with",
     44      "webrtc-indicator-menuitem-sharing-application-with-n-tabs",
     45    ],
     46  ],
     47  [
     48    "Screen",
     49    [
     50      "webrtc-indicator-menuitem-sharing-screen-with",
     51      "webrtc-indicator-menuitem-sharing-screen-with-n-tabs",
     52    ],
     53  ],
     54  [
     55    "Window",
     56    [
     57      "webrtc-indicator-menuitem-sharing-window-with",
     58      "webrtc-indicator-menuitem-sharing-window-with-n-tabs",
     59    ],
     60  ],
     61  [
     62    "Browser",
     63    [
     64      "webrtc-indicator-menuitem-sharing-browser-with",
     65      "webrtc-indicator-menuitem-sharing-browser-with-n-tabs",
     66    ],
     67  ],
     68 ]);
     69 
     70 // These identifiers are defined in MediaStreamTrack.webidl
     71 const MEDIA_SOURCE_L10NID_BY_TYPE = new Map([
     72  ["camera", "webrtc-item-camera"],
     73  ["screen", "webrtc-item-screen"],
     74  ["application", "webrtc-item-application"],
     75  ["window", "webrtc-item-window"],
     76  ["browser", "webrtc-item-browser"],
     77  ["microphone", "webrtc-item-microphone"],
     78  ["audioCapture", "webrtc-item-audio-capture"],
     79 ]);
     80 
     81 export var webrtcUI = {
     82  initialized: false,
     83 
     84  peerConnectionBlockers: new Set(),
     85  emitter: new EventEmitter(),
     86 
     87  init() {
     88    if (!this.initialized) {
     89      Services.obs.addObserver(this, "browser-delayed-startup-finished");
     90      this.initialized = true;
     91 
     92      XPCOMUtils.defineLazyPreferenceGetter(
     93        this,
     94        "deviceGracePeriodTimeoutMs",
     95        "privacy.webrtc.deviceGracePeriodTimeoutMs"
     96      );
     97      XPCOMUtils.defineLazyPreferenceGetter(
     98        this,
     99        "showIndicatorsOnMacos14AndAbove",
    100        "privacy.webrtc.showIndicatorsOnMacos14AndAbove",
    101        true
    102      );
    103    }
    104  },
    105 
    106  uninit() {
    107    if (this.initialized) {
    108      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
    109      this.initialized = false;
    110    }
    111  },
    112 
    113  observe(subject, topic) {
    114    if (topic == "browser-delayed-startup-finished") {
    115      if (webrtcUI.showGlobalIndicator) {
    116        showOrCreateMenuForWindow(subject);
    117      }
    118    }
    119  },
    120 
    121  SHARING_NONE: 0,
    122  SHARING_WINDOW: 1,
    123  SHARING_SCREEN: 2,
    124 
    125  // Set of browser windows that are being shared over WebRTC.
    126  sharedBrowserWindows: new WeakSet(),
    127 
    128  // True if one or more screens is being shared.
    129  sharingScreen: false,
    130 
    131  allowedSharedBrowsers: new WeakSet(),
    132  allowTabSwitchesForSession: false,
    133  tabSwitchCountForSession: 0,
    134 
    135  // Map of browser elements to indicator data.
    136  perTabIndicators: new Map(),
    137  activePerms: new Map(),
    138 
    139  get showGlobalIndicator() {
    140    for (let [, indicators] of this.perTabIndicators) {
    141      if (
    142        indicators.showCameraIndicator ||
    143        indicators.showMicrophoneIndicator ||
    144        indicators.showScreenSharingIndicator
    145      ) {
    146        return true;
    147      }
    148    }
    149    return false;
    150  },
    151 
    152  get showCameraIndicator() {
    153    // Bug 1857254 - MacOS 14 displays two camera icons in menu bar
    154    // temporarily disabled the firefox camera icon until a better fix comes around
    155    if (
    156      AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) &&
    157      !this.showIndicatorsOnMacos14AndAbove
    158    ) {
    159      return false;
    160    }
    161 
    162    for (let [, indicators] of this.perTabIndicators) {
    163      if (indicators.showCameraIndicator) {
    164        return true;
    165      }
    166    }
    167    return false;
    168  },
    169 
    170  get showMicrophoneIndicator() {
    171    // Bug 1857254 - MacOS 14 displays two microphone icons in menu bar
    172    // temporarily disabled the firefox camera icon until a better fix comes around
    173    if (
    174      AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) &&
    175      !this.showIndicatorsOnMacos14AndAbove
    176    ) {
    177      return false;
    178    }
    179 
    180    for (let [, indicators] of this.perTabIndicators) {
    181      if (indicators.showMicrophoneIndicator) {
    182        return true;
    183      }
    184    }
    185    return false;
    186  },
    187 
    188  get showScreenSharingIndicator() {
    189    // Bug 1857254 - MacOS 14 displays two screen share icons in menu bar
    190    // temporarily disabled the firefox camera icon until a better fix comes around
    191    if (
    192      AppConstants.isPlatformAndVersionAtLeast("macosx", 14.0) &&
    193      !this.showIndicatorsOnMacos14AndAbove
    194    ) {
    195      return "";
    196    }
    197 
    198    let list = [""];
    199    for (let [, indicators] of this.perTabIndicators) {
    200      if (indicators.showScreenSharingIndicator) {
    201        list.push(indicators.showScreenSharingIndicator);
    202      }
    203    }
    204 
    205    let precedence = ["Screen", "Window", "Application", "Browser", ""];
    206 
    207    list.sort((a, b) => {
    208      return precedence.indexOf(a) - precedence.indexOf(b);
    209    });
    210 
    211    return list[0];
    212  },
    213 
    214  _streams: [],
    215  // The boolean parameters indicate which streams should be included in the result.
    216  getActiveStreams(aCamera, aMicrophone, aScreen, aTab, aWindow = false) {
    217    return webrtcUI._streams
    218      .filter(aStream => {
    219        let state = aStream.state;
    220        return (
    221          (aCamera && state.camera) ||
    222          (aMicrophone && state.microphone) ||
    223          (aScreen && state.screen) ||
    224          (aTab && state.browser) ||
    225          (aWindow && state.window)
    226        );
    227      })
    228      .map(aStream => {
    229        let state = aStream.state;
    230        let types = {
    231          camera: state.camera,
    232          microphone: state.microphone,
    233          screen: state.screen,
    234          window: state.window,
    235        };
    236        let browser = aStream.topBrowsingContext.embedderElement;
    237        // browser can be null when we are in the process of closing a tab
    238        // and our stream list hasn't been updated yet.
    239        // gBrowser will be null if a stream is used outside a tabbrowser window.
    240        let tab = browser?.ownerGlobal.gBrowser?.getTabForBrowser(browser);
    241        return {
    242          uri: state.documentURI,
    243          tab,
    244          browser,
    245          types,
    246          devices: state.devices,
    247        };
    248      });
    249  },
    250 
    251  /**
    252   * Returns true if aBrowser has an active WebRTC stream.
    253   */
    254  browserHasStreams(aBrowser) {
    255    for (let stream of this._streams) {
    256      if (stream.topBrowsingContext.embedderElement == aBrowser) {
    257        return true;
    258      }
    259    }
    260 
    261    return false;
    262  },
    263 
    264  /**
    265   * Determine the combined state of all the active streams associated with
    266   * the specified top-level browsing context.
    267   */
    268  getCombinedStateForBrowser(aTopBrowsingContext) {
    269    function combine(x, y) {
    270      if (
    271        x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
    272        y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
    273      ) {
    274        return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
    275      }
    276      if (
    277        x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
    278        y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
    279      ) {
    280        return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
    281      }
    282      return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
    283    }
    284 
    285    let camera, microphone, screen, window, browser;
    286    for (let stream of this._streams) {
    287      if (stream.topBrowsingContext == aTopBrowsingContext) {
    288        camera = combine(stream.state.camera, camera);
    289        microphone = combine(stream.state.microphone, microphone);
    290        screen = combine(stream.state.screen, screen);
    291        window = combine(stream.state.window, window);
    292        browser = combine(stream.state.browser, browser);
    293      }
    294    }
    295 
    296    let tabState = { camera, microphone };
    297    if (screen == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED) {
    298      tabState.screen = "Screen";
    299    } else if (window == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED) {
    300      tabState.screen = "Window";
    301    } else if (browser == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED) {
    302      tabState.screen = "Browser";
    303    } else if (screen == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED) {
    304      tabState.screen = "ScreenPaused";
    305    } else if (window == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED) {
    306      tabState.screen = "WindowPaused";
    307    } else if (browser == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED) {
    308      tabState.screen = "BrowserPaused";
    309    }
    310 
    311    let screenEnabled = tabState.screen && !tabState.screen.includes("Paused");
    312    let cameraEnabled =
    313      tabState.camera == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
    314    let microphoneEnabled =
    315      tabState.microphone == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
    316 
    317    // tabState.sharing controls which global indicator should be shown
    318    // for the tab. It should always be set to the _enabled_ device which
    319    // we consider most intrusive (screen > camera > microphone).
    320    if (screenEnabled) {
    321      tabState.sharing = "screen";
    322    } else if (cameraEnabled) {
    323      tabState.sharing = "camera";
    324    } else if (microphoneEnabled) {
    325      tabState.sharing = "microphone";
    326    } else if (tabState.screen) {
    327      tabState.sharing = "screen";
    328    } else if (tabState.camera) {
    329      tabState.sharing = "camera";
    330    } else if (tabState.microphone) {
    331      tabState.sharing = "microphone";
    332    }
    333 
    334    // The stream is considered paused when we're sharing something
    335    // but all devices are off or set to disabled.
    336    tabState.paused =
    337      tabState.sharing &&
    338      !screenEnabled &&
    339      !cameraEnabled &&
    340      !microphoneEnabled;
    341 
    342    if (
    343      tabState.camera == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
    344      tabState.camera == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
    345    ) {
    346      tabState.showCameraIndicator = true;
    347    }
    348    if (
    349      tabState.microphone == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
    350      tabState.microphone == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
    351    ) {
    352      tabState.showMicrophoneIndicator = true;
    353    }
    354 
    355    tabState.showScreenSharingIndicator = "";
    356    if (tabState.screen) {
    357      if (tabState.screen.startsWith("Screen")) {
    358        tabState.showScreenSharingIndicator = "Screen";
    359      } else if (tabState.screen.startsWith("Window")) {
    360        if (tabState.showScreenSharingIndicator != "Screen") {
    361          tabState.showScreenSharingIndicator = "Window";
    362        }
    363      } else if (tabState.screen.startsWith("Browser")) {
    364        if (!tabState.showScreenSharingIndicator) {
    365          tabState.showScreenSharingIndicator = "Browser";
    366        }
    367      }
    368    }
    369 
    370    return tabState;
    371  },
    372 
    373  /*
    374   * Indicate that a stream has been added or removed from the given
    375   * browsing context. If it has been added, aData specifies the
    376   * specific indicator types it uses. If aData is null or has no
    377   * documentURI assigned, then the stream has been removed.
    378   */
    379  streamAddedOrRemoved(aBrowsingContext, aData) {
    380    this.init();
    381 
    382    let index;
    383    for (index = 0; index < webrtcUI._streams.length; ++index) {
    384      let stream = this._streams[index];
    385      if (stream.browsingContext == aBrowsingContext) {
    386        break;
    387      }
    388    }
    389    // The update is a removal of the stream, triggered by the
    390    // recording-window-ended notification.
    391    if (aData.remove) {
    392      if (index < this._streams.length) {
    393        this._streams.splice(index, 1);
    394      }
    395    } else {
    396      this._streams[index] = {
    397        browsingContext: aBrowsingContext,
    398        topBrowsingContext: aBrowsingContext.top,
    399        state: aData,
    400      };
    401    }
    402 
    403    // Reset our internal notion of whether or not we're sharing
    404    // a screen or browser window. Now we'll go through the shared
    405    // devices and re-determine what's being shared.
    406    let sharingBrowserWindow = false;
    407    let sharedWindowRawDeviceIds = new Set();
    408    this.sharingScreen = false;
    409    let suppressNotifications = false;
    410 
    411    // First, go through the streams and collect the counts on things
    412    // like the total number of shared windows, and whether or not we're
    413    // sharing screens.
    414    for (let stream of this._streams) {
    415      let { state } = stream;
    416      suppressNotifications |= state.suppressNotifications;
    417 
    418      for (let device of state.devices) {
    419        if (!device.scary) {
    420          continue;
    421        }
    422 
    423        let mediaSource = device.mediaSource;
    424        if (mediaSource == "window") {
    425          sharedWindowRawDeviceIds.add(device.rawId);
    426        } else if (mediaSource == "screen") {
    427          this.sharingScreen = true;
    428        }
    429 
    430        // If the user has granted a particular site the ability
    431        // to get a stream from a window or screen, we will
    432        // presume that it's exempt from the tab switch warning.
    433        //
    434        // We use the permanentKey here so that the allowing of
    435        // the tab survives tab tear-in and tear-out. We ignore
    436        // browsers that don't have permanentKey, since those aren't
    437        // tabbrowser browsers.
    438        let browser = stream.topBrowsingContext.embedderElement;
    439        if (browser.permanentKey) {
    440          this.allowedSharedBrowsers.add(browser.permanentKey);
    441        }
    442      }
    443    }
    444 
    445    // Next, go through the list of shared windows, and map them
    446    // to our browser windows so that we know which ones are shared.
    447    this.sharedBrowserWindows = new WeakSet();
    448 
    449    for (let win of lazy.BrowserWindowTracker.orderedWindows) {
    450      let rawDeviceId;
    451      try {
    452        rawDeviceId = win.windowUtils.webrtcRawDeviceId;
    453      } catch (e) {
    454        // This can theoretically throw if some of the underlying
    455        // window primitives don't exist. In that case, we can skip
    456        // to the next window.
    457        continue;
    458      }
    459      if (sharedWindowRawDeviceIds.has(rawDeviceId)) {
    460        this.sharedBrowserWindows.add(win);
    461 
    462        // If we've shared a window, then the initially selected tab
    463        // in that window should be exempt from tab switch warnings,
    464        // since it's already been shared.
    465        let selectedBrowser = win.gBrowser.selectedBrowser;
    466        this.allowedSharedBrowsers.add(selectedBrowser.permanentKey);
    467 
    468        sharingBrowserWindow = true;
    469      }
    470    }
    471 
    472    // Since we're not sharing a screen or browser window,
    473    // we can clear these state variables, which are used
    474    // to warn users on tab switching when sharing. These
    475    // are safe to reset even if we hadn't been sharing
    476    // the screen or browser window already.
    477    if (!this.sharingScreen && !sharingBrowserWindow) {
    478      this.allowedSharedBrowsers = new WeakSet();
    479      this.allowTabSwitchesForSession = false;
    480      this.tabSwitchCountForSession = 0;
    481    }
    482 
    483    this._setSharedData();
    484    if (
    485      Services.prefs.getBoolPref(
    486        "privacy.webrtc.allowSilencingNotifications",
    487        false
    488      )
    489    ) {
    490      let alertsService = Cc["@mozilla.org/alerts-service;1"]
    491        .getService(Ci.nsIAlertsService)
    492        .QueryInterface(Ci.nsIAlertsDoNotDisturb);
    493      alertsService.suppressForScreenSharing = suppressNotifications;
    494    }
    495  },
    496 
    497  /**
    498   * Remove all the streams associated with a given
    499   * browsing context.
    500   */
    501  forgetStreamsFromBrowserContext(aBrowsingContext) {
    502    for (let index = 0; index < webrtcUI._streams.length; ) {
    503      let stream = this._streams[index];
    504      if (stream.browsingContext == aBrowsingContext) {
    505        this._streams.splice(index, 1);
    506      } else {
    507        index++;
    508      }
    509    }
    510 
    511    // Remove the per-tab indicator if it no longer needs to be displayed.
    512    let topBC = aBrowsingContext.top;
    513    if (this.perTabIndicators.has(topBC)) {
    514      let tabState = this.getCombinedStateForBrowser(topBC);
    515      if (
    516        !tabState.showCameraIndicator &&
    517        !tabState.showMicrophoneIndicator &&
    518        !tabState.showScreenSharingIndicator
    519      ) {
    520        this.perTabIndicators.delete(topBC);
    521      }
    522    }
    523 
    524    this.updateGlobalIndicator();
    525    this._setSharedData();
    526  },
    527 
    528  /**
    529   * Given some set of streams, stops device access for those streams.
    530   * Optionally, it's possible to stop a subset of the devices on those
    531   * streams by passing in optional arguments.
    532   *
    533   * Once the streams have been stopped, this method will also find the
    534   * newest stream's <xul:browser> and window, focus the window, and
    535   * select the browser.
    536   *
    537   * For camera and microphone streams, this will also revoke any associated
    538   * permissions from SitePermissions.
    539   *
    540   * @param {Array<object>} activeStreams - An array of streams obtained via webrtcUI.getActiveStreams.
    541   * @param {boolean} stopCameras - True to stop the camera streams (defaults to true)
    542   * @param {boolean} stopMics - True to stop the microphone streams (defaults to true)
    543   * @param {boolean} stopScreens - True to stop the screen streams (defaults to true)
    544   * @param {boolean} stopWindows - True to stop the window streams (defaults to true)
    545   */
    546  stopSharingStreams(
    547    activeStreams,
    548    stopCameras = true,
    549    stopMics = true,
    550    stopScreens = true,
    551    stopWindows = true
    552  ) {
    553    if (!activeStreams.length) {
    554      return;
    555    }
    556 
    557    let ids = [];
    558    if (stopCameras) {
    559      ids.push("camera");
    560    }
    561    if (stopMics) {
    562      ids.push("microphone");
    563    }
    564    if (stopScreens || stopWindows) {
    565      ids.push("screen");
    566    }
    567 
    568    for (let stream of activeStreams) {
    569      let { browser } = stream;
    570 
    571      let gBrowser = browser.getTabBrowser();
    572      if (!gBrowser) {
    573        console.error("Can't stop sharing stream - cannot find gBrowser.");
    574        continue;
    575      }
    576 
    577      let tab = gBrowser.getTabForBrowser(browser);
    578      if (!tab) {
    579        console.error("Can't stop sharing stream - cannot find tab.");
    580        continue;
    581      }
    582 
    583      this.clearPermissionsAndStopSharing(ids, tab);
    584    }
    585 
    586    // Switch to the newest stream's browser.
    587    let mostRecentStream = activeStreams[activeStreams.length - 1];
    588    let { browser: browserToSelect } = mostRecentStream;
    589 
    590    let window = browserToSelect.ownerGlobal;
    591    let gBrowser = browserToSelect.getTabBrowser();
    592    let tab = gBrowser.getTabForBrowser(browserToSelect);
    593    window.focus();
    594    gBrowser.selectedTab = tab;
    595  },
    596 
    597  /**
    598   * Clears permissions and stops sharing (if active) for a list of device types
    599   * and a specific tab.
    600   *
    601   * @param {("camera"|"microphone"|"screen")[]} types - Device types to stop
    602   * and clear permissions for.
    603   * @param tab - Tab of the devices to stop and clear permissions.
    604   */
    605  clearPermissionsAndStopSharing(types, tab) {
    606    let invalidTypes = types.filter(
    607      type => !["camera", "screen", "microphone", "speaker"].includes(type)
    608    );
    609    if (invalidTypes.length) {
    610      throw new Error(`Invalid device types ${invalidTypes.join(",")}`);
    611    }
    612    let browser = tab.linkedBrowser;
    613    let sharingState = tab._sharingState?.webRTC;
    614 
    615    // If we clear a WebRTC permission we need to remove all permissions of
    616    // the same type across device ids. We also need to stop active WebRTC
    617    // devices related to the permission.
    618    let perms = lazy.SitePermissions.getAllForBrowser(browser);
    619 
    620    // If capturing, don't revoke one of camera/microphone without the other.
    621    let sharingCameraOrMic =
    622      (sharingState?.camera || sharingState?.microphone) &&
    623      (types.includes("camera") || types.includes("microphone"));
    624 
    625    perms
    626      .filter(perm => {
    627        let [id] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
    628        if (sharingCameraOrMic && (id == "camera" || id == "microphone")) {
    629          return true;
    630        }
    631        return types.includes(id);
    632      })
    633      .forEach(perm => {
    634        lazy.SitePermissions.removeFromPrincipal(
    635          browser.contentPrincipal,
    636          perm.id,
    637          browser
    638        );
    639      });
    640 
    641    if (!sharingState?.windowId) {
    642      return;
    643    }
    644 
    645    // If the device of the permission we're clearing is currently active,
    646    // tell the WebRTC implementation to stop sharing it.
    647    let { windowId } = sharingState;
    648 
    649    let windowIds = [];
    650    if (types.includes("screen") && sharingState.screen) {
    651      windowIds.push(`screen:${windowId}`);
    652    }
    653    if (sharingCameraOrMic) {
    654      windowIds.push(windowId);
    655    }
    656 
    657    if (!windowIds.length) {
    658      return;
    659    }
    660 
    661    let actor =
    662      sharingState.browsingContext.currentWindowGlobal.getActor("WebRTC");
    663 
    664    // Delete activePerms for all outerWindowIds under the current browser. We
    665    // need to do this prior to sending the stopSharing message, so WebRTCParent
    666    // can skip adding grace periods for these devices.
    667    webrtcUI.forgetActivePermissionsFromBrowser(browser);
    668 
    669    windowIds.forEach(id => actor.sendAsyncMessage("webrtc:StopSharing", id));
    670  },
    671 
    672  updateIndicators(aTopBrowsingContext) {
    673    let tabState = this.getCombinedStateForBrowser(aTopBrowsingContext);
    674 
    675    let indicators;
    676    if (this.perTabIndicators.has(aTopBrowsingContext)) {
    677      indicators = this.perTabIndicators.get(aTopBrowsingContext);
    678    } else {
    679      indicators = {};
    680      this.perTabIndicators.set(aTopBrowsingContext, indicators);
    681    }
    682 
    683    indicators.showCameraIndicator = tabState.showCameraIndicator;
    684    indicators.showMicrophoneIndicator = tabState.showMicrophoneIndicator;
    685    indicators.showScreenSharingIndicator = tabState.showScreenSharingIndicator;
    686    this.updateGlobalIndicator();
    687 
    688    return tabState;
    689  },
    690 
    691  swapBrowserForNotification(aOldBrowser, aNewBrowser) {
    692    for (let stream of this._streams) {
    693      if (stream.browser == aOldBrowser) {
    694        stream.browser = aNewBrowser;
    695      }
    696    }
    697  },
    698 
    699  /**
    700   * Remove all entries from the activePerms map for a browser, including all
    701   * child frames.
    702   * Note: activePerms is an internal WebRTC UI permission map and does not
    703   * reflect the PermissionManager or SitePermissions state.
    704   *
    705   * @param aBrowser - Browser to clear active permissions for.
    706   */
    707  forgetActivePermissionsFromBrowser(aBrowser) {
    708    let browserWindowIds = aBrowser.browsingContext
    709      .getAllBrowsingContextsInSubtree()
    710      .map(bc => bc.currentWindowGlobal?.outerWindowId)
    711      .filter(id => id != null);
    712    browserWindowIds.push(aBrowser.outerWindowId);
    713    browserWindowIds.forEach(id => this.activePerms.delete(id));
    714  },
    715 
    716  /**
    717   * Shows the Permission Panel for the tab associated with the provided
    718   * active stream.
    719   *
    720   * @param aActiveStream - The stream that the user wants to see permissions for.
    721   * @param aEvent - The user input event that is invoking the panel. This can be
    722   *        undefined / null if no such event exists.
    723   */
    724  showSharingDoorhanger(aActiveStream, aEvent) {
    725    let browserWindow = aActiveStream.browser.ownerGlobal;
    726    if (aActiveStream.tab) {
    727      browserWindow.gBrowser.selectedTab = aActiveStream.tab;
    728    } else {
    729      aActiveStream.browser.focus();
    730    }
    731    browserWindow.focus();
    732 
    733    if (AppConstants.platform == "macosx" && !Services.focus.activeWindow) {
    734      browserWindow.addEventListener(
    735        "activate",
    736        function () {
    737          Services.tm.dispatchToMainThread(function () {
    738            browserWindow.gPermissionPanel.openPopup(aEvent);
    739          });
    740        },
    741        { once: true }
    742      );
    743      Cc["@mozilla.org/widget/macdocksupport;1"]
    744        .getService(Ci.nsIMacDockSupport)
    745        .activateApplication(true);
    746      return;
    747    }
    748    browserWindow.gPermissionPanel.openPopup(aEvent);
    749  },
    750 
    751  updateWarningLabel(aMenuList) {
    752    let type = aMenuList.selectedItem.getAttribute("devicetype");
    753    let document = aMenuList.ownerDocument;
    754    document.getElementById("webRTC-all-windows-shared").hidden =
    755      type != "screen";
    756  },
    757 
    758  // Add-ons can override stock permission behavior by doing:
    759  //
    760  //   webrtcUI.addPeerConnectionBlocker(function(aParams) {
    761  //     // new permission checking logic
    762  //   }));
    763  //
    764  // The blocking function receives an object with origin, callID, and windowID
    765  // parameters.  If it returns the string "deny" or a Promise that resolves
    766  // to "deny", the connection is immediately blocked.  With any other return
    767  // value (though the string "allow" is suggested for consistency), control
    768  // is passed to other registered blockers.  If no registered blockers block
    769  // the connection (or of course if there are no registered blockers), then
    770  // the connection is allowed.
    771  //
    772  // Add-ons may also use webrtcUI.on/off to listen to events without
    773  // blocking anything:
    774  //   peer-request-allowed is emitted when a new peer connection is
    775  //                        established (and not blocked).
    776  //   peer-request-blocked is emitted when a peer connection request is
    777  //                        blocked by some blocking connection handler.
    778  //   peer-request-cancel is emitted when a peer-request connection request
    779  //                       is canceled.  (This would typically be used in
    780  //                       conjunction with a blocking handler to cancel
    781  //                       a user prompt or other work done by the handler)
    782  addPeerConnectionBlocker(aCallback) {
    783    this.peerConnectionBlockers.add(aCallback);
    784  },
    785 
    786  removePeerConnectionBlocker(aCallback) {
    787    this.peerConnectionBlockers.delete(aCallback);
    788  },
    789 
    790  on(...args) {
    791    return this.emitter.on(...args);
    792  },
    793 
    794  off(...args) {
    795    return this.emitter.off(...args);
    796  },
    797 
    798  getHostOrExtensionName(uri, href) {
    799    let host;
    800    try {
    801      if (!uri) {
    802        uri = Services.io.newURI(href);
    803      }
    804 
    805      let addonPolicy = WebExtensionPolicy.getByURI(uri);
    806      host = addonPolicy?.name ?? uri.hostPort;
    807    } catch (ex) {}
    808 
    809    if (!host) {
    810      if (uri && uri.scheme.toLowerCase() == "about") {
    811        // For about URIs, just use the full spec, without any #hash parts.
    812        host = uri.specIgnoringRef;
    813      } else {
    814        // This is unfortunate, but we should display *something*...
    815        host = lazy.syncL10n.formatValueSync(
    816          "webrtc-sharing-menuitem-unknown-host"
    817        );
    818      }
    819    }
    820    return host;
    821  },
    822 
    823  updateGlobalIndicator() {
    824    for (let chromeWin of Services.wm.getEnumerator("navigator:browser")) {
    825      if (this.showGlobalIndicator) {
    826        showOrCreateMenuForWindow(chromeWin);
    827      } else {
    828        let doc = chromeWin.document;
    829        let existingMenu = doc.getElementById("tabSharingMenu");
    830        if (existingMenu) {
    831          existingMenu.hidden = true;
    832        }
    833        if (AppConstants.platform == "macosx") {
    834          let separator = doc.getElementById("tabSharingSeparator");
    835          if (separator) {
    836            separator.hidden = true;
    837          }
    838        }
    839      }
    840    }
    841 
    842    if (this.showGlobalIndicator) {
    843      if (!gIndicatorWindow) {
    844        gIndicatorWindow = getGlobalIndicator();
    845      } else {
    846        try {
    847          gIndicatorWindow.updateIndicatorState();
    848        } catch (err) {
    849          console.error(
    850            `error in gIndicatorWindow.updateIndicatorState(): ${err.message}`
    851          );
    852        }
    853      }
    854    } else if (gIndicatorWindow) {
    855      if (gIndicatorWindow.closingInternally) {
    856        // Before calling .close(), we call .closingInternally() to allow us to
    857        // differentiate between situations where the indicator closes because
    858        // we no longer want to show the indicator (this case), and cases where
    859        // the user has found a way to close the indicator via OS window control
    860        // mechanisms.
    861        gIndicatorWindow.closingInternally();
    862      }
    863      gIndicatorWindow.close();
    864      gIndicatorWindow = null;
    865    }
    866  },
    867 
    868  getWindowShareState(window) {
    869    if (this.sharingScreen) {
    870      return this.SHARING_SCREEN;
    871    } else if (this.sharedBrowserWindows.has(window)) {
    872      return this.SHARING_WINDOW;
    873    }
    874    return this.SHARING_NONE;
    875  },
    876 
    877  tabAddedWhileSharing(tab) {
    878    this.allowedSharedBrowsers.add(tab.linkedBrowser.permanentKey);
    879  },
    880 
    881  shouldShowSharedTabWarning(tab) {
    882    if (!tab || !tab.linkedBrowser) {
    883      return false;
    884    }
    885 
    886    let browser = tab.linkedBrowser;
    887    // We want the user to be able to switch to one tab after starting
    888    // to share their window or screen. The presumption here is that
    889    // most users will have a single window with multiple tabs, where
    890    // the selected tab will be the one with the screen or window
    891    // sharing web application, and it's most likely that the contents
    892    // that the user wants to share are in another tab that they'll
    893    // switch to immediately upon sharing. These presumptions are based
    894    // on research that our user research team did with users using
    895    // video conferencing web applications.
    896    if (!this.tabSwitchCountForSession) {
    897      this.allowedSharedBrowsers.add(browser.permanentKey);
    898    }
    899 
    900    this.tabSwitchCountForSession++;
    901    let shouldShow =
    902      !this.allowTabSwitchesForSession &&
    903      !this.allowedSharedBrowsers.has(browser.permanentKey);
    904 
    905    return shouldShow;
    906  },
    907 
    908  allowSharedTabSwitch(tab, allowForSession) {
    909    let browser = tab.linkedBrowser;
    910    let gBrowser = browser.getTabBrowser();
    911    this.allowedSharedBrowsers.add(browser.permanentKey);
    912    gBrowser.selectedTab = tab;
    913    this.allowTabSwitchesForSession = allowForSession;
    914  },
    915 
    916  /**
    917   * Updates the sharedData structure to reflect shared screen and window
    918   * state. This sets the following key: data pairs on sharedData.
    919   * - "webrtcUI:isSharingScreen": a boolean value reflecting
    920   * this.sharingScreen.
    921   * - "webrtcUI:sharedTopInnerWindowIds": a set containing the inner window
    922   * ids of each top level browser window that is in sharedBrowserWindows.
    923   */
    924  _setSharedData() {
    925    let sharedTopInnerWindowIds = new Set();
    926    for (let win of lazy.BrowserWindowTracker.orderedWindows) {
    927      if (this.sharedBrowserWindows.has(win)) {
    928        sharedTopInnerWindowIds.add(
    929          win.browsingContext.currentWindowGlobal.innerWindowId
    930        );
    931      }
    932    }
    933    Services.ppmm.sharedData.set(
    934      "webrtcUI:isSharingScreen",
    935      this.sharingScreen
    936    );
    937    Services.ppmm.sharedData.set(
    938      "webrtcUI:sharedTopInnerWindowIds",
    939      sharedTopInnerWindowIds
    940    );
    941  },
    942 };
    943 
    944 function getGlobalIndicator() {
    945  const INDICATOR_CHROME_URI = "chrome://browser/content/webrtcIndicator.xhtml";
    946  let features = "chrome,titlebar=no,alwaysontop,minimizable,dialog";
    947 
    948  return Services.ww.openWindow(
    949    null,
    950    INDICATOR_CHROME_URI,
    951    "_blank",
    952    features,
    953    null
    954  );
    955 }
    956 
    957 /**
    958 * Add a localized stream sharing menu to the event target
    959 *
    960 * @param {Window} win - The parent `window`
    961 * @param {Event} event - The popupshowing event for the <menu>.
    962 * @param {boolean} inclWindow - Should the window stream be included in the active streams.
    963 */
    964 export function showStreamSharingMenu(win, event, inclWindow = false) {
    965  win.MozXULElement.insertFTLIfNeeded("browser/webrtcIndicator.ftl");
    966  const doc = win.document;
    967  const menu = event.target;
    968 
    969  let type = menu.getAttribute("type");
    970  let activeStreams;
    971  if (type == "Camera") {
    972    activeStreams = webrtcUI.getActiveStreams(true, false, false, false);
    973  } else if (type == "Microphone") {
    974    activeStreams = webrtcUI.getActiveStreams(false, true, false, false);
    975  } else if (type == "Screen") {
    976    activeStreams = webrtcUI.getActiveStreams(
    977      false,
    978      false,
    979      true,
    980      inclWindow,
    981      inclWindow
    982    );
    983    type = webrtcUI.showScreenSharingIndicator;
    984  }
    985 
    986  if (!activeStreams.length) {
    987    event.preventDefault();
    988    return;
    989  }
    990 
    991  const l10nIds = SHARING_L10NID_BY_TYPE.get(type) ?? [];
    992  if (activeStreams.length == 1) {
    993    let stream = activeStreams[0];
    994 
    995    const sharingItem = doc.createXULElement("menuitem");
    996    const displayHost = getDisplayHostForStream(stream);
    997    doc.l10n.setAttributes(sharingItem, l10nIds[0], {
    998      streamTitle: displayHost,
    999    });
   1000    sharingItem.setAttribute("disabled", "true");
   1001    menu.appendChild(sharingItem);
   1002 
   1003    const controlItem = doc.createXULElement("menuitem");
   1004    doc.l10n.setAttributes(
   1005      controlItem,
   1006      "webrtc-indicator-menuitem-control-sharing"
   1007    );
   1008    controlItem.stream = stream;
   1009    controlItem.addEventListener("command", this);
   1010 
   1011    menu.appendChild(controlItem);
   1012  } else {
   1013    // We show a different menu when there are several active streams.
   1014    const sharingItem = doc.createXULElement("menuitem");
   1015    doc.l10n.setAttributes(sharingItem, l10nIds[1], {
   1016      tabCount: activeStreams.length,
   1017    });
   1018    sharingItem.setAttribute("disabled", "true");
   1019    menu.appendChild(sharingItem);
   1020 
   1021    for (let stream of activeStreams) {
   1022      const controlItem = doc.createXULElement("menuitem");
   1023      const displayHost = getDisplayHostForStream(stream);
   1024      doc.l10n.setAttributes(
   1025        controlItem,
   1026        "webrtc-indicator-menuitem-control-sharing-on",
   1027        { streamTitle: displayHost }
   1028      );
   1029      controlItem.stream = stream;
   1030      controlItem.addEventListener("command", this);
   1031      menu.appendChild(controlItem);
   1032    }
   1033  }
   1034 }
   1035 
   1036 function getDisplayHostForStream(stream) {
   1037  let uri = Services.io.newURI(stream.uri);
   1038 
   1039  let displayHost;
   1040 
   1041  try {
   1042    displayHost = uri.displayHost;
   1043  } catch (ex) {
   1044    displayHost = null;
   1045  }
   1046 
   1047  // Host getter threw or returned "". Fall back to spec.
   1048  if (displayHost == null || displayHost == "") {
   1049    displayHost = uri.displaySpec;
   1050  }
   1051 
   1052  return displayHost;
   1053 }
   1054 
   1055 function onTabSharingMenuPopupShowing(e) {
   1056  const streams = webrtcUI.getActiveStreams(true, true, true, true, true);
   1057  for (let streamInfo of streams) {
   1058    const names = streamInfo.devices.map(({ mediaSource }) => {
   1059      const l10nId = MEDIA_SOURCE_L10NID_BY_TYPE.get(mediaSource);
   1060      return l10nId ? lazy.syncL10n.formatValueSync(l10nId) : mediaSource;
   1061    });
   1062 
   1063    const doc = e.target.ownerDocument;
   1064    const menuitem = doc.createXULElement("menuitem");
   1065    doc.l10n.setAttributes(menuitem, "webrtc-sharing-menuitem", {
   1066      origin: webrtcUI.getHostOrExtensionName(null, streamInfo.uri),
   1067      itemList: lazy.listFormat.format(names),
   1068    });
   1069    menuitem.stream = streamInfo;
   1070    menuitem.addEventListener("command", onTabSharingMenuPopupCommand);
   1071    e.target.appendChild(menuitem);
   1072  }
   1073 }
   1074 
   1075 function onTabSharingMenuPopupHiding() {
   1076  while (this.lastChild) {
   1077    this.lastChild.remove();
   1078  }
   1079 }
   1080 
   1081 function onTabSharingMenuPopupCommand(e) {
   1082  webrtcUI.showSharingDoorhanger(e.target.stream, e);
   1083 }
   1084 
   1085 function showOrCreateMenuForWindow(aWindow) {
   1086  let document = aWindow.document;
   1087  let menu = document.getElementById("tabSharingMenu");
   1088  if (!menu) {
   1089    menu = document.createXULElement("menu");
   1090    menu.id = "tabSharingMenu";
   1091    document.l10n.setAttributes(menu, "webrtc-sharing-menu");
   1092 
   1093    let container, insertionPoint;
   1094    if (AppConstants.platform == "macosx") {
   1095      container = document.getElementById("menu_ToolsPopup");
   1096      insertionPoint = document.getElementById("devToolsSeparator");
   1097      let separator = document.createXULElement("menuseparator");
   1098      separator.id = "tabSharingSeparator";
   1099      container.insertBefore(separator, insertionPoint);
   1100    } else {
   1101      container = document.getElementById("main-menubar");
   1102      insertionPoint = document.getElementById("helpMenu");
   1103    }
   1104    let popup = document.createXULElement("menupopup");
   1105    popup.id = "tabSharingMenuPopup";
   1106    popup.addEventListener("popupshowing", onTabSharingMenuPopupShowing);
   1107    popup.addEventListener("popuphiding", onTabSharingMenuPopupHiding);
   1108    menu.appendChild(popup);
   1109    container.insertBefore(menu, insertionPoint);
   1110  } else {
   1111    menu.hidden = false;
   1112    if (AppConstants.platform == "macosx") {
   1113      document.getElementById("tabSharingSeparator").hidden = false;
   1114    }
   1115  }
   1116 }
   1117 
   1118 var gIndicatorWindow = null;