Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | this.EXPORTED_SYMBOLS = ["webrtcUI"]; |
michael@0 | 8 | |
michael@0 | 9 | const Cu = Components.utils; |
michael@0 | 10 | const Cc = Components.classes; |
michael@0 | 11 | const Ci = Components.interfaces; |
michael@0 | 12 | |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
michael@0 | 17 | "resource://gre/modules/PluralForm.jsm"); |
michael@0 | 18 | |
michael@0 | 19 | XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", |
michael@0 | 20 | "@mozilla.org/mediaManagerService;1", |
michael@0 | 21 | "nsIMediaManagerService"); |
michael@0 | 22 | |
michael@0 | 23 | this.webrtcUI = { |
michael@0 | 24 | init: function () { |
michael@0 | 25 | Services.obs.addObserver(handleRequest, "getUserMedia:request", false); |
michael@0 | 26 | Services.obs.addObserver(updateIndicators, "recording-device-events", false); |
michael@0 | 27 | Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false); |
michael@0 | 28 | }, |
michael@0 | 29 | |
michael@0 | 30 | uninit: function () { |
michael@0 | 31 | Services.obs.removeObserver(handleRequest, "getUserMedia:request"); |
michael@0 | 32 | Services.obs.removeObserver(updateIndicators, "recording-device-events"); |
michael@0 | 33 | Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended"); |
michael@0 | 34 | }, |
michael@0 | 35 | |
michael@0 | 36 | showGlobalIndicator: false, |
michael@0 | 37 | |
michael@0 | 38 | get activeStreams() { |
michael@0 | 39 | let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows; |
michael@0 | 40 | let count = contentWindowSupportsArray.Count(); |
michael@0 | 41 | let activeStreams = []; |
michael@0 | 42 | for (let i = 0; i < count; i++) { |
michael@0 | 43 | let contentWindow = contentWindowSupportsArray.GetElementAt(i); |
michael@0 | 44 | let browser = getBrowserForWindow(contentWindow); |
michael@0 | 45 | let browserWindow = browser.ownerDocument.defaultView; |
michael@0 | 46 | let tab = browserWindow.gBrowser && |
michael@0 | 47 | browserWindow.gBrowser._getTabForContentWindow(contentWindow.top); |
michael@0 | 48 | activeStreams.push({ |
michael@0 | 49 | uri: contentWindow.location.href, |
michael@0 | 50 | tab: tab, |
michael@0 | 51 | browser: browser |
michael@0 | 52 | }); |
michael@0 | 53 | } |
michael@0 | 54 | return activeStreams; |
michael@0 | 55 | } |
michael@0 | 56 | } |
michael@0 | 57 | |
michael@0 | 58 | function getBrowserForWindowId(aWindowID) { |
michael@0 | 59 | return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID)); |
michael@0 | 60 | } |
michael@0 | 61 | |
michael@0 | 62 | function getBrowserForWindow(aContentWindow) { |
michael@0 | 63 | return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 64 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 65 | .QueryInterface(Ci.nsIDocShell) |
michael@0 | 66 | .chromeEventHandler; |
michael@0 | 67 | } |
michael@0 | 68 | |
michael@0 | 69 | function handleRequest(aSubject, aTopic, aData) { |
michael@0 | 70 | let constraints = aSubject.getConstraints(); |
michael@0 | 71 | let secure = aSubject.isSecure; |
michael@0 | 72 | let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); |
michael@0 | 73 | |
michael@0 | 74 | contentWindow.navigator.mozGetUserMediaDevices( |
michael@0 | 75 | constraints, |
michael@0 | 76 | function (devices) { |
michael@0 | 77 | prompt(contentWindow, aSubject.callID, constraints.audio, |
michael@0 | 78 | constraints.video || constraints.picture, devices, secure); |
michael@0 | 79 | }, |
michael@0 | 80 | function (error) { |
michael@0 | 81 | // bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND |
michael@0 | 82 | // and allow the user to plug in a device, instead of immediately failing. |
michael@0 | 83 | denyRequest(aSubject.callID, error); |
michael@0 | 84 | }, |
michael@0 | 85 | aSubject.innerWindowID); |
michael@0 | 86 | } |
michael@0 | 87 | |
michael@0 | 88 | function denyRequest(aCallID, aError) { |
michael@0 | 89 | let msg = null; |
michael@0 | 90 | if (aError) { |
michael@0 | 91 | msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); |
michael@0 | 92 | msg.data = aError; |
michael@0 | 93 | } |
michael@0 | 94 | Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID); |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevices, aSecure) { |
michael@0 | 98 | let audioDevices = []; |
michael@0 | 99 | let videoDevices = []; |
michael@0 | 100 | for (let device of aDevices) { |
michael@0 | 101 | device = device.QueryInterface(Ci.nsIMediaDevice); |
michael@0 | 102 | switch (device.type) { |
michael@0 | 103 | case "audio": |
michael@0 | 104 | if (aAudioRequested) |
michael@0 | 105 | audioDevices.push(device); |
michael@0 | 106 | break; |
michael@0 | 107 | case "video": |
michael@0 | 108 | if (aVideoRequested) |
michael@0 | 109 | videoDevices.push(device); |
michael@0 | 110 | break; |
michael@0 | 111 | } |
michael@0 | 112 | } |
michael@0 | 113 | |
michael@0 | 114 | let requestType; |
michael@0 | 115 | if (audioDevices.length && videoDevices.length) |
michael@0 | 116 | requestType = "CameraAndMicrophone"; |
michael@0 | 117 | else if (audioDevices.length) |
michael@0 | 118 | requestType = "Microphone"; |
michael@0 | 119 | else if (videoDevices.length) |
michael@0 | 120 | requestType = "Camera"; |
michael@0 | 121 | else { |
michael@0 | 122 | denyRequest(aCallID, "NO_DEVICES_FOUND"); |
michael@0 | 123 | return; |
michael@0 | 124 | } |
michael@0 | 125 | |
michael@0 | 126 | let uri = aContentWindow.document.documentURIObject; |
michael@0 | 127 | let browser = getBrowserForWindow(aContentWindow); |
michael@0 | 128 | let chromeDoc = browser.ownerDocument; |
michael@0 | 129 | let chromeWin = chromeDoc.defaultView; |
michael@0 | 130 | let stringBundle = chromeWin.gNavigatorBundle; |
michael@0 | 131 | let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", |
michael@0 | 132 | [ uri.host ]); |
michael@0 | 133 | |
michael@0 | 134 | let mainAction = { |
michael@0 | 135 | label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, |
michael@0 | 136 | stringBundle.getString("getUserMedia.shareSelectedDevices.label")), |
michael@0 | 137 | accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"), |
michael@0 | 138 | // The real callback will be set during the "showing" event. The |
michael@0 | 139 | // empty function here is so that PopupNotifications.show doesn't |
michael@0 | 140 | // reject the action. |
michael@0 | 141 | callback: function() {} |
michael@0 | 142 | }; |
michael@0 | 143 | |
michael@0 | 144 | let secondaryActions = [ |
michael@0 | 145 | { |
michael@0 | 146 | label: stringBundle.getString("getUserMedia.denyRequest.label"), |
michael@0 | 147 | accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"), |
michael@0 | 148 | callback: function () { |
michael@0 | 149 | denyRequest(aCallID); |
michael@0 | 150 | } |
michael@0 | 151 | }, |
michael@0 | 152 | { |
michael@0 | 153 | label: stringBundle.getString("getUserMedia.never.label"), |
michael@0 | 154 | accessKey: stringBundle.getString("getUserMedia.never.accesskey"), |
michael@0 | 155 | callback: function () { |
michael@0 | 156 | denyRequest(aCallID); |
michael@0 | 157 | // Let someone save "Never" for http sites so that they can be stopped from |
michael@0 | 158 | // bothering you with doorhangers. |
michael@0 | 159 | let perms = Services.perms; |
michael@0 | 160 | if (audioDevices.length) |
michael@0 | 161 | perms.add(uri, "microphone", perms.DENY_ACTION); |
michael@0 | 162 | if (videoDevices.length) |
michael@0 | 163 | perms.add(uri, "camera", perms.DENY_ACTION); |
michael@0 | 164 | } |
michael@0 | 165 | } |
michael@0 | 166 | ]; |
michael@0 | 167 | |
michael@0 | 168 | if (aSecure) { |
michael@0 | 169 | // Don't show the 'Always' action if the connection isn't secure. |
michael@0 | 170 | secondaryActions.unshift({ |
michael@0 | 171 | label: stringBundle.getString("getUserMedia.always.label"), |
michael@0 | 172 | accessKey: stringBundle.getString("getUserMedia.always.accesskey"), |
michael@0 | 173 | callback: function () { |
michael@0 | 174 | mainAction.callback(true); |
michael@0 | 175 | } |
michael@0 | 176 | }); |
michael@0 | 177 | } |
michael@0 | 178 | |
michael@0 | 179 | let options = { |
michael@0 | 180 | eventCallback: function(aTopic, aNewBrowser) { |
michael@0 | 181 | if (aTopic == "swapping") |
michael@0 | 182 | return true; |
michael@0 | 183 | |
michael@0 | 184 | let chromeDoc = this.browser.ownerDocument; |
michael@0 | 185 | |
michael@0 | 186 | if (aTopic == "shown") { |
michael@0 | 187 | let PopupNotifications = chromeDoc.defaultView.PopupNotifications; |
michael@0 | 188 | let popupId = requestType == "Microphone" ? "Microphone" : "Devices"; |
michael@0 | 189 | PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-share" + popupId); |
michael@0 | 190 | } |
michael@0 | 191 | |
michael@0 | 192 | if (aTopic != "showing") |
michael@0 | 193 | return false; |
michael@0 | 194 | |
michael@0 | 195 | // DENY_ACTION is handled immediately by MediaManager, but handling |
michael@0 | 196 | // of ALLOW_ACTION is delayed until the popupshowing event |
michael@0 | 197 | // to avoid granting permissions automatically to background tabs. |
michael@0 | 198 | if (aSecure) { |
michael@0 | 199 | let perms = Services.perms; |
michael@0 | 200 | |
michael@0 | 201 | let micPerm = perms.testExactPermission(uri, "microphone"); |
michael@0 | 202 | if (micPerm == perms.PROMPT_ACTION) |
michael@0 | 203 | micPerm = perms.UNKNOWN_ACTION; |
michael@0 | 204 | |
michael@0 | 205 | let camPerm = perms.testExactPermission(uri, "camera"); |
michael@0 | 206 | if (camPerm == perms.PROMPT_ACTION) |
michael@0 | 207 | camPerm = perms.UNKNOWN_ACTION; |
michael@0 | 208 | |
michael@0 | 209 | // We don't check that permissions are set to ALLOW_ACTION in this |
michael@0 | 210 | // test; only that they are set. This is because if audio is allowed |
michael@0 | 211 | // and video is denied persistently, we don't want to show the prompt, |
michael@0 | 212 | // and will grant audio access immediately. |
michael@0 | 213 | if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) { |
michael@0 | 214 | // All permissions we were about to request are already persistently set. |
michael@0 | 215 | let allowedDevices = Cc["@mozilla.org/supports-array;1"] |
michael@0 | 216 | .createInstance(Ci.nsISupportsArray); |
michael@0 | 217 | if (videoDevices.length && camPerm == perms.ALLOW_ACTION) |
michael@0 | 218 | allowedDevices.AppendElement(videoDevices[0]); |
michael@0 | 219 | if (audioDevices.length && micPerm == perms.ALLOW_ACTION) |
michael@0 | 220 | allowedDevices.AppendElement(audioDevices[0]); |
michael@0 | 221 | Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); |
michael@0 | 222 | this.remove(); |
michael@0 | 223 | return true; |
michael@0 | 224 | } |
michael@0 | 225 | } |
michael@0 | 226 | |
michael@0 | 227 | function listDevices(menupopup, devices) { |
michael@0 | 228 | while (menupopup.lastChild) |
michael@0 | 229 | menupopup.removeChild(menupopup.lastChild); |
michael@0 | 230 | |
michael@0 | 231 | let deviceIndex = 0; |
michael@0 | 232 | for (let device of devices) { |
michael@0 | 233 | addDeviceToList(menupopup, device.name, deviceIndex); |
michael@0 | 234 | deviceIndex++; |
michael@0 | 235 | } |
michael@0 | 236 | } |
michael@0 | 237 | |
michael@0 | 238 | function addDeviceToList(menupopup, deviceName, deviceIndex) { |
michael@0 | 239 | let menuitem = chromeDoc.createElement("menuitem"); |
michael@0 | 240 | menuitem.setAttribute("value", deviceIndex); |
michael@0 | 241 | menuitem.setAttribute("label", deviceName); |
michael@0 | 242 | menuitem.setAttribute("tooltiptext", deviceName); |
michael@0 | 243 | menupopup.appendChild(menuitem); |
michael@0 | 244 | } |
michael@0 | 245 | |
michael@0 | 246 | chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length; |
michael@0 | 247 | chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length; |
michael@0 | 248 | |
michael@0 | 249 | let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup"); |
michael@0 | 250 | let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup"); |
michael@0 | 251 | listDevices(camMenupopup, videoDevices); |
michael@0 | 252 | listDevices(micMenupopup, audioDevices); |
michael@0 | 253 | if (requestType == "CameraAndMicrophone") { |
michael@0 | 254 | let stringBundle = chromeDoc.defaultView.gNavigatorBundle; |
michael@0 | 255 | addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1"); |
michael@0 | 256 | addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1"); |
michael@0 | 257 | } |
michael@0 | 258 | |
michael@0 | 259 | this.mainAction.callback = function(aRemember) { |
michael@0 | 260 | let allowedDevices = Cc["@mozilla.org/supports-array;1"] |
michael@0 | 261 | .createInstance(Ci.nsISupportsArray); |
michael@0 | 262 | let perms = Services.perms; |
michael@0 | 263 | if (videoDevices.length) { |
michael@0 | 264 | let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value; |
michael@0 | 265 | let allowCamera = videoDeviceIndex != "-1"; |
michael@0 | 266 | if (allowCamera) |
michael@0 | 267 | allowedDevices.AppendElement(videoDevices[videoDeviceIndex]); |
michael@0 | 268 | if (aRemember) { |
michael@0 | 269 | perms.add(uri, "camera", |
michael@0 | 270 | allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION); |
michael@0 | 271 | } |
michael@0 | 272 | } |
michael@0 | 273 | if (audioDevices.length) { |
michael@0 | 274 | let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value; |
michael@0 | 275 | let allowMic = audioDeviceIndex != "-1"; |
michael@0 | 276 | if (allowMic) |
michael@0 | 277 | allowedDevices.AppendElement(audioDevices[audioDeviceIndex]); |
michael@0 | 278 | if (aRemember) { |
michael@0 | 279 | perms.add(uri, "microphone", |
michael@0 | 280 | allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION); |
michael@0 | 281 | } |
michael@0 | 282 | } |
michael@0 | 283 | |
michael@0 | 284 | if (allowedDevices.Count() == 0) { |
michael@0 | 285 | denyRequest(aCallID); |
michael@0 | 286 | return; |
michael@0 | 287 | } |
michael@0 | 288 | |
michael@0 | 289 | Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); |
michael@0 | 290 | }; |
michael@0 | 291 | return false; |
michael@0 | 292 | } |
michael@0 | 293 | }; |
michael@0 | 294 | |
michael@0 | 295 | let anchorId = requestType == "Microphone" ? "webRTC-shareMicrophone-notification-icon" |
michael@0 | 296 | : "webRTC-shareDevices-notification-icon"; |
michael@0 | 297 | chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message, |
michael@0 | 298 | anchorId, mainAction, secondaryActions, options); |
michael@0 | 299 | } |
michael@0 | 300 | |
michael@0 | 301 | function updateIndicators() { |
michael@0 | 302 | webrtcUI.showGlobalIndicator = |
michael@0 | 303 | MediaManagerService.activeMediaCaptureWindows.Count() > 0; |
michael@0 | 304 | |
michael@0 | 305 | let e = Services.wm.getEnumerator("navigator:browser"); |
michael@0 | 306 | while (e.hasMoreElements()) |
michael@0 | 307 | e.getNext().WebrtcIndicator.updateButton(); |
michael@0 | 308 | |
michael@0 | 309 | for (let {browser: browser} of webrtcUI.activeStreams) |
michael@0 | 310 | showBrowserSpecificIndicator(browser); |
michael@0 | 311 | } |
michael@0 | 312 | |
michael@0 | 313 | function showBrowserSpecificIndicator(aBrowser) { |
michael@0 | 314 | let hasVideo = {}; |
michael@0 | 315 | let hasAudio = {}; |
michael@0 | 316 | MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow, |
michael@0 | 317 | hasVideo, hasAudio); |
michael@0 | 318 | let captureState; |
michael@0 | 319 | if (hasVideo.value && hasAudio.value) { |
michael@0 | 320 | captureState = "CameraAndMicrophone"; |
michael@0 | 321 | } else if (hasVideo.value) { |
michael@0 | 322 | captureState = "Camera"; |
michael@0 | 323 | } else if (hasAudio.value) { |
michael@0 | 324 | captureState = "Microphone"; |
michael@0 | 325 | } else { |
michael@0 | 326 | Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access"); |
michael@0 | 327 | return; |
michael@0 | 328 | } |
michael@0 | 329 | |
michael@0 | 330 | let chromeWin = aBrowser.ownerDocument.defaultView; |
michael@0 | 331 | let stringBundle = chromeWin.gNavigatorBundle; |
michael@0 | 332 | |
michael@0 | 333 | let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); |
michael@0 | 334 | |
michael@0 | 335 | let uri = aBrowser.contentWindow.document.documentURIObject; |
michael@0 | 336 | let windowId = aBrowser.contentWindow |
michael@0 | 337 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 338 | .getInterface(Ci.nsIDOMWindowUtils) |
michael@0 | 339 | .currentInnerWindowID; |
michael@0 | 340 | let mainAction = { |
michael@0 | 341 | label: stringBundle.getString("getUserMedia.continueSharing.label"), |
michael@0 | 342 | accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"), |
michael@0 | 343 | callback: function () {}, |
michael@0 | 344 | dismiss: true |
michael@0 | 345 | }; |
michael@0 | 346 | let secondaryActions = [{ |
michael@0 | 347 | label: stringBundle.getString("getUserMedia.stopSharing.label"), |
michael@0 | 348 | accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"), |
michael@0 | 349 | callback: function () { |
michael@0 | 350 | let perms = Services.perms; |
michael@0 | 351 | if (hasVideo.value && |
michael@0 | 352 | perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION) |
michael@0 | 353 | perms.remove(uri.host, "camera"); |
michael@0 | 354 | if (hasAudio.value && |
michael@0 | 355 | perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION) |
michael@0 | 356 | perms.remove(uri.host, "microphone"); |
michael@0 | 357 | |
michael@0 | 358 | Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId); |
michael@0 | 359 | } |
michael@0 | 360 | }]; |
michael@0 | 361 | let options = { |
michael@0 | 362 | hideNotNow: true, |
michael@0 | 363 | dismissed: true, |
michael@0 | 364 | eventCallback: function(aTopic) { |
michael@0 | 365 | if (aTopic == "shown") { |
michael@0 | 366 | let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications; |
michael@0 | 367 | let popupId = captureState == "Microphone" ? "Microphone" : "Devices"; |
michael@0 | 368 | PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId); |
michael@0 | 369 | } |
michael@0 | 370 | return aTopic == "swapping"; |
michael@0 | 371 | } |
michael@0 | 372 | }; |
michael@0 | 373 | let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon" |
michael@0 | 374 | : "webRTC-sharingDevices-notification-icon"; |
michael@0 | 375 | chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message, |
michael@0 | 376 | anchorId, mainAction, secondaryActions, options); |
michael@0 | 377 | } |
michael@0 | 378 | |
michael@0 | 379 | function removeBrowserSpecificIndicator(aSubject, aTopic, aData) { |
michael@0 | 380 | let browser = getBrowserForWindowId(aData); |
michael@0 | 381 | let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; |
michael@0 | 382 | let notification = PopupNotifications && |
michael@0 | 383 | PopupNotifications.getNotification("webRTC-sharingDevices", |
michael@0 | 384 | browser); |
michael@0 | 385 | if (notification) |
michael@0 | 386 | PopupNotifications.remove(notification); |
michael@0 | 387 | } |