You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
969 lines
36 KiB
969 lines
36 KiB
/* 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; |
|
try { |
|
// This fails for principals that serialize to "null", e.g. file URIs. |
|
uri = Services.io.newURI(aRequest.origin, null, null); |
|
} catch (e) { |
|
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; |
|
} |
|
}
|
|
|