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

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

mercurial