bug 1088758 - Add the ability to mirror tabs from desktop to a second screen r=mconley, mfinkle
authorBrad Lassey <blassey@mozilla.com>
Mon, 15 Dec 2014 18:52:55 -0500
changeset 219914 a7770ec46f0401c0a855e68f656ad8362be77ffc
parent 219913 d0be7c6566ae0d21a411cfb413b75cde7b81b611
child 219915 295f228e8a9bbc02d894d3525a8465feb78462d7
push id52946
push usercbook@mozilla.com
push dateTue, 16 Dec 2014 12:43:15 +0000
treeherdermozilla-inbound@df8db207bf66 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, mfinkle
bugs1088758
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1088758 - Add the ability to mirror tabs from desktop to a second screen r=mconley, mfinkle
browser/base/content/browser-menubar.inc
browser/base/content/browser.js
browser/base/content/content.js
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/modules/CastingApps.jsm
mobile/android/chrome/content/CastingApps.js
toolkit/modules/secondscreen/RokuApp.jsm
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -446,17 +446,18 @@
       <menuitem id="menu_unsortedBookmarks"
                 label="&unsortedBookmarksCmd.label;"
                 oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/>
     </menupopup>
   </menu>
 
             <menu id="tools-menu"
                   label="&toolsMenu.label;"
-                  accesskey="&toolsMenu.accesskey;">
+                  accesskey="&toolsMenu.accesskey;"
+                  onpopupshowing="mirrorShow(this)">
               <menupopup id="menu_ToolsPopup"
 #ifdef MOZ_SERVICES_SYNC
 # We have to use setTimeout() here to avoid a flickering menu bar when opening
 # the Tools menu, see bug 970769. This can be removed once we got rid of the
 # event loop spinning in Weave.Status._authManager.
                          onpopupshowing="setTimeout(() => gSyncUI.updateUI());"
 #endif
                          >
@@ -543,16 +544,22 @@
               </menu>
               <menuitem id="menu_pageInfo"
                         accesskey="&pageInfoCmd.accesskey;"
                         label="&pageInfoCmd.label;"
 #ifndef XP_WIN
                         key="key_viewInfo"
 #endif
                         command="View:PageInfo"/>
+              <menu id="menu_mirrorTabCmd"
+                    accesskey="&mirrorTabCmd.accesskey;"
+                    label="&mirrorTabCmd.label;">
+                <menupopup id="menu_mirrorTab-popup"
+                           onpopupshowing="populateMirrorTabMenu(this)"/>
+              </menu>
 #ifndef XP_UNIX
               <menuseparator id="prefSep"/>
               <menuitem id="menu_preferences"
                         label="&preferencesCmd2.label;"
                         accesskey="&preferencesCmd2.accesskey;"
                         oncommand="openPreferences();"/>
 #endif
               </menupopup>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2963,16 +2963,40 @@ function getMeOutOfHere() {
   gBrowser.loadURI(url);
 }
 
 function BrowserFullScreen()
 {
   window.fullScreen = !window.fullScreen;
 }
 
+function mirrorShow(popup) {
+  let services = CastingApps.getServicesForMirroring();
+  popup.ownerDocument.getElementById("menu_mirrorTabCmd").disabled = !services.length;
+}
+
+function mirrorMenuItemClicked(event) {
+  gBrowser.selectedBrowser.messageManager.sendAsyncMessage("SecondScreen:tab-mirror",
+                                                           {service: event.originalTarget._service});
+}
+
+function populateMirrorTabMenu(popup) {
+  let videoEl = this.target;
+  popup.innerHTML = null;
+  let doc = popup.ownerDocument;
+  let services = CastingApps.getServicesForMirroring();
+  services.forEach(service => {
+    let item = doc.createElement("menuitem");
+    item.setAttribute("label", service.friendlyName);
+    item._service = service;
+    item.addEventListener("command", mirrorMenuItemClicked);
+    popup.appendChild(item);
+  });
+};
+
 function _checkDefaultAndSwitchToMetro() {
 #ifdef HAVE_SHELL_SERVICE
 #ifdef XP_WIN
 #ifdef MOZ_METRO
   let shell = Components.classes["@mozilla.org/browser/shell-service;1"].
     getService(Components.interfaces.nsIShellService);
   let isDefault = shell.isDefaultBrowser(false, false);
 
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -22,16 +22,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
   "resource:///modules/PluginContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
+XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() {
+  let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery;
+  // Register targets
+  ssdp.registerDevice({
+    id: "roku:ecp",
+    target: "roku:ecp",
+    factory: function(aService) {
+      Cu.import("resource://gre/modules/RokuApp.jsm");
+      return new RokuApp(aService);
+    },
+    mirror: true,
+    types: ["video/mp4"],
+    extensions: ["mp4"]
+  });
+  return ssdp;
+});
 
 // TabChildGlobal
 var global = this;
 
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
 
 addMessageListener("Browser:HideSessionRestoreButton", function (message) {
@@ -71,16 +87,26 @@ addMessageListener("Browser:Reload", fun
     handlingUserInput.destruct();
   }
 });
 
 addMessageListener("MixedContent:ReenableProtection", function() {
   docShell.mixedContentChannel = null;
 });
 
+addMessageListener("SecondScreen:tab-mirror", function(message) {
+  let app = SimpleServiceDiscovery.findAppForService(message.data.service);
+  if (app) {
+    let width = content.scrollWidth;
+    let height = content.scrollHeight;
+    let viewport = {cssWidth: width, cssHeight: height, width: width, height: height};
+    app.mirror(function() {}, content, viewport, function() {}, content);
+  }
+});
+
 addEventListener("DOMFormHasPassword", function(event) {
   InsecurePasswordUtils.checkForInsecurePasswords(event.target);
   LoginManagerContent.onFormPassword(event);
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -802,17 +802,17 @@ BrowserGlue.prototype = {
   _initServiceDiscovery: function () {
     var rokuDevice = {
       id: "roku:ecp",
       target: "roku:ecp",
       factory: function(aService) {
         Cu.import("resource://gre/modules/RokuApp.jsm");
         return new RokuApp(aService);
       },
-      mirror: false,
+      mirror: true,
       types: ["video/mp4"],
       extensions: ["mp4"]
     };
 
     // Register targets
     SimpleServiceDiscovery.registerDevice(rokuDevice);
 
     // Search for devices continuously every 120 seconds
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -90,16 +90,18 @@ when there are no windows but Firefox is
 <!ENTITY toolbarContextMenu.undoCloseTab.accesskey "U">
 
 <!ENTITY pageSourceCmd.label "Page Source">
 <!ENTITY pageSourceCmd.accesskey "o">
 <!ENTITY pageSourceCmd.commandkey "u">
 <!ENTITY pageInfoCmd.label "Page Info">
 <!ENTITY pageInfoCmd.accesskey "I">
 <!ENTITY pageInfoCmd.commandkey "i">
+<!ENTITY mirrorTabCmd.label "Mirror Tab">
+<!ENTITY mirrorTabCmd.accesskey "m">
 <!-- LOCALIZATION NOTE (enterFullScreenCmd.label, exitFullScreenCmd.label):
 These should match what Safari and other Apple applications use on OS X Lion. -->
 <!ENTITY enterFullScreenCmd.label "Enter Full Screen">
 <!ENTITY enterFullScreenCmd.accesskey "F">
 <!ENTITY exitFullScreenCmd.label "Exit Full Screen">
 <!ENTITY exitFullScreenCmd.accesskey "F">
 <!ENTITY fullScreenCmd.label "Full Screen">
 <!ENTITY fullScreenCmd.accesskey "F">
--- a/browser/modules/CastingApps.jsm
+++ b/browser/modules/CastingApps.jsm
@@ -125,16 +125,20 @@ var CastingApps = {
     let filteredServices = SimpleServiceDiscovery.services.filter(service => {
       return this.allowableExtension(video.sourceURI, service.extensions) ||
              this.allowableMimeType(video.type, service.types);
     });
 
     return filteredServices;
   },
 
+  getServicesForMirroring: function () {
+    return SimpleServiceDiscovery.services.filter(service => service.mirror);
+  },
+
   // RemoteMedia callback API methods
   onRemoteMediaStart: function (remoteMedia) {
     if (!this.session) {
       return;
     }
 
     remoteMedia.load(this.session.data);
 
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -93,17 +93,17 @@ var CastingApps = {
   serviceAdded: function(aService) {
     if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) {
       this.mirrorStartMenuId = NativeWindow.menu.add({
         name: Strings.browser.GetStringFromName("casting.mirrorTab"),
         callback: function() {
           let callbackFunc = function(aService) {
             let app = SimpleServiceDiscovery.findAppForService(aService);
             if (app) {
-              app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this));
+              app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this), window.BrowserApp.selectedBrowser.contentWindow);
             }
           }.bind(this);
 
           this.prompt(callbackFunc, aService => aService.mirror);
         }.bind(this),
         parent: NativeWindow.menu.toolsMenuID
       });
 
--- a/toolkit/modules/secondscreen/RokuApp.jsm
+++ b/toolkit/modules/secondscreen/RokuApp.jsm
@@ -142,39 +142,39 @@ RokuApp.prototype = {
       }
     } else {
       if (callback) {
         callback();
       }
     }
   },
 
-  mirror: function(callback, win, viewport, mirrorStartedCallback) {
+  mirror: function(callback, win, viewport, mirrorStartedCallback, contentWindow) {
     if (this.mirrorAppID == -1) {
       // The status function may not have been called yet if mirrorAppID is -1
-      this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback));
+      this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback, contentWindow));
     } else {
-      this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback);
+      this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback, contentWindow);
     }
   },
 
-  _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) {
+  _createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback, contentWindow) {
     if (this.mirrorAppID == -1) {
       // TODO: Inform user to install Roku WebRTC Player Channel.
       log("RokuApp: Failed to find Mirror App ID.");
     } else {
       let url = this.resourceURL + "launch/" + this.mirrorAppID;
       let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
       xhr.open("POST", url, true);
       xhr.overrideMimeType("text/plain");
 
       xhr.addEventListener("load", (function() {
         // 204 seems to be returned if the channel is already running
         if ((xhr.status == 200) || (xhr.status == 204)) {
-          this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback);
+          this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback, contentWindow);
         }
       }).bind(this), false);
 
       xhr.addEventListener("error", function() {
         log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME);
       }, false);
 
       xhr.send(null);
@@ -274,25 +274,25 @@ RemoteMedia.prototype = {
     this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster });
   },
 
   get status() {
     return this._status;
   }
 }
 
-function RemoteMirror(url, win, viewport, mirrorStartedCallback) {
+function RemoteMirror(url, win, viewport, mirrorStartedCallback, contentWindow) {
   this._serverURI = Services.io.newURI(url , null, null);
   this._window = win;
   this._iceCandidates = [];
   this.mirrorStarted = mirrorStartedCallback;
 
   // This code insures the generated tab mirror is not wider than 800 nor taller than 600
   // Better dimensions should be chosen after the Roku Channel is working.
-  let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+  let windowId = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
   let cWidth =  Math.max(viewport.cssWidth, viewport.width);
   let cHeight = Math.max(viewport.cssHeight, viewport.height);
 
   const MAX_WIDTH = 800;
   const MAX_HEIGHT = 600;
 
   let tWidth = 0;
   let tHeight = 0;