mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1499 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1499 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | 
						|
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | 
						|
  SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
 | 
						|
  webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  lazy,
 | 
						|
  "OSPermissions",
 | 
						|
  "@mozilla.org/ospermissionrequest;1",
 | 
						|
  "nsIOSPermissionRequest"
 | 
						|
);
 | 
						|
 | 
						|
export class WebRTCParent extends JSWindowActorParent {
 | 
						|
  didDestroy() {
 | 
						|
    // Media stream tracks end on unload, so call stopRecording() on them early
 | 
						|
    // *before* we go away, to ensure we're working with the right principal.
 | 
						|
    this.stopRecording(this.manager.outerWindowId);
 | 
						|
    lazy.webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext);
 | 
						|
    // Must clear activePerms here to prevent them from being read by laggard
 | 
						|
    // stopRecording() calls, which due to IPC, may come in *after* navigation.
 | 
						|
    // This is to prevent granting temporary grace periods to the wrong page.
 | 
						|
    lazy.webrtcUI.activePerms.delete(this.manager.outerWindowId);
 | 
						|
  }
 | 
						|
 | 
						|
  getBrowser() {
 | 
						|
    return this.browsingContext.top.embedderElement;
 | 
						|
  }
 | 
						|
 | 
						|
  receiveMessage(aMessage) {
 | 
						|
    switch (aMessage.name) {
 | 
						|
      case "rtcpeer:Request": {
 | 
						|
        let params = Object.freeze(
 | 
						|
          Object.assign(
 | 
						|
            {
 | 
						|
              origin: this.manager.documentPrincipal.origin,
 | 
						|
            },
 | 
						|
            aMessage.data
 | 
						|
          )
 | 
						|
        );
 | 
						|
 | 
						|
        let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);
 | 
						|
 | 
						|
        (async function () {
 | 
						|
          for (let blocker of blockers) {
 | 
						|
            try {
 | 
						|
              let result = await blocker(params);
 | 
						|
              if (result == "deny") {
 | 
						|
                return false;
 | 
						|
              }
 | 
						|
            } catch (err) {
 | 
						|
              console.error(`error in PeerConnection blocker: ${err.message}`);
 | 
						|
            }
 | 
						|
          }
 | 
						|
          return true;
 | 
						|
        })().then(decision => {
 | 
						|
          let message;
 | 
						|
          if (decision) {
 | 
						|
            lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
 | 
						|
            message = "rtcpeer:Allow";
 | 
						|
          } else {
 | 
						|
            lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
 | 
						|
            message = "rtcpeer:Deny";
 | 
						|
          }
 | 
						|
 | 
						|
          this.sendAsyncMessage(message, {
 | 
						|
            callID: params.callID,
 | 
						|
            windowID: params.windowID,
 | 
						|
          });
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "rtcpeer:CancelRequest": {
 | 
						|
        let params = Object.freeze({
 | 
						|
          origin: this.manager.documentPrincipal.origin,
 | 
						|
          callID: aMessage.data,
 | 
						|
        });
 | 
						|
        lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "webrtc:Request": {
 | 
						|
        let data = aMessage.data;
 | 
						|
 | 
						|
        // Record third party origins for telemetry.
 | 
						|
        let isThirdPartyOrigin =
 | 
						|
          this.manager.documentPrincipal.origin !=
 | 
						|
          this.manager.topWindowContext.documentPrincipal.origin;
 | 
						|
        data.isThirdPartyOrigin = isThirdPartyOrigin;
 | 
						|
 | 
						|
        data.origin = this.manager.topWindowContext.documentPrincipal.origin;
 | 
						|
 | 
						|
        let browser = this.getBrowser();
 | 
						|
        if (browser.fxrPermissionPrompt) {
 | 
						|
          // For Firefox Reality on Desktop, switch to a different mechanism to
 | 
						|
          // prompt the user since fewer permissions are available and since many
 | 
						|
          // UI dependencies are not available.
 | 
						|
          browser.fxrPermissionPrompt(data);
 | 
						|
        } else {
 | 
						|
          prompt(this, this.getBrowser(), data);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "webrtc:StopRecording":
 | 
						|
        this.stopRecording(
 | 
						|
          aMessage.data.windowID,
 | 
						|
          aMessage.data.mediaSource,
 | 
						|
          aMessage.data.rawID
 | 
						|
        );
 | 
						|
        break;
 | 
						|
      case "webrtc:CancelRequest": {
 | 
						|
        let browser = this.getBrowser();
 | 
						|
        // browser can be null when closing the window
 | 
						|
        if (browser) {
 | 
						|
          removePrompt(browser, aMessage.data);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "webrtc:UpdateIndicators": {
 | 
						|
        let { data } = aMessage;
 | 
						|
        data.documentURI = this.manager.documentURI?.spec;
 | 
						|
        if (data.windowId) {
 | 
						|
          if (!data.remove) {
 | 
						|
            data.principal = this.manager.topWindowContext.documentPrincipal;
 | 
						|
          }
 | 
						|
          lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
 | 
						|
        }
 | 
						|
        this.updateIndicators(data);
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  updateIndicators(aData) {
 | 
						|
    let browsingContext = this.browsingContext;
 | 
						|
    let state = lazy.webrtcUI.updateIndicators(browsingContext.top);
 | 
						|
 | 
						|
    let browser = this.getBrowser();
 | 
						|
    if (!browser) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    state.browsingContext = browsingContext;
 | 
						|
    state.windowId = aData.windowId;
 | 
						|
 | 
						|
    let tabbrowser = browser.ownerGlobal.gBrowser;
 | 
						|
    if (tabbrowser) {
 | 
						|
      tabbrowser.updateBrowserSharing(browser, {
 | 
						|
        webRTC: state,
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  denyRequest(aRequest) {
 | 
						|
    this.sendAsyncMessage("webrtc:Deny", {
 | 
						|
      callID: aRequest.callID,
 | 
						|
      windowID: aRequest.windowID,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  //
 | 
						|
  // Deny the request because the browser does not have access to the
 | 
						|
  // camera or microphone due to OS security restrictions. The user may
 | 
						|
  // have granted camera/microphone access to the site, but not have
 | 
						|
  // allowed the browser access in OS settings.
 | 
						|
  //
 | 
						|
  denyRequestNoPermission(aRequest) {
 | 
						|
    this.sendAsyncMessage("webrtc:Deny", {
 | 
						|
      callID: aRequest.callID,
 | 
						|
      windowID: aRequest.windowID,
 | 
						|
      noOSPermission: true,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  //
 | 
						|
  // Check if we have permission to access the camera or screen-sharing and/or
 | 
						|
  // microphone at the OS level. Triggers a request to access the device if access
 | 
						|
  // is needed and the permission state has not yet been determined.
 | 
						|
  //
 | 
						|
  async checkOSPermission(camNeeded, micNeeded, scrNeeded) {
 | 
						|
    // Don't trigger OS permission requests for fake devices. Fake devices don't
 | 
						|
    // require OS permission and the dialogs are problematic in automated testing
 | 
						|
    // (where fake devices are used) because they require user interaction.
 | 
						|
    if (
 | 
						|
      !scrNeeded &&
 | 
						|
      Services.prefs.getBoolPref("media.navigator.streams.fake", false)
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    let camStatus = {},
 | 
						|
      micStatus = {};
 | 
						|
    if (camNeeded || micNeeded) {
 | 
						|
      lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
 | 
						|
    }
 | 
						|
    if (camNeeded) {
 | 
						|
      let camPermission = camStatus.value;
 | 
						|
      let camAccessible = await this.checkAndGetOSPermission(
 | 
						|
        camPermission,
 | 
						|
        lazy.OSPermissions.requestVideoCapturePermission
 | 
						|
      );
 | 
						|
      if (!camAccessible) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (micNeeded) {
 | 
						|
      let micPermission = micStatus.value;
 | 
						|
      let micAccessible = await this.checkAndGetOSPermission(
 | 
						|
        micPermission,
 | 
						|
        lazy.OSPermissions.requestAudioCapturePermission
 | 
						|
      );
 | 
						|
      if (!micAccessible) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    let scrStatus = {};
 | 
						|
    if (scrNeeded) {
 | 
						|
      lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
 | 
						|
      if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
 | 
						|
        lazy.OSPermissions.maybeRequestScreenCapturePermission();
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  //
 | 
						|
  // Given a device's permission, return true if the device is accessible. If
 | 
						|
  // the device's permission is not yet determined, request access to the device.
 | 
						|
  // |requestPermissionFunc| must return a promise that resolves with true
 | 
						|
  // if the device is accessible and false otherwise.
 | 
						|
  //
 | 
						|
  async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
 | 
						|
    if (
 | 
						|
      devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
 | 
						|
      devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
 | 
						|
      let deviceAllowed = await requestPermissionFunc();
 | 
						|
      if (!deviceAllowed) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  stopRecording(aOuterWindowId, aMediaSource, aRawId) {
 | 
						|
    for (let { browsingContext, state } of lazy.webrtcUI._streams) {
 | 
						|
      if (browsingContext == this.browsingContext) {
 | 
						|
        let { principal } = state;
 | 
						|
        for (let { mediaSource, rawId } of state.devices) {
 | 
						|
          if (aRawId && (aRawId != rawId || aMediaSource != mediaSource)) {
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
          // Deactivate this device (no aRawId means all devices).
 | 
						|
          this.deactivateDevicePerm(
 | 
						|
            aOuterWindowId,
 | 
						|
            mediaSource,
 | 
						|
            rawId,
 | 
						|
            principal
 | 
						|
          );
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Add a device record to webrtcUI.activePerms, denoting a device as in use.
 | 
						|
   * Important to call for permission grace periods to work correctly.
 | 
						|
   */
 | 
						|
  activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
 | 
						|
    if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
 | 
						|
      lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
 | 
						|
    }
 | 
						|
    lazy.webrtcUI.activePerms
 | 
						|
      .get(this.manager.outerWindowId)
 | 
						|
      .set(aOuterWindowId + aMediaSource + aId, aMediaSource);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove a device record from webrtcUI.activePerms, denoting a device as
 | 
						|
   * no longer in use by the site. Meaning: gUM requests for this device will
 | 
						|
   * no longer be implicitly granted through the webrtcUI.activePerms mechanism.
 | 
						|
   *
 | 
						|
   * However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
 | 
						|
   * grant is extended for an additional period of time through SitePermissions.
 | 
						|
   */
 | 
						|
  deactivateDevicePerm(
 | 
						|
    aOuterWindowId,
 | 
						|
    aMediaSource,
 | 
						|
    aId,
 | 
						|
    aPermissionPrincipal
 | 
						|
  ) {
 | 
						|
    // If we don't have active permissions for the given window anymore don't
 | 
						|
    // set a grace period. This happens if there has been a user revoke and
 | 
						|
    // webrtcUI clears the permissions.
 | 
						|
    if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
 | 
						|
    map.delete(aOuterWindowId + aMediaSource + aId);
 | 
						|
 | 
						|
    // Add a permission grace period for camera and microphone only
 | 
						|
    if (
 | 
						|
      (aMediaSource != "camera" && aMediaSource != "microphone") ||
 | 
						|
      !this.browsingContext.top.embedderElement
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let gracePeriodMs = lazy.webrtcUI.deviceGracePeriodTimeoutMs;
 | 
						|
    if (gracePeriodMs > 0) {
 | 
						|
      // A grace period is extended (even past navigation) to this outer window
 | 
						|
      // + origin + deviceId only. This avoids re-prompting without the user
 | 
						|
      // having to persist permission to the site, in a common case of a web
 | 
						|
      // conference asking them for the camera in a lobby page, before
 | 
						|
      // navigating to the actual meeting room page. Does not survive tab close.
 | 
						|
      //
 | 
						|
      // Caution: since navigation causes deactivation, we may be in the middle
 | 
						|
      // of one. We must pass in a principal & URI for SitePermissions to use
 | 
						|
      // instead of browser.currentURI, because the latter may point to a new
 | 
						|
      // page already, and we must not leak permission to unrelated pages.
 | 
						|
      //
 | 
						|
      let permissionName = [aMediaSource, aId].join("^");
 | 
						|
      lazy.SitePermissions.setForPrincipal(
 | 
						|
        aPermissionPrincipal,
 | 
						|
        permissionName,
 | 
						|
        lazy.SitePermissions.ALLOW,
 | 
						|
        lazy.SitePermissions.SCOPE_TEMPORARY,
 | 
						|
        this.browsingContext.top.embedderElement,
 | 
						|
        gracePeriodMs
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Checks if the principal has sufficient permissions
 | 
						|
   * to fulfill the given request. If the request can be
 | 
						|
   * fulfilled, a message is sent to the child
 | 
						|
   * signaling that WebRTC permissions were given and
 | 
						|
   * this function will return true.
 | 
						|
   */
 | 
						|
  checkRequestAllowed(aRequest, aPrincipal) {
 | 
						|
    if (!aRequest.secure) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    // Always prompt for screen sharing
 | 
						|
    if (aRequest.sharingScreen) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    let {
 | 
						|
      callID,
 | 
						|
      windowID,
 | 
						|
      audioInputDevices,
 | 
						|
      videoInputDevices,
 | 
						|
      audioOutputDevices,
 | 
						|
      hasInherentAudioConstraints,
 | 
						|
      hasInherentVideoConstraints,
 | 
						|
      audioOutputId,
 | 
						|
    } = aRequest;
 | 
						|
 | 
						|
    if (audioOutputDevices?.length) {
 | 
						|
      // Prompt if a specific device is not requested, available and allowed.
 | 
						|
      let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
 | 
						|
      if (
 | 
						|
        !device ||
 | 
						|
        !lazy.SitePermissions.getForPrincipal(
 | 
						|
          aPrincipal,
 | 
						|
          ["speaker", device.id].join("^"),
 | 
						|
          this.getBrowser()
 | 
						|
        ).state == lazy.SitePermissions.ALLOW
 | 
						|
      ) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
      this.sendAsyncMessage("webrtc:Allow", {
 | 
						|
        callID,
 | 
						|
        windowID,
 | 
						|
        devices: [device.deviceIndex],
 | 
						|
      });
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    let { perms } = Services;
 | 
						|
    if (
 | 
						|
      perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
 | 
						|
    ) {
 | 
						|
      perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't use persistent permissions from the top-level principal
 | 
						|
    // if we're handling a potentially insecure third party
 | 
						|
    // through a wildcard ("*") allow attribute.
 | 
						|
    let limited = aRequest.secondOrigin;
 | 
						|
 | 
						|
    let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
 | 
						|
    // We consider a camera or mic active if it is active or was active within a
 | 
						|
    // grace period of milliseconds ago.
 | 
						|
    const isAllowed = ({ mediaSource, rawId }, permissionID) =>
 | 
						|
      map?.get(windowID + mediaSource + rawId) ||
 | 
						|
      (!limited &&
 | 
						|
        (lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
 | 
						|
          lazy.SitePermissions.ALLOW ||
 | 
						|
          lazy.SitePermissions.getForPrincipal(
 | 
						|
            aPrincipal,
 | 
						|
            [mediaSource, rawId].join("^"),
 | 
						|
            this.getBrowser()
 | 
						|
          ).state == lazy.SitePermissions.ALLOW));
 | 
						|
 | 
						|
    let microphone;
 | 
						|
    if (audioInputDevices.length) {
 | 
						|
      for (let device of audioInputDevices) {
 | 
						|
        if (isAllowed(device, "microphone")) {
 | 
						|
          microphone = device;
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        if (hasInherentAudioConstraints) {
 | 
						|
          // Inherent constraints suggest site is looking for a specific mic
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        // Some sites don't look too hard at what they get, and spam gUM without
 | 
						|
        // adjusting what they ask for to match what they got last time. To keep
 | 
						|
        // users in charge and reduce prompts, ignore other constraints by
 | 
						|
        // returning the most-fit microphone a site already has access to.
 | 
						|
      }
 | 
						|
      if (!microphone) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    let camera;
 | 
						|
    if (videoInputDevices.length) {
 | 
						|
      for (let device of videoInputDevices) {
 | 
						|
        if (isAllowed(device, "camera")) {
 | 
						|
          camera = device;
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        if (hasInherentVideoConstraints) {
 | 
						|
          // Inherent constraints suggest site is looking for a specific camera
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        // Some sites don't look too hard at what they get, and spam gUM without
 | 
						|
        // adjusting what they ask for to match what they got last time. To keep
 | 
						|
        // users in charge and reduce prompts, ignore other constraints by
 | 
						|
        // returning the most-fit camera a site already has access to.
 | 
						|
      }
 | 
						|
      if (!camera) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    let devices = [];
 | 
						|
    if (camera) {
 | 
						|
      perms.addFromPrincipal(
 | 
						|
        aPrincipal,
 | 
						|
        "MediaManagerVideo",
 | 
						|
        perms.ALLOW_ACTION,
 | 
						|
        perms.EXPIRE_SESSION
 | 
						|
      );
 | 
						|
      devices.push(camera.deviceIndex);
 | 
						|
      this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
 | 
						|
    }
 | 
						|
    if (microphone) {
 | 
						|
      devices.push(microphone.deviceIndex);
 | 
						|
      this.activateDevicePerm(
 | 
						|
        windowID,
 | 
						|
        microphone.mediaSource,
 | 
						|
        microphone.rawId
 | 
						|
      );
 | 
						|
    }
 | 
						|
    this.checkOSPermission(!!camera, !!microphone, false).then(
 | 
						|
      havePermission => {
 | 
						|
        if (havePermission) {
 | 
						|
          this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
 | 
						|
        } else {
 | 
						|
          this.denyRequestNoPermission(aRequest);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    );
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function prompt(aActor, aBrowser, aRequest) {
 | 
						|
  let {
 | 
						|
    audioInputDevices,
 | 
						|
    videoInputDevices,
 | 
						|
    audioOutputDevices,
 | 
						|
    sharingScreen,
 | 
						|
    sharingAudio,
 | 
						|
    requestTypes,
 | 
						|
  } = aRequest;
 | 
						|
 | 
						|
  let principal =
 | 
						|
    Services.scriptSecurityManager.createContentPrincipalFromOrigin(
 | 
						|
      aRequest.origin
 | 
						|
    );
 | 
						|
 | 
						|
  // For add-on principals, we immediately check for permission instead
 | 
						|
  // of waiting for the notification to focus. This allows for supporting
 | 
						|
  // cases such as browserAction popups where no prompt is shown.
 | 
						|
  if (principal.addonPolicy) {
 | 
						|
    let isPopup = false;
 | 
						|
    let isBackground = false;
 | 
						|
 | 
						|
    for (let view of principal.addonPolicy.extension.views) {
 | 
						|
      if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
 | 
						|
        isPopup = true;
 | 
						|
      }
 | 
						|
      if (view.viewType == "background" && view.xulBrowser == aBrowser) {
 | 
						|
        isBackground = true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Recording from background pages is considered too sensitive and will
 | 
						|
    // always be denied.
 | 
						|
    if (isBackground) {
 | 
						|
      aActor.denyRequest(aRequest);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If the request comes from a popup, we don't want to show the prompt,
 | 
						|
    // but we do want to allow the request if the user previously gave permission.
 | 
						|
    if (isPopup) {
 | 
						|
      if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
 | 
						|
        aActor.denyRequest(aRequest);
 | 
						|
      }
 | 
						|
      return;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // If the user has already denied access once in this tab,
 | 
						|
  // deny again without even showing the notification icon.
 | 
						|
  for (const type of requestTypes) {
 | 
						|
    const permissionID =
 | 
						|
      type == "AudioCapture" ? "microphone" : type.toLowerCase();
 | 
						|
    if (
 | 
						|
      lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
 | 
						|
        .state == lazy.SitePermissions.BLOCK
 | 
						|
    ) {
 | 
						|
      aActor.denyRequest(aRequest);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  let chromeDoc = aBrowser.ownerDocument;
 | 
						|
  const localization = new Localization(
 | 
						|
    ["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
 | 
						|
    true
 | 
						|
  );
 | 
						|
 | 
						|
  /** @type {"Screen" | "Camera" | null} */
 | 
						|
  let reqVideoInput = null;
 | 
						|
  if (videoInputDevices.length) {
 | 
						|
    reqVideoInput = sharingScreen ? "Screen" : "Camera";
 | 
						|
  }
 | 
						|
  /** @type {"AudioCapture" | "Microphone" | null} */
 | 
						|
  let reqAudioInput = null;
 | 
						|
  if (audioInputDevices.length) {
 | 
						|
    reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
 | 
						|
  }
 | 
						|
  const reqAudioOutput = !!audioOutputDevices.length;
 | 
						|
 | 
						|
  const stringId = getPromptMessageId(
 | 
						|
    reqVideoInput,
 | 
						|
    reqAudioInput,
 | 
						|
    reqAudioOutput,
 | 
						|
    !!aRequest.secondOrigin
 | 
						|
  );
 | 
						|
  let message;
 | 
						|
  let originToShow;
 | 
						|
  if (principal.schemeIs("file")) {
 | 
						|
    message = localization.formatValueSync(stringId + "-with-file");
 | 
						|
    originToShow = null;
 | 
						|
  } else {
 | 
						|
    message = localization.formatValueSync(stringId, {
 | 
						|
      origin: "<>",
 | 
						|
      thirdParty: "{}",
 | 
						|
    });
 | 
						|
    originToShow = lazy.webrtcUI.getHostOrExtensionName(principal.URI);
 | 
						|
  }
 | 
						|
  let notification; // Used by action callbacks.
 | 
						|
  const actionL10nIds = [{ id: "webrtc-action-allow" }];
 | 
						|
 | 
						|
  let notificationSilencingEnabled = Services.prefs.getBoolPref(
 | 
						|
    "privacy.webrtc.allowSilencingNotifications"
 | 
						|
  );
 | 
						|
 | 
						|
  const isNotNowLabelEnabled =
 | 
						|
    reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
 | 
						|
  let secondaryActions = [];
 | 
						|
  if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
 | 
						|
    // We want to free up the checkbox at the bottom of the permission
 | 
						|
    // panel for the notification silencing option, so we use a
 | 
						|
    // different configuration for the permissions panel when
 | 
						|
    // notification silencing is enabled.
 | 
						|
 | 
						|
    let permissionName = reqAudioOutput ? "speaker" : "screen";
 | 
						|
    // When selecting speakers, we always offer 'Not now' instead of 'Block'.
 | 
						|
    // When selecting screens, we offer 'Not now' if and only if we have a
 | 
						|
    // (temporary) allow permission for some mic/cam device.
 | 
						|
    const id = isNotNowLabelEnabled
 | 
						|
      ? "webrtc-action-not-now"
 | 
						|
      : "webrtc-action-block";
 | 
						|
    actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
 | 
						|
    secondaryActions = [
 | 
						|
      {
 | 
						|
        callback() {
 | 
						|
          aActor.denyRequest(aRequest);
 | 
						|
          if (!isNotNowLabelEnabled) {
 | 
						|
            lazy.SitePermissions.setForPrincipal(
 | 
						|
              principal,
 | 
						|
              permissionName,
 | 
						|
              lazy.SitePermissions.BLOCK,
 | 
						|
              lazy.SitePermissions.SCOPE_TEMPORARY,
 | 
						|
              notification.browser
 | 
						|
            );
 | 
						|
          }
 | 
						|
        },
 | 
						|
      },
 | 
						|
      {
 | 
						|
        callback() {
 | 
						|
          aActor.denyRequest(aRequest);
 | 
						|
          lazy.SitePermissions.setForPrincipal(
 | 
						|
            principal,
 | 
						|
            permissionName,
 | 
						|
            lazy.SitePermissions.BLOCK,
 | 
						|
            lazy.SitePermissions.SCOPE_PERSISTENT,
 | 
						|
            notification.browser
 | 
						|
          );
 | 
						|
        },
 | 
						|
      },
 | 
						|
    ];
 | 
						|
  } else {
 | 
						|
    // We have a (temporary) allow permission for some device
 | 
						|
    // hence we offer a 'Not now' label instead of 'Block'.
 | 
						|
    const id = isNotNowLabelEnabled
 | 
						|
      ? "webrtc-action-not-now"
 | 
						|
      : "webrtc-action-block";
 | 
						|
    actionL10nIds.push({ id });
 | 
						|
    secondaryActions = [
 | 
						|
      {
 | 
						|
        callback(aState) {
 | 
						|
          aActor.denyRequest(aRequest);
 | 
						|
 | 
						|
          const isPersistent = aState?.checkboxChecked;
 | 
						|
 | 
						|
          // Choosing 'Not now' will not set a block permission
 | 
						|
          // we just deny the request. This enables certain use cases
 | 
						|
          // where sites want to switch devices, but users back out of the permission request
 | 
						|
          // (See Bug 1609578).
 | 
						|
          // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
 | 
						|
          if (!isPersistent && isNotNowLabelEnabled) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
 | 
						|
          // Denying a camera / microphone prompt means we set a temporary or
 | 
						|
          // persistent permission block. There may still be active grace period
 | 
						|
          // permissions at this point. We need to remove them.
 | 
						|
          clearTemporaryGrants(
 | 
						|
            notification.browser,
 | 
						|
            reqVideoInput === "Camera",
 | 
						|
            !!reqAudioInput
 | 
						|
          );
 | 
						|
 | 
						|
          const scope = isPersistent
 | 
						|
            ? lazy.SitePermissions.SCOPE_PERSISTENT
 | 
						|
            : lazy.SitePermissions.SCOPE_TEMPORARY;
 | 
						|
          if (reqAudioInput) {
 | 
						|
            lazy.SitePermissions.setForPrincipal(
 | 
						|
              principal,
 | 
						|
              "microphone",
 | 
						|
              lazy.SitePermissions.BLOCK,
 | 
						|
              scope,
 | 
						|
              notification.browser
 | 
						|
            );
 | 
						|
          }
 | 
						|
          if (reqVideoInput) {
 | 
						|
            lazy.SitePermissions.setForPrincipal(
 | 
						|
              principal,
 | 
						|
              sharingScreen ? "screen" : "camera",
 | 
						|
              lazy.SitePermissions.BLOCK,
 | 
						|
              scope,
 | 
						|
              notification.browser
 | 
						|
            );
 | 
						|
          }
 | 
						|
        },
 | 
						|
      },
 | 
						|
    ];
 | 
						|
  }
 | 
						|
 | 
						|
  // The formatMessagesSync method returns an array of results
 | 
						|
  // for each message that was requested, and for the ones with
 | 
						|
  // attributes, returns an attributes array with objects like:
 | 
						|
  //     { name: "label", value: "somevalue" }
 | 
						|
  const [mainMessage, ...secondaryMessages] = localization
 | 
						|
    .formatMessagesSync(actionL10nIds)
 | 
						|
    .map(msg =>
 | 
						|
      msg.attributes.reduce(
 | 
						|
        (acc, { name, value }) => ({ ...acc, [name]: value }),
 | 
						|
        {}
 | 
						|
      )
 | 
						|
    );
 | 
						|
 | 
						|
  const mainAction = {
 | 
						|
    label: mainMessage.label,
 | 
						|
    accessKey: mainMessage.accesskey,
 | 
						|
    // The real callback will be set during the "showing" event. The
 | 
						|
    // empty function here is so that PopupNotifications.show doesn't
 | 
						|
    // reject the action.
 | 
						|
    callback() {},
 | 
						|
  };
 | 
						|
 | 
						|
  for (let i = 0; i < secondaryActions.length; ++i) {
 | 
						|
    secondaryActions[i].label = secondaryMessages[i].label;
 | 
						|
    secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
 | 
						|
  }
 | 
						|
 | 
						|
  let options = {
 | 
						|
    name: originToShow,
 | 
						|
    persistent: true,
 | 
						|
    hideClose: true,
 | 
						|
    eventCallback(aTopic, aNewBrowser, isCancel) {
 | 
						|
      if (aTopic == "swapping") {
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      let doc = this.browser.ownerDocument;
 | 
						|
 | 
						|
      // Clean-up video streams of screensharing previews.
 | 
						|
      if (
 | 
						|
        reqVideoInput !== "Screen" ||
 | 
						|
        aTopic == "dismissed" ||
 | 
						|
        aTopic == "removed"
 | 
						|
      ) {
 | 
						|
        let video = doc.getElementById("webRTC-previewVideo");
 | 
						|
        video.deviceId = null; // Abort previews still being started.
 | 
						|
        if (video.stream) {
 | 
						|
          video.stream.getTracks().forEach(t => t.stop());
 | 
						|
          video.stream = null;
 | 
						|
          video.src = null;
 | 
						|
          doc.getElementById("webRTC-preview").hidden = true;
 | 
						|
        }
 | 
						|
        let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
 | 
						|
        if (menupopup._commandEventListener) {
 | 
						|
          menupopup.removeEventListener(
 | 
						|
            "command",
 | 
						|
            menupopup._commandEventListener
 | 
						|
          );
 | 
						|
          menupopup._commandEventListener = null;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      if (aTopic == "removed" && notification && isCancel) {
 | 
						|
        // The notification has been cancelled (e.g. due to entering
 | 
						|
        // full-screen).  Also cancel the webRTC request.
 | 
						|
        aActor.denyRequest(aRequest);
 | 
						|
      } else if (
 | 
						|
        aTopic == "shown" &&
 | 
						|
        audioOutputDevices.length > 1 &&
 | 
						|
        !notification.wasDismissed
 | 
						|
      ) {
 | 
						|
        // Focus the list on first show so that arrow keys select the speaker.
 | 
						|
        doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
 | 
						|
      }
 | 
						|
 | 
						|
      if (aTopic != "showing") {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
 | 
						|
      // If BLOCK has been set persistently in the permission manager or has
 | 
						|
      // been set on the tab, then it is handled synchronously before we add
 | 
						|
      // the notification.
 | 
						|
      // Handling of ALLOW is delayed until the popupshowing event,
 | 
						|
      // to avoid granting permissions automatically to background tabs.
 | 
						|
      if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
 | 
						|
        this.remove();
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      /**
 | 
						|
       * Prepare the device selector for one kind of device.
 | 
						|
       * @param {Object[]} devices - available devices of this kind.
 | 
						|
       * @param {string} IDPrefix - indicating kind of device and so
 | 
						|
       *   associated UI elements.
 | 
						|
       * @param {string[]} describedByIDs - an array to which might be
 | 
						|
       *   appended ids of elements that describe the panel, for the caller to
 | 
						|
       *   use in the aria-describedby attribute.
 | 
						|
       */
 | 
						|
      function listDevices(devices, IDPrefix, describedByIDs) {
 | 
						|
        let labelID = `${IDPrefix}-single-device-label`;
 | 
						|
        let list;
 | 
						|
        let itemParent;
 | 
						|
        if (IDPrefix == "webRTC-selectSpeaker") {
 | 
						|
          list = doc.getElementById(`${IDPrefix}-richlistbox`);
 | 
						|
          itemParent = list;
 | 
						|
        } else {
 | 
						|
          itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
 | 
						|
          list = itemParent.parentNode; // menulist
 | 
						|
        }
 | 
						|
        while (itemParent.lastChild) {
 | 
						|
          itemParent.removeChild(itemParent.lastChild);
 | 
						|
        }
 | 
						|
 | 
						|
        // Removing the child nodes of a menupopup doesn't clear the value
 | 
						|
        // attribute of its menulist. Similary for richlistbox state. This can
 | 
						|
        // have unfortunate side effects when the list is rebuilt with a
 | 
						|
        // different content, so we set the selectedIndex explicitly to reset
 | 
						|
        // state.
 | 
						|
        let defaultIndex = 0;
 | 
						|
 | 
						|
        for (let device of devices) {
 | 
						|
          let item = addDeviceToList(list, device.name, device.deviceIndex);
 | 
						|
          if (IDPrefix == "webRTC-selectSpeaker") {
 | 
						|
            item.addEventListener("dblclick", event => {
 | 
						|
              // Allow the chosen speakers via
 | 
						|
              // .popup-notification-primary-button so that
 | 
						|
              // "security.notification_enable_delay" is checked.
 | 
						|
              event.target.closest("popupnotification").button.doCommand();
 | 
						|
            });
 | 
						|
            if (device.id == aRequest.audioOutputId) {
 | 
						|
              defaultIndex = device.deviceIndex;
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
        list.selectedIndex = defaultIndex;
 | 
						|
 | 
						|
        let label = doc.getElementById(labelID);
 | 
						|
        if (devices.length == 1) {
 | 
						|
          describedByIDs.push(`${IDPrefix}-icon`, labelID);
 | 
						|
          label.value = devices[0].name;
 | 
						|
          label.hidden = false;
 | 
						|
          list.hidden = true;
 | 
						|
        } else {
 | 
						|
          label.hidden = true;
 | 
						|
          list.hidden = false;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      let notificationElement = doc.getElementById(
 | 
						|
        "webRTC-shareDevices-notification"
 | 
						|
      );
 | 
						|
 | 
						|
      function checkDisabledWindowMenuItem() {
 | 
						|
        let list = doc.getElementById("webRTC-selectWindow-menulist");
 | 
						|
        let item = list.selectedItem;
 | 
						|
        if (!item || item.hasAttribute("disabled")) {
 | 
						|
          notificationElement.setAttribute("invalidselection", "true");
 | 
						|
        } else {
 | 
						|
          notificationElement.removeAttribute("invalidselection");
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      function listScreenShareDevices(menupopup, devices) {
 | 
						|
        while (menupopup.lastChild) {
 | 
						|
          menupopup.removeChild(menupopup.lastChild);
 | 
						|
        }
 | 
						|
 | 
						|
        // Removing the child nodes of the menupopup doesn't clear the value
 | 
						|
        // attribute of the menulist. This can have unfortunate side effects
 | 
						|
        // when the list is rebuilt with a different content, so we remove
 | 
						|
        // the value attribute and unset the selectedItem explicitly.
 | 
						|
        menupopup.parentNode.removeAttribute("value");
 | 
						|
        menupopup.parentNode.selectedItem = null;
 | 
						|
 | 
						|
        // "Select a Window or Screen" is the default because we can't and don't
 | 
						|
        // want to pick a 'default' window to share (Full screen is "scary").
 | 
						|
        addDeviceToList(
 | 
						|
          menupopup.parentNode,
 | 
						|
          localization.formatValueSync("webrtc-pick-window-or-screen"),
 | 
						|
          "-1"
 | 
						|
        );
 | 
						|
        menupopup.appendChild(doc.createXULElement("menuseparator"));
 | 
						|
 | 
						|
        let isPipeWireDetected = false;
 | 
						|
 | 
						|
        // Build the list of 'devices'.
 | 
						|
        let monitorIndex = 1;
 | 
						|
        for (let i = 0; i < devices.length; ++i) {
 | 
						|
          let device = devices[i];
 | 
						|
          let type = device.mediaSource;
 | 
						|
          let name;
 | 
						|
          if (device.canRequestOsLevelPrompt) {
 | 
						|
            // When we share content by PipeWire add only one item to the device
 | 
						|
            // list. When it's selected PipeWire portal dialog is opened and
 | 
						|
            // user confirms actual window/screen sharing there.
 | 
						|
            // Don't mark it as scary as there's an extra confirmation step by
 | 
						|
            // PipeWire portal dialog.
 | 
						|
 | 
						|
            isPipeWireDetected = true;
 | 
						|
            let item = addDeviceToList(
 | 
						|
              menupopup.parentNode,
 | 
						|
              localization.formatValueSync("webrtc-share-pipe-wire-portal"),
 | 
						|
              i,
 | 
						|
              type
 | 
						|
            );
 | 
						|
            item.deviceId = device.rawId;
 | 
						|
            item.mediaSource = type;
 | 
						|
 | 
						|
            // In this case the OS sharing dialog will be the only option and
 | 
						|
            // can be safely pre-selected.
 | 
						|
            menupopup.parentNode.selectedItem = item;
 | 
						|
            continue;
 | 
						|
          } else if (type == "screen") {
 | 
						|
            // Building screen list from available screens.
 | 
						|
            if (device.name == "Primary Monitor") {
 | 
						|
              name = localization.formatValueSync("webrtc-share-entire-screen");
 | 
						|
            } else {
 | 
						|
              name = localization.formatValueSync("webrtc-share-monitor", {
 | 
						|
                monitorIndex,
 | 
						|
              });
 | 
						|
              ++monitorIndex;
 | 
						|
            }
 | 
						|
          } else {
 | 
						|
            name = device.name;
 | 
						|
 | 
						|
            if (type == "application") {
 | 
						|
              // The application names returned by the platform are of the form:
 | 
						|
              // <window count>\x1e<application name>
 | 
						|
              const [count, appName] = name.split("\x1e");
 | 
						|
              name = localization.formatValueSync("webrtc-share-application", {
 | 
						|
                appName,
 | 
						|
                windowCount: parseInt(count),
 | 
						|
              });
 | 
						|
            }
 | 
						|
          }
 | 
						|
          let item = addDeviceToList(menupopup.parentNode, name, i, type);
 | 
						|
          item.deviceId = device.rawId;
 | 
						|
          item.mediaSource = type;
 | 
						|
          if (device.scary) {
 | 
						|
            item.scary = true;
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        // Always re-select the "No <type>" item.
 | 
						|
        doc
 | 
						|
          .getElementById("webRTC-selectWindow-menulist")
 | 
						|
          .removeAttribute("value");
 | 
						|
        doc.getElementById("webRTC-all-windows-shared").hidden = true;
 | 
						|
 | 
						|
        menupopup._commandEventListener = event => {
 | 
						|
          checkDisabledWindowMenuItem();
 | 
						|
          let video = doc.getElementById("webRTC-previewVideo");
 | 
						|
          if (video.stream) {
 | 
						|
            video.stream.getTracks().forEach(t => t.stop());
 | 
						|
            video.stream = null;
 | 
						|
          }
 | 
						|
 | 
						|
          const { deviceId, mediaSource, scary } = event.target;
 | 
						|
          if (deviceId == undefined) {
 | 
						|
            doc.getElementById("webRTC-preview").hidden = true;
 | 
						|
            video.src = null;
 | 
						|
            return;
 | 
						|
          }
 | 
						|
 | 
						|
          let warning = doc.getElementById("webRTC-previewWarning");
 | 
						|
          let warningBox = doc.getElementById("webRTC-previewWarningBox");
 | 
						|
          warningBox.hidden = !scary;
 | 
						|
          let chromeWin = doc.defaultView;
 | 
						|
          if (scary) {
 | 
						|
            const warnId =
 | 
						|
              mediaSource == "screen"
 | 
						|
                ? "webrtc-share-screen-warning"
 | 
						|
                : "webrtc-share-browser-warning";
 | 
						|
            doc.l10n.setAttributes(warning, warnId);
 | 
						|
 | 
						|
            const learnMore = doc.getElementById(
 | 
						|
              "webRTC-previewWarning-learnMore"
 | 
						|
            );
 | 
						|
            const baseURL = Services.urlFormatter.formatURLPref(
 | 
						|
              "app.support.baseURL"
 | 
						|
            );
 | 
						|
            learnMore.setAttribute("href", baseURL + "screenshare-safety");
 | 
						|
            doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");
 | 
						|
 | 
						|
            // On Catalina, we don't want to blow our chance to show the
 | 
						|
            // OS-level helper prompt to enable screen recording if the user
 | 
						|
            // intends to reject anyway. OTOH showing it when they click Allow
 | 
						|
            // is too late. A happy middle is to show it when the user makes a
 | 
						|
            // choice in the picker. This already happens implicitly if the
 | 
						|
            // user chooses "Entire desktop", as a side-effect of our preview,
 | 
						|
            // we just need to also do it if they choose "Firefox". These are
 | 
						|
            // the lone two options when permission is absent on Catalina.
 | 
						|
            // Ironically, these are the two sources marked "scary" from a
 | 
						|
            // web-sharing perspective, which is why this code resides here.
 | 
						|
            // A restart doesn't appear to be necessary in spite of OS wording.
 | 
						|
            let scrStatus = {};
 | 
						|
            lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
 | 
						|
            if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
 | 
						|
              lazy.OSPermissions.maybeRequestScreenCapturePermission();
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          let perms = Services.perms;
 | 
						|
          let chromePrincipal =
 | 
						|
            Services.scriptSecurityManager.getSystemPrincipal();
 | 
						|
          perms.addFromPrincipal(
 | 
						|
            chromePrincipal,
 | 
						|
            "MediaManagerVideo",
 | 
						|
            perms.ALLOW_ACTION,
 | 
						|
            perms.EXPIRE_SESSION
 | 
						|
          );
 | 
						|
 | 
						|
          // We don't have access to any screen content besides our browser tabs
 | 
						|
          // on Wayland, therefore there are no previews we can show.
 | 
						|
          if (
 | 
						|
            (!isPipeWireDetected || mediaSource == "browser") &&
 | 
						|
            Services.prefs.getBoolPref(
 | 
						|
              "media.getdisplaymedia.previews.enabled",
 | 
						|
              true
 | 
						|
            )
 | 
						|
          ) {
 | 
						|
            video.deviceId = deviceId;
 | 
						|
            let constraints = {
 | 
						|
              video: { mediaSource, deviceId: { exact: deviceId } },
 | 
						|
            };
 | 
						|
            chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
 | 
						|
              stream => {
 | 
						|
                if (video.deviceId != deviceId) {
 | 
						|
                  // The user has selected a different device or closed the panel
 | 
						|
                  // before getUserMedia finished.
 | 
						|
                  stream.getTracks().forEach(t => t.stop());
 | 
						|
                  return;
 | 
						|
                }
 | 
						|
                video.srcObject = stream;
 | 
						|
                video.stream = stream;
 | 
						|
                doc.getElementById("webRTC-preview").hidden = false;
 | 
						|
                video.onloadedmetadata = function () {
 | 
						|
                  video.play();
 | 
						|
                };
 | 
						|
              },
 | 
						|
              err => {
 | 
						|
                if (
 | 
						|
                  err.name == "OverconstrainedError" &&
 | 
						|
                  err.constraint == "deviceId"
 | 
						|
                ) {
 | 
						|
                  // Window has disappeared since enumeration, which can happen.
 | 
						|
                  // No preview for you.
 | 
						|
                  return;
 | 
						|
                }
 | 
						|
                console.error(
 | 
						|
                  `error in preview: ${err.message} ${err.constraint}`
 | 
						|
                );
 | 
						|
              }
 | 
						|
            );
 | 
						|
          }
 | 
						|
        };
 | 
						|
        menupopup.addEventListener("command", menupopup._commandEventListener);
 | 
						|
      }
 | 
						|
 | 
						|
      function addDeviceToList(list, deviceName, deviceIndex, type) {
 | 
						|
        let item = list.appendItem(deviceName, deviceIndex);
 | 
						|
        item.setAttribute("tooltiptext", deviceName);
 | 
						|
        if (type) {
 | 
						|
          item.setAttribute("devicetype", type);
 | 
						|
        }
 | 
						|
 | 
						|
        if (deviceIndex == "-1") {
 | 
						|
          item.setAttribute("disabled", true);
 | 
						|
        }
 | 
						|
 | 
						|
        return item;
 | 
						|
      }
 | 
						|
 | 
						|
      doc.getElementById("webRTC-selectCamera").hidden =
 | 
						|
        reqVideoInput !== "Camera";
 | 
						|
      doc.getElementById("webRTC-selectWindowOrScreen").hidden =
 | 
						|
        reqVideoInput !== "Screen";
 | 
						|
      doc.getElementById("webRTC-selectMicrophone").hidden =
 | 
						|
        reqAudioInput !== "Microphone";
 | 
						|
      doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
 | 
						|
 | 
						|
      let describedByIDs = ["webRTC-shareDevices-notification-description"];
 | 
						|
 | 
						|
      if (sharingScreen) {
 | 
						|
        let windowMenupopup = doc.getElementById(
 | 
						|
          "webRTC-selectWindow-menupopup"
 | 
						|
        );
 | 
						|
        listScreenShareDevices(windowMenupopup, videoInputDevices);
 | 
						|
        checkDisabledWindowMenuItem();
 | 
						|
      } else {
 | 
						|
        listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
 | 
						|
        notificationElement.removeAttribute("invalidselection");
 | 
						|
      }
 | 
						|
      if (!sharingAudio) {
 | 
						|
        listDevices(
 | 
						|
          audioInputDevices,
 | 
						|
          "webRTC-selectMicrophone",
 | 
						|
          describedByIDs
 | 
						|
        );
 | 
						|
      }
 | 
						|
      listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);
 | 
						|
 | 
						|
      // PopupNotifications knows to clear the aria-describedby attribute
 | 
						|
      // when hiding, so we don't have to worry about cleaning it up ourselves.
 | 
						|
      chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
 | 
						|
        "aria-describedby",
 | 
						|
        describedByIDs.join(" ")
 | 
						|
      );
 | 
						|
 | 
						|
      this.mainAction.callback = async function (aState) {
 | 
						|
        let remember = false;
 | 
						|
        let silenceNotifications = false;
 | 
						|
 | 
						|
        if (notificationSilencingEnabled && sharingScreen) {
 | 
						|
          silenceNotifications = aState && aState.checkboxChecked;
 | 
						|
        } else {
 | 
						|
          remember = aState && aState.checkboxChecked;
 | 
						|
        }
 | 
						|
 | 
						|
        let allowedDevices = [];
 | 
						|
        let perms = Services.perms;
 | 
						|
        if (reqVideoInput) {
 | 
						|
          let listId = sharingScreen
 | 
						|
            ? "webRTC-selectWindow-menulist"
 | 
						|
            : "webRTC-selectCamera-menulist";
 | 
						|
          let videoDeviceIndex = doc.getElementById(listId).value;
 | 
						|
          let allowVideoDevice = videoDeviceIndex != "-1";
 | 
						|
          if (allowVideoDevice) {
 | 
						|
            allowedDevices.push(videoDeviceIndex);
 | 
						|
            // Session permission will be removed after use
 | 
						|
            // (it's really one-shot, not for the entire session)
 | 
						|
            perms.addFromPrincipal(
 | 
						|
              principal,
 | 
						|
              "MediaManagerVideo",
 | 
						|
              perms.ALLOW_ACTION,
 | 
						|
              perms.EXPIRE_SESSION
 | 
						|
            );
 | 
						|
            let { mediaSource, rawId } = videoInputDevices.find(
 | 
						|
              ({ deviceIndex }) => deviceIndex == videoDeviceIndex
 | 
						|
            );
 | 
						|
            aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
 | 
						|
            if (remember) {
 | 
						|
              lazy.SitePermissions.setForPrincipal(
 | 
						|
                principal,
 | 
						|
                "camera",
 | 
						|
                lazy.SitePermissions.ALLOW
 | 
						|
              );
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (reqAudioInput === "Microphone") {
 | 
						|
          let audioDeviceIndex = doc.getElementById(
 | 
						|
            "webRTC-selectMicrophone-menulist"
 | 
						|
          ).value;
 | 
						|
          let allowMic = audioDeviceIndex != "-1";
 | 
						|
          if (allowMic) {
 | 
						|
            allowedDevices.push(audioDeviceIndex);
 | 
						|
            let { mediaSource, rawId } = audioInputDevices.find(
 | 
						|
              ({ deviceIndex }) => deviceIndex == audioDeviceIndex
 | 
						|
            );
 | 
						|
            aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
 | 
						|
            if (remember) {
 | 
						|
              lazy.SitePermissions.setForPrincipal(
 | 
						|
                principal,
 | 
						|
                "microphone",
 | 
						|
                lazy.SitePermissions.ALLOW
 | 
						|
              );
 | 
						|
            }
 | 
						|
          }
 | 
						|
        } else if (reqAudioInput === "AudioCapture") {
 | 
						|
          // Only one device possible for audio capture.
 | 
						|
          allowedDevices.push(0);
 | 
						|
        }
 | 
						|
 | 
						|
        if (reqAudioOutput) {
 | 
						|
          let audioDeviceIndex = doc.getElementById(
 | 
						|
            "webRTC-selectSpeaker-richlistbox"
 | 
						|
          ).value;
 | 
						|
          let allowSpeaker = audioDeviceIndex != "-1";
 | 
						|
          if (allowSpeaker) {
 | 
						|
            allowedDevices.push(audioDeviceIndex);
 | 
						|
            let { id } = audioOutputDevices.find(
 | 
						|
              ({ deviceIndex }) => deviceIndex == audioDeviceIndex
 | 
						|
            );
 | 
						|
            lazy.SitePermissions.setForPrincipal(
 | 
						|
              principal,
 | 
						|
              ["speaker", id].join("^"),
 | 
						|
              lazy.SitePermissions.ALLOW
 | 
						|
            );
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (!allowedDevices.length) {
 | 
						|
          aActor.denyRequest(aRequest);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        const camNeeded = reqVideoInput === "Camera";
 | 
						|
        const micNeeded = !!reqAudioInput;
 | 
						|
        const scrNeeded = reqVideoInput === "Screen";
 | 
						|
        const havePermission = await aActor.checkOSPermission(
 | 
						|
          camNeeded,
 | 
						|
          micNeeded,
 | 
						|
          scrNeeded
 | 
						|
        );
 | 
						|
        if (!havePermission) {
 | 
						|
          aActor.denyRequestNoPermission(aRequest);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        aActor.sendAsyncMessage("webrtc:Allow", {
 | 
						|
          callID: aRequest.callID,
 | 
						|
          windowID: aRequest.windowID,
 | 
						|
          devices: allowedDevices,
 | 
						|
          suppressNotifications: silenceNotifications,
 | 
						|
        });
 | 
						|
      };
 | 
						|
 | 
						|
      // If we haven't handled the permission yet, we want to show the doorhanger.
 | 
						|
      return false;
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  function shouldShowAlwaysRemember() {
 | 
						|
    // Don't offer "always remember" action in PB mode
 | 
						|
    if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't offer "always remember" action in maybe unsafe permission
 | 
						|
    // delegation
 | 
						|
    if (aRequest.secondOrigin) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Speaker grants are always remembered, so no checkbox is required.
 | 
						|
    if (reqAudioOutput) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  function getRememberCheckboxLabel() {
 | 
						|
    if (reqVideoInput == "Camera") {
 | 
						|
      if (reqAudioInput == "Microphone") {
 | 
						|
        return "webrtc-remember-allow-checkbox-camera-and-microphone";
 | 
						|
      }
 | 
						|
      return "webrtc-remember-allow-checkbox-camera";
 | 
						|
    }
 | 
						|
 | 
						|
    if (reqAudioInput == "Microphone") {
 | 
						|
      return "webrtc-remember-allow-checkbox-microphone";
 | 
						|
    }
 | 
						|
 | 
						|
    return "webrtc-remember-allow-checkbox";
 | 
						|
  }
 | 
						|
 | 
						|
  if (shouldShowAlwaysRemember()) {
 | 
						|
    // Disable the permanent 'Allow' action if the connection isn't secure, or for
 | 
						|
    // screen/audio sharing (because we can't guess which window the user wants to
 | 
						|
    // share without prompting). Note that we never enter this block for private
 | 
						|
    // browsing windows.
 | 
						|
    let reason = "";
 | 
						|
    if (sharingScreen) {
 | 
						|
      reason = "webrtc-reason-for-no-permanent-allow-screen";
 | 
						|
    } else if (sharingAudio) {
 | 
						|
      reason = "webrtc-reason-for-no-permanent-allow-audio";
 | 
						|
    } else if (!aRequest.secure) {
 | 
						|
      reason = "webrtc-reason-for-no-permanent-allow-insecure";
 | 
						|
    }
 | 
						|
 | 
						|
    options.checkbox = {
 | 
						|
      label: localization.formatValueSync(getRememberCheckboxLabel()),
 | 
						|
      checked: principal.isAddonOrExpandedAddonPrincipal,
 | 
						|
      checkedState: reason
 | 
						|
        ? {
 | 
						|
            disableMainAction: true,
 | 
						|
            warningLabel: localization.formatValueSync(reason),
 | 
						|
          }
 | 
						|
        : undefined,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  // If the notification silencing feature is enabled and we're sharing a
 | 
						|
  // screen, then the checkbox for the permission panel is what controls
 | 
						|
  // notification silencing.
 | 
						|
  if (notificationSilencingEnabled && sharingScreen) {
 | 
						|
    options.checkbox = {
 | 
						|
      label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
 | 
						|
      checked: false,
 | 
						|
      checkedState: {
 | 
						|
        disableMainAction: false,
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  let anchorId = "webRTC-shareDevices-notification-icon";
 | 
						|
  if (reqVideoInput === "Screen") {
 | 
						|
    anchorId = "webRTC-shareScreen-notification-icon";
 | 
						|
  } else if (!reqVideoInput) {
 | 
						|
    if (reqAudioInput && !reqAudioOutput) {
 | 
						|
      anchorId = "webRTC-shareMicrophone-notification-icon";
 | 
						|
    } else if (!reqAudioInput && reqAudioOutput) {
 | 
						|
      anchorId = "webRTC-shareSpeaker-notification-icon";
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (aRequest.secondOrigin) {
 | 
						|
    options.secondName = lazy.webrtcUI.getHostOrExtensionName(
 | 
						|
      null,
 | 
						|
      aRequest.secondOrigin
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  notification = chromeDoc.defaultView.PopupNotifications.show(
 | 
						|
    aBrowser,
 | 
						|
    "webRTC-shareDevices",
 | 
						|
    message,
 | 
						|
    anchorId,
 | 
						|
    mainAction,
 | 
						|
    secondaryActions,
 | 
						|
    options
 | 
						|
  );
 | 
						|
  notification.callID = aRequest.callID;
 | 
						|
 | 
						|
  let schemeHistogram = Services.telemetry.getKeyedHistogramById(
 | 
						|
    "PERMISSION_REQUEST_ORIGIN_SCHEME"
 | 
						|
  );
 | 
						|
  let userInputHistogram = Services.telemetry.getKeyedHistogramById(
 | 
						|
    "PERMISSION_REQUEST_HANDLING_USER_INPUT"
 | 
						|
  );
 | 
						|
 | 
						|
  let docURI = aRequest.documentURI;
 | 
						|
  let scheme = 0;
 | 
						|
  if (docURI.startsWith("https")) {
 | 
						|
    scheme = 2;
 | 
						|
  } else if (docURI.startsWith("http")) {
 | 
						|
    scheme = 1;
 | 
						|
  }
 | 
						|
 | 
						|
  for (let requestType of requestTypes) {
 | 
						|
    if (requestType == "AudioCapture") {
 | 
						|
      requestType = "Microphone";
 | 
						|
    }
 | 
						|
    requestType = requestType.toLowerCase();
 | 
						|
 | 
						|
    schemeHistogram.add(requestType, scheme);
 | 
						|
    userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {"Screen" | "Camera" | null} reqVideoInput
 | 
						|
 * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
 | 
						|
 * @param {boolean} reqAudioOutput
 | 
						|
 * @param {boolean} delegation - Is the access delegated to a third party?
 | 
						|
 * @returns {string} Localization message identifier
 | 
						|
 */
 | 
						|
function getPromptMessageId(
 | 
						|
  reqVideoInput,
 | 
						|
  reqAudioInput,
 | 
						|
  reqAudioOutput,
 | 
						|
  delegation
 | 
						|
) {
 | 
						|
  switch (reqVideoInput) {
 | 
						|
    case "Camera":
 | 
						|
      switch (reqAudioInput) {
 | 
						|
        case "Microphone":
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-camera-and-microphone";
 | 
						|
        case "AudioCapture":
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-camera-and-audio-capture";
 | 
						|
        default:
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-camera-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-camera";
 | 
						|
      }
 | 
						|
 | 
						|
    case "Screen":
 | 
						|
      switch (reqAudioInput) {
 | 
						|
        case "Microphone":
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-screen-and-microphone";
 | 
						|
        case "AudioCapture":
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-screen-and-audio-capture";
 | 
						|
        default:
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-screen-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-screen";
 | 
						|
      }
 | 
						|
 | 
						|
    default:
 | 
						|
      switch (reqAudioInput) {
 | 
						|
        case "Microphone":
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-microphone-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-microphone";
 | 
						|
        case "AudioCapture":
 | 
						|
          return delegation
 | 
						|
            ? "webrtc-allow-share-audio-capture-unsafe-delegation"
 | 
						|
            : "webrtc-allow-share-audio-capture";
 | 
						|
        default:
 | 
						|
          // This should be always true, if we've reached this far.
 | 
						|
          if (reqAudioOutput) {
 | 
						|
            return delegation
 | 
						|
              ? "webrtc-allow-share-speaker-unsafe-delegation"
 | 
						|
              : "webrtc-allow-share-speaker";
 | 
						|
          }
 | 
						|
          return undefined;
 | 
						|
      }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks whether we have a microphone/camera in use by checking the activePerms map
 | 
						|
 * or if we have an allow permission for a microphone/camera in sitePermissions
 | 
						|
 * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
 | 
						|
 * @return true if one of the above conditions is met
 | 
						|
 */
 | 
						|
function allowedOrActiveCameraOrMicrophone(browser) {
 | 
						|
  // Do we have an allow permission for cam/mic in the permissions manager?
 | 
						|
  if (
 | 
						|
    lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
 | 
						|
      return (
 | 
						|
        perm.state == lazy.SitePermissions.ALLOW &&
 | 
						|
        (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
 | 
						|
      );
 | 
						|
    })
 | 
						|
  ) {
 | 
						|
    // Return early, no need to check for active devices
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  // Do we have an active device?
 | 
						|
  return (
 | 
						|
    // Find all windowIDs that belong to our browsing contexts
 | 
						|
    browser.browsingContext
 | 
						|
      .getAllBrowsingContextsInSubtree()
 | 
						|
      // Only keep the outerWindowIds
 | 
						|
      .map(bc => bc.currentWindowGlobal?.outerWindowId)
 | 
						|
      .filter(id => id != null)
 | 
						|
      // We have an active device if one of our windowIds has a non empty map in the activePerms map
 | 
						|
      // that includes one device of type "camera" or "microphone"
 | 
						|
      .some(id => {
 | 
						|
        let map = lazy.webrtcUI.activePerms.get(id);
 | 
						|
        if (!map) {
 | 
						|
          // This windowId has no active device
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        // Let's see if one of the devices is a camera or a microphone
 | 
						|
        let types = [...map.values()];
 | 
						|
        return types.includes("microphone") || types.includes("camera");
 | 
						|
      })
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
function removePrompt(aBrowser, aCallId) {
 | 
						|
  let chromeWin = aBrowser.ownerGlobal;
 | 
						|
  let notification = chromeWin.PopupNotifications.getNotification(
 | 
						|
    "webRTC-shareDevices",
 | 
						|
    aBrowser
 | 
						|
  );
 | 
						|
  if (notification && notification.callID == aCallId) {
 | 
						|
    notification.remove();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Clears temporary permission grants used for WebRTC device grace periods.
 | 
						|
 * @param browser - Browser element to clear permissions for.
 | 
						|
 * @param {boolean} clearCamera - Clear camera grants.
 | 
						|
 * @param {boolean} clearMicrophone - Clear microphone grants.
 | 
						|
 */
 | 
						|
function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
 | 
						|
  if (!clearCamera && !clearMicrophone) {
 | 
						|
    // Nothing to clear.
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  let perms = lazy.SitePermissions.getAllForBrowser(browser);
 | 
						|
  perms
 | 
						|
    .filter(perm => {
 | 
						|
      let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
 | 
						|
      // We only want to clear WebRTC grace periods. These are temporary, device
 | 
						|
      // specifc (double-keyed) microphone or camera permissions.
 | 
						|
      return (
 | 
						|
        key &&
 | 
						|
        perm.state == lazy.SitePermissions.ALLOW &&
 | 
						|
        perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
 | 
						|
        ((clearCamera && id == "camera") ||
 | 
						|
          (clearMicrophone && id == "microphone"))
 | 
						|
      );
 | 
						|
    })
 | 
						|
    .forEach(perm =>
 | 
						|
      lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
 | 
						|
    );
 | 
						|
}
 |