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;