summaryrefslogtreecommitdiffstats
path: root/application/basilisk/modules/webrtcUI.jsm
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-02 03:32:58 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-02 03:32:58 -0500
commite72ef92b5bdc43cd2584198e2e54e951b70299e8 (patch)
tree01ceb4a897c33eca9e7ccf2bc3aefbe530169fe5 /application/basilisk/modules/webrtcUI.jsm
parent0d19b77d3eaa5b8d837bf52c19759e68e42a1c4c (diff)
downloadUXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.gz
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.lz
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.tar.xz
UXP-e72ef92b5bdc43cd2584198e2e54e951b70299e8.zip
Add Basilisk
Diffstat (limited to 'application/basilisk/modules/webrtcUI.jsm')
-rw-r--r--application/basilisk/modules/webrtcUI.jsm1087
1 files changed, 1087 insertions, 0 deletions
diff --git a/application/basilisk/modules/webrtcUI.jsm b/application/basilisk/modules/webrtcUI.jsm
new file mode 100644
index 000000000..b43d2108f
--- /dev/null
+++ b/application/basilisk/modules/webrtcUI.jsm
@@ -0,0 +1,1087 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["webrtcUI"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
+ "resource:///modules/SitePermissions.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
+ return Services.strings.createBundle("chrome://branding/locale/brand.properties");
+});
+
+this.webrtcUI = {
+ peerConnectionBlockers: new Set(),
+ emitter: new EventEmitter(),
+
+ init() {
+ Services.obs.addObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished", false);
+
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("webrtc:UpdatingIndicators", this);
+ ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
+ ppmm.addMessageListener("child-process-shutdown", this);
+
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("rtcpeer:Request", this);
+ mm.addMessageListener("rtcpeer:CancelRequest", this);
+ mm.addMessageListener("webrtc:Request", this);
+ mm.addMessageListener("webrtc:StopRecording", this);
+ mm.addMessageListener("webrtc:CancelRequest", this);
+ mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
+
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.removeMessageListener("webrtc:UpdatingIndicators", this);
+ ppmm.removeMessageListener("webrtc:UpdateGlobalIndicators", this);
+
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ mm.removeMessageListener("rtcpeer:Request", this);
+ mm.removeMessageListener("rtcpeer:CancelRequest", this);
+ mm.removeMessageListener("webrtc:Request", this);
+ mm.removeMessageListener("webrtc:StopRecording", this);
+ mm.removeMessageListener("webrtc:CancelRequest", this);
+ mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
+
+ if (gIndicatorWindow) {
+ gIndicatorWindow.close();
+ gIndicatorWindow = null;
+ }
+ },
+
+ processIndicators: new Map(),
+ activePerms: new Map(),
+
+ get showGlobalIndicator() {
+ for (let [, indicators] of this.processIndicators) {
+ if (indicators.showGlobalIndicator)
+ return true;
+ }
+ return false;
+ },
+
+ get showCameraIndicator() {
+ for (let [, indicators] of this.processIndicators) {
+ if (indicators.showCameraIndicator)
+ return true;
+ }
+ return false;
+ },
+
+ get showMicrophoneIndicator() {
+ for (let [, indicators] of this.processIndicators) {
+ if (indicators.showMicrophoneIndicator)
+ return true;
+ }
+ return false;
+ },
+
+ get showScreenSharingIndicator() {
+ let list = [""];
+ for (let [, indicators] of this.processIndicators) {
+ if (indicators.showScreenSharingIndicator)
+ list.push(indicators.showScreenSharingIndicator);
+ }
+
+ let precedence =
+ ["Screen", "Window", "Application", "Browser", ""];
+
+ list.sort((a, b) => {
+ return precedence.indexOf(a) -
+ precedence.indexOf(b);
+});
+
+ return list[0];
+ },
+
+ _streams: [],
+ // The boolean parameters indicate which streams should be included in the result.
+ getActiveStreams(aCamera, aMicrophone, aScreen) {
+ return webrtcUI._streams.filter(aStream => {
+ let state = aStream.state;
+ return aCamera && state.camera ||
+ aMicrophone && state.microphone ||
+ aScreen && state.screen;
+ }).map(aStream => {
+ let state = aStream.state;
+ let types = {camera: state.camera, microphone: state.microphone,
+ screen: state.screen};
+ let browser = aStream.browser;
+ let browserWindow = browser.ownerGlobal;
+ let tab = browserWindow.gBrowser &&
+ browserWindow.gBrowser.getTabForBrowser(browser);
+ return {uri: state.documentURI, tab, browser, types};
+ });
+ },
+
+ swapBrowserForNotification(aOldBrowser, aNewBrowser) {
+ for (let stream of this._streams) {
+ if (stream.browser == aOldBrowser)
+ stream.browser = aNewBrowser;
+ }
+ },
+
+ forgetActivePermissionsFromBrowser(aBrowser) {
+ webrtcUI.activePerms.delete(aBrowser.outerWindowID);
+ },
+
+ forgetStreamsFromBrowser(aBrowser) {
+ this._streams = this._streams.filter(stream => stream.browser != aBrowser);
+ webrtcUI.forgetActivePermissionsFromBrowser(aBrowser);
+ },
+
+ showSharingDoorhanger(aActiveStream) {
+ let browserWindow = aActiveStream.browser.ownerGlobal;
+ if (aActiveStream.tab) {
+ browserWindow.gBrowser.selectedTab = aActiveStream.tab;
+ } else {
+ aActiveStream.browser.focus();
+ }
+ browserWindow.focus();
+ let identityBox = browserWindow.document.getElementById("identity-box");
+#ifdef XP_MACOSX
+ if (!Services.focus.activeWindow) {
+ browserWindow.addEventListener("activate", function onActivate() {
+ browserWindow.removeEventListener("activate", onActivate);
+ Services.tm.mainThread.dispatch(function() {
+ identityBox.click();
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsIMacDockSupport)
+ .activateApplication(true);
+ return;
+ }
+#endif
+ identityBox.click();
+ },
+
+ updateWarningLabel(aMenuList) {
+ let type = aMenuList.selectedItem.getAttribute("devicetype");
+ let document = aMenuList.ownerDocument;
+ document.getElementById("webRTC-all-windows-shared").hidden = type != "Screen";
+ },
+
+ // Add-ons can override stock permission behavior by doing:
+ //
+ // webrtcUI.addPeerConnectionBlocker(function(aParams) {
+ // // new permission checking logic
+ // }));
+ //
+ // The blocking function receives an object with origin, callID, and windowID
+ // parameters. If it returns the string "deny" or a Promise that resolves
+ // to "deny", the connection is immediately blocked. With any other return
+ // value (though the string "allow" is suggested for consistency), control
+ // is passed to other registered blockers. If no registered blockers block
+ // the connection (or of course if there are no registered blockers), then
+ // the connection is allowed.
+ //
+ // Add-ons may also use webrtcUI.on/off to listen to events without
+ // blocking anything:
+ // peer-request-allowed is emitted when a new peer connection is
+ // established (and not blocked).
+ // peer-request-blocked is emitted when a peer connection request is
+ // blocked by some blocking connection handler.
+ // peer-request-cancel is emitted when a peer-request connection request
+ // is canceled. (This would typically be used in
+ // conjunction with a blocking handler to cancel
+ // a user prompt or other work done by the handler)
+ addPeerConnectionBlocker(aCallback) {
+ this.peerConnectionBlockers.add(aCallback);
+ },
+
+ removePeerConnectionBlocker(aCallback) {
+ this.peerConnectionBlockers.delete(aCallback);
+ },
+
+ on(...args) {
+ return this.emitter.on(...args);
+ },
+
+ off(...args) {
+ return this.emitter.off(...args);
+ },
+
+ receiveMessage(aMessage) {
+ switch (aMessage.name) {
+
+ case "rtcpeer:Request": {
+ let params = Object.freeze(Object.assign({
+ origin: aMessage.target.contentPrincipal.origin
+ }, aMessage.data));
+
+ let blockers = Array.from(this.peerConnectionBlockers);
+
+ Task.spawn(function*() {
+ for (let blocker of blockers) {
+ try {
+ let result = yield blocker(params);
+ if (result == "deny") {
+ return false;
+ }
+ } catch (err) {
+ Cu.reportError(`error in PeerConnection blocker: ${err.message}`);
+ }
+ }
+ return true;
+ }).then(decision => {
+ let message;
+ if (decision) {
+ this.emitter.emit("peer-request-allowed", params);
+ message = "rtcpeer:Allow";
+ } else {
+ this.emitter.emit("peer-request-blocked", params);
+ message = "rtcpeer:Deny";
+ }
+
+ aMessage.target.messageManager.sendAsyncMessage(message, {
+ callID: params.callID,
+ windowID: params.windowID,
+ });
+ });
+ break;
+ }
+ case "rtcpeer:CancelRequest": {
+ let params = Object.freeze({
+ origin: aMessage.target.contentPrincipal.origin,
+ callID: aMessage.data
+ });
+ this.emitter.emit("peer-request-cancel", params);
+ break;
+ }
+ case "webrtc:Request":
+ prompt(aMessage.target, aMessage.data);
+ break;
+ case "webrtc:StopRecording":
+ stopRecording(aMessage.target, aMessage.data);
+ break;
+ case "webrtc:CancelRequest":
+ removePrompt(aMessage.target, aMessage.data);
+ break;
+ case "webrtc:UpdatingIndicators":
+ webrtcUI._streams = [];
+ break;
+ case "webrtc:UpdateGlobalIndicators":
+ updateIndicators(aMessage.data, aMessage.target);
+ break;
+ case "webrtc:UpdateBrowserIndicators":
+ let id = aMessage.data.windowId;
+ let index;
+ for (index = 0; index < webrtcUI._streams.length; ++index) {
+ if (webrtcUI._streams[index].state.windowId == id)
+ break;
+ }
+ // If there's no documentURI, the update is actually a removal of the
+ // stream, triggered by the recording-window-ended notification.
+ if (!aMessage.data.documentURI && index < webrtcUI._streams.length)
+ webrtcUI._streams.splice(index, 1);
+ else
+ webrtcUI._streams[index] = {browser: aMessage.target, state: aMessage.data};
+ let tabbrowser = aMessage.target.ownerGlobal.gBrowser;
+ if (tabbrowser)
+ tabbrowser.setBrowserSharing(aMessage.target, aMessage.data);
+ break;
+ case "child-process-shutdown":
+ webrtcUI.processIndicators.delete(aMessage.target);
+ updateIndicators(null, null);
+ break;
+ }
+ }
+};
+
+function getBrowserForWindow(aContentWindow) {
+ return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+}
+
+function denyRequest(aBrowser, aRequest) {
+ aBrowser.messageManager.sendAsyncMessage("webrtc:Deny",
+ {callID: aRequest.callID,
+ windowID: aRequest.windowID});
+}
+
+function getHost(uri, href) {
+ let host;
+ try {
+ if (!uri) {
+ uri = Services.io.newURI(href);
+ }
+ host = uri.host;
+ } catch (ex) {}
+ if (!host) {
+ if (uri && uri.scheme.toLowerCase() == "about") {
+ // For about URIs, just use the full spec, without any #hash parts.
+ host = uri.specIgnoringRef;
+ } else {
+ // This is unfortunate, but we should display *something*...
+ const kBundleURI = "chrome://browser/locale/browser.properties";
+ let bundle = Services.strings.createBundle(kBundleURI);
+ host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
+ }
+ }
+ return host;
+}
+
+function stopRecording(aBrowser, aRequest) {
+ let outerWindowID = aBrowser.outerWindowID;
+
+ if (!webrtcUI.activePerms.has(outerWindowID)) {
+ return;
+ }
+
+ if (!aRequest.rawID) {
+ webrtcUI.activePerms.delete(outerWindowID);
+ } else {
+ let set = webrtcUI.activePerms.get(outerWindowID);
+ set.delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID);
+ }
+}
+
+function prompt(aBrowser, aRequest) {
+ let { audioDevices, videoDevices, sharingScreen, sharingAudio,
+ requestTypes } = aRequest;
+
+ // If the user has already denied access once in this tab,
+ // deny again without even showing the notification icon.
+ if ((audioDevices.length && SitePermissions
+ .get(null, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
+ (videoDevices.length && SitePermissions
+ .get(null, sharingScreen ? "screen" : "camera", aBrowser).state == SitePermissions.BLOCK)) {
+ denyRequest(aBrowser, aRequest);
+ return;
+ }
+
+ // Tell the browser to refresh the identity block display in case there
+ // are expired permission states.
+ aBrowser.dispatchEvent(new aBrowser.ownerGlobal
+ .CustomEvent("PermissionStateChange"));
+
+ let uri = Services.io.newURI(aRequest.documentURI);
+ let host = getHost(uri);
+ let chromeDoc = aBrowser.ownerDocument;
+ let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
+
+ // Mind the order, because for simplicity we're iterating over the list using
+ // "includes()". This allows the rotation of string identifiers. We list the
+ // full identifiers here so they can be cross-referenced more easily.
+ let joinedRequestTypes = requestTypes.join("And");
+ let stringId = [
+ // Individual request types first.
+ "getUserMedia.shareCamera2.message",
+ "getUserMedia.shareMicrophone2.message",
+ "getUserMedia.shareScreen3.message",
+ "getUserMedia.shareAudioCapture2.message",
+ // Combinations of the above request types last.
+ "getUserMedia.shareCameraAndMicrophone2.message",
+ "getUserMedia.shareCameraAndAudioCapture2.message",
+ "getUserMedia.shareScreenAndMicrophone3.message",
+ "getUserMedia.shareScreenAndAudioCapture3.message",
+ ].find(id => id.includes(joinedRequestTypes));
+
+ let message = stringBundle.getFormattedString(stringId, [host]);
+
+ let notification; // Used by action callbacks.
+ let mainAction = {
+ label: stringBundle.getString("getUserMedia.allow.label"),
+ accessKey: stringBundle.getString("getUserMedia.allow.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() {}
+ };
+
+ let secondaryActions = [
+ {
+ label: stringBundle.getString("getUserMedia.dontAllow.label"),
+ accessKey: stringBundle.getString("getUserMedia.dontAllow.accesskey"),
+ callback(aState) {
+ denyRequest(notification.browser, aRequest);
+ let scope = SitePermissions.SCOPE_TEMPORARY;
+ if (aState && aState.checkboxChecked) {
+ scope = SitePermissions.SCOPE_PERSISTENT;
+ }
+ if (audioDevices.length)
+ SitePermissions.set(uri, "microphone",
+ SitePermissions.BLOCK, scope, notification.browser);
+ if (videoDevices.length)
+ SitePermissions.set(uri, sharingScreen ? "screen" : "camera",
+ SitePermissions.BLOCK, scope, notification.browser);
+ }
+ }
+ ];
+
+ let productName = gBrandBundle.GetStringFromName("brandShortName");
+
+ let options = {
+ persistent: true,
+ hideClose: !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton"),
+ eventCallback(aTopic, aNewBrowser) {
+ if (aTopic == "swapping")
+ return true;
+
+ let doc = this.browser.ownerDocument;
+
+ // Clean-up video streams of screensharing previews.
+ if ((aTopic == "dismissed" || aTopic == "removed") &&
+ requestTypes.includes("Screen")) {
+ let video = doc.getElementById("webRTC-previewVideo");
+ video.deviceId = undefined;
+ 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 != "showing")
+ return false;
+
+ // BLOCK is handled immediately by MediaManager if it has been set
+ // persistently in the permission manager. If it has been set on the tab,
+ // 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 (aRequest.secure) {
+ let micAllowed =
+ SitePermissions.get(uri, "microphone").state == SitePermissions.ALLOW;
+ let camAllowed =
+ SitePermissions.get(uri, "camera").state == SitePermissions.ALLOW;
+
+ let perms = Services.perms;
+ let mediaManagerPerm =
+ perms.testExactPermission(uri, "MediaManagerVideo");
+ if (mediaManagerPerm) {
+ perms.remove(uri, "MediaManagerVideo");
+ }
+
+ // Screen sharing shouldn't follow the camera permissions.
+ if (videoDevices.length && sharingScreen)
+ camAllowed = false;
+
+ let activeCamera;
+ let activeMic;
+
+ for (let device of videoDevices) {
+ let set = webrtcUI.activePerms.get(aBrowser.outerWindowID);
+ if (set && set.has(aRequest.windowID + device.mediaSource + device.id)) {
+ activeCamera = device;
+ break;
+ }
+ }
+
+ for (let device of audioDevices) {
+ let set = webrtcUI.activePerms.get(aBrowser.outerWindowID);
+ if (set && set.has(aRequest.windowID + device.mediaSource + device.id)) {
+ activeMic = device;
+ break;
+ }
+ }
+
+ if ((!audioDevices.length || micAllowed || activeMic) &&
+ (!videoDevices.length || camAllowed || activeCamera)) {
+ let allowedDevices = [];
+ if (videoDevices.length) {
+ allowedDevices.push((activeCamera || videoDevices[0]).deviceIndex);
+ Services.perms.add(uri, "MediaManagerVideo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_SESSION);
+ }
+ if (audioDevices.length) {
+ allowedDevices.push((activeMic || audioDevices[0]).deviceIndex);
+ }
+
+ // Remember on which URIs we found persistent permissions so that we
+ // can remove them if the user clicks 'Stop Sharing'. There's no
+ // other way for the stop sharing code to know the hostnames of frames
+ // using devices until bug 1066082 is fixed.
+ let browser = this.browser;
+ browser._devicePermissionURIs = browser._devicePermissionURIs || [];
+ browser._devicePermissionURIs.push(uri);
+
+ let mm = browser.messageManager;
+ mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
+ windowID: aRequest.windowID,
+ devices: allowedDevices});
+ this.remove();
+ return true;
+ }
+ }
+
+ function listDevices(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 explicitly.
+ menupopup.parentNode.removeAttribute("value");
+
+ for (let device of devices)
+ addDeviceToList(menupopup, device.name, device.deviceIndex);
+ }
+
+ function listScreenShareDevices(menupopup, devices) {
+ while (menupopup.lastChild)
+ menupopup.removeChild(menupopup.lastChild);
+
+ let type = devices[0].mediaSource;
+ let typeName = type.charAt(0).toUpperCase() + type.substr(1);
+
+ let label = doc.getElementById("webRTC-selectWindow-label");
+ let gumStringId = "getUserMedia.select" + typeName;
+ label.setAttribute("value",
+ stringBundle.getString(gumStringId + ".label"));
+ label.setAttribute("accesskey",
+ stringBundle.getString(gumStringId + ".accesskey"));
+
+ // "No <type>" is the default because we can't pick a
+ // 'default' window to share.
+ addDeviceToList(menupopup,
+ stringBundle.getString("getUserMedia.no" + typeName + ".label"),
+ "-1");
+ menupopup.appendChild(doc.createElement("menuseparator"));
+
+ // Build the list of 'devices'.
+ let monitorIndex = 1;
+ for (let i = 0; i < devices.length; ++i) {
+ let device = devices[i];
+
+ let name;
+ // Building screen list from available screens.
+ if (type == "screen") {
+ if (device.name == "Primary Monitor") {
+ name = stringBundle.getString("getUserMedia.shareEntireScreen.label");
+ } else {
+ name = stringBundle.getFormattedString("getUserMedia.shareMonitor.label",
+ [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>
+ let sepIndex = name.indexOf("\x1e");
+ let count = name.slice(0, sepIndex);
+ let sawcStringId = "getUserMedia.shareApplicationWindowCount.label";
+ name = PluralForm.get(parseInt(count), stringBundle.getString(sawcStringId))
+ .replace("#1", name.slice(sepIndex + 1))
+ .replace("#2", count);
+ }
+ }
+ let item = addDeviceToList(menupopup, name, i, typeName);
+ item.deviceId = device.id;
+ 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 => {
+ let video = doc.getElementById("webRTC-previewVideo");
+ if (video.stream) {
+ video.stream.getTracks().forEach(t => t.stop());
+ video.stream = null;
+ }
+
+ let deviceId = event.target.deviceId;
+ if (deviceId == undefined) {
+ doc.getElementById("webRTC-preview").hidden = true;
+ video.src = null;
+ return;
+ }
+
+ let scary = event.target.scary;
+ let warning = doc.getElementById("webRTC-previewWarning");
+ warning.hidden = !scary;
+ let chromeWin = doc.defaultView;
+ if (scary) {
+ warning.hidden = false;
+ let string;
+ let bundle = chromeWin.gNavigatorBundle;
+
+ let learnMoreText =
+ bundle.getString("getUserMedia.shareScreen.learnMoreLabel");
+ let baseURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let learnMore =
+ "<label class='text-link' href='" + baseURL + "screenshare-safety'>" +
+ learnMoreText + "</label>";
+
+ if (type == "screen") {
+ string = bundle.getFormattedString("getUserMedia.shareScreenWarning.message",
+ [learnMore]);
+ } else {
+ let brand =
+ doc.getElementById("bundle_brand").getString("brandShortName");
+ string = bundle.getFormattedString("getUserMedia.shareBasiliskWarning.message",
+ [brand, learnMore]);
+ }
+ warning.innerHTML = string;
+ }
+
+ let perms = Services.perms;
+ let chromeUri = Services.io.newURI(doc.documentURI);
+ perms.add(chromeUri, "MediaManagerVideo", perms.ALLOW_ACTION,
+ perms.EXPIRE_SESSION);
+
+ video.deviceId = deviceId;
+ let constraints = { video: { mediaSource: type, 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.src = chromeWin.URL.createObjectURL(stream);
+ video.stream = stream;
+ doc.getElementById("webRTC-preview").hidden = false;
+ video.onloadedmetadata = function(e) {
+ video.play();
+ };
+ });
+ };
+ menupopup.addEventListener("command", menupopup._commandEventListener);
+ }
+
+ function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("value", deviceIndex);
+ menuitem.setAttribute("label", deviceName);
+ menuitem.setAttribute("tooltiptext", deviceName);
+ if (type)
+ menuitem.setAttribute("devicetype", type);
+ menupopup.appendChild(menuitem);
+ return menuitem;
+ }
+
+ doc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length || sharingScreen;
+ doc.getElementById("webRTC-selectWindowOrScreen").hidden = !sharingScreen || !videoDevices.length;
+ doc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length || sharingAudio;
+
+ let camMenupopup = doc.getElementById("webRTC-selectCamera-menupopup");
+ let windowMenupopup = doc.getElementById("webRTC-selectWindow-menupopup");
+ let micMenupopup = doc.getElementById("webRTC-selectMicrophone-menupopup");
+ if (sharingScreen)
+ listScreenShareDevices(windowMenupopup, videoDevices);
+ else
+ listDevices(camMenupopup, videoDevices);
+
+ if (!sharingAudio)
+ listDevices(micMenupopup, audioDevices);
+
+ this.mainAction.callback = function(aState) {
+ let remember = aState && aState.checkboxChecked;
+ let allowedDevices = [];
+ let perms = Services.perms;
+ if (videoDevices.length) {
+ let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-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.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
+ perms.EXPIRE_SESSION);
+ if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set());
+ }
+
+ for (let device of videoDevices) {
+ if (device.deviceIndex == videoDeviceIndex) {
+ webrtcUI.activePerms.get(aBrowser.outerWindowID)
+ .add(aRequest.windowID + device.mediaSource + device.id);
+ break;
+ }
+ }
+ if (remember)
+ SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+ }
+ }
+ if (audioDevices.length) {
+ if (!sharingAudio) {
+ let audioDeviceIndex = doc.getElementById("webRTC-selectMicrophone-menulist").value;
+ let allowMic = audioDeviceIndex != "-1";
+ if (allowMic) {
+ allowedDevices.push(audioDeviceIndex);
+ if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set());
+ }
+
+ for (let device of audioDevices) {
+ if (device.deviceIndex == audioDeviceIndex) {
+ webrtcUI.activePerms.get(aBrowser.outerWindowID)
+ .add(aRequest.windowID + device.mediaSource + device.id);
+ break;
+ }
+ }
+ if (remember)
+ SitePermissions.set(uri, "microphone", SitePermissions.ALLOW);
+ }
+ } else {
+ // Only one device possible for audio capture.
+ allowedDevices.push(0);
+ }
+ }
+
+ if (!allowedDevices.length) {
+ denyRequest(notification.browser, aRequest);
+ return;
+ }
+
+ if (remember) {
+ // Remember on which URIs we set persistent permissions so that we
+ // can remove them if the user clicks 'Stop Sharing'.
+ aBrowser._devicePermissionURIs = aBrowser._devicePermissionURIs || [];
+ aBrowser._devicePermissionURIs.push(uri);
+ }
+
+ let mm = notification.browser.messageManager;
+ mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
+ windowID: aRequest.windowID,
+ devices: allowedDevices});
+ };
+ return false;
+ }
+ };
+
+ // Don't offer "always remember" action in PB mode.
+ if (!PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
+
+ // 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).
+ let reasonForNoPermanentAllow = "";
+ if (sharingScreen) {
+ reasonForNoPermanentAllow = "getUserMedia.reasonForNoPermanentAllow.screen2";
+ } else if (sharingAudio) {
+ reasonForNoPermanentAllow = "getUserMedia.reasonForNoPermanentAllow.audio";
+ } else if (!aRequest.secure) {
+ reasonForNoPermanentAllow = "getUserMedia.reasonForNoPermanentAllow.insecure";
+ }
+
+ options.checkbox = {
+ label: stringBundle.getString("getUserMedia.remember"),
+ checkedState: reasonForNoPermanentAllow ? {
+ disableMainAction: true,
+ warningLabel: stringBundle.getFormattedString(reasonForNoPermanentAllow,
+ [productName])
+ } : undefined,
+ };
+ }
+
+ let iconType = "Devices";
+ if (requestTypes.length == 1 && (requestTypes[0] == "Microphone" ||
+ requestTypes[0] == "AudioCapture"))
+ iconType = "Microphone";
+ if (requestTypes.includes("Screen"))
+ iconType = "Screen";
+ let anchorId = "webRTC-share" + iconType + "-notification-icon";
+
+ let iconClass = iconType.toLowerCase();
+ if (iconClass == "devices")
+ iconClass = "camera";
+ options.popupIconClass = iconClass + "-icon";
+
+ notification =
+ chromeDoc.defaultView
+ .PopupNotifications
+ .show(aBrowser, "webRTC-shareDevices", message,
+ anchorId, mainAction, secondaryActions,
+ options);
+ notification.callID = aRequest.callID;
+}
+
+function removePrompt(aBrowser, aCallId) {
+ let chromeWin = aBrowser.ownerGlobal;
+ let notification =
+ chromeWin.PopupNotifications.getNotification("webRTC-shareDevices", aBrowser);
+ if (notification && notification.callID == aCallId)
+ notification.remove();
+}
+
+function getGlobalIndicator() {
+#ifndef XP_MACOSX
+ const INDICATOR_CHROME_URI = "chrome://browser/content/webrtcIndicator.xul";
+ const features = "chrome,dialog=yes,titlebar=no,popup=yes";
+
+ return Services.ww.openWindow(null, INDICATOR_CHROME_URI, "_blank", features, []);
+#endif
+
+ let indicator = {
+ _camera: null,
+ _microphone: null,
+ _screen: null,
+
+ _hiddenDoc: Cc["@mozilla.org/appshell/appShellService;1"]
+ .getService(Ci.nsIAppShellService)
+ .hiddenDOMWindow.document,
+ _statusBar: Cc["@mozilla.org/widget/macsystemstatusbar;1"]
+ .getService(Ci.nsISystemStatusBar),
+
+ _command(aEvent) {
+ webrtcUI.showSharingDoorhanger(aEvent.target.stream);
+ },
+
+ _popupShowing(aEvent) {
+ let type = this.getAttribute("type");
+ let activeStreams;
+ if (type == "Camera") {
+ activeStreams = webrtcUI.getActiveStreams(true, false, false);
+ } else if (type == "Microphone") {
+ activeStreams = webrtcUI.getActiveStreams(false, true, false);
+ } else if (type == "Screen") {
+ activeStreams = webrtcUI.getActiveStreams(false, false, true);
+ type = webrtcUI.showScreenSharingIndicator;
+ }
+
+ let bundle =
+ Services.strings.createBundle("chrome://browser/locale/webrtcIndicator.properties");
+
+ if (activeStreams.length == 1) {
+ let stream = activeStreams[0];
+
+ let menuitem = this.ownerDocument.createElement("menuitem");
+ let labelId = "webrtcIndicator.sharing" + type + "With.menuitem";
+ let label = stream.browser.contentTitle || stream.uri;
+ menuitem.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
+ menuitem.setAttribute("disabled", "true");
+ this.appendChild(menuitem);
+
+ menuitem = this.ownerDocument.createElement("menuitem");
+ menuitem.setAttribute("label",
+ bundle.GetStringFromName("webrtcIndicator.controlSharing.menuitem"));
+ menuitem.stream = stream;
+ menuitem.addEventListener("command", indicator._command);
+
+ this.appendChild(menuitem);
+ return true;
+ }
+
+ // We show a different menu when there are several active streams.
+ let menuitem = this.ownerDocument.createElement("menuitem");
+ let labelId = "webrtcIndicator.sharing" + type + "WithNTabs.menuitem";
+ let count = activeStreams.length;
+ let label = PluralForm.get(count, bundle.GetStringFromName(labelId)).replace("#1", count);
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute("disabled", "true");
+ this.appendChild(menuitem);
+
+ for (let stream of activeStreams) {
+ let item = this.ownerDocument.createElement("menuitem");
+ labelId = "webrtcIndicator.controlSharingOn.menuitem";
+ label = stream.browser.contentTitle || stream.uri;
+ item.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
+ item.stream = stream;
+ item.addEventListener("command", indicator._command);
+ this.appendChild(item);
+ }
+
+ return true;
+ },
+
+ _popupHiding(aEvent) {
+ while (this.firstChild)
+ this.firstChild.remove();
+ },
+
+ _setIndicatorState(aName, aState) {
+ let field = "_" + aName.toLowerCase();
+ if (aState && !this[field]) {
+ let menu = this._hiddenDoc.createElement("menu");
+ menu.setAttribute("id", "webRTC-sharing" + aName + "-menu");
+
+ // The CSS will only be applied if the menu is actually inserted in the DOM.
+ this._hiddenDoc.documentElement.appendChild(menu);
+
+ this._statusBar.addItem(menu);
+
+ let menupopup = this._hiddenDoc.createElement("menupopup");
+ menupopup.setAttribute("type", aName);
+ menupopup.addEventListener("popupshowing", this._popupShowing);
+ menupopup.addEventListener("popuphiding", this._popupHiding);
+ menupopup.addEventListener("command", this._command);
+ menu.appendChild(menupopup);
+
+ this[field] = menu;
+ } else if (this[field] && !aState) {
+ this._statusBar.removeItem(this[field]);
+ this[field].remove();
+ this[field] = null
+ }
+ },
+ updateIndicatorState() {
+ this._setIndicatorState("Camera", webrtcUI.showCameraIndicator);
+ this._setIndicatorState("Microphone", webrtcUI.showMicrophoneIndicator);
+ this._setIndicatorState("Screen", webrtcUI.showScreenSharingIndicator);
+ },
+ close() {
+ this._setIndicatorState("Camera", false);
+ this._setIndicatorState("Microphone", false);
+ this._setIndicatorState("Screen", false);
+ }
+ };
+
+ indicator.updateIndicatorState();
+ return indicator;
+}
+
+function onTabSharingMenuPopupShowing(e) {
+ let streams = webrtcUI.getActiveStreams(true, true, true);
+ for (let streamInfo of streams) {
+ let stringName = "getUserMedia.sharingMenu";
+ let types = streamInfo.types;
+ if (types.camera)
+ stringName += "Camera";
+ if (types.microphone)
+ stringName += "Microphone";
+ if (types.screen)
+ stringName += types.screen;
+
+ let doc = e.target.ownerDocument;
+ let bundle = doc.defaultView.gNavigatorBundle;
+
+ let origin = getHost(null, streamInfo.uri);
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("label", bundle.getFormattedString(stringName, [origin]));
+ menuitem.stream = streamInfo;
+ menuitem.addEventListener("command", onTabSharingMenuPopupCommand);
+ e.target.appendChild(menuitem);
+ }
+}
+
+function onTabSharingMenuPopupHiding(e) {
+ while (this.lastChild)
+ this.lastChild.remove();
+}
+
+function onTabSharingMenuPopupCommand(e) {
+ webrtcUI.showSharingDoorhanger(e.target.stream);
+}
+
+function showOrCreateMenuForWindow(aWindow) {
+ let document = aWindow.document;
+ let menu = document.getElementById("tabSharingMenu");
+ if (!menu) {
+ let stringBundle = aWindow.gNavigatorBundle;
+ menu = document.createElement("menu");
+ menu.id = "tabSharingMenu";
+ let labelStringId = "getUserMedia.sharingMenu.label";
+ menu.setAttribute("label", stringBundle.getString(labelStringId));
+
+ let container, insertionPoint;
+#ifdef XP_MACOSX
+ container = document.getElementById("windowPopup");
+ insertionPoint = document.getElementById("sep-window-list");
+ let separator = document.createElement("menuseparator");
+ separator.id = "tabSharingSeparator";
+ container.insertBefore(separator, insertionPoint);
+#else
+ let accesskeyStringId = "getUserMedia.sharingMenu.accesskey";
+ menu.setAttribute("accesskey", stringBundle.getString(accesskeyStringId));
+ container = document.getElementById("main-menubar");
+ insertionPoint = document.getElementById("helpMenu");
+#endif
+ let popup = document.createElement("menupopup");
+ popup.id = "tabSharingMenuPopup";
+ popup.addEventListener("popupshowing", onTabSharingMenuPopupShowing);
+ popup.addEventListener("popuphiding", onTabSharingMenuPopupHiding);
+ menu.appendChild(popup);
+ container.insertBefore(menu, insertionPoint);
+ } else {
+ menu.hidden = false;
+#ifdef XP_MACOSX
+ document.getElementById("tabSharingSeparator").hidden = false;
+#endif
+ }
+}
+
+function maybeAddMenuIndicator(window) {
+ if (webrtcUI.showGlobalIndicator) {
+ showOrCreateMenuForWindow(window);
+ }
+}
+
+var gIndicatorWindow = null;
+
+function updateIndicators(data, target) {
+ if (data) {
+ // the global indicators specific to this process
+ let indicators;
+ if (webrtcUI.processIndicators.has(target)) {
+ indicators = webrtcUI.processIndicators.get(target);
+ } else {
+ indicators = {};
+ webrtcUI.processIndicators.set(target, indicators);
+ }
+
+ indicators.showGlobalIndicator = data.showGlobalIndicator;
+ indicators.showCameraIndicator = data.showCameraIndicator;
+ indicators.showMicrophoneIndicator = data.showMicrophoneIndicator;
+ indicators.showScreenSharingIndicator = data.showScreenSharingIndicator;
+ }
+
+ let browserWindowEnum = Services.wm.getEnumerator("navigator:browser");
+ while (browserWindowEnum.hasMoreElements()) {
+ let chromeWin = browserWindowEnum.getNext();
+ if (webrtcUI.showGlobalIndicator) {
+ showOrCreateMenuForWindow(chromeWin);
+ } else {
+ let doc = chromeWin.document;
+ let existingMenu = doc.getElementById("tabSharingMenu");
+ if (existingMenu) {
+ existingMenu.hidden = true;
+ }
+#ifdef XP_MACOSX
+ let separator = doc.getElementById("tabSharingSeparator");
+ if (separator) {
+ separator.hidden = true;
+ }
+#endif
+ }
+ }
+
+ if (webrtcUI.showGlobalIndicator) {
+ if (!gIndicatorWindow)
+ gIndicatorWindow = getGlobalIndicator();
+ else
+ gIndicatorWindow.updateIndicatorState();
+ } else if (gIndicatorWindow) {
+ gIndicatorWindow.close();
+ gIndicatorWindow = null;
+ }
+}