merge m-c to fx-team
authorTim Taubert <tim.taubert@gmx.de>
Tue, 06 Dec 2011 08:55:18 +0100
changeset 83112 338fae43b9d0d174fb58e7a086f9f8de565c420c
parent 83099 fafaf614791f698e9f66902560206e37065eee39 (current diff)
parent 83111 290d329672e5b696809a8958e7e4c3408c01e2c8 (diff)
child 83136 bf8259fcab61b5966835b7cfc21ed5565c42fcfc
push id519
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 00:38:35 +0000
treeherdermozilla-beta@788ea1ef610b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone11.0a1
first release with
nightly win64
338fae43b9d0 / 11.0a1 / 20111206031117 / files
nightly linux32
nightly linux64
nightly mac
nightly win32
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly win64
merge m-c to fx-team
--- a/browser/base/content/aboutHome.js
+++ b/browser/base/content/aboutHome.js
@@ -175,16 +175,19 @@ function onSearchSubmit(aEvent)
   aEvent.preventDefault();
 }
 
 
 function setupSearchEngine()
 {
   gSearchEngine = JSON.parse(localStorage["search-engine"]);
 
+  if (!gSearchEngine)
+    return;
+
   // Look for extended information, like logo and links.
   let searchEngineInfo = SEARCH_ENGINES[gSearchEngine.name];
   if (searchEngineInfo) {
     for (let prop in searchEngineInfo)
       gSearchEngine[prop] = searchEngineInfo[prop];
   }
 
   // Enqueue additional params if required by the engine definition.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1958,16 +1958,18 @@
         <body>
         <![CDATA[
           Array.forEach(this.tabs, function(tab) {
             if (aTabs.indexOf(tab) == -1)
               this.hideTab(tab);
             else
               this.showTab(tab);
           }, this);
+
+          this.tabContainer.mTabstrip.ensureElementIsVisible(this.selectedTab, false);
         ]]>
         </body>
       </method>
 
       <method name="showTab">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
--- a/browser/components/certerror/content/aboutCertError.xhtml
+++ b/browser/components/certerror/content/aboutCertError.xhtml
@@ -118,21 +118,22 @@
         };
         replaceWithHost(intro);
         
         if (getCSSClass() == "expertBadCert") {
           toggle('technicalContent');
           toggle('expertContent');
         }
 
-        // if this is a Strict-Transport-Security host and the cert
-        // is bad, don't allow overrides (STS Spec section 7.3).
-        if (getCSSClass() == "badStsCert") {
+        // Disallow overrides if this is a Strict-Transport-Security
+        // host and the cert is bad (STS Spec section 7.3) or if the
+        // certerror is in a frame (bug 633691).
+        if (getCSSClass() == "badStsCert" || window != top) {
           var ec = document.getElementById('expertContent');
-          document.getElementById('errorLongContent').removeChild(ec);
+          ec.parentNode.removeChild(ec);
         }
         
         var tech = document.getElementById("technicalContentText");
         if (tech)
           tech.textContent = getDescription();
         
         addDomainErrorLink();
       }
--- a/browser/components/sessionstore/src/nsSessionStore.js
+++ b/browser/components/sessionstore/src/nsSessionStore.js
@@ -173,16 +173,26 @@ function SessionStoreService() {
   });
 
   // when crash recovery is disabled, session data is not written to disk
   XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function () {
     // get crash recovery state from prefs and allow for proper reaction to state changes
     this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true);
     return this._prefBranch.getBoolPref("sessionstore.resume_from_crash");
   });
+
+  XPCOMUtils.defineLazyGetter(this, "_max_tabs_undo", function () {
+    this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
+    return this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
+  });
+
+  XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
+    this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
+    return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
+  });
 }
 
 SessionStoreService.prototype = {
   classID: Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
                                          Ci.nsIDOMEventListener,
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
@@ -286,20 +296,16 @@ SessionStoreService.prototype = {
     }, this);
 
     var pbs = Cc["@mozilla.org/privatebrowsing;1"].
               getService(Ci.nsIPrivateBrowsingService);
     this._inPrivateBrowsing = pbs.privateBrowsingEnabled;
 
     // Do pref migration before we store any values and start observing changes
     this._migratePrefs();
-
-    // observe prefs changes so we can modify stored data to match
-    this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
-    this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
     
     // this pref is only read at startup, so no need to observe it
     this._sessionhistory_max_entries =
       this._prefBranch.getIntPref("sessionhistory.max_entries");
 
     this._restoreOnDemand =
       this._prefBranch.getBoolPref("sessionstore.restore_on_demand");
     this._prefBranch.addObserver("sessionstore.restore_on_demand", this, true);
@@ -637,21 +643,23 @@ SessionStoreService.prototype = {
 
       this._clearRestoringWindows();
       break;
     case "nsPref:changed": // catch pref changes
       switch (aData) {
       // if the user decreases the max number of closed tabs they want
       // preserved update our internal states to match that max
       case "sessionstore.max_tabs_undo":
+        this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
         for (let ix in this._windows) {
-          this._windows[ix]._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"), this._windows[ix]._closedTabs.length);
+          this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
         }
         break;
       case "sessionstore.max_windows_undo":
+        this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
         this._capClosedWindows();
         break;
       case "sessionstore.interval":
         this._interval = this._prefBranch.getIntPref("sessionstore.interval");
         // reset timer and save
         if (this._saveTimer) {
           this._saveTimer.cancel();
           this._saveTimer = null;
@@ -1089,20 +1097,19 @@ SessionStoreService.prototype = {
    *        Tab reference
    */
   onTabClose: function sss_onTabClose(aWindow, aTab) {
     // notify the tabbrowser that the tab state will be retrieved for the last time
     // (so that extension authors can easily set data on soon-to-be-closed tabs)
     var event = aWindow.document.createEvent("Events");
     event.initEvent("SSTabClosing", true, false);
     aTab.dispatchEvent(event);
-    
-    var maxTabsUndo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
+
     // don't update our internal state if we don't have to
-    if (maxTabsUndo == 0) {
+    if (this._max_tabs_undo == 0) {
       return;
     }
     
     // make sure that the tab related data is up-to-date
     var tabState = this._collectTabData(aTab);
     this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
 
     // store closed-tab data for undo
@@ -1113,18 +1120,18 @@ SessionStoreService.prototype = {
       
       this._windows[aWindow.__SSi]._closedTabs.unshift({
         state: tabState,
         title: tabTitle,
         image: aTab.getAttribute("image"),
         pos: aTab._tPos
       });
       var length = this._windows[aWindow.__SSi]._closedTabs.length;
-      if (length > maxTabsUndo)
-        this._windows[aWindow.__SSi]._closedTabs.splice(maxTabsUndo, length - maxTabsUndo);
+      if (length > this._max_tabs_undo)
+        this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo);
     }
   },
 
   /**
    * When a tab loads, save state.
    * @param aWindow
    *        Window reference
    * @param aBrowser
@@ -2513,17 +2520,17 @@ SessionStoreService.prototype = {
     //XXXzpao We should do this for _restoreLastWindow == true, but that has
     //        its own check for popups. c.f. bug 597619
     if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 &&
         this._loadState == STATE_QUITTING) {
       // prepend the last non-popup browser window, so that if the user loads more tabs
       // at startup we don't accidentally add them to a popup window
       do {
         total.unshift(lastClosedWindowsCopy.shift())
-      } while (total[0].isPopup)
+      } while (total[0].isPopup && lastClosedWindowsCopy.length > 0)
     }
 #endif
 
     if (aPinnedOnly) {
       // perform a deep copy so that existing session variables are not changed.
       total = JSON.parse(this._toJSONString(total));
       total = total.filter(function (win) {
         win.tabs = win.tabs.filter(function (tab) tab.pinned);
@@ -4259,27 +4266,26 @@ SessionStoreService.prototype = {
   },
 
   /**
    * Resize this._closedWindows to the value of the pref, except in the case
    * where we don't have any non-popup windows on Windows and Linux. Then we must
    * resize such that we have at least one non-popup window.
    */
   _capClosedWindows : function sss_capClosedWindows() {
-    let maxWindowsUndo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
-    if (this._closedWindows.length <= maxWindowsUndo)
+    if (this._closedWindows.length <= this._max_windows_undo)
       return;
-    let spliceTo = maxWindowsUndo;
+    let spliceTo = this._max_windows_undo;
 #ifndef XP_MACOSX
     let normalWindowIndex = 0;
     // try to find a non-popup window in this._closedWindows
     while (normalWindowIndex < this._closedWindows.length &&
            !!this._closedWindows[normalWindowIndex].isPopup)
       normalWindowIndex++;
-    if (normalWindowIndex >= maxWindowsUndo)
+    if (normalWindowIndex >= this._max_windows_undo)
       spliceTo = normalWindowIndex + 1;
 #endif
     this._closedWindows.splice(spliceTo, this._closedWindows.length);
   },
 
   _clearRestoringWindows: function sss__clearRestoringWindows() {
     for (let i = 0; i < this._closedWindows.length; i++) {
       delete this._closedWindows[i]._shouldRestore;
@@ -4520,17 +4526,18 @@ let XPathHelper = {
 // This is used to help meter the number of restoring tabs. This is the control
 // point for telling the next tab to restore. It gets attached to each gBrowser
 // via gBrowser.addTabsProgressListener
 let gRestoreTabsProgressListener = {
   ss: null,
   onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
     // Ignore state changes on browsers that we've already restored and state
     // changes that aren't applicable.
-    if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
+    if (aBrowser.__SS_restoreState &&
+        aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
         aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
         aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
         aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
       // We need to reset the tab before starting the next restore.
       let tab = this.ss._getTabForBrowser(aBrowser);
       this.ss._resetTabRestoringState(tab);
       this.ss.restoreNextTab();
     }
--- a/browser/components/tabview/test/Makefile.in
+++ b/browser/components/tabview/test/Makefile.in
@@ -159,16 +159,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug673196.js \
                  browser_tabview_bug673729.js \
                  browser_tabview_bug677310.js \
                  browser_tabview_bug679853.js \
                  browser_tabview_bug681599.js \
                  browser_tabview_bug685476.js \
                  browser_tabview_bug685692.js \
                  browser_tabview_bug686654.js \
+                 browser_tabview_bug696602.js \
                  browser_tabview_bug697390.js \
                  browser_tabview_bug705621.js \
                  browser_tabview_bug706430.js \
                  browser_tabview_bug706736.js \
                  browser_tabview_click_group.js \
                  browser_tabview_dragdrop.js \
                  browser_tabview_exit_button.js \
                  browser_tabview_expander.js \
new file mode 100644
--- /dev/null
+++ b/browser/components/tabview/test/browser_tabview_bug696602.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+let win;
+
+function test() {
+  waitForExplicitFinish();
+
+  newWindowWithTabView(function(newWin) {
+    win = newWin;
+
+    registerCleanupFunction(function() {
+      win.close();
+    });
+
+    let cw = win.TabView.getContentWindow();
+    let groupItemOne = cw.GroupItems.groupItems[0];
+    let groupItemTwo = createGroupItemWithBlankTabs(win, 300, 300, 20, 10);
+
+    is(win.gBrowser.tabContainer.getAttribute("overflow"), "true",
+       "The tabstrip is overflow");
+
+    is(groupItemOne.getChildren().length, 10, "Group one has 10 tabs");
+    is(groupItemTwo.getChildren().length, 10, "Group two has 10 tabs");
+
+    checkSelectedTabVisible("two", function() {
+      showTabView(function() {
+        checkSelectedTabVisible("one", finish);
+        groupItemOne.getChild(9).zoomIn();
+      }, win);
+    });
+    groupItemTwo.getChild(9).zoomIn();
+  }, function(newWin) {
+    for (let i = 0; i < 9; i++)
+      newWin.gBrowser.addTab();
+  });
+}
+
+function isSelectedTabVisible() {
+  let tabstrip = win.gBrowser.tabContainer.mTabstrip;
+  let scrollRect = tabstrip.scrollClientRect;
+  let tab = win.gBrowser.selectedTab.getBoundingClientRect();
+
+  return (scrollRect.left <= tab.left && tab.right <= scrollRect.right);
+}
+
+function checkSelectedTabVisible(groupName, callback) {
+  whenTabViewIsHidden(function() {
+    ok(isSelectedTabVisible(), "Group " + groupName + " selected tab is visible");
+    callback();
+  }, win);
+}
--- a/browser/components/tabview/ui.js
+++ b/browser/components/tabview/ui.js
@@ -147,16 +147,20 @@ let UI = {
   // Variable: ignoreKeypressForSearch
   // Used to prevent keypress being handled after quitting search mode.
   ignoreKeypressForSearch: false,
 
   // Variable: _lastOpenedTab
   // Used to keep track of the last opened tab.
   _lastOpenedTab: null,
 
+  // Variable: _originalSmoothScroll
+  // Used to keep track of the tab strip smooth scroll value.
+  _originalSmoothScroll: null,
+
   // ----------
   // Function: toString
   // Prints [UI] for debug use
   toString: function UI_toString() {
     return "[UI]";
   },
 
   // ----------
@@ -509,16 +513,21 @@ let UI = {
   // Parameters:
   //   zoomOut - true for zoom out animation, false for nothing.
   showTabView: function UI_showTabView(zoomOut) {
     if (this.isTabViewVisible() || this._isChangingVisibility)
       return;
 
     this._isChangingVisibility = true;
 
+    // store tab strip smooth scroll value and disable it.
+    let tabStrip = gBrowser.tabContainer.mTabstrip;
+    this._originalSmoothScroll = tabStrip.smoothScroll;
+    tabStrip.smoothScroll = false;
+
     // initialize the direction of the page
     this._initPageDirection();
 
     var self = this;
     var currentTab = this._currentTab;
 
     this._reorderTabItemsOnShow.forEach(function(groupItem) {
       groupItem.reorderTabItemsBasedOnTabOrder();
@@ -604,16 +613,17 @@ let UI = {
     // as well as avoiding the flash of black as we animate out
     gTabViewFrame.style.marginTop = gBrowser.boxObject.y + "px";
 #endif
     gTabViewDeck.selectedPanel = gBrowserPanel;
     gWindow.TabsInTitlebar.allowedBy("tabview-open", true);
     gBrowser.selectedBrowser.focus();
 
     gBrowser.updateTitlebar();
+    gBrowser.tabContainer.mTabstrip.smoothScroll = this._originalSmoothScroll;
 #ifdef XP_MACOSX
     this.setTitlebarColors(false);
 #endif
     Storage.saveVisibilityData(gWindow, "false");
 
     this._isChangingVisibility = false;
 
     let event = document.createEvent("Events");
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -280,16 +280,17 @@ Highlighter.prototype = {
     };
   },
 
   /**
    * Destroy the nodes.
    */
   destroy: function Highlighter_destroy()
   {
+    this.IUI.win.clearTimeout(this.transitionDisabler);
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("resize", this, true);
     this.boundCloseEventHandler = null;
     this._contentRect = null;
     this._highlightRect = null;
     this._highlighting = false;
     this.veilTopBox = null;
     this.veilLeftBox = null;
--- a/toolkit/content/widgets/scrollbox.xml
+++ b/toolkit/content/widgets/scrollbox.xml
@@ -484,17 +484,18 @@
         // Because of this, we need to avoid scrolling chaos on trackpads
         // and mouse wheels that support simultaneous scrolling in both axes.
         // We do this by scrolling only when the last two scroll events were
         // on the same axis as the current scroll event.
         else {
           let isVertical = event.axis == event.VERTICAL_AXIS;
 
           if (this._prevMouseScrolls.every(function(prev) prev == isVertical))
-            this.scrollByIndex(this._isRTLScrollbox ? -event.detail : event.detail);
+            this.scrollByIndex(isVertical && this._isRTLScrollbox ? -event.detail :
+                                                                    event.detail);
 
           if (this._prevMouseScrolls.length > 1)
             this._prevMouseScrolls.shift();
           this._prevMouseScrolls.push(isVertical);
         }
 
         event.stopPropagation();
         event.preventDefault();
--- a/toolkit/content/widgets/videocontrols.css
+++ b/toolkit/content/widgets/videocontrols.css
@@ -89,8 +89,22 @@ html|span.statActivity[seeking] > html|s
 
 .controlBar[size="small"] .scrubberStack,
 .controlBar[size="small"] .backgroundBar,
 .controlBar[size="small"] .bufferBar,
 .controlBar[size="small"] .progressBar,
 .controlBar[size="small"] .scrubber {
   visibility: hidden;
 }
+
+/* Error description formatting */
+.errorLabel {
+  display: none;
+}
+
+[error="errorAborted"]         > [anonid="errorAborted"],
+[error="errorNetwork"]         > [anonid="errorNetwork"],
+[error="errorDecode"]          > [anonid="errorDecode"],
+[error="errorSrcNotSupported"] > [anonid="errorSrcNotSupported"],
+[error="errorNoSource"]        > [anonid="errorNoSource"],
+[error="errorGeneric"]         > [anonid="errorGeneric"] {
+  display: inline;
+}
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -193,16 +193,22 @@
         <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
     </resources>
 
     <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
                  class="mediaControlsFrame">
         <stack flex="1">
             <vbox flex="1" class="statusOverlay" hidden="true">
                 <box class="statusIcon"/>
+                <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
+                <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
+                <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
+                <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
+                <label class="errorLabel" anonid="errorNoSource">&error.noSource;</label>
+                <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
             </vbox>
 
             <vbox class="statsOverlay" hidden="true">
                 <html:div class="statsDiv" xmlns="http://www.w3.org/1999/xhtml">
                     <table class="statsTable">
                         <tr>
                             <td class="statLabel">&stats.media;</td>
                             <td class="statValue filename"><span class="statFilename"/></td>
@@ -407,18 +413,19 @@
                     // and we'll figure out the exact state then.)
                     this.bufferBar.setAttribute("max", 100);
                     if (this.video.readyState >= this.video.HAVE_METADATA)
                         this.showBuffered();
                     else
                         this.bufferBar.setAttribute("value", 0);
 
                     // Set the current status icon.
-                    if (this.video.error || this.video.networkState == this.video.NETWORK_NO_SOURCE) {
+                    if (this.hasError()) {
                         this.statusIcon.setAttribute("type", "error");
+                        this.updateErrorText();
                         this.setupStatusFader(true);
                     } else {
                         this.statusIcon.setAttribute("type", "throbber");
                         this.setupStatusFader();
                     }
 
                     // An event handler for |onresize| should be added when bug 227495 is fixed.
                     let controlBarWasHidden = this.controlBar.getAttribute("hidden") == "true";
@@ -447,17 +454,17 @@
                     var enabled = !this.isAudioOnly;
 
                     // Allow tests to explicitly suppress the fading of controls.
                     if (this.video.hasAttribute("mozNoDynamicControls"))
                         enabled = false;
 
                     // If the video hits an error, suppress controls if it
                     // hasn't managed to do anything else yet.
-                    if (!this.firstFrameShown && (this.video.error || this.video.networkState == this.video.NETWORK_NO_SOURCE))
+                    if (!this.firstFrameShown && this.hasError())
                         enabled = false;
 
                     return enabled;
                 },
                 
                 handleEvent : function (aEvent) {
                     this.log("Got media event ----> " + aEvent.type);
 
@@ -509,16 +516,18 @@
                             this.showDuration(Math.round(this.video.duration * 1000));
                             break;
                         case "loadeddata":
                             this.firstFrameShown = true;
                             this.setupStatusFader();
                             break;
                         case "loadstart":
                             this.maxCurrentTimeSeen = 0;
+                            this.controlsSpacer.removeAttribute("aria-label");
+                            this.statusOverlay.removeAttribute("error");
                             this.statusIcon.setAttribute("type", "throbber");
                             this.isAudioOnly = (this.video instanceof HTMLAudioElement);
                             this.setPlayButtonState(true);
                             break;
                         case "progress":
                             this.statusIcon.removeAttribute("stalled");
                             this.showBuffered();
                             this.setupStatusFader();
@@ -581,42 +590,83 @@
                             // under either of the following conditions:
                             // 1. The video has its error attribute set; this means we're loading
                             //    from our src attribute, and the load failed, or we we're loading 
                             //    from source children and the decode or playback failed after we 
                             //    determined our selected resource was playable.
                             // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
                             //    loading from child source elements, but we were unable to select
                             //    any of the child elements for playback during resource selection.
-                            if (this.video.error || this.video.networkState == this.video.NETWORK_NO_SOURCE) {
+                            if (this.hasError()) {
                               this.statusIcon.setAttribute("type", "error");
+                              this.updateErrorText();
                               this.setupStatusFader(true);
                               // If video hasn't shown anything yet, disable the controls.
                               if (!this.firstFrameShown)
                                   this.startFadeOut(this.controlBar);
                             }
                             break;
                         default:
                             this.log("!!! event " + aEvent.type + " not handled!");
                     }
                 },
 
                 terminateEventListeners : function () {
                     if (this.statsInterval) {
                         clearInterval(this.statsInterval);
                         this.statsInterval = null;
                     }
-                    for each (var event in this.videoEvents)
+                    for each (let event in this.videoEvents)
                         this.video.removeEventListener(event, this, false);
                     this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false);
-                    this.video.ownerDocument.removeEventListener("mozfullscreenchange", this.setFullscreenButtonState, false);
                     delete this._handleCustomEventsBound;
+                    this.video.ownerDocument.removeEventListener("mozfullscreenchange", this._setFullscreenButtonStateBound, false);
+                    delete this._setFullscreenButtonStateBound;
                     this.log("--- videocontrols terminated ---");
                 },
 
+                hasError : function () {
+                    return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
+                },
+
+                updateErrorText : function () {
+                    let error;
+                    let v = this.video;
+                    // It is possible to have both v.networkState == NETWORK_NO_SOURCE
+                    // as well as v.error being non-null. In this case, we will show
+                    // the v.error.code instead of the v.networkState error.
+                    if (v.error) {
+                        switch (v.error.code) {
+                          case v.error.MEDIA_ERR_ABORTED:
+                              error = "errorAborted";
+                              break;
+                          case v.error.MEDIA_ERR_NETWORK:
+                              error = "errorNetwork";
+                              break;
+                          case v.error.MEDIA_ERR_DECODE:
+                              error = "errorDecode";
+                              break;
+                          case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
+                              error = "errorSrcNotSupported";
+                              break;
+                          default:
+                              error = "errorGeneric";
+                              break;
+                         }
+                    } else if (v.networkState == v.NETWORK_NO_SOURCE) {
+                        error = "errorNoSource";
+                    } else {
+                        return; // No error found.
+                    }
+
+                    let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
+                    this.controlsSpacer.setAttribute("aria-label", label.textContent);
+                    this.statusOverlay.setAttribute("error", error);
+                },
+
                 formatTime : function(aTime) {
                     // Format the duration as "h:mm:ss" or "m:ss"
                     aTime = Math.round(aTime / 1000);
                     let hours = Math.floor(aTime / 3600);
                     let mins  = Math.floor((aTime % 3600) / 60);
                     let secs  = Math.floor(aTime % 60);
                     let timeString;
                     if (secs < 10)
@@ -733,21 +783,53 @@
                     // Ignore events caused by transitions between mute button and volumeStack,
                     // or between nodes inside these two elements.
                     if (this.isEventWithin(event, this.muteButton, this.volumeStack))
                         return;
                     var isMouseOver = (event.type == "mouseover");
                     this.startFade(this.volumeStack, isMouseOver);
                 },
 
+                _hideControlsTimeout : 0,
+                _hideControlsFn : function () {
+                    if (!Utils.scrubber.isDragging)
+                        Utils.startFade(Utils.controlBar, false);
+                },
+                HIDE_CONTROLS_TIMEOUT_MS : 2000,
+                onMouseMove : function (event) {
+                    // If the controls are static, don't change anything.
+                    if (!this.dynamicControls)
+                        return;
+
+                    clearTimeout(this._hideControlsTimeout);
+
+                    // Suppress fading out the controls until the video has rendered
+                    // its first frame. But since autoplay videos start off with no
+                    // controls, let them fade-out so the controls don't get stuck on.
+                    if (!this.firstFrameShown &&
+                        !(this.video.autoplay && this.video.mozAutoplayEnabled))
+                        return;
+
+                    this.startFade(this.controlBar, true);
+                    // Only hide the controls if the mouse cursor is not left on top of
+                    // the control bar. We only need to check the Y position of the cursor
+                    // since the controls span the width of the video and are always located
+                    // at the bottom of the video.
+                    if (event.clientY < this.controlBar.getBoundingClientRect().top) {
+                        this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+                    }
+                },
+
                 onMouseInOut : function (event) {
                     // If the controls are static, don't change anything.
                     if (!this.dynamicControls)
                         return;
 
+                    clearTimeout(this._hideControlsTimeout);
+
                     // Ignore events caused by transitions between child nodes.
                     // Note that the videocontrols element is the same
                     // size as the *content area* of the video element,
                     // but this is not the same as the video element's
                     // border area if the video has border or padding.
                     if (this.isEventWithin(event, this.videocontrols))
                         return;
 
@@ -755,36 +837,43 @@
 
                     // Suppress fading out the controls until the video has rendered
                     // its first frame. But since autoplay videos start off with no
                     // controls, let them fade-out so the controls don't get stuck on.
                     if (!this.firstFrameShown && !isMouseOver &&
                         !(this.video.autoplay && this.video.mozAutoplayEnabled))
                         return;
 
-                    if (!isMouseOver)
+                    if (!isMouseOver) {
                         this.adjustControlSize();
 
-                    this.startFade(this.controlBar, isMouseOver);
+                        // Setting a timer here to handle the case where the mouse leaves
+                        // the video from hovering over the controls.
+                        this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+                    }
                 },
 
                 startFadeIn : function (element, immediate) {
                     this.startFade(element, true, immediate);
                 },
 
                 startFadeOut : function (element, immediate) {
                     this.startFade(element, false, immediate);
                 },
 
                 startFade : function (element, fadeIn, immediate) {
-                    // Bug 493523, the scrubber doesn't call valueChanged while hidden,
-                    // so our dependent state (eg, timestamp in the thumb) will be stale.
-                    // As a workaround, update it manually when it first becomes unhidden.
-                    if (element.className == "controlBar" && fadeIn && element.hidden)
-                        this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
+                    if (element.className == "controlBar" && fadeIn) {
+                        clearTimeout(this._hideControlsTimeout);
+
+                        // Bug 493523, the scrubber doesn't call valueChanged while hidden,
+                        // so our dependent state (eg, timestamp in the thumb) will be stale.
+                        // As a workaround, update it manually when it first becomes unhidden.
+                        if (element.hidden)
+                            this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
+                    }
 
                     if (immediate)
                         element.setAttribute("immediate", true);
                     else
                         element.removeAttribute("immediate");
 
                     if (fadeIn) {
                         element.setAttribute("hidden", false);
@@ -1179,33 +1268,34 @@
                     //
                     // (Note: the |controls| attribute is already handled via layout/style/html.css)
                     var shouldShow = (!(this.video.autoplay && this.video.mozAutoplayEnabled) || !this.dynamicControls);
                     this.startFade(this.controlBar, shouldShow, true);
 
                     // Use the handleEvent() callback for all media events.
                     // The "error" event listener must capture, so that it can trap error events
                     // from the <source> children, which don't bubble.
-                    for each (var event in this.videoEvents)
+                    for each (let event in this.videoEvents)
                         this.video.addEventListener(event, this, (event == "error") ? true : false);
 
                     var self = this;
                     this.muteButton.addEventListener("command", function() { self.toggleMute(); }, false);
                     this.playButton.addEventListener("command", function() { self.togglePause(); }, false);
-                    this.controlsSpacer.addEventListener("click", function(e) { if (e.button == 0) { self.togglePause(); } }, false);
+                    this.controlsSpacer.addEventListener("click", function(e) { if (e.button == 0 && !self.hasError()) { self.togglePause(); } }, false);
                     this.fullscreenButton.addEventListener("command", function() { self.toggleFullscreen(); }, false );
                     if (!this.isAudioOnly) {
                       this.muteButton.addEventListener("mouseover",  function(e) { self.onVolumeMouseInOut(e); }, false);
                       this.muteButton.addEventListener("mouseout",   function(e) { self.onVolumeMouseInOut(e); }, false);
                       this.volumeStack.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false);
                       this.volumeStack.addEventListener("mouseout",  function(e) { self.onVolumeMouseInOut(e); }, false);
                     }
 
                     this.videocontrols.addEventListener("transitionend", function(e) { self.onTransitionEnd(e); }, false);
-                    this.video.ownerDocument.addEventListener("mozfullscreenchange", this.setFullscreenButtonState, false);
+                    this._setFullscreenButtonStateBound = this.setFullscreenButtonState.bind(this);
+                    this.video.ownerDocument.addEventListener("mozfullscreenchange", this._setFullscreenButtonStateBound, false);
 
                     // Make the <video> element keyboard accessible.
                     this.video.setAttribute("tabindex", 0);
                     this.video.addEventListener("keypress", function (e) { self.keyHandler(e) }, false);
 
                     this.log("--- videocontrols initialized ---");
                 }
             }) ]]>
@@ -1219,16 +1309,20 @@
         <handler event="mouseover">
             if (!this.isTouchControl)
                 this.Utils.onMouseInOut(event);
         </handler>
         <handler event="mouseout">
             if (!this.isTouchControl)
                 this.Utils.onMouseInOut(event);
         </handler>
+        <handler event="mousemove">
+            if (!this.isTouchControl)
+                this.Utils.onMouseMove(event);
+        </handler>
     </handlers>
   </binding>
   
   <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
 
     <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
         <stack flex="1">
             <vbox flex="1" class="statusOverlay" hidden="true">
--- a/toolkit/locales/en-US/chrome/global/videocontrols.dtd
+++ b/toolkit/locales/en-US/chrome/global/videocontrols.dtd
@@ -15,14 +15,21 @@
 <!ENTITY stats.volume "Volume">
 <!ENTITY stats.channels "Channels">
 <!ENTITY stats.sampleRate "Sample Rate">
 <!ENTITY stats.framesParsed "Frames parsed">
 <!ENTITY stats.framesDecoded "Frames decoded">
 <!ENTITY stats.framesPresented "Frames presented">
 <!ENTITY stats.framesPainted "Frames painted">
 
+<!ENTITY error.aborted "Video loading stopped.">
+<!ENTITY error.network "Video playback aborted due to a network error.">
+<!ENTITY error.decode "Video can't be played because the file is corrupt.">
+<!ENTITY error.srcNotSupported "Video format or MIME type is not supported.">
+<!ENTITY error.noSource "Video not found.">
+<!ENTITY error.generic "Video playback aborted due to an unknown error.">
+
 <!-- LOCALIZATION NOTE (scrubberScale.nameFormat): the #1 string is the current
 media position, and the #2 string is the total duration. For example, when at
 the 5 minute mark in a 6 hour long video, #1 would be "5:00" and #2 would be
 "6:00:00", result string would be "5:00 of 6:00:00 elapsed".
 -->
 <!ENTITY scrubberScale.nameFormat "#1 of #2 elapsed">
--- a/toolkit/themes/pinstripe/global/media/videocontrols.css
+++ b/toolkit/themes/pinstripe/global/media/videocontrols.css
@@ -201,17 +201,17 @@ html|*.statsDiv {
   position: relative;
 }
 html|td {
   height: 1em;
   max-height: 1em;
   padding: 0 2px;
 }
 html|table {
-  font-family: Helvetica, Ariel, sans-serif;
+  font-family: Helvetica, Arial, sans-serif;
   font-size: 11px;
   color: white;
   text-shadow:
     -1px -1px 0 #000,
     1px -1px 0 #000,
     -1px 1px 0 #000,
     1px 1px 0 #000;
   min-width: 100%;
@@ -240,8 +240,22 @@ html|table {
 .statusOverlay:not([immediate]) {
   -moz-transition-property: opacity;
   -moz-transition-duration: 300ms;
   -moz-transition-delay: 750ms;
 }
 .statusOverlay[fadeout] {
   opacity: 0;
 }
+
+/* Error description formatting */
+.errorLabel {
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 11px;
+  color: #bbb;
+  text-shadow:
+    -1px -1px 0 #000,
+    1px -1px 0 #000,
+    -1px 1px 0 #000,
+    1px 1px 0 #000;
+  padding: 0 10px;
+  text-align: center;
+}
--- a/toolkit/themes/winstripe/global/media/videocontrols.css
+++ b/toolkit/themes/winstripe/global/media/videocontrols.css
@@ -210,17 +210,17 @@ html|*.statsDiv {
   position: relative;
 }
 html|td {
   height: 1em;
   max-height: 1em;
   padding: 0 2px;
 }
 html|table {
-  font-family: Helvetica, Ariel, sans-serif;
+  font-family: Helvetica, Arial, sans-serif;
   font-size: 11px;
   color: white;
   text-shadow:
     -1px -1px 0 #000,
     1px -1px 0 #000,
     -1px 1px 0 #000,
     1px 1px 0 #000;
   min-width: 100%;
@@ -249,8 +249,22 @@ html|table {
 .statusOverlay:not([immediate]) {
   -moz-transition-property: opacity;
   -moz-transition-duration: 300ms;
   -moz-transition-delay: 750ms;
 }
 .statusOverlay[fadeout] {
   opacity: 0;
 }
+
+/* Error description formatting */
+.errorLabel {
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 11px;
+  color: #bbb;
+  text-shadow:
+    -1px -1px 0 #000,
+    1px -1px 0 #000,
+    -1px 1px 0 #000,
+    1px 1px 0 #000;
+  padding: 0 10px;
+  text-align: center;
+}