Bug 1284877 - show a preview when the user selects a screen/window/application to share, r=Gijs.
authorFlorian Quèze <florian@queze.net>
Mon, 07 Nov 2016 12:14:29 +0100
changeset 351435 f5c8208b0139c97d9470ea64251884acfcc6ac82
parent 351434 7c1c7953fe42420d2a66951b9b1d46171cf0d044
child 351436 e8669d6d4b436de48a73bb913ce0d9e23e33b92d
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1284877
milestone52.0a1
Bug 1284877 - show a preview when the user selects a screen/window/application to share, r=Gijs.
browser/base/content/popup-notifications.inc
browser/modules/ContentWebRTC.jsm
browser/modules/webrtcUI.jsm
browser/themes/shared/notification-icons.inc.css
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -23,16 +23,20 @@
                control="webRTC-selectWindow-menulist"/>
         <menulist id="webRTC-selectWindow-menulist"
                   oncommand="webrtcUI.updateMainActionLabel(this);">
           <menupopup id="webRTC-selectWindow-menupopup"/>
         </menulist>
         <description id="webRTC-all-windows-shared" hidden="true">&getUserMedia.allWindowsShared.message;</description>
       </popupnotificationcontent>
 
+      <popupnotificationcontent id="webRTC-preview" hidden="true">
+        <html:video id="webRTC-previewVideo"/>
+      </popupnotificationcontent>
+
       <popupnotificationcontent id="webRTC-selectMicrophone" orient="vertical">
         <label value="&getUserMedia.selectMicrophone.label;"
                accesskey="&getUserMedia.selectMicrophone.accesskey;"
                control="webRTC-selectMicrophone-menulist"/>
         <menulist id="webRTC-selectMicrophone-menulist">
           <menupopup id="webRTC-selectMicrophone-menupopup"/>
         </menulist>
       </popupnotificationcontent>
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -9,16 +9,18 @@ const {classes: Cc, interfaces: Ci, resu
 this.EXPORTED_SYMBOLS = [ "ContentWebRTC" ];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
                                    "@mozilla.org/mediaManagerService;1",
                                    "nsIMediaManagerService");
 
+const kBrowserURL = "chrome://browser/content/browser.xul";
+
 this.ContentWebRTC = {
   _initialized: false,
 
   init: function() {
     if (this._initialized)
       return;
 
     this._initialized = true;
@@ -163,26 +165,26 @@ function prompt(aContentWindow, aWindowI
     device = device.QueryInterface(Ci.nsIMediaDevice);
     switch (device.type) {
       case "audio":
         // Check that if we got a microphone, we have not requested an audio
         // capture, and if we have requested an audio capture, we are not
         // getting a microphone instead.
         if (audio && (device.mediaSource == "microphone") != sharingAudio) {
           audioDevices.push({name: device.name, deviceIndex: devices.length,
-                             mediaSource: device.mediaSource});
+                             id: device.rawId, mediaSource: device.mediaSource});
           devices.push(device);
         }
         break;
       case "video":
         // Verify that if we got a camera, we haven't requested a screen share,
         // or that if we requested a screen share we aren't getting a camera.
         if (video && (device.mediaSource == "camera") != sharingScreen) {
           videoDevices.push({name: device.name, deviceIndex: devices.length,
-                             mediaSource: device.mediaSource});
+                             id: device.rawId, mediaSource: device.mediaSource});
           devices.push(device);
         }
         break;
     }
   }
 
   let requestTypes = [];
   if (videoDevices.length)
@@ -255,17 +257,23 @@ function forgetPendingListsEventually(aC
       aContentWindow.pendingPeerConnectionRequests.size) {
     return;
   }
   aContentWindow.pendingGetUserMediaRequests = null;
   aContentWindow.pendingPeerConnectionRequests = null;
   aContentWindow.removeEventListener("unload", ContentWebRTC);
 }
 
-function updateIndicators() {
+function updateIndicators(aSubject, aTopic, aData) {
+  if (aSubject instanceof Ci.nsIPropertyBag &&
+      aSubject.getProperty("requestURL") == kBrowserURL) {
+    // Ignore notifications caused by the browser UI showing previews.
+    return;
+  }
+
   let contentWindowArray = MediaManagerService.activeMediaCaptureWindows;
   let count = contentWindowArray.length;
 
   let state = {
     showGlobalIndicator: count > 0,
     showCameraIndicator: false,
     showMicrophoneIndicator: false,
     showScreenSharingIndicator: ""
@@ -279,16 +287,21 @@ function updateIndicators() {
   // have the same top level window several times. We use a Set to avoid
   // sending duplicate notifications.
   let contentWindows = new Set();
   for (let i = 0; i < count; ++i) {
     contentWindows.add(contentWindowArray.queryElementAt(i, Ci.nsISupports).top);
   }
 
   for (let contentWindow of contentWindows) {
+    if (contentWindow.document.documentURI == kBrowserURL) {
+      // There may be a preview shown at the same time as other streams.
+      continue;
+    }
+
     let tabState = getTabStateForContentWindow(contentWindow);
     if (tabState.camera)
       state.showCameraIndicator = true;
     if (tabState.microphone)
       state.showMicrophoneIndicator = true;
     if (tabState.screen) {
       if (tabState.screen == "Screen") {
         state.showScreenSharingIndicator = "Screen";
@@ -310,16 +323,21 @@ function updateIndicators() {
     mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
   }
 
   cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state);
 }
 
 function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
   let contentWindow = Services.wm.getOuterWindowWithId(aData).top;
+  if (contentWindow.document.documentURI == kBrowserURL) {
+    // Ignore notifications caused by the browser UI showing previews.
+    return;
+  }
+
   let tabState = getTabStateForContentWindow(contentWindow);
   if (!tabState.camera && !tabState.microphone && !tabState.screen)
     tabState = {windowId: tabState.windowId};
 
   let mm = getMessageManagerForWindow(contentWindow);
   if (mm)
     mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
 }
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -363,22 +363,40 @@ function prompt(aBrowser, aRequest) {
 
       let chromeDoc = this.browser.ownerDocument;
 
       if (aTopic == "shown") {
         let popupId = "Devices";
         if (requestTypes.length == 1 && (requestTypes[0] == "Microphone" ||
                                          requestTypes[0] == "AudioCapture"))
           popupId = "Microphone";
-        if (requestTypes.indexOf("Screen") != -1)
+        if (requestTypes.includes("Screen"))
           popupId = "Screen";
         chromeDoc.getElementById("webRTC-shareDevices-notification")
                  .setAttribute("popupid", "webRTC-share" + popupId);
       }
 
+      // Clean-up video streams of screensharing previews.
+      if ((aTopic == "dismissed" || aTopic == "removed") &&
+          requestTypes.includes("Screen")) {
+        let video = chromeDoc.getElementById("webRTC-previewVideo");
+        video.deviceId = undefined;
+        if (video.stream) {
+          video.stream.getTracks().forEach(t => t.stop());
+          video.stream = null;
+          video.src = null;
+          chromeDoc.getElementById("webRTC-preview").hidden = true;
+        }
+        let menupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
+        if (menupopup._commandEventListener) {
+          menupopup.removeEventListener("command", menupopup._commandEventListener);
+          menupopup._commandEventListener = null;
+        }
+      }
+
       if (aTopic != "showing")
         return false;
 
       // DENY_ACTION is handled immediately by MediaManager, but handling
       // of ALLOW_ACTION is delayed until the popupshowing event
       // to avoid granting permissions automatically to background tabs.
       if (aRequest.secure) {
         let perms = Services.perms;
@@ -486,32 +504,72 @@ function prompt(aBrowser, aRequest) {
               let sepIndex = name.indexOf("\x1e");
               let count = name.slice(0, sepIndex);
               let stringId = "getUserMedia.shareApplicationWindowCount.label";
               name = PluralForm.get(parseInt(count), stringBundle.getString(stringId))
                                .replace("#1", name.slice(sepIndex + 1))
                                .replace("#2", count);
             }
           }
-          addDeviceToList(menupopup, name, i, typeName);
+          let item = addDeviceToList(menupopup, name, i, typeName);
+          item.deviceId = devices[i].id;
         }
 
         // Always re-select the "No <type>" item.
         chromeDoc.getElementById("webRTC-selectWindow-menulist").removeAttribute("value");
         chromeDoc.getElementById("webRTC-all-windows-shared").hidden = true;
+        menupopup._commandEventListener = event => {
+          let video = chromeDoc.getElementById("webRTC-previewVideo");
+          if (video.stream) {
+            video.stream.getTracks().forEach(t => t.stop());
+            video.stream = null;
+          }
+
+          let deviceId = event.target.deviceId;
+          if (deviceId == undefined) {
+            chromeDoc.getElementById("webRTC-preview").hidden = true;
+            video.src = null;
+            return;
+          }
+
+          let perms = Services.perms;
+          let chromeUri = Services.io.newURI(chromeDoc.documentURI, null, null);
+          perms.add(chromeUri, "MediaManagerVideo", perms.ALLOW_ACTION,
+                    perms.EXPIRE_SESSION);
+
+          video.deviceId = deviceId;
+          let constraints = { video: { mediaSource: type, deviceId: {exact: deviceId } } };
+          let chromeWin = chromeDoc.defaultView;
+          chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(stream => {
+            if (video.deviceId != deviceId) {
+              // The user has selected a different device or closed the panel
+              // before getUserMedia finished.
+              stream.getTracks().forEach(t => t.stop());
+              return;
+            }
+            video.src = chromeWin.URL.createObjectURL(stream);
+            video.stream = stream;
+            chromeDoc.getElementById("webRTC-preview").hidden = false;
+            video.onloadedmetadata = function(e) {
+              video.play();
+            };
+          });
+        };
+        menupopup.addEventListener("command", menupopup._commandEventListener);
       }
 
       function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
         let menuitem = chromeDoc.createElement("menuitem");
         menuitem.setAttribute("value", deviceIndex);
         menuitem.setAttribute("label", deviceName);
         menuitem.setAttribute("tooltiptext", deviceName);
         if (type)
           menuitem.setAttribute("devicetype", type);
         menupopup.appendChild(menuitem);
+        return menuitem;
       }
 
       chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length || sharingScreen;
       chromeDoc.getElementById("webRTC-selectWindowOrScreen").hidden = !sharingScreen || !videoDevices.length;
       chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length || sharingAudio;
 
       let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup");
       let windowMenupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
@@ -578,17 +636,17 @@ function prompt(aBrowser, aRequest) {
       };
       return false;
     }
   };
 
   let anchorId = "webRTC-shareDevices-notification-icon";
   if (requestTypes.length == 1 && requestTypes[0] == "Microphone")
     anchorId = "webRTC-shareMicrophone-notification-icon";
-  if (requestTypes.indexOf("Screen") != -1)
+  if (requestTypes.includes("Screen"))
     anchorId = "webRTC-shareScreen-notification-icon";
   notification =
     chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
                                       anchorId, mainAction, secondaryActions,
                                       options);
   notification.callID = aRequest.callID;
 }
 
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -150,16 +150,23 @@
 .screen-icon.in-use {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#screen-sharing);
 }
 
 .screen-icon.blocked-permission-icon {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#screen-blocked);
 }
 
+html|*#webRTC-previewVideo {
+  width: 300px;
+  /* If we don't set the min-width, width is ignored. */
+  min-width: 300px;
+  max-height: 200px;
+}
+
 /* This icon has a block sign in it, so we don't need a blocked version. */
 .popup-icon {
   list-style-image: url("chrome://browser/skin/notification-icons.svg#popup");
 }
 
 /* EME */
 
 .popup-notification-icon[popupid="drmContentPlaying"],