1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/modules/webrtcUI.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,387 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["webrtcUI"]; 1.11 + 1.12 +const Cu = Components.utils; 1.13 +const Cc = Components.classes; 1.14 +const Ci = Components.interfaces; 1.15 + 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", 1.20 + "resource://gre/modules/PluralForm.jsm"); 1.21 + 1.22 +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", 1.23 + "@mozilla.org/mediaManagerService;1", 1.24 + "nsIMediaManagerService"); 1.25 + 1.26 +this.webrtcUI = { 1.27 + init: function () { 1.28 + Services.obs.addObserver(handleRequest, "getUserMedia:request", false); 1.29 + Services.obs.addObserver(updateIndicators, "recording-device-events", false); 1.30 + Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false); 1.31 + }, 1.32 + 1.33 + uninit: function () { 1.34 + Services.obs.removeObserver(handleRequest, "getUserMedia:request"); 1.35 + Services.obs.removeObserver(updateIndicators, "recording-device-events"); 1.36 + Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended"); 1.37 + }, 1.38 + 1.39 + showGlobalIndicator: false, 1.40 + 1.41 + get activeStreams() { 1.42 + let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows; 1.43 + let count = contentWindowSupportsArray.Count(); 1.44 + let activeStreams = []; 1.45 + for (let i = 0; i < count; i++) { 1.46 + let contentWindow = contentWindowSupportsArray.GetElementAt(i); 1.47 + let browser = getBrowserForWindow(contentWindow); 1.48 + let browserWindow = browser.ownerDocument.defaultView; 1.49 + let tab = browserWindow.gBrowser && 1.50 + browserWindow.gBrowser._getTabForContentWindow(contentWindow.top); 1.51 + activeStreams.push({ 1.52 + uri: contentWindow.location.href, 1.53 + tab: tab, 1.54 + browser: browser 1.55 + }); 1.56 + } 1.57 + return activeStreams; 1.58 + } 1.59 +} 1.60 + 1.61 +function getBrowserForWindowId(aWindowID) { 1.62 + return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID)); 1.63 +} 1.64 + 1.65 +function getBrowserForWindow(aContentWindow) { 1.66 + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.67 + .getInterface(Ci.nsIWebNavigation) 1.68 + .QueryInterface(Ci.nsIDocShell) 1.69 + .chromeEventHandler; 1.70 +} 1.71 + 1.72 +function handleRequest(aSubject, aTopic, aData) { 1.73 + let constraints = aSubject.getConstraints(); 1.74 + let secure = aSubject.isSecure; 1.75 + let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); 1.76 + 1.77 + contentWindow.navigator.mozGetUserMediaDevices( 1.78 + constraints, 1.79 + function (devices) { 1.80 + prompt(contentWindow, aSubject.callID, constraints.audio, 1.81 + constraints.video || constraints.picture, devices, secure); 1.82 + }, 1.83 + function (error) { 1.84 + // bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND 1.85 + // and allow the user to plug in a device, instead of immediately failing. 1.86 + denyRequest(aSubject.callID, error); 1.87 + }, 1.88 + aSubject.innerWindowID); 1.89 +} 1.90 + 1.91 +function denyRequest(aCallID, aError) { 1.92 + let msg = null; 1.93 + if (aError) { 1.94 + msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); 1.95 + msg.data = aError; 1.96 + } 1.97 + Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID); 1.98 +} 1.99 + 1.100 +function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevices, aSecure) { 1.101 + let audioDevices = []; 1.102 + let videoDevices = []; 1.103 + for (let device of aDevices) { 1.104 + device = device.QueryInterface(Ci.nsIMediaDevice); 1.105 + switch (device.type) { 1.106 + case "audio": 1.107 + if (aAudioRequested) 1.108 + audioDevices.push(device); 1.109 + break; 1.110 + case "video": 1.111 + if (aVideoRequested) 1.112 + videoDevices.push(device); 1.113 + break; 1.114 + } 1.115 + } 1.116 + 1.117 + let requestType; 1.118 + if (audioDevices.length && videoDevices.length) 1.119 + requestType = "CameraAndMicrophone"; 1.120 + else if (audioDevices.length) 1.121 + requestType = "Microphone"; 1.122 + else if (videoDevices.length) 1.123 + requestType = "Camera"; 1.124 + else { 1.125 + denyRequest(aCallID, "NO_DEVICES_FOUND"); 1.126 + return; 1.127 + } 1.128 + 1.129 + let uri = aContentWindow.document.documentURIObject; 1.130 + let browser = getBrowserForWindow(aContentWindow); 1.131 + let chromeDoc = browser.ownerDocument; 1.132 + let chromeWin = chromeDoc.defaultView; 1.133 + let stringBundle = chromeWin.gNavigatorBundle; 1.134 + let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", 1.135 + [ uri.host ]); 1.136 + 1.137 + let mainAction = { 1.138 + label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, 1.139 + stringBundle.getString("getUserMedia.shareSelectedDevices.label")), 1.140 + accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"), 1.141 + // The real callback will be set during the "showing" event. The 1.142 + // empty function here is so that PopupNotifications.show doesn't 1.143 + // reject the action. 1.144 + callback: function() {} 1.145 + }; 1.146 + 1.147 + let secondaryActions = [ 1.148 + { 1.149 + label: stringBundle.getString("getUserMedia.denyRequest.label"), 1.150 + accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"), 1.151 + callback: function () { 1.152 + denyRequest(aCallID); 1.153 + } 1.154 + }, 1.155 + { 1.156 + label: stringBundle.getString("getUserMedia.never.label"), 1.157 + accessKey: stringBundle.getString("getUserMedia.never.accesskey"), 1.158 + callback: function () { 1.159 + denyRequest(aCallID); 1.160 + // Let someone save "Never" for http sites so that they can be stopped from 1.161 + // bothering you with doorhangers. 1.162 + let perms = Services.perms; 1.163 + if (audioDevices.length) 1.164 + perms.add(uri, "microphone", perms.DENY_ACTION); 1.165 + if (videoDevices.length) 1.166 + perms.add(uri, "camera", perms.DENY_ACTION); 1.167 + } 1.168 + } 1.169 + ]; 1.170 + 1.171 + if (aSecure) { 1.172 + // Don't show the 'Always' action if the connection isn't secure. 1.173 + secondaryActions.unshift({ 1.174 + label: stringBundle.getString("getUserMedia.always.label"), 1.175 + accessKey: stringBundle.getString("getUserMedia.always.accesskey"), 1.176 + callback: function () { 1.177 + mainAction.callback(true); 1.178 + } 1.179 + }); 1.180 + } 1.181 + 1.182 + let options = { 1.183 + eventCallback: function(aTopic, aNewBrowser) { 1.184 + if (aTopic == "swapping") 1.185 + return true; 1.186 + 1.187 + let chromeDoc = this.browser.ownerDocument; 1.188 + 1.189 + if (aTopic == "shown") { 1.190 + let PopupNotifications = chromeDoc.defaultView.PopupNotifications; 1.191 + let popupId = requestType == "Microphone" ? "Microphone" : "Devices"; 1.192 + PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-share" + popupId); 1.193 + } 1.194 + 1.195 + if (aTopic != "showing") 1.196 + return false; 1.197 + 1.198 + // DENY_ACTION is handled immediately by MediaManager, but handling 1.199 + // of ALLOW_ACTION is delayed until the popupshowing event 1.200 + // to avoid granting permissions automatically to background tabs. 1.201 + if (aSecure) { 1.202 + let perms = Services.perms; 1.203 + 1.204 + let micPerm = perms.testExactPermission(uri, "microphone"); 1.205 + if (micPerm == perms.PROMPT_ACTION) 1.206 + micPerm = perms.UNKNOWN_ACTION; 1.207 + 1.208 + let camPerm = perms.testExactPermission(uri, "camera"); 1.209 + if (camPerm == perms.PROMPT_ACTION) 1.210 + camPerm = perms.UNKNOWN_ACTION; 1.211 + 1.212 + // We don't check that permissions are set to ALLOW_ACTION in this 1.213 + // test; only that they are set. This is because if audio is allowed 1.214 + // and video is denied persistently, we don't want to show the prompt, 1.215 + // and will grant audio access immediately. 1.216 + if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) { 1.217 + // All permissions we were about to request are already persistently set. 1.218 + let allowedDevices = Cc["@mozilla.org/supports-array;1"] 1.219 + .createInstance(Ci.nsISupportsArray); 1.220 + if (videoDevices.length && camPerm == perms.ALLOW_ACTION) 1.221 + allowedDevices.AppendElement(videoDevices[0]); 1.222 + if (audioDevices.length && micPerm == perms.ALLOW_ACTION) 1.223 + allowedDevices.AppendElement(audioDevices[0]); 1.224 + Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); 1.225 + this.remove(); 1.226 + return true; 1.227 + } 1.228 + } 1.229 + 1.230 + function listDevices(menupopup, devices) { 1.231 + while (menupopup.lastChild) 1.232 + menupopup.removeChild(menupopup.lastChild); 1.233 + 1.234 + let deviceIndex = 0; 1.235 + for (let device of devices) { 1.236 + addDeviceToList(menupopup, device.name, deviceIndex); 1.237 + deviceIndex++; 1.238 + } 1.239 + } 1.240 + 1.241 + function addDeviceToList(menupopup, deviceName, deviceIndex) { 1.242 + let menuitem = chromeDoc.createElement("menuitem"); 1.243 + menuitem.setAttribute("value", deviceIndex); 1.244 + menuitem.setAttribute("label", deviceName); 1.245 + menuitem.setAttribute("tooltiptext", deviceName); 1.246 + menupopup.appendChild(menuitem); 1.247 + } 1.248 + 1.249 + chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length; 1.250 + chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length; 1.251 + 1.252 + let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup"); 1.253 + let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup"); 1.254 + listDevices(camMenupopup, videoDevices); 1.255 + listDevices(micMenupopup, audioDevices); 1.256 + if (requestType == "CameraAndMicrophone") { 1.257 + let stringBundle = chromeDoc.defaultView.gNavigatorBundle; 1.258 + addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1"); 1.259 + addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1"); 1.260 + } 1.261 + 1.262 + this.mainAction.callback = function(aRemember) { 1.263 + let allowedDevices = Cc["@mozilla.org/supports-array;1"] 1.264 + .createInstance(Ci.nsISupportsArray); 1.265 + let perms = Services.perms; 1.266 + if (videoDevices.length) { 1.267 + let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value; 1.268 + let allowCamera = videoDeviceIndex != "-1"; 1.269 + if (allowCamera) 1.270 + allowedDevices.AppendElement(videoDevices[videoDeviceIndex]); 1.271 + if (aRemember) { 1.272 + perms.add(uri, "camera", 1.273 + allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION); 1.274 + } 1.275 + } 1.276 + if (audioDevices.length) { 1.277 + let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value; 1.278 + let allowMic = audioDeviceIndex != "-1"; 1.279 + if (allowMic) 1.280 + allowedDevices.AppendElement(audioDevices[audioDeviceIndex]); 1.281 + if (aRemember) { 1.282 + perms.add(uri, "microphone", 1.283 + allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION); 1.284 + } 1.285 + } 1.286 + 1.287 + if (allowedDevices.Count() == 0) { 1.288 + denyRequest(aCallID); 1.289 + return; 1.290 + } 1.291 + 1.292 + Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); 1.293 + }; 1.294 + return false; 1.295 + } 1.296 + }; 1.297 + 1.298 + let anchorId = requestType == "Microphone" ? "webRTC-shareMicrophone-notification-icon" 1.299 + : "webRTC-shareDevices-notification-icon"; 1.300 + chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message, 1.301 + anchorId, mainAction, secondaryActions, options); 1.302 +} 1.303 + 1.304 +function updateIndicators() { 1.305 + webrtcUI.showGlobalIndicator = 1.306 + MediaManagerService.activeMediaCaptureWindows.Count() > 0; 1.307 + 1.308 + let e = Services.wm.getEnumerator("navigator:browser"); 1.309 + while (e.hasMoreElements()) 1.310 + e.getNext().WebrtcIndicator.updateButton(); 1.311 + 1.312 + for (let {browser: browser} of webrtcUI.activeStreams) 1.313 + showBrowserSpecificIndicator(browser); 1.314 +} 1.315 + 1.316 +function showBrowserSpecificIndicator(aBrowser) { 1.317 + let hasVideo = {}; 1.318 + let hasAudio = {}; 1.319 + MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow, 1.320 + hasVideo, hasAudio); 1.321 + let captureState; 1.322 + if (hasVideo.value && hasAudio.value) { 1.323 + captureState = "CameraAndMicrophone"; 1.324 + } else if (hasVideo.value) { 1.325 + captureState = "Camera"; 1.326 + } else if (hasAudio.value) { 1.327 + captureState = "Microphone"; 1.328 + } else { 1.329 + Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access"); 1.330 + return; 1.331 + } 1.332 + 1.333 + let chromeWin = aBrowser.ownerDocument.defaultView; 1.334 + let stringBundle = chromeWin.gNavigatorBundle; 1.335 + 1.336 + let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); 1.337 + 1.338 + let uri = aBrowser.contentWindow.document.documentURIObject; 1.339 + let windowId = aBrowser.contentWindow 1.340 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.341 + .getInterface(Ci.nsIDOMWindowUtils) 1.342 + .currentInnerWindowID; 1.343 + let mainAction = { 1.344 + label: stringBundle.getString("getUserMedia.continueSharing.label"), 1.345 + accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"), 1.346 + callback: function () {}, 1.347 + dismiss: true 1.348 + }; 1.349 + let secondaryActions = [{ 1.350 + label: stringBundle.getString("getUserMedia.stopSharing.label"), 1.351 + accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"), 1.352 + callback: function () { 1.353 + let perms = Services.perms; 1.354 + if (hasVideo.value && 1.355 + perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION) 1.356 + perms.remove(uri.host, "camera"); 1.357 + if (hasAudio.value && 1.358 + perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION) 1.359 + perms.remove(uri.host, "microphone"); 1.360 + 1.361 + Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId); 1.362 + } 1.363 + }]; 1.364 + let options = { 1.365 + hideNotNow: true, 1.366 + dismissed: true, 1.367 + eventCallback: function(aTopic) { 1.368 + if (aTopic == "shown") { 1.369 + let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications; 1.370 + let popupId = captureState == "Microphone" ? "Microphone" : "Devices"; 1.371 + PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId); 1.372 + } 1.373 + return aTopic == "swapping"; 1.374 + } 1.375 + }; 1.376 + let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon" 1.377 + : "webRTC-sharingDevices-notification-icon"; 1.378 + chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message, 1.379 + anchorId, mainAction, secondaryActions, options); 1.380 +} 1.381 + 1.382 +function removeBrowserSpecificIndicator(aSubject, aTopic, aData) { 1.383 + let browser = getBrowserForWindowId(aData); 1.384 + let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; 1.385 + let notification = PopupNotifications && 1.386 + PopupNotifications.getNotification("webRTC-sharingDevices", 1.387 + browser); 1.388 + if (notification) 1.389 + PopupNotifications.remove(notification); 1.390 +}