/* 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, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService"); this.webrtcUI = { init: function () { Services.obs.addObserver(handleRequest, "getUserMedia:request", false); Services.obs.addObserver(updateIndicators, "recording-device-events", false); Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false); }, uninit: function () { Services.obs.removeObserver(handleRequest, "getUserMedia:request"); Services.obs.removeObserver(updateIndicators, "recording-device-events"); Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended"); }, showGlobalIndicator: false, get activeStreams() { let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows; let count = contentWindowSupportsArray.Count(); let activeStreams = []; for (let i = 0; i < count; i++) { let contentWindow = contentWindowSupportsArray.GetElementAt(i); let browser = getBrowserForWindow(contentWindow); let browserWindow = browser.ownerDocument.defaultView; let tab = browserWindow.gBrowser && browserWindow.gBrowser._getTabForContentWindow(contentWindow.top); activeStreams.push({ uri: contentWindow.location.href, tab: tab, browser: browser }); } return activeStreams; } } function getBrowserForWindowId(aWindowID) { return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID)); } function getBrowserForWindow(aContentWindow) { return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .chromeEventHandler; } function handleRequest(aSubject, aTopic, aData) { let constraints = aSubject.getConstraints(); let secure = aSubject.isSecure; let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); contentWindow.navigator.mozGetUserMediaDevices( constraints, function (devices) { prompt(contentWindow, aSubject.callID, constraints.audio, constraints.video || constraints.picture, devices, secure); }, function (error) { // bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND // and allow the user to plug in a device, instead of immediately failing. denyRequest(aSubject.callID, error); }, aSubject.innerWindowID); } function denyRequest(aCallID, aError) { let msg = null; if (aError) { msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); msg.data = aError; } Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID); } function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevices, aSecure) { let audioDevices = []; let videoDevices = []; for (let device of aDevices) { device = device.QueryInterface(Ci.nsIMediaDevice); switch (device.type) { case "audio": if (aAudioRequested) audioDevices.push(device); break; case "video": if (aVideoRequested) videoDevices.push(device); break; } } let requestType; if (audioDevices.length && videoDevices.length) requestType = "CameraAndMicrophone"; else if (audioDevices.length) requestType = "Microphone"; else if (videoDevices.length) requestType = "Camera"; else { denyRequest(aCallID, "NO_DEVICES_FOUND"); return; } let uri = aContentWindow.document.documentURIObject; let browser = getBrowserForWindow(aContentWindow); let chromeDoc = browser.ownerDocument; let chromeWin = chromeDoc.defaultView; let stringBundle = chromeWin.gNavigatorBundle; #ifdef MOZ_LOOP let host; // For Loop protocols that start with about:, use brandShortName instead of the host for now. // Bug 990678 will implement improvements/replacements for the permissions dialog, so this // should become unnecessary. if (uri.spec.startsWith("about:loop")) { let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties"); host = brandBundle.GetStringFromName("brandShortName"); } else { // uri.host throws for about: protocols, so we have to do this once we know // it isn't about:loop. host = uri.host; } let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", [ host ]); #else let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", [ uri.host ]); #endif let mainAction = { label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, stringBundle.getString("getUserMedia.shareSelectedDevices.label")), 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(aCallID); } }, { label: stringBundle.getString("getUserMedia.never.label"), accessKey: stringBundle.getString("getUserMedia.never.accesskey"), callback: function () { denyRequest(aCallID); // 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 (aSecure) { // Don't show the 'Always' action if the connection isn't secure. secondaryActions.unshift({ label: stringBundle.getString("getUserMedia.always.label"), accessKey: stringBundle.getString("getUserMedia.always.accesskey"), callback: function () { mainAction.callback(true); } }); } let options = { eventCallback: function(aTopic, aNewBrowser) { if (aTopic == "swapping") return true; let chromeDoc = this.browser.ownerDocument; if (aTopic == "shown") { let PopupNotifications = chromeDoc.defaultView.PopupNotifications; let popupId = requestType == "Microphone" ? "Microphone" : "Devices"; PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-share" + popupId); } 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 (aSecure) { 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"); if (camPerm == perms.PROMPT_ACTION) 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 = Cc["@mozilla.org/supports-array;1"] .createInstance(Ci.nsISupportsArray); if (videoDevices.length && camPerm == perms.ALLOW_ACTION) allowedDevices.AppendElement(videoDevices[0]); if (audioDevices.length && micPerm == perms.ALLOW_ACTION) allowedDevices.AppendElement(audioDevices[0]); Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); this.remove(); return true; } } function listDevices(menupopup, devices) { while (menupopup.lastChild) menupopup.removeChild(menupopup.lastChild); let deviceIndex = 0; for (let device of devices) { addDeviceToList(menupopup, device.name, deviceIndex); deviceIndex++; } } function addDeviceToList(menupopup, deviceName, deviceIndex) { let menuitem = chromeDoc.createElement("menuitem"); menuitem.setAttribute("value", deviceIndex); menuitem.setAttribute("label", deviceName); menuitem.setAttribute("tooltiptext", deviceName); menupopup.appendChild(menuitem); } chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length; chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length; let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup"); let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup"); listDevices(camMenupopup, videoDevices); listDevices(micMenupopup, audioDevices); if (requestType == "CameraAndMicrophone") { let stringBundle = chromeDoc.defaultView.gNavigatorBundle; addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1"); addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1"); } this.mainAction.callback = function(aRemember) { let allowedDevices = Cc["@mozilla.org/supports-array;1"] .createInstance(Ci.nsISupportsArray); let perms = Services.perms; if (videoDevices.length) { let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value; let allowCamera = videoDeviceIndex != "-1"; if (allowCamera) allowedDevices.AppendElement(videoDevices[videoDeviceIndex]); if (aRemember) { perms.add(uri, "camera", allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION); } } if (audioDevices.length) { let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value; let allowMic = audioDeviceIndex != "-1"; if (allowMic) allowedDevices.AppendElement(audioDevices[audioDeviceIndex]); if (aRemember) { perms.add(uri, "microphone", allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION); } } if (allowedDevices.Count() == 0) { denyRequest(aCallID); return; } Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); }; return false; } }; let anchorId = requestType == "Microphone" ? "webRTC-shareMicrophone-notification-icon" : "webRTC-shareDevices-notification-icon"; chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message, anchorId, mainAction, secondaryActions, options); } function updateIndicators() { webrtcUI.showGlobalIndicator = MediaManagerService.activeMediaCaptureWindows.Count() > 0; let e = Services.wm.getEnumerator("navigator:browser"); while (e.hasMoreElements()) e.getNext().WebrtcIndicator.updateButton(); for (let {browser: browser} of webrtcUI.activeStreams) showBrowserSpecificIndicator(browser); } function showBrowserSpecificIndicator(aBrowser) { let hasVideo = {}; let hasAudio = {}; MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow, hasVideo, hasAudio); let captureState; if (hasVideo.value && hasAudio.value) { captureState = "CameraAndMicrophone"; } else if (hasVideo.value) { captureState = "Camera"; } else if (hasAudio.value) { captureState = "Microphone"; } else { Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access"); return; } let chromeWin = aBrowser.ownerDocument.defaultView; let stringBundle = chromeWin.gNavigatorBundle; let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); let uri = aBrowser.contentWindow.document.documentURIObject; let windowId = aBrowser.contentWindow .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .currentInnerWindowID; let mainAction = { label: stringBundle.getString("getUserMedia.continueSharing.label"), accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"), callback: function () {}, dismiss: true }; let secondaryActions = [{ label: stringBundle.getString("getUserMedia.stopSharing.label"), accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"), callback: function () { let perms = Services.perms; if (hasVideo.value && perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION) perms.remove(uri.host, "camera"); if (hasAudio.value && perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION) perms.remove(uri.host, "microphone"); Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId); } }]; let options = { hideNotNow: true, dismissed: true, eventCallback: function(aTopic) { if (aTopic == "shown") { let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications; let popupId = captureState == "Microphone" ? "Microphone" : "Devices"; PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId); } return aTopic == "swapping"; } }; let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon" : "webRTC-sharingDevices-notification-icon"; chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message, anchorId, mainAction, secondaryActions, options); } function removeBrowserSpecificIndicator(aSubject, aTopic, aData) { let browser = getBrowserForWindowId(aData); let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; let notification = PopupNotifications && PopupNotifications.getNotification("webRTC-sharingDevices", browser); if (notification) PopupNotifications.remove(notification); }