browser/modules/webrtcUI.jsm

changeset 0
6474c204b198
     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 +}

mercurial