summaryrefslogtreecommitdiffstats
path: root/browser/base/content/webrtcIndicator.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/webrtcIndicator.js')
-rw-r--r--browser/base/content/webrtcIndicator.js590
1 files changed, 590 insertions, 0 deletions
diff --git a/browser/base/content/webrtcIndicator.js b/browser/base/content/webrtcIndicator.js
new file mode 100644
index 0000000000..f38c7446ba
--- /dev/null
+++ b/browser/base/content/webrtcIndicator.js
@@ -0,0 +1,590 @@
+/* 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/. */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { showStreamSharingMenu, webrtcUI } = ChromeUtils.importESModule(
+ "resource:///modules/webrtcUI.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gScreenManager",
+ "@mozilla.org/gfx/screenmanager;1",
+ "nsIScreenManager"
+);
+
+/**
+ * Public function called by webrtcUI to update the indicator
+ * display when the active streams change.
+ */
+function updateIndicatorState() {
+ WebRTCIndicator.updateIndicatorState();
+}
+
+/**
+ * Public function called by webrtcUI to indicate that webrtcUI
+ * is about to close the indicator. This is so that we can differentiate
+ * between closes that are caused by webrtcUI, and closes that are
+ * caused by other reasons (like the user closing the window via the
+ * OS window controls somehow).
+ *
+ * If the window is closed without having called this method first, the
+ * indicator will ask webrtcUI to shutdown any remaining streams and then
+ * select and focus the most recent browser tab that a stream was shared
+ * with.
+ */
+function closingInternally() {
+ WebRTCIndicator.closingInternally();
+}
+
+/**
+ * Main control object for the WebRTC global indicator
+ */
+const WebRTCIndicator = {
+ init(event) {
+ addEventListener("load", this);
+ addEventListener("unload", this);
+
+ // If the user customizes the position of the indicator, we will
+ // not try to re-center it on the primary display after indicator
+ // state updates.
+ this.positionCustomized = false;
+
+ this.updatingIndicatorState = false;
+ this.loaded = false;
+ this.isClosingInternally = false;
+
+ this.statusBar = null;
+ this.statusBarMenus = new Set();
+
+ this.showGlobalMuteToggles = Services.prefs.getBoolPref(
+ "privacy.webrtc.globalMuteToggles",
+ false
+ );
+
+ this.hideGlobalIndicator =
+ Services.prefs.getBoolPref("privacy.webrtc.hideGlobalIndicator", false) ||
+ Services.appinfo.isWayland;
+
+ if (this.hideGlobalIndicator) {
+ this.setVisibility(false);
+ }
+ },
+
+ /**
+ * Controls the visibility of the global indicator. Also sets the value of
+ * a "visible" attribute on the document element to "true" or "false".
+ *
+ * @param isVisible (boolean)
+ * Whether or not the global indicator should be visible.
+ */
+ setVisibility(isVisible) {
+ let baseWin = window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
+ baseWin.visibility = isVisible;
+ // AppWindow::GetVisibility _always_ returns true (see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=306245), so we'll set an
+ // attribute on the document to make it easier for tests to know that the
+ // indicator is not visible.
+ document.documentElement.setAttribute("visible", isVisible);
+ },
+
+ /**
+ * Exposed externally so that webrtcUI can alert the indicator to
+ * update itself when sharing states have changed.
+ */
+ updateIndicatorState() {
+ // It's possible that we were called externally before the indicator
+ // finished loading. If so, then bail out - we're going to call
+ // updateIndicatorState ourselves automatically once the load
+ // event fires.
+ if (!this.loaded) {
+ return;
+ }
+
+ // We've started to update the indicator state. We set this flag so
+ // that the MozUpdateWindowPos event handler doesn't interpret indicator
+ // state updates as window movement caused by the user.
+ this.updatingIndicatorState = true;
+
+ let showCameraIndicator = webrtcUI.showCameraIndicator;
+ let showMicrophoneIndicator = webrtcUI.showMicrophoneIndicator;
+ let showScreenSharingIndicator = webrtcUI.showScreenSharingIndicator;
+ if (this.statusBar) {
+ let statusMenus = new Map([
+ ["Camera", showCameraIndicator],
+ ["Microphone", showMicrophoneIndicator],
+ ["Screen", showScreenSharingIndicator],
+ ]);
+
+ for (let [name, shouldShow] of statusMenus) {
+ let menu = document.getElementById(`webRTC-sharing${name}-menu`);
+ if (shouldShow && !this.statusBarMenus.has(menu)) {
+ this.statusBar.addItem(menu);
+ this.statusBarMenus.add(menu);
+ } else if (!shouldShow && this.statusBarMenus.has(menu)) {
+ this.statusBar.removeItem(menu);
+ this.statusBarMenus.delete(menu);
+ }
+ }
+ }
+
+ if (!this.showGlobalMuteToggles && !webrtcUI.showScreenSharingIndicator) {
+ this.setVisibility(false);
+ } else if (!this.hideGlobalIndicator) {
+ this.setVisibility(true);
+ }
+
+ if (this.showGlobalMuteToggles) {
+ this.updateWindowAttr("sharingvideo", showCameraIndicator);
+ this.updateWindowAttr("sharingaudio", showMicrophoneIndicator);
+ }
+
+ let sharingScreen = showScreenSharingIndicator.startsWith("Screen");
+ this.updateWindowAttr("sharingscreen", sharingScreen);
+
+ // We don't currently support the browser-tab sharing case, so we don't
+ // check if the screen sharing indicator starts with "Browser".
+
+ // We special-case sharing a window, because we want to have a slightly
+ // different UI if we're sharing a browser window.
+ let sharingWindow = showScreenSharingIndicator.startsWith("Window");
+ this.updateWindowAttr("sharingwindow", sharingWindow);
+
+ if (sharingWindow) {
+ // Get the active window streams and see if any of them are "scary".
+ // If so, then we're sharing a browser window.
+ let activeStreams = webrtcUI.getActiveStreams(
+ false /* camera */,
+ false /* microphone */,
+ false /* screen */,
+ true /* window */
+ );
+ let hasBrowserWindow = activeStreams.some(stream => {
+ return stream.devices.some(device => device.scary);
+ });
+
+ this.updateWindowAttr("sharingbrowserwindow", hasBrowserWindow);
+ this.sharingBrowserWindow = hasBrowserWindow;
+ } else {
+ this.updateWindowAttr("sharingbrowserwindow");
+ this.sharingBrowserWindow = false;
+ }
+
+ // The label that's displayed when sharing a display followed a priority.
+ // The more "risky" we deem the display is for sharing, the higher priority.
+ // This gives us the following priorities, from highest to lowest.
+ //
+ // 1. Screen
+ // 2. Browser window
+ // 3. Other application window
+ // 4. Browser tab (unimplemented)
+ //
+ // The CSS for the indicator does the work of showing or hiding these labels
+ // for us, but we need to update the aria-labelledby attribute on the container
+ // of those labels to make it clearer for screenreaders which one the user cares
+ // about.
+ let displayShare = document.getElementById("display-share");
+ let labelledBy;
+ if (sharingScreen) {
+ labelledBy = "screen-share-info";
+ } else if (this.sharingBrowserWindow) {
+ labelledBy = "browser-window-share-info";
+ } else if (sharingWindow) {
+ labelledBy = "window-share-info";
+ }
+ displayShare.setAttribute("aria-labelledby", labelledBy);
+
+ if (window.windowState != window.STATE_MINIMIZED) {
+ // Resize and ensure the window position is correct
+ // (sizeToContent messes with our position).
+ let docElStyle = document.documentElement.style;
+ docElStyle.minWidth = docElStyle.maxWidth = "unset";
+ docElStyle.minHeight = docElStyle.maxHeight = "unset";
+ window.sizeToContent();
+
+ // On Linux GTK, the style of window we're using by default is resizable. We
+ // workaround this by setting explicit limits on the height and width of the
+ // window.
+ if (AppConstants.platform == "linux") {
+ let { width, height } = window.windowUtils.getBoundsWithoutFlushing(
+ document.documentElement
+ );
+
+ docElStyle.minWidth = docElStyle.maxWidth = `${width}px`;
+ docElStyle.minHeight = docElStyle.maxHeight = `${height}px`;
+ }
+
+ this.ensureOnScreen();
+
+ if (!this.positionCustomized) {
+ this.centerOnLatestBrowser();
+ }
+ }
+
+ this.updatingIndicatorState = false;
+ },
+
+ /**
+ * After the indicator has been updated, checks to see if it has expanded
+ * such that part of the indicator is now outside of the screen. If so,
+ * it then adjusts the position to put the entire indicator on screen.
+ */
+ ensureOnScreen() {
+ let desiredX = Math.max(window.screenX, screen.availLeft);
+ let maxX =
+ screen.availLeft +
+ screen.availWidth -
+ document.documentElement.clientWidth;
+ window.moveTo(Math.min(desiredX, maxX), window.screenY);
+ },
+
+ /**
+ * If the indicator is first being opened, we'll find the browser window
+ * associated with the most recent share, and pin the indicator to the
+ * very top of the content area.
+ */
+ centerOnLatestBrowser() {
+ let activeStreams = webrtcUI.getActiveStreams(
+ true /* camera */,
+ true /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+
+ if (!activeStreams.length) {
+ return;
+ }
+
+ let browser = activeStreams[activeStreams.length - 1].browser;
+ let browserWindow = browser.ownerGlobal;
+ let browserRect =
+ browserWindow.windowUtils.getBoundsWithoutFlushing(browser);
+
+ // This should be called in initialize right after we've just called
+ // updateIndicatorState. Since updateIndicatorState uses
+ // window.sizeToContent, the layout information should be up to date,
+ // and so the numbers that we get without flushing should be sufficient.
+ let { width: windowWidth } = window.windowUtils.getBoundsWithoutFlushing(
+ document.documentElement
+ );
+
+ window.moveTo(
+ browserWindow.mozInnerScreenX +
+ browserRect.left +
+ (browserRect.width - windowWidth) / 2,
+ browserWindow.mozInnerScreenY + browserRect.top
+ );
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "load": {
+ this.onLoad();
+ break;
+ }
+ case "unload": {
+ this.onUnload();
+ break;
+ }
+ case "click": {
+ this.onClick(event);
+ break;
+ }
+ case "change": {
+ this.onChange(event);
+ break;
+ }
+ case "MozUpdateWindowPos": {
+ if (!this.updatingIndicatorState) {
+ // The window moved while not updating the indicator state,
+ // so the user probably moved it.
+ this.positionCustomized = true;
+ }
+ break;
+ }
+ case "sizemodechange": {
+ if (window.windowState != window.STATE_MINIMIZED) {
+ this.updateIndicatorState();
+ }
+ break;
+ }
+ case "popupshowing": {
+ this.onPopupShowing(event);
+ break;
+ }
+ case "popuphiding": {
+ this.onPopupHiding(event);
+ break;
+ }
+ case "command": {
+ this.onCommand(event);
+ break;
+ }
+ case "DOMWindowClose":
+ case "close": {
+ this.onClose(event);
+ break;
+ }
+ }
+ },
+
+ onLoad() {
+ this.loaded = true;
+
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "win") {
+ this.statusBar = Cc["@mozilla.org/widget/systemstatusbar;1"].getService(
+ Ci.nsISystemStatusBar
+ );
+ }
+
+ this.updateIndicatorState();
+
+ window.addEventListener("click", this);
+ window.addEventListener("change", this);
+ window.addEventListener("sizemodechange", this);
+
+ // There are two ways that the dialog can close - either via the
+ // .close() window method, or via the OS. We handle both of those
+ // cases here.
+ window.addEventListener("DOMWindowClose", this);
+ window.addEventListener("close", this);
+
+ if (this.statusBar) {
+ // We only want these events for the system status bar menus.
+ window.addEventListener("popupshowing", this);
+ window.addEventListener("popuphiding", this);
+ window.addEventListener("command", this);
+ }
+
+ window.windowRoot.addEventListener("MozUpdateWindowPos", this);
+
+ // Alert accessibility implementations stuff just changed. We only need to do
+ // this initially, because changes after this will automatically fire alert
+ // events if things change materially.
+ let ev = new CustomEvent("AlertActive", {
+ bubbles: true,
+ cancelable: true,
+ });
+ document.documentElement.dispatchEvent(ev);
+
+ this.loaded = true;
+ },
+
+ onClose(event) {
+ // This event is fired from when the indicator window tries to be closed.
+ // If we preventDefault() the event, we are able to cancel that close
+ // attempt.
+ //
+ // We want to do that if we're not showing the global mute toggles
+ // and we're still sharing a camera or a microphone so that we can
+ // keep the status bar indicators present (since those status bar
+ // indicators are bound to this window).
+ if (
+ !this.showGlobalMuteToggles &&
+ (webrtcUI.showCameraIndicator || webrtcUI.showMicrophoneIndicator)
+ ) {
+ event.preventDefault();
+ this.setVisibility(false);
+ }
+
+ if (!this.isClosingInternally) {
+ // Something has tried to close the indicator, but it wasn't webrtcUI.
+ // This means we might still have some streams being shared. To protect
+ // the user from unknowingly sharing streams, we shut those streams
+ // down.
+ //
+ // This only includes the camera and microphone streams if the user
+ // has the global mute toggles enabled, since these toggles visually
+ // associate the indicator with those streams.
+ let activeStreams = webrtcUI.getActiveStreams(
+ this.showGlobalMuteToggles /* camera */,
+ this.showGlobalMuteToggles /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+ webrtcUI.stopSharingStreams(
+ activeStreams,
+ this.showGlobalMuteToggles /* camera */,
+ this.showGlobalMuteToggles /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+ }
+ },
+
+ onUnload() {
+ Services.ppmm.sharedData.set("WebRTC:GlobalCameraMute", false);
+ Services.ppmm.sharedData.set("WebRTC:GlobalMicrophoneMute", false);
+ Services.ppmm.sharedData.flush();
+
+ if (this.statusBar) {
+ for (let menu of this.statusBarMenus) {
+ this.statusBar.removeItem(menu);
+ }
+ }
+ },
+
+ onClick(event) {
+ switch (event.target.id) {
+ case "stop-sharing": {
+ let activeStreams = webrtcUI.getActiveStreams(
+ false /* camera */,
+ false /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+
+ if (!activeStreams.length) {
+ return;
+ }
+
+ // getActiveStreams is filtering for streams that have screen
+ // sharing, but those streams might _also_ be sharing other
+ // devices like camera or microphone. This is why we need to
+ // tell stopSharingStreams explicitly which device type we want
+ // to stop.
+ webrtcUI.stopSharingStreams(
+ activeStreams,
+ false /* camera */,
+ false /* microphone */,
+ true /* screen */,
+ true /* window */
+ );
+ break;
+ }
+ case "minimize": {
+ window.minimize();
+ break;
+ }
+ }
+ },
+
+ onChange(event) {
+ switch (event.target.id) {
+ case "microphone-mute-toggle": {
+ this.toggleMicrophoneMute(event.target);
+ break;
+ }
+ case "camera-mute-toggle": {
+ this.toggleCameraMute(event.target);
+ break;
+ }
+ }
+ },
+
+ onPopupShowing(event) {
+ if (this.eventIsForDeviceMenuPopup(event)) {
+ // When the indicator is hidden by default, opening the menu from the
+ // system tray _might_ cause the indicator to try to become visible again.
+ // We work around this by re-hiding it if it wasn't already visible.
+ if (document.documentElement.getAttribute("visible") != "true") {
+ let baseWin = window.docShell.treeOwner.QueryInterface(
+ Ci.nsIBaseWindow
+ );
+ baseWin.visibility = false;
+ }
+
+ showStreamSharingMenu(window, event, true);
+ }
+ },
+
+ onPopupHiding(event) {
+ if (!this.eventIsForDeviceMenuPopup(event)) {
+ return;
+ }
+
+ let menu = event.target;
+ while (menu.firstChild) {
+ menu.firstChild.remove();
+ }
+ },
+
+ onCommand(event) {
+ webrtcUI.showSharingDoorhanger(event.target.stream, event);
+ },
+
+ /**
+ * Returns true if an event was fired for one of the shared device
+ * menupopups.
+ *
+ * @param event (Event)
+ * The event to check.
+ * @returns True if the event was for one of the shared device
+ * menupopups.
+ */
+ eventIsForDeviceMenuPopup(event) {
+ let menupopup = event.target;
+ let type = menupopup.getAttribute("type");
+
+ return ["Camera", "Microphone", "Screen"].includes(type);
+ },
+
+ /**
+ * Mutes or unmutes the microphone globally based on the checked
+ * state of toggleEl. Also updates the tooltip of toggleEl once
+ * the state change is done.
+ *
+ * @param toggleEl (Element)
+ * The input[type="checkbox"] for toggling the microphone mute
+ * state.
+ */
+ toggleMicrophoneMute(toggleEl) {
+ Services.ppmm.sharedData.set(
+ "WebRTC:GlobalMicrophoneMute",
+ toggleEl.checked
+ );
+ Services.ppmm.sharedData.flush();
+ let l10nId =
+ "webrtc-microphone-" + (toggleEl.checked ? "muted" : "unmuted");
+ document.l10n.setAttributes(toggleEl, l10nId);
+ },
+
+ /**
+ * Mutes or unmutes the camera globally based on the checked
+ * state of toggleEl. Also updates the tooltip of toggleEl once
+ * the state change is done.
+ *
+ * @param toggleEl (Element)
+ * The input[type="checkbox"] for toggling the camera mute
+ * state.
+ */
+ toggleCameraMute(toggleEl) {
+ Services.ppmm.sharedData.set("WebRTC:GlobalCameraMute", toggleEl.checked);
+ Services.ppmm.sharedData.flush();
+ let l10nId = "webrtc-camera-" + (toggleEl.checked ? "muted" : "unmuted");
+ document.l10n.setAttributes(toggleEl, l10nId);
+ },
+
+ /**
+ * Updates an attribute on the <window> element.
+ *
+ * @param attr (String)
+ * The name of the attribute to update.
+ * @param value (String?)
+ * A string to set the attribute to. If the value is false-y,
+ * the attribute is removed.
+ */
+ updateWindowAttr(attr, value) {
+ let docEl = document.documentElement;
+ if (value) {
+ docEl.setAttribute(attr, "true");
+ } else {
+ docEl.removeAttribute(attr);
+ }
+ },
+
+ /**
+ * See the documentation on the script global closingInternally() function.
+ */
+ closingInternally() {
+ this.isClosingInternally = true;
+ },
+};
+
+WebRTCIndicator.init();