michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["webrtcUI"]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", michael@0: "@mozilla.org/mediaManagerService;1", michael@0: "nsIMediaManagerService"); michael@0: michael@0: this.webrtcUI = { michael@0: init: function () { michael@0: Services.obs.addObserver(handleRequest, "getUserMedia:request", false); michael@0: Services.obs.addObserver(updateIndicators, "recording-device-events", false); michael@0: Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false); michael@0: }, michael@0: michael@0: uninit: function () { michael@0: Services.obs.removeObserver(handleRequest, "getUserMedia:request"); michael@0: Services.obs.removeObserver(updateIndicators, "recording-device-events"); michael@0: Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended"); michael@0: }, michael@0: michael@0: showGlobalIndicator: false, michael@0: michael@0: get activeStreams() { michael@0: let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows; michael@0: let count = contentWindowSupportsArray.Count(); michael@0: let activeStreams = []; michael@0: for (let i = 0; i < count; i++) { michael@0: let contentWindow = contentWindowSupportsArray.GetElementAt(i); michael@0: let browser = getBrowserForWindow(contentWindow); michael@0: let browserWindow = browser.ownerDocument.defaultView; michael@0: let tab = browserWindow.gBrowser && michael@0: browserWindow.gBrowser._getTabForContentWindow(contentWindow.top); michael@0: activeStreams.push({ michael@0: uri: contentWindow.location.href, michael@0: tab: tab, michael@0: browser: browser michael@0: }); michael@0: } michael@0: return activeStreams; michael@0: } michael@0: } michael@0: michael@0: function getBrowserForWindowId(aWindowID) { michael@0: return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID)); michael@0: } michael@0: michael@0: function getBrowserForWindow(aContentWindow) { michael@0: return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler; michael@0: } michael@0: michael@0: function handleRequest(aSubject, aTopic, aData) { michael@0: let constraints = aSubject.getConstraints(); michael@0: let secure = aSubject.isSecure; michael@0: let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); michael@0: michael@0: contentWindow.navigator.mozGetUserMediaDevices( michael@0: constraints, michael@0: function (devices) { michael@0: prompt(contentWindow, aSubject.callID, constraints.audio, michael@0: constraints.video || constraints.picture, devices, secure); michael@0: }, michael@0: function (error) { michael@0: // bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND michael@0: // and allow the user to plug in a device, instead of immediately failing. michael@0: denyRequest(aSubject.callID, error); michael@0: }, michael@0: aSubject.innerWindowID); michael@0: } michael@0: michael@0: function denyRequest(aCallID, aError) { michael@0: let msg = null; michael@0: if (aError) { michael@0: msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); michael@0: msg.data = aError; michael@0: } michael@0: Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID); michael@0: } michael@0: michael@0: function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevices, aSecure) { michael@0: let audioDevices = []; michael@0: let videoDevices = []; michael@0: for (let device of aDevices) { michael@0: device = device.QueryInterface(Ci.nsIMediaDevice); michael@0: switch (device.type) { michael@0: case "audio": michael@0: if (aAudioRequested) michael@0: audioDevices.push(device); michael@0: break; michael@0: case "video": michael@0: if (aVideoRequested) michael@0: videoDevices.push(device); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: let requestType; michael@0: if (audioDevices.length && videoDevices.length) michael@0: requestType = "CameraAndMicrophone"; michael@0: else if (audioDevices.length) michael@0: requestType = "Microphone"; michael@0: else if (videoDevices.length) michael@0: requestType = "Camera"; michael@0: else { michael@0: denyRequest(aCallID, "NO_DEVICES_FOUND"); michael@0: return; michael@0: } michael@0: michael@0: let uri = aContentWindow.document.documentURIObject; michael@0: let browser = getBrowserForWindow(aContentWindow); michael@0: let chromeDoc = browser.ownerDocument; michael@0: let chromeWin = chromeDoc.defaultView; michael@0: let stringBundle = chromeWin.gNavigatorBundle; michael@0: let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", michael@0: [ uri.host ]); michael@0: michael@0: let mainAction = { michael@0: label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, michael@0: stringBundle.getString("getUserMedia.shareSelectedDevices.label")), michael@0: accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"), michael@0: // The real callback will be set during the "showing" event. The michael@0: // empty function here is so that PopupNotifications.show doesn't michael@0: // reject the action. michael@0: callback: function() {} michael@0: }; michael@0: michael@0: let secondaryActions = [ michael@0: { michael@0: label: stringBundle.getString("getUserMedia.denyRequest.label"), michael@0: accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"), michael@0: callback: function () { michael@0: denyRequest(aCallID); michael@0: } michael@0: }, michael@0: { michael@0: label: stringBundle.getString("getUserMedia.never.label"), michael@0: accessKey: stringBundle.getString("getUserMedia.never.accesskey"), michael@0: callback: function () { michael@0: denyRequest(aCallID); michael@0: // Let someone save "Never" for http sites so that they can be stopped from michael@0: // bothering you with doorhangers. michael@0: let perms = Services.perms; michael@0: if (audioDevices.length) michael@0: perms.add(uri, "microphone", perms.DENY_ACTION); michael@0: if (videoDevices.length) michael@0: perms.add(uri, "camera", perms.DENY_ACTION); michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: if (aSecure) { michael@0: // Don't show the 'Always' action if the connection isn't secure. michael@0: secondaryActions.unshift({ michael@0: label: stringBundle.getString("getUserMedia.always.label"), michael@0: accessKey: stringBundle.getString("getUserMedia.always.accesskey"), michael@0: callback: function () { michael@0: mainAction.callback(true); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: let options = { michael@0: eventCallback: function(aTopic, aNewBrowser) { michael@0: if (aTopic == "swapping") michael@0: return true; michael@0: michael@0: let chromeDoc = this.browser.ownerDocument; michael@0: michael@0: if (aTopic == "shown") { michael@0: let PopupNotifications = chromeDoc.defaultView.PopupNotifications; michael@0: let popupId = requestType == "Microphone" ? "Microphone" : "Devices"; michael@0: PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-share" + popupId); michael@0: } michael@0: michael@0: if (aTopic != "showing") michael@0: return false; michael@0: michael@0: // DENY_ACTION is handled immediately by MediaManager, but handling michael@0: // of ALLOW_ACTION is delayed until the popupshowing event michael@0: // to avoid granting permissions automatically to background tabs. michael@0: if (aSecure) { michael@0: let perms = Services.perms; michael@0: michael@0: let micPerm = perms.testExactPermission(uri, "microphone"); michael@0: if (micPerm == perms.PROMPT_ACTION) michael@0: micPerm = perms.UNKNOWN_ACTION; michael@0: michael@0: let camPerm = perms.testExactPermission(uri, "camera"); michael@0: if (camPerm == perms.PROMPT_ACTION) michael@0: camPerm = perms.UNKNOWN_ACTION; michael@0: michael@0: // We don't check that permissions are set to ALLOW_ACTION in this michael@0: // test; only that they are set. This is because if audio is allowed michael@0: // and video is denied persistently, we don't want to show the prompt, michael@0: // and will grant audio access immediately. michael@0: if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) { michael@0: // All permissions we were about to request are already persistently set. michael@0: let allowedDevices = Cc["@mozilla.org/supports-array;1"] michael@0: .createInstance(Ci.nsISupportsArray); michael@0: if (videoDevices.length && camPerm == perms.ALLOW_ACTION) michael@0: allowedDevices.AppendElement(videoDevices[0]); michael@0: if (audioDevices.length && micPerm == perms.ALLOW_ACTION) michael@0: allowedDevices.AppendElement(audioDevices[0]); michael@0: Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); michael@0: this.remove(); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: function listDevices(menupopup, devices) { michael@0: while (menupopup.lastChild) michael@0: menupopup.removeChild(menupopup.lastChild); michael@0: michael@0: let deviceIndex = 0; michael@0: for (let device of devices) { michael@0: addDeviceToList(menupopup, device.name, deviceIndex); michael@0: deviceIndex++; michael@0: } michael@0: } michael@0: michael@0: function addDeviceToList(menupopup, deviceName, deviceIndex) { michael@0: let menuitem = chromeDoc.createElement("menuitem"); michael@0: menuitem.setAttribute("value", deviceIndex); michael@0: menuitem.setAttribute("label", deviceName); michael@0: menuitem.setAttribute("tooltiptext", deviceName); michael@0: menupopup.appendChild(menuitem); michael@0: } michael@0: michael@0: chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length; michael@0: chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length; michael@0: michael@0: let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup"); michael@0: let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup"); michael@0: listDevices(camMenupopup, videoDevices); michael@0: listDevices(micMenupopup, audioDevices); michael@0: if (requestType == "CameraAndMicrophone") { michael@0: let stringBundle = chromeDoc.defaultView.gNavigatorBundle; michael@0: addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1"); michael@0: addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1"); michael@0: } michael@0: michael@0: this.mainAction.callback = function(aRemember) { michael@0: let allowedDevices = Cc["@mozilla.org/supports-array;1"] michael@0: .createInstance(Ci.nsISupportsArray); michael@0: let perms = Services.perms; michael@0: if (videoDevices.length) { michael@0: let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value; michael@0: let allowCamera = videoDeviceIndex != "-1"; michael@0: if (allowCamera) michael@0: allowedDevices.AppendElement(videoDevices[videoDeviceIndex]); michael@0: if (aRemember) { michael@0: perms.add(uri, "camera", michael@0: allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION); michael@0: } michael@0: } michael@0: if (audioDevices.length) { michael@0: let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value; michael@0: let allowMic = audioDeviceIndex != "-1"; michael@0: if (allowMic) michael@0: allowedDevices.AppendElement(audioDevices[audioDeviceIndex]); michael@0: if (aRemember) { michael@0: perms.add(uri, "microphone", michael@0: allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION); michael@0: } michael@0: } michael@0: michael@0: if (allowedDevices.Count() == 0) { michael@0: denyRequest(aCallID); michael@0: return; michael@0: } michael@0: michael@0: Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); michael@0: }; michael@0: return false; michael@0: } michael@0: }; michael@0: michael@0: let anchorId = requestType == "Microphone" ? "webRTC-shareMicrophone-notification-icon" michael@0: : "webRTC-shareDevices-notification-icon"; michael@0: chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message, michael@0: anchorId, mainAction, secondaryActions, options); michael@0: } michael@0: michael@0: function updateIndicators() { michael@0: webrtcUI.showGlobalIndicator = michael@0: MediaManagerService.activeMediaCaptureWindows.Count() > 0; michael@0: michael@0: let e = Services.wm.getEnumerator("navigator:browser"); michael@0: while (e.hasMoreElements()) michael@0: e.getNext().WebrtcIndicator.updateButton(); michael@0: michael@0: for (let {browser: browser} of webrtcUI.activeStreams) michael@0: showBrowserSpecificIndicator(browser); michael@0: } michael@0: michael@0: function showBrowserSpecificIndicator(aBrowser) { michael@0: let hasVideo = {}; michael@0: let hasAudio = {}; michael@0: MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow, michael@0: hasVideo, hasAudio); michael@0: let captureState; michael@0: if (hasVideo.value && hasAudio.value) { michael@0: captureState = "CameraAndMicrophone"; michael@0: } else if (hasVideo.value) { michael@0: captureState = "Camera"; michael@0: } else if (hasAudio.value) { michael@0: captureState = "Microphone"; michael@0: } else { michael@0: Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access"); michael@0: return; michael@0: } michael@0: michael@0: let chromeWin = aBrowser.ownerDocument.defaultView; michael@0: let stringBundle = chromeWin.gNavigatorBundle; michael@0: michael@0: let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); michael@0: michael@0: let uri = aBrowser.contentWindow.document.documentURIObject; michael@0: let windowId = aBrowser.contentWindow michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils) michael@0: .currentInnerWindowID; michael@0: let mainAction = { michael@0: label: stringBundle.getString("getUserMedia.continueSharing.label"), michael@0: accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"), michael@0: callback: function () {}, michael@0: dismiss: true michael@0: }; michael@0: let secondaryActions = [{ michael@0: label: stringBundle.getString("getUserMedia.stopSharing.label"), michael@0: accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"), michael@0: callback: function () { michael@0: let perms = Services.perms; michael@0: if (hasVideo.value && michael@0: perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION) michael@0: perms.remove(uri.host, "camera"); michael@0: if (hasAudio.value && michael@0: perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION) michael@0: perms.remove(uri.host, "microphone"); michael@0: michael@0: Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId); michael@0: } michael@0: }]; michael@0: let options = { michael@0: hideNotNow: true, michael@0: dismissed: true, michael@0: eventCallback: function(aTopic) { michael@0: if (aTopic == "shown") { michael@0: let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications; michael@0: let popupId = captureState == "Microphone" ? "Microphone" : "Devices"; michael@0: PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId); michael@0: } michael@0: return aTopic == "swapping"; michael@0: } michael@0: }; michael@0: let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon" michael@0: : "webRTC-sharingDevices-notification-icon"; michael@0: chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message, michael@0: anchorId, mainAction, secondaryActions, options); michael@0: } michael@0: michael@0: function removeBrowserSpecificIndicator(aSubject, aTopic, aData) { michael@0: let browser = getBrowserForWindowId(aData); michael@0: let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; michael@0: let notification = PopupNotifications && michael@0: PopupNotifications.getNotification("webRTC-sharingDevices", michael@0: browser); michael@0: if (notification) michael@0: PopupNotifications.remove(notification); michael@0: }