Bug 590715 - Fullscreen video for Fennec [r=mfinkle]
authorMatt Brubeck <mbrubeck@mozilla.com>
Sat, 16 Oct 2010 06:25:15 -0700
changeset 66844 14d65e6d45002124b5cc92164abe4fe331864efb
parent 66843 36c10444ee90ad862f133f5b63e4bb0d4f3cea0f
child 66845 2e1cebf652f1e492313e2ff0c2126f6c285dce07
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs590715
Bug 590715 - Fullscreen video for Fennec [r=mfinkle]
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/content.js
mobile/chrome/content/fullscreen-video.js
mobile/chrome/content/fullscreen-video.xhtml
mobile/chrome/jar.mn
mobile/locales/en-US/chrome/browser.dtd
mobile/modules/Makefile.in
mobile/modules/video.jsm
mobile/themes/core/browser.css
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -16,16 +16,17 @@
  *
  * The Initial Developer of the Original Code is
  * Mozilla Corporation.
  * Portions created by the Initial Developer are Copyright (C) 2008
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Mark Finkle <mfinkle@mozilla.com>
+ *   Matt Brubeck <mbrubeck@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -477,16 +478,17 @@ var BrowserUI = {
       // We unhide the panelUI so the XBL and settings can initialize
       Elements.panelUI.hidden = false;
 
       // Init the views
       ExtensionsView.init();
       DownloadsView.init();
       PreferencesView.init();
       ConsoleView.init();
+      FullScreenVideo.init();
 
 #ifdef MOZ_IPC
       // Pre-start the content process
       Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
           .ensureContentProcess();
 #endif
 
 #ifdef MOZ_SERVICES_SYNC
@@ -2541,24 +2543,19 @@ var ContextCommands = {
     let state = ContextHelper.popupState;
     SharingUI.show(state.linkURL, state.linkTitle);
   },
 
   shareMedia: function cc_shareMedia() {
     SharingUI.show(ContextHelper.popupState.mediaURL, null);
   },
 
-  playMedia: function cc_playVideo() {
+  sendCommand: function cc_playVideo(aCommand) {
     let browser = ContextHelper.popupState.target;
-    browser.messageManager.sendAsyncMessage("Browser:MediaCommand", { command: "play"});
-  },
-
-  pauseMedia: function cc_playVideo() {
-    let browser = ContextHelper.popupState.target;
-    browser.messageManager.sendAsyncMessage("Browser:MediaCommand", { command: "pause" });
+    browser.messageManager.sendAsyncMessage("Browser:ContextCommand", { command: aCommand });
   },
 
   editBookmark: function cc_editBookmark() {
     let target = ContextHelper.popupState.target;
     target.startEditing();
   },
 
   removeBookmark: function cc_removeBookmark() {
@@ -2713,8 +2710,74 @@ var BadgeHandlers = {
       aValue = this.clampBadge(aValue);
       aBadge.set(aValue);
     } else {
       aBadge.set("");
     }
     return aValue;
   }
 };
+
+var FullScreenVideo = {
+  browser: null,
+
+  init: function fsv_init() {
+    messageManager.addMessageListener("Browser:FullScreenVideo:Start", this.show.bind(this));
+    messageManager.addMessageListener("Browser:FullScreenVideo:Close", this.hide.bind(this));
+  },
+
+  show: function fsv_show() {
+    this.createBrowser();
+    window.fullScreen = true;
+    BrowserUI.pushPopup(this, this.browser);
+  },
+
+  hide: function fsv_hide() {
+    this.destroyBrowser();
+    window.fullScreen = false;
+    BrowserUI.popPopup();
+  },
+
+  createBrowser: function fsv_createBrowser() {
+    let browser = this.browser = document.createElement("browser");
+    browser.className = "window-width window-height full-screen";
+    browser.setAttribute("type", "content");
+    browser.setAttribute("remote", "true");
+    browser.setAttribute("src", "chrome://browser/content/fullscreen-video.xhtml");
+    document.getElementById("main-window").appendChild(browser);
+
+    let mm = browser.messageManager;
+    mm.loadFrameScript("chrome://browser/content/fullscreen-video.js", true);
+
+    browser.addEventListener("TapDown", this, true);
+    browser.addEventListener("TapSingle", this, false);
+
+    return browser;
+  },
+
+  destroyBrowser: function fsv_destroyBrowser() {
+    let browser = this.browser;
+    browser.removeEventListener("TapDown", this, false);
+    browser.removeEventListener("TapSingle", this, false);
+    browser.parentNode.removeChild(browser);
+    this.browser = null;
+  },
+
+  handleEvent: function fsv_handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "TapDown":
+        this._dispatchMouseEvent("Browser:MouseDown", aEvent.clientX, aEvent.clientY);
+        break;
+      case "TapSingle":
+        this._dispatchMouseEvent("Browser:MouseUp", aEvent.clientX, aEvent.clientY);
+        break;
+    }
+  },
+
+  _dispatchMouseEvent: function fsv_dispatchMouseEvent(aName, aX, aY) {
+    let pos = this.browser.transformClientToBrowser(aX, aY);
+    this.browser.messageManager.sendAsyncMessage(aName, {
+      x: pos.x,
+      y: pos.y,
+      messageId: null
+    });
+  }
+};
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -160,18 +160,18 @@ function onDebugKeyPress(ev) {
     break;
 #endif
   default:
     break;
   }
 }
 
 var Browser = {
-  _tabs : [],
-  _selectedTab : null,
+  _tabs: [],
+  _selectedTab: null,
   windowUtils: window.QueryInterface(Ci.nsIInterfaceRequestor)
                      .getInterface(Ci.nsIDOMWindowUtils),
   controlsScrollbox: null,
   controlsScrollboxScroller: null,
   pageScrollbox: null,
   pageScrollboxScroller: null,
   styles: {},
 
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -559,22 +559,25 @@
             <label value="&contextSaveImage.label;"/>
           </richlistitem>
           <richlistitem class="context-command" id="context-share-link" type="link-shareable" onclick="ContextCommands.shareLink();">
             <label value="&contextShareLink.label;"/>
           </richlistitem>
           <richlistitem class="context-command" id="context-share-image" type="image-shareable" onclick="ContextCommands.shareMedia();">
             <label value="&contextShareImage.label;"/>
           </richlistitem>
-          <richlistitem class="context-command" id="context-play-media" type="media-paused" onclick="ContextCommands.playMedia();">
+          <richlistitem class="context-command" id="context-play-media" type="media-paused" onclick="ContextCommands.sendCommand('play');">
             <label value="&contextPlayMedia.label;"/>
           </richlistitem>
-          <richlistitem class="context-command" id="context-pause-video" type="media-playing" onclick="ContextCommands.pauseMedia();">
+          <richlistitem class="context-command" id="context-pause-video" type="media-playing" onclick="ContextCommands.sendCommand('pause');">
             <label value="&contextPauseMedia.label;"/>
           </richlistitem>
+          <richlistitem class="context-command" id="context-fullscreen" type="video" onclick="ContextCommands.sendCommand('fullscreen');">
+            <label value="&contextFullScreen.label;"/>
+          </richlistitem>
           <richlistitem class="context-command" id="context-save-video" type="video" onclick="ContextCommands.saveImage();">
             <label value="&contextSaveVideo.label;"/>
           </richlistitem>
           <richlistitem class="context-command" id="context-share-video" type="video-shareable" onclick="ContextCommands.shareMedia();">
             <label value="&contextShareVideo.label;"/>
           </richlistitem>
           <richlistitem class="context-command" id="context-editbookmark" type="edit-bookmark" onclick="ContextCommands.editBookmark();">
             <label value="&contextEditBookmark.label;"/>
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -651,17 +651,17 @@ var ContextHandler = {
     if (aURI)
       return aURI.scheme;
     return null;
   },
 
   init: function ch_init() {
     addEventListener("contextmenu", this, false);
     addEventListener("pagehide", this, false);
-    addMessageListener("Browser:MediaCommand", this, false);
+    addMessageListener("Browser:ContextCommand", this, false);
     this.popupNode = null;
   },
 
   reset: function ch_reset() {
     this.popupNode = null;
   },
 
   handleEvent: function ch_handleEvent(aEvent) {
@@ -730,20 +730,33 @@ var ContextHandler = {
         state.types.push(this._types[i].name);
 
     state.messageId = this.messageId;
 
     sendAsyncMessage("Browser:ContextMenu", state);
   },
 
   receiveMessage: function ch_receiveMessage(aMessage) {
-    switch (aMessage.name) {
-      case "Browser:MediaCommand":
-        if (this.popupNode instanceof Ci.nsIDOMHTMLMediaElement)
-          this.popupNode[aMessage.json.command]();
+    let node = this.popupNode;
+    let command = aMessage.json.command;
+
+    switch (command) {
+      case "play":
+      case "pause":
+        if (node instanceof Ci.nsIDOMHTMLMediaElement)
+          node[command]();
+        break;
+
+      case "fullscreen":
+        if (node instanceof Ci.nsIDOMHTMLVideoElement) {
+          node.pause();
+          Cu.import("resource:///modules/video.jsm");
+          Video.fullScreenSourceElement = node;
+          sendAsyncMessage("Browser:FullScreenVideo:Start");
+        }
         break;
     }
   },
 
   /**
    * For add-ons to add new types and data to the ContextMenu message.
    *
    * @param aName A string to identify the new type.
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/fullscreen-video.js
@@ -0,0 +1,47 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Matt Brubeck <mbrubeck@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+function closeFullScreen() {
+  sendAsyncMessage("Browser:FullScreenVideo:Close");
+}
+
+addEventListener("click", function(aEvent) {
+  if (aEvent.target.id == "close")
+    closeFullScreen();
+}, false);
+
+addEventListener("CloseVideo", closeFullScreen, false);
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/fullscreen-video.xhtml
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html>
+<!--
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Dão Gottwald <dao@mozilla.com>
+#   Matt Brubeck <mbrubeck@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" accelerated="11">
+<head>
+  <style type="text/css"><![CDATA[
+    html,
+    body,
+    video {
+      height: 100%;
+    }
+    body {
+      margin: 0;
+      background: black;
+      overflow: -moz-hidden-unscrollable;
+    }
+    body.userIdle {
+      cursor: none;
+    }
+    video {
+      width: 100%;
+      max-height: 100%;
+    }
+    body.loadingdata > video,
+    body.loadingdata > #close,
+    body.userIdle > #close {
+      visibility: hidden;
+    }
+    #close {
+      position: absolute;
+      top: 0;
+      right: 0;
+      width: 32px;
+      height: 32px;
+      background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAB3RJTUUH2AUXDg4nBoBhWwAAAAlwSFlzAAAPYAAAD2ABenhFjQAAAARnQU1BAACxjwv8YQUAAAEYSURBVHjaY2BgYOAAYtXk5OQAEA3EnAzkAUYg5gFidahZikDMBpJQvXHjxqP/QHDhwoUHmpqarlCFpBou4OzsHH7nzp3XILOOHDlyHigmz1BQUBD5Hwlcu3bthYaGhhsJloANd3Jyinz8+PEnmDl//vz5Z2RkZAFSoAGzlQxLsBoOAidPnrwJlFMAKeIBeQ1dARGW4DQcpFdLS8sZFp94FeKwhBg93KRq4CFDLcmu4gViQXIMJ8oSJSWlIBsbmyRyDSdoCTCfvH/w4MFXCpM1fkuoYTiyJYLW1tbJ9+/f/4Zu+MWLF98pKysHQOMFJ2Aix2ZqAJoGEU0jmabJlKYZjaZFBc0LO5oX17StcOhRZdK20megcbMFAI0gGfYvgPhiAAAAAElFTkSuQmCC) center center no-repeat;
+    }
+  ]]></style>
+  <script type="application/javascript;version=1.8"><![CDATA[
+
+Components.utils.import("resource:///modules/video.jsm");
+
+var contentVideo = Video.fullScreenSourceElement;
+Video.fullScreenSourceElement = null;
+
+var video;
+
+function init() {
+  video = document.querySelector("video");
+
+  video.addEventListener("loadeddata", function () {
+    video.removeEventListener("loadeddata", arguments.callee, false);
+    video.volume = contentVideo.volume;
+    video.muted = contentVideo.muted;
+    video.poster = contentVideo.poster;
+
+    if (contentVideo.currentTime && !contentVideo.ended) {
+      video.addEventListener("seeked", function () {
+        video.removeEventListener("seeked", arguments.callee, false);
+        playbackStarts();
+      }, false);
+
+      video.currentTime = contentVideo.currentTime;
+    } else {
+      playbackStarts();
+    }
+
+    showUI();
+    resetIdleTimer();
+
+    video.controls = true;
+    video.play();
+  }, false);
+
+  // Automatically close this window when the playback ended, unless the user
+  // interacted with it.
+  video.addEventListener("ended", close, false);
+  window.addEventListener("click", cancelAutoClose, false);
+  window.addEventListener("keypress", cancelAutoClose, false);
+
+  video.addEventListener("seeked", hideUI, false);
+  video.addEventListener("seeking", showUI, false);
+  video.addEventListener("pause", showUI, false);
+  video.addEventListener("ended", showUI, false);
+
+  window.addEventListener("click", function () {
+    toggleUI();
+    resetIdleTimer();
+  }, false);
+
+  video.mozLoadFrom(contentVideo);
+}
+
+window.addEventListener("unload", function () {
+  if (video.currentSrc) {
+    contentVideo.currentTime = video.currentTime;
+    contentVideo.volume = video.volume;
+    contentVideo.muted = video.muted;
+    if (!video.paused && !video.ended) {
+      video.pause();
+      contentVideo.play();
+    }
+  }
+}, false);
+
+window.addEventListener("keypress", function (event) {
+  if (event.keyCode == event.DOM_VK_ESCAPE) {
+    close();
+    return;
+  }
+
+  resetIdleTimer();
+
+  if (!video.controls &&
+      String.fromCharCode(event.charCode) == " ")
+    video.pause();
+}, false);
+
+function playbackStarts() {
+  // Loading the data from the content video may take a second or two. We hide
+  // the video during that period.
+  document.body.classList.remove("loadingdata");
+  video.focus();
+}
+
+function close() {
+  let event = document.createEvent("Events");
+  event.initEvent("CloseVideo", true, true);
+  window.dispatchEvent(event);
+}
+
+function cancelAutoClose() {
+  video.removeEventListener("ended", close, false);
+  window.removeEventListener("click", cancelAutoClose, false);
+  window.removeEventListener("keypress", cancelAutoClose, false);
+}
+
+var idleTimer;
+function resetIdleTimer() {
+  if (idleTimer) {
+    clearTimeout(idleTimer);
+    idleTimer = 0;
+  }
+  idleTimer = setTimeout(function () {
+    idleTimer = 0;
+    hideUI();
+  }, 5000);
+}
+
+var showingUI = false;
+
+function toggleUI() {
+  if (showingUI)
+    hideUI();
+  else
+    showUI();
+}
+
+function showUI() {
+  showingUI = true;
+  document.body.classList.remove("userIdle");
+}
+
+function hideUI() {
+  showingUI = false;
+  document.body.classList.add("userIdle");
+}
+
+  ]]></script>
+</head>
+<body class="loadingdata" onload="init();">
+  <span id="close"/>
+  <video/>
+</body>
+</html>
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -54,10 +54,12 @@ chrome.jar:
   content/prompt/select.xul            (content/prompt/select.xul)
   content/prompt/prompt.js             (content/prompt/prompt.js)
   content/share.xul                    (content/share.xul)
   content/AnimatedZoom.js              (content/AnimatedZoom.js)
 #ifdef MOZ_SERVICES_SYNC
   content/sync.js                      (content/sync.js)
 #endif
   content/LoginManagerChild.js         (content/LoginManagerChild.js)
+  content/fullscreen-video.js          (content/fullscreen-video.js)
+  content/fullscreen-video.xhtml       (content/fullscreen-video.xhtml)
 
 % override chrome://global/content/config.xul chrome://browser/content/config.xul
--- a/mobile/locales/en-US/chrome/browser.dtd
+++ b/mobile/locales/en-US/chrome/browser.dtd
@@ -91,16 +91,17 @@
 <!ENTITY contextSaveLink.label        "Save Link">
 <!ENTITY contextSaveImage.label       "Save Image">
 <!ENTITY contextShareLink.label       "Share Link">
 <!ENTITY contextShareImage.label      "Share Image">
 <!ENTITY contextSaveVideo.label       "Save Video">
 <!ENTITY contextShareVideo.label      "Share Video">
 <!ENTITY contextPlayMedia.label       "Play">
 <!ENTITY contextPauseMedia.label      "Pause">
+<!ENTITY contextFullScreen.label      "Full Screen">
 <!ENTITY contextEditBookmark.label    "Edit">
 <!ENTITY contextRemoveBookmark.label  "Remove">
 
 <!ENTITY pageactions.saveas.pdf      "Save As PDF">
 <!ENTITY pageactions.share.page      "Share Page">
 <!ENTITY pageactions.password.forget "Forget Password">
 <!ENTITY pageactions.reset           "Clear Site Preferences">
 <!ENTITY pageactions.findInPage      "Find In Page">
--- a/mobile/modules/Makefile.in
+++ b/mobile/modules/Makefile.in
@@ -39,15 +39,16 @@ DEPTH      = ../..
 topsrcdir  = @top_srcdir@
 srcdir     = @srcdir@
 VPATH      = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
   linuxTypes.jsm \
+  video.jsm \
   $(NULL)
 
 EXTRA_PP_JS_MODULES = \
   contacts.jsm \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mobile/modules/video.jsm
@@ -0,0 +1,5 @@
+var EXPORTED_SYMBOLS = ["Video"];
+
+var Video = {
+  fullScreenSourceElement: null
+};
--- a/mobile/themes/core/browser.css
+++ b/mobile/themes/core/browser.css
@@ -1520,8 +1520,14 @@ echrome-select-option[disabled="true"] {
   -moz-border-radius: 0 0 8px 8px;
 }
 
 /* Force any command tap to highlight */
 .context-command:hover:active {
   background: #8db8d8;
 }
 
+
+/* full-screen video ------------------------------------------------------- */
+.full-screen {
+  position: absolute;
+  z-index: 500;
+}