diff options
Diffstat (limited to 'application/basilisk/modules/webrtcUI.jsm')
-rw-r--r-- | application/basilisk/modules/webrtcUI.jsm | 1087 |
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; + } +} |