summaryrefslogtreecommitdiffstats
path: root/browser/modules/webrtcUI.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/webrtcUI.jsm')
-rw-r--r--browser/modules/webrtcUI.jsm963
1 files changed, 963 insertions, 0 deletions
diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm
new file mode 100644
index 000000000..b24135bfc
--- /dev/null
+++ b/browser/modules/webrtcUI.jsm
@@ -0,0 +1,963 @@
+/* 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://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+this.webrtcUI = {
+ init: function () {
+ 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:CancelRequest", this);
+ mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
+ },
+
+ uninit: function () {
+ 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:CancelRequest", this);
+ mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
+
+ if (gIndicatorWindow) {
+ gIndicatorWindow.close();
+ gIndicatorWindow = null;
+ }
+ },
+
+ processIndicators: 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: function(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: tab, browser: browser, types: types};
+ });
+ },
+
+ swapBrowserForNotification: function(aOldBrowser, aNewBrowser) {
+ for (let stream of this._streams) {
+ if (stream.browser == aOldBrowser)
+ stream.browser = aNewBrowser;
+ }
+ },
+
+ forgetStreamsFromBrowser: function(aBrowser) {
+ this._streams = this._streams.filter(stream => stream.browser != aBrowser);
+ },
+
+ showSharingDoorhanger: function(aActiveStream, aType) {
+ 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");
+ if (AppConstants.platform == "macosx" && !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;
+ }
+ identityBox.click();
+ },
+
+ updateMainActionLabel: function(aMenuList) {
+ let type = aMenuList.selectedItem.getAttribute("devicetype");
+ let document = aMenuList.ownerDocument;
+ document.getElementById("webRTC-all-windows-shared").hidden = type != "Screen";
+
+ // If we are also requesting audio in addition to screen sharing,
+ // always use a generic label.
+ if (!document.getElementById("webRTC-selectMicrophone").hidden)
+ type = "";
+
+ let bundle = document.defaultView.gNavigatorBundle;
+ let stringId = "getUserMedia.share" + (type || "SelectedItems") + ".label";
+ let popupnotification = aMenuList.parentNode.parentNode;
+ popupnotification.setAttribute("buttonlabel", bundle.getString(stringId));
+ },
+
+ receiveMessage: function(aMessage) {
+ switch (aMessage.name) {
+
+ // Add-ons can override stock permission behavior by doing:
+ //
+ // var stockReceiveMessage = webrtcUI.receiveMessage;
+ //
+ // webrtcUI.receiveMessage = function(aMessage) {
+ // switch (aMessage.name) {
+ // case "rtcpeer:Request": {
+ // // new code.
+ // break;
+ // ...
+ // default:
+ // return stockReceiveMessage.call(this, aMessage);
+ //
+ // Intercepting gUM and peerConnection requests should let an add-on
+ // limit PeerConnection activity with automatic rules and/or prompts
+ // in a sensible manner that avoids double-prompting in typical
+ // gUM+PeerConnection scenarios. For example:
+ //
+ // State Sample Action
+ // --------------------------------------------------------------
+ // No IP leaked yet + No gUM granted Warn user
+ // No IP leaked yet + gUM granted Avoid extra dialog
+ // No IP leaked yet + gUM request pending. Delay until gUM grant
+ // IP already leaked Too late to warn
+
+ case "rtcpeer:Request": {
+ // Always allow. This code-point exists for add-ons to override.
+ let { callID, windowID } = aMessage.data;
+ // Also available: isSecure, innerWindowID. For contentWindow:
+ //
+ // let contentWindow = Services.wm.getOuterWindowWithId(windowID);
+
+ let mm = aMessage.target.messageManager;
+ mm.sendAsyncMessage("rtcpeer:Allow",
+ { callID: callID, windowID: windowID });
+ break;
+ }
+ case "rtcpeer:CancelRequest":
+ // No data to release. This code-point exists for add-ons to override.
+ break;
+ case "webrtc:Request":
+ prompt(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, null, null);
+ }
+ 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 prompt(aBrowser, aRequest) {
+ let {audioDevices: audioDevices, videoDevices: videoDevices,
+ sharingScreen: sharingScreen, sharingAudio: sharingAudio,
+ requestTypes: requestTypes} = aRequest;
+ let uri = Services.io.newURI(aRequest.documentURI, null, null);
+ let host = getHost(uri);
+ let chromeDoc = aBrowser.ownerDocument;
+ let chromeWin = chromeDoc.defaultView;
+ let stringBundle = chromeWin.gNavigatorBundle;
+ let stringId = "getUserMedia.share" + requestTypes.join("And") + ".message";
+ let message = stringBundle.getFormattedString(stringId, [host]);
+
+ let mainLabel;
+ if (sharingScreen || sharingAudio) {
+ mainLabel = stringBundle.getString("getUserMedia.shareSelectedItems.label");
+ } else {
+ let string = stringBundle.getString("getUserMedia.shareSelectedDevices.label");
+ mainLabel = PluralForm.get(requestTypes.length, string);
+ }
+
+ let notification; // Used by action callbacks.
+ let mainAction = {
+ label: mainLabel,
+ accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.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: function() {}
+ };
+
+ let secondaryActions = [
+ {
+ label: stringBundle.getString("getUserMedia.denyRequest.label"),
+ accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
+ callback: function () {
+ denyRequest(notification.browser, aRequest);
+ }
+ }
+ ];
+ // Bug 1037438: implement 'never' for screen sharing.
+ if (!sharingScreen && !sharingAudio) {
+ secondaryActions.push({
+ label: stringBundle.getString("getUserMedia.never.label"),
+ accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
+ callback: function () {
+ denyRequest(notification.browser, aRequest);
+ // Let someone save "Never" for http sites so that they can be stopped from
+ // bothering you with doorhangers.
+ let perms = Services.perms;
+ if (audioDevices.length)
+ perms.add(uri, "microphone", perms.DENY_ACTION);
+ if (videoDevices.length)
+ perms.add(uri, "camera", perms.DENY_ACTION);
+ }
+ });
+ }
+
+ if (aRequest.secure && !sharingScreen && !sharingAudio) {
+ // Don't show the 'Always' 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).
+ secondaryActions.unshift({
+ label: stringBundle.getString("getUserMedia.always.label"),
+ accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
+ callback: function (aState) {
+ mainAction.callback(aState, true);
+ }
+ });
+ }
+
+ let options = {
+ eventCallback: function(aTopic, aNewBrowser) {
+ if (aTopic == "swapping")
+ return true;
+
+ let chromeDoc = this.browser.ownerDocument;
+
+ // Clean-up video streams of screensharing previews.
+ if ((aTopic == "dismissed" || aTopic == "removed") &&
+ requestTypes.includes("Screen")) {
+ let video = chromeDoc.getElementById("webRTC-previewVideo");
+ video.deviceId = undefined;
+ if (video.stream) {
+ video.stream.getTracks().forEach(t => t.stop());
+ video.stream = null;
+ video.src = null;
+ chromeDoc.getElementById("webRTC-preview").hidden = true;
+ }
+ let menupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
+ if (menupopup._commandEventListener) {
+ menupopup.removeEventListener("command", menupopup._commandEventListener);
+ menupopup._commandEventListener = null;
+ }
+ }
+
+ if (aTopic != "showing")
+ return false;
+
+ // DENY_ACTION is handled immediately by MediaManager, but handling
+ // of ALLOW_ACTION is delayed until the popupshowing event
+ // to avoid granting permissions automatically to background tabs.
+ if (aRequest.secure) {
+ let perms = Services.perms;
+
+ let micPerm = perms.testExactPermission(uri, "microphone");
+ if (micPerm == perms.PROMPT_ACTION)
+ micPerm = perms.UNKNOWN_ACTION;
+
+ let camPerm = perms.testExactPermission(uri, "camera");
+
+ let mediaManagerPerm =
+ perms.testExactPermission(uri, "MediaManagerVideo");
+ if (mediaManagerPerm) {
+ perms.remove(uri, "MediaManagerVideo");
+ }
+
+ if (camPerm == perms.PROMPT_ACTION)
+ camPerm = perms.UNKNOWN_ACTION;
+
+ // Screen sharing shouldn't follow the camera permissions.
+ if (videoDevices.length && sharingScreen)
+ camPerm = perms.UNKNOWN_ACTION;
+
+ // We don't check that permissions are set to ALLOW_ACTION in this
+ // test; only that they are set. This is because if audio is allowed
+ // and video is denied persistently, we don't want to show the prompt,
+ // and will grant audio access immediately.
+ if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
+ // All permissions we were about to request are already persistently set.
+ let allowedDevices = [];
+ if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
+ allowedDevices.push(videoDevices[0].deviceIndex);
+ let perms = Services.perms;
+ perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
+ perms.EXPIRE_SESSION);
+ }
+ if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
+ allowedDevices.push(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);
+
+ 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 = chromeDoc.getElementById("webRTC-selectWindow-label");
+ let stringId = "getUserMedia.select" + typeName;
+ label.setAttribute("value",
+ stringBundle.getString(stringId + ".label"));
+ label.setAttribute("accesskey",
+ stringBundle.getString(stringId + ".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(chromeDoc.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 stringId = "getUserMedia.shareApplicationWindowCount.label";
+ name = PluralForm.get(parseInt(count), stringBundle.getString(stringId))
+ .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.
+ chromeDoc.getElementById("webRTC-selectWindow-menulist").removeAttribute("value");
+ chromeDoc.getElementById("webRTC-all-windows-shared").hidden = true;
+ menupopup._commandEventListener = event => {
+ let video = chromeDoc.getElementById("webRTC-previewVideo");
+ if (video.stream) {
+ video.stream.getTracks().forEach(t => t.stop());
+ video.stream = null;
+ }
+
+ let deviceId = event.target.deviceId;
+ if (deviceId == undefined) {
+ chromeDoc.getElementById("webRTC-preview").hidden = true;
+ video.src = null;
+ return;
+ }
+
+ let scary = event.target.scary;
+ let warning = chromeDoc.getElementById("webRTC-previewWarning");
+ warning.hidden = !scary;
+ let chromeWin = chromeDoc.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 =
+ chromeDoc.getElementById("bundle_brand").getString("brandShortName");
+ string = bundle.getFormattedString("getUserMedia.shareFirefoxWarning.message",
+ [brand, learnMore]);
+ }
+ warning.innerHTML = string;
+ }
+
+ let perms = Services.perms;
+ let chromeUri = Services.io.newURI(chromeDoc.documentURI, null, null);
+ 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;
+ chromeDoc.getElementById("webRTC-preview").hidden = false;
+ video.onloadedmetadata = function(e) {
+ video.play();
+ };
+ });
+ };
+ menupopup.addEventListener("command", menupopup._commandEventListener);
+ }
+
+ function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
+ let menuitem = chromeDoc.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;
+ }
+
+ chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length || sharingScreen;
+ chromeDoc.getElementById("webRTC-selectWindowOrScreen").hidden = !sharingScreen || !videoDevices.length;
+ chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length || sharingAudio;
+
+ let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup");
+ let windowMenupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
+ let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup");
+ if (sharingScreen)
+ listScreenShareDevices(windowMenupopup, videoDevices);
+ else
+ listDevices(camMenupopup, videoDevices);
+
+ if (!sharingAudio)
+ listDevices(micMenupopup, audioDevices);
+
+ this.mainAction.callback = function(aState, aRemember) {
+ let allowedDevices = [];
+ let perms = Services.perms;
+ if (videoDevices.length) {
+ let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-menulist";
+ let videoDeviceIndex = chromeDoc.getElementById(listId).value;
+ let allowCamera = videoDeviceIndex != "-1";
+ if (allowCamera) {
+ 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 (aRemember) {
+ perms.add(uri, "camera",
+ allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
+ }
+ }
+ if (audioDevices.length) {
+ if (!sharingAudio) {
+ let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
+ let allowMic = audioDeviceIndex != "-1";
+ if (allowMic)
+ allowedDevices.push(audioDeviceIndex);
+ if (aRemember) {
+ perms.add(uri, "microphone",
+ allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
+ }
+ } else {
+ // Only one device possible for audio capture.
+ allowedDevices.push(0);
+ }
+ }
+
+ if (!allowedDevices.length) {
+ denyRequest(notification.browser, aRequest);
+ return;
+ }
+
+ if (aRemember) {
+ // 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;
+ }
+ };
+
+ 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 =
+ chromeWin.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() {
+ if (AppConstants.platform != "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, []);
+ }
+
+ 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: function(aEvent) {
+ let type = this.getAttribute("type");
+ if (type == "Camera" || type == "Microphone")
+ type = "Devices";
+ else if (type == "Window" || type == "Application" || type == "Browser")
+ type = "Screen";
+ webrtcUI.showSharingDoorhanger(aEvent.target.stream, type);
+ },
+
+ _popupShowing: function(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.setAttribute("type", type);
+ 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");
+ let labelId = "webrtcIndicator.controlSharingOn.menuitem";
+ let label = stream.browser.contentTitle || stream.uri;
+ item.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
+ item.setAttribute("type", type);
+ item.stream = stream;
+ item.addEventListener("command", indicator._command);
+ this.appendChild(item);
+ }
+
+ return true;
+ },
+
+ _popupHiding: function(aEvent) {
+ while (this.firstChild)
+ this.firstChild.remove();
+ },
+
+ _setIndicatorState: function(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: function() {
+ this._setIndicatorState("Camera", webrtcUI.showCameraIndicator);
+ this._setIndicatorState("Microphone", webrtcUI.showMicrophoneIndicator);
+ this._setIndicatorState("Screen", webrtcUI.showScreenSharingIndicator);
+ },
+ close: function() {
+ 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;
+
+ // We can only open 1 doorhanger at a time. Guessing that users would be
+ // most eager to control screen/window/app sharing, and only then
+ // camera/microphone sharing, in that (decreasing) order of priority.
+ let doorhangerType;
+ if ((/Screen|Window|Application/).test(stringName)) {
+ doorhangerType = "Screen";
+ } else {
+ doorhangerType = "Devices";
+ }
+ menuitem.setAttribute("doorhangertype", doorhangerType);
+ menuitem.addEventListener("command", onTabSharingMenuPopupCommand);
+ e.target.appendChild(menuitem);
+ }
+}
+
+function onTabSharingMenuPopupHiding(e) {
+ while (this.lastChild)
+ this.lastChild.remove();
+}
+
+function onTabSharingMenuPopupCommand(e) {
+ let type = e.target.getAttribute("doorhangertype");
+ webrtcUI.showSharingDoorhanger(e.target.stream, type);
+}
+
+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;
+ if (AppConstants.platform == "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");
+ }
+ 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;
+ if (AppConstants.platform == "macosx") {
+ document.getElementById("tabSharingSeparator").hidden = false;
+ }
+ }
+}
+
+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;
+ }
+ if (AppConstants.platform == "macosx") {
+ let separator = doc.getElementById("tabSharingSeparator");
+ if (separator) {
+ separator.hidden = true;
+ }
+ }
+ }
+ }
+
+ if (webrtcUI.showGlobalIndicator) {
+ if (!gIndicatorWindow)
+ gIndicatorWindow = getGlobalIndicator();
+ else
+ gIndicatorWindow.updateIndicatorState();
+ } else if (gIndicatorWindow) {
+ gIndicatorWindow.close();
+ gIndicatorWindow = null;
+ }
+}