Bug 513405 - Add text to video controls to describe the cause of errors. r=dolske
authorJared Wein <jwein@mozilla.com>
Mon, 05 Dec 2011 14:00:28 -0800
changeset 81401 35066406c3d7512087eabe975fdc4fd0b5c5cf1b
parent 81400 0c8f003450653a055e2ae6f509d2c22ce70fead6
child 81402 cdee05798c4abce28d16cce222ea47383b97452b
push id371
push userjwein@mozilla.com
push dateMon, 05 Dec 2011 22:29:37 +0000
treeherderfx-team@290d329672e5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdolske
bugs513405
milestone11.0a1
Bug 513405 - Add text to video controls to describe the cause of errors. r=dolske
toolkit/content/widgets/videocontrols.css
toolkit/content/widgets/videocontrols.xml
toolkit/locales/en-US/chrome/global/videocontrols.dtd
toolkit/themes/pinstripe/global/media/videocontrols.css
toolkit/themes/winstripe/global/media/videocontrols.css
--- 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,43 +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);
                     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)
@@ -1180,23 +1229,23 @@
                     //
                     // (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);
                     }
 
--- 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;
+}