browser/modules/webrtcUI.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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 }

mercurial