Bug 1323767 - Fix the indentation and inconsistent brace-style, and switch to using ES6 method definitions in videocontrols.xml. r?ralin draft
authorJared Wein <jwein@mozilla.com>
Thu, 15 Dec 2016 12:29:04 -0500 (2016-12-15)
changeset 449994 33815cf1f150fe810bb53a5c4f2c0c6c1b2d8984
parent 449987 6dbc6e9f62a705d5f523cc750811bd01c8275ec6
child 539640 70eea43d7d6e050ecf5f87e618e1f06cbe6b9176
push id38731
push userjwein@mozilla.com
push dateThu, 15 Dec 2016 17:29:40 +0000 (2016-12-15)
reviewersralin
bugs1323767
milestone53.0a1
Bug 1323767 - Fix the indentation and inconsistent brace-style, and switch to using ES6 method definitions in videocontrols.xml. r?ralin MozReview-Commit-ID: 9GNVgX5gZpc
toolkit/content/widgets/videocontrols.xml
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -1,2208 +1,2275 @@
 <?xml version="1.0"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-
 <!DOCTYPE bindings [
-  <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
-  %videocontrolsDTD;
+<!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
+%videocontrolsDTD;
 ]>
 
 <bindings id="videoControlBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns:html="http://www.w3.org/1999/xhtml">
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl"
+          xmlns:svg="http://www.w3.org/2000/svg"
+          xmlns:html="http://www.w3.org/1999/xhtml">
+
+<binding id="timeThumb"
+         extends="chrome://global/content/bindings/scale.xml#scalethumb">
+  <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+    <xbl:children/>
+    <hbox class="timeThumb" xbl:inherits="showhours">
+      <label class="timeLabel"/>
+    </hbox>
+  </xbl:content>
+  <implementation>
+
+  <constructor>
+    <![CDATA[
+    this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
+    this.timeLabel.setAttribute("value", "0:00");
+    ]]>
+  </constructor>
+
+  <property name="showHours">
+    <getter>
+      <![CDATA[
+      return this.getAttribute("showhours") == "true";
+      ]]>
+    </getter>
+    <setter>
+      <![CDATA[
+      this.setAttribute("showhours", val);
+      // If the duration becomes known while we're still showing the value
+      // for time=0, immediately update the value to show or hide the hours.
+      // It's less intrusive to do it now than when the user clicks play and
+      // is looking right next to the thumb.
+      if (!this.timeLabel) {
+        return;
+      }
+      var displayedTime = this.timeLabel.getAttribute("value");
+      if (val && displayedTime == "0:00") {
+        this.timeLabel.setAttribute("value", "0:00:00");
+      } else if (!val && displayedTime == "0:00:00") {
+        this.timeLabel.setAttribute("value", "0:00");
+      }
+      ]]>
+    </setter>
+  </property>
 
-  <binding id="timeThumb"
-           extends="chrome://global/content/bindings/scale.xml#scalethumb">
-      <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-          <xbl:children/>
-          <hbox class="timeThumb" xbl:inherits="showhours">
-              <label class="timeLabel"/>
-          </hbox>
-      </xbl:content>
-      <implementation>
+  <method name="setTime">
+    <parameter name="time"/>
+    <body>
+      <![CDATA[
+      var timeString;
+      time = Math.round(time / 1000);
+      var hours = Math.floor(time / 3600);
+      var mins  = Math.floor((time % 3600) / 60);
+      var secs  = Math.floor(time % 60);
+      if (secs < 10) {
+        secs = "0" + secs;
+      }
+      if (hours || this.showHours) {
+        if (mins < 10) {
+          mins = "0" + mins;
+        }
+        timeString = hours + ":" + mins + ":" + secs;
+      } else {
+        timeString = mins + ":" + secs;
+      }
 
-      <constructor>
-          <![CDATA[
-          this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
-          this.timeLabel.setAttribute("value", "0:00");
-          ]]>
-      </constructor>
+      this.timeLabel.setAttribute("value", timeString);
+      ]]>
+    </body>
+  </method>
+  </implementation>
+</binding>
+
+<binding id="suppressChangeEvent"
+         extends="chrome://global/content/bindings/scale.xml#scale">
+<implementation implements="nsIXBLAccessible">
+  <!-- nsIXBLAccessible -->
+  <property name="accessibleName" readonly="true">
+    <getter>
+      if (this.type != "scrubber") {
+        return "";
+      }
+
+      var currTime = this.thumb.timeLabel.getAttribute("value");
+      var totalTime = this.durationValue;
+
+      return this.scrubberNameFormat
+                 .replace(/#1/, currTime)
+                 .replace(/#2/, totalTime);
+    </getter>
+  </property>
+
+  <constructor>
+    <![CDATA[
+    this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
+    this.durationValue = "";
+    this.valueBar = null;
+    this.isDragging = false;
+    this.isPausedByDragging = false;
 
-      <property name="showHours">
-        <getter>
-          <![CDATA[
-          return this.getAttribute("showhours") == "true";
-          ]]>
-        </getter>
-        <setter>
-          <![CDATA[
-          this.setAttribute("showhours", val);
-          // If the duration becomes known while we're still showing the value
-          // for time=0, immediately update the value to show or hide the hours.
-          // It's less intrusive to do it now than when the user clicks play and
-          // is looking right next to the thumb.
-          if (!this.timeLabel) return;
-          var displayedTime = this.timeLabel.getAttribute("value");
-          if (val && displayedTime == "0:00")
-              this.timeLabel.setAttribute("value", "0:00:00");
-          else if (!val && displayedTime == "0:00:00")
-              this.timeLabel.setAttribute("value", "0:00");
-          ]]>
-        </setter>
-      </property>
+    this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
+    this.type = this.getAttribute("class");
+    this.Utils = document.getBindingParent(this.parentNode).Utils;
+    if (this.type == "scrubber") {
+      this.valueBar = this.Utils.progressBar;
+    }
+    ]]>
+  </constructor>
+
+  <method name="valueChanged">
+    <parameter name="which"/>
+    <parameter name="newValue"/>
+    <parameter name="userChanged"/>
+    <body>
+      <![CDATA[
+      // This method is a copy of the base binding's valueChanged(), except that it does
+      // not dispatch a |change| event (to avoid exposing the event to web content), and
+      // just calls the videocontrol's seekToPosition() method directly.
+      switch (which) {
+        case "curpos":
+          if (this.type == "scrubber") {
+            // Update the time shown in the thumb.
+            this.thumb.setTime(newValue);
+            this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
+            // Update the value bar to match the thumb position.
+            let percent = newValue / this.max;
+            if (!isNaN(percent) && percent != Infinity) {
+              this.valueBar.value = Math.round(percent * 10000); // has max=10000
+            } else {
+              this.valueBar.removeAttribute("value");
+            }
+          }
+
+          // The value of userChanged is true when changing the position with the mouse,
+          // but not when pressing an arrow key. However, the base binding sets
+          // ._userChanged in its keypress handlers, so we just need to check both.
+          if (!userChanged && !this._userChanged) {
+            return;
+          }
+          this.setAttribute("value", newValue);
+
+          if (this.type == "scrubber") {
+            this.Utils.seekToPosition(newValue);
+          } else if (this.type == "volumeControl") {
+            this.Utils.setVolume(newValue / 100);
+          }
+          break;
+
+        case "minpos":
+          this.setAttribute("min", newValue);
+          break;
+
+        case "maxpos":
+          if (this.type == "scrubber") {
+            // Update the value bar to match the thumb position.
+            let percent = this.value / newValue;
+            this.valueBar.value = Math.round(percent * 10000); // has max=10000
+          }
+          this.setAttribute("max", newValue);
+          break;
+      }
+      ]]>
+    </body>
+  </method>
+
+  <method name="dragStateChanged">
+    <parameter name="isDragging"/>
+    <body>
+      <![CDATA[
+      if (this.type == "scrubber") {
+        this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
+        this.isDragging = isDragging;
+        if (this.isPausedByDragging && !isDragging) {
+          // After the drag ends, resume playing.
+          this.Utils.video.play();
+          this.isPausedByDragging = false;
+        }
+      }
+      ]]>
+    </body>
+  </method>
+
+  <method name="pauseVideoDuringDragging">
+    <body>
+      <![CDATA[
+      if (this.isDragging &&
+         !this.Utils.video.paused && !this.isPausedByDragging) {
+        this.isPausedByDragging = true;
+        this.Utils.video.pause();
+      }
+      ]]>
+    </body>
+  </method>
+
+</implementation>
+</binding>
+
+<binding id="videoControls">
+  <resources>
+    <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
+    <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
+  </resources>
+
+  <xbl:content xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    xmlns="http://www.w3.org/1999/xhtml" class="mediaControlsFrame">
+    <div anonid="controlsContainer" class="controlsContainer" role="none">
+      <div anonid="statusOverlay" class="statusOverlay stackItem" hidden="true">
+        <div anonid="statusIcon" class="statusIcon"></div>
+        <span class="errorLabel" anonid="errorAborted">&error.aborted;</span>
+        <span class="errorLabel" anonid="errorNetwork">&error.network;</span>
+        <span class="errorLabel" anonid="errorDecode">&error.decode;</span>
+        <span class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</span>
+        <span class="errorLabel" anonid="errorNoSource">&error.noSource2;</span>
+        <span class="errorLabel" anonid="errorGeneric">&error.generic;</span>
+      </div>
 
-      <method name="setTime">
-          <parameter name="time"/>
-          <body>
-          <![CDATA[
-              var timeString;
-              time = Math.round(time / 1000);
-              var hours = Math.floor(time / 3600);
-              var mins  = Math.floor((time % 3600) / 60);
-              var secs  = Math.floor(time % 60);
-              if (secs < 10)
-                  secs = "0" + secs;
-              if (hours || this.showHours) {
-                  if (mins < 10)
-                      mins = "0" + mins;
-                  timeString = hours + ":" + mins + ":" + secs;
-              } else {
-                  timeString = mins + ":" + secs;
-              }
+      <div anonid="controlsOverlay" class="controlsOverlay stackItem">
+        <div class="controlsSpacerStack" aria-hideen="true">
+          <div anonid="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
+          <div anonid="clickToPlay" class="clickToPlay" hidden="true"></div>
+        </div>
+        <div anonid="controlBar" class="controlBar" hidden="true">
+          <button anonid="playButton"
+                  class="playButton"
+                  playlabel="&playButton.playLabel;"
+                  pauselabel="&playButton.pauseLabel;"/>
+          <div anonid="scrubberStack" class="scrubberStack progressContainer" role="none">
+            <div class="progressBackgroundBar stackItem" role="none">
+              <div class="progressStack" role="none">
+                <progress anonid="bufferBar" class="bufferBar" value="0" max="100"></progress>
+                <progress anonid="progressBar" class="progressBar" value="0" max="100"></progress>
+              </div>
+            </div>
+            <input type="range" anonid="scrubber" class="scrubber"/>
+          </div>
+          <span anonid="positionLabel" class="positionLabel" role="presentation"></span>
+          <span anonid="durationLabel" class="durationLabel" role="presentation"></span>
+          <span anonid="positionDurationBox" class="positionDurationBox" aria-hidden="true">
+            &positionAndDuration.nameFormat;
+          </span>
+          <div anonid="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div>
+          <button anonid="muteButton"
+                  class="muteButton"
+                  mutelabel="&muteButton.muteLabel;"
+                  unmutelabel="&muteButton.unmuteLabel;"/>
+          <div anonid="volumeStack" class="volumeStack progressContainer" role="none">
+            <input type="range" anonid="volumeControl" class="volumeControl" min="0" max="100" step="1"/>
+          </div>
+          <button anonid="closedCaptionButton" class="closedCaptionButton"/>
+          <button anonid="fullscreenButton"
+                  class="fullscreenButton"
+                  enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+                  exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+        </div>
+        <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
+      </div>
+    </div>
+  </xbl:content>
+
+  <implementation>
+
+  <constructor>
+    <![CDATA[
+    this.isTouchControls = false;
+    this.randomID = 0;
 
-              this.timeLabel.setAttribute("value", timeString);
-          ]]>
-          </body>
-      </method>
-      </implementation>
-  </binding>
+    this.Utils = {
+      debug : false,
+      video : null,
+      videocontrols : null,
+      controlBar : null,
+      playButton : null,
+      muteButton : null,
+      volumeControl  : null,
+      durationLabel  : null,
+      positionLabel  : null,
+      scrubber       : null,
+      progressBar    : null,
+      bufferBar      : null,
+      statusOverlay  : null,
+      controlsSpacer : null,
+      clickToPlay    : null,
+      controlsOverlay : null,
+      fullscreenButton : null,
+      currentTextTrackIndex: 0,
+
+      textTracksCount: 0,
+      randomID : 0,
+      videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
+                     "loadstart", "timeupdate", "progress",
+                     "playing", "waiting", "canplay", "canplaythrough",
+                     "seeking", "seeked", "emptied", "loadedmetadata",
+                     "error", "suspend", "stalled",
+                     "mozinterruptbegin", "mozinterruptend" ],
 
-  <binding id="suppressChangeEvent"
-            extends="chrome://global/content/bindings/scale.xml#scale">
-    <implementation implements="nsIXBLAccessible">
-      <!-- nsIXBLAccessible -->
-      <property name="accessibleName" readonly="true">
-        <getter>
-          if (this.type != "scrubber")
-              return "";
+      firstFrameShown : false,
+      timeUpdateCount : 0,
+      maxCurrentTimeSeen : 0,
+      isPausedByDragging: false,
+      _isAudioOnly : false,
+
+      get isAudioOnly() { return this._isAudioOnly; },
+      set isAudioOnly(val) {
+        this._isAudioOnly = val;
+        this.setFullscreenButtonState();
 
-          var currTime = this.thumb.timeLabel.getAttribute("value");
-          var totalTime = this.durationValue;
+        if (!this.isTopLevelSyntheticDocument) {
+          return;
+        }
+        if (this._isAudioOnly) {
+          this.video.style.height = this.controlBar.minHeight + "px";
+          this.video.style.width = "66%";
+        } else {
+          this.video.style.removeProperty("height");
+          this.video.style.removeProperty("width");
+        }
+      },
+      suppressError : false,
+
+      setupStatusFader(immediate) {
+        // Since the play button will be showing, we don't want to
+        // show the throbber behind it. The throbber here will
+        // only show if needed after the play button has been pressed.
+        if (!this.clickToPlay.hidden) {
+          this.startFadeOut(this.statusOverlay, true);
+          return;
+        }
 
-          return this.scrubberNameFormat.replace(/#1/, currTime).
-              replace(/#2/, totalTime);
-        </getter>
-      </property>
+        var show = false;
+        if (this.video.seeking ||
+            (this.video.error && !this.suppressError) ||
+            this.video.networkState == this.video.NETWORK_NO_SOURCE ||
+            (this.video.networkState == this.video.NETWORK_LOADING &&
+              (this.video.paused || this.video.ended
+                ? this.video.readyState < this.video.HAVE_CURRENT_DATA
+                : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
+            (this.timeUpdateCount <= 1 && !this.video.ended &&
+             this.video.readyState < this.video.HAVE_FUTURE_DATA &&
+             this.video.networkState == this.video.NETWORK_LOADING)) {
+          show = true;
+        }
+
+        // Explicitly hide the status fader if this
+        // is audio only until bug 619421 is fixed.
+        if (this.isAudioOnly) {
+          show = false;
+        }
+
+        this.log("Status overlay: seeking=" + this.video.seeking +
+                 " error=" + this.video.error + " readyState=" + this.video.readyState +
+                 " paused=" + this.video.paused + " ended=" + this.video.ended +
+                 " networkState=" + this.video.networkState +
+                 " timeUpdateCount=" + this.timeUpdateCount +
+                 " --> " + (show ? "SHOW" : "HIDE"));
+        this.startFade(this.statusOverlay, show, immediate);
+      },
 
-      <constructor>
-          <![CDATA[
-          this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
-          this.durationValue = "";
-          this.valueBar = null;
-          this.isDragging = false;
-          this.isPausedByDragging = false;
+      /*
+      * Set the initial state of the controls. The binding is normally created along
+      * with video element, but could be attached at any point (eg, if the video is
+      * removed from the document and then reinserted). Thus, some one-time events may
+      * have already fired, and so we'll need to explicitly check the initial state.
+      */
+      setupInitialState() {
+        this.randomID = Math.random();
+        this.videocontrols.randomID = this.randomID;
+
+        this.setPlayButtonState(this.video.paused);
+
+        this.setFullscreenButtonState();
 
-          this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
-          this.type = this.getAttribute("class");
-          this.Utils = document.getBindingParent(this.parentNode).Utils;
-          if (this.type == "scrubber")
-              this.valueBar = this.Utils.progressBar;
-          ]]>
-      </constructor>
+        var duration = Math.round(this.video.duration * 1000); // in ms
+        var currentTime = Math.round(this.video.currentTime * 1000); // in ms
+        this.log("Initial playback position is at " + currentTime + " of " + duration);
+        // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
+        // to determine if the media source changed while we were detached.
+        this.initPositionDurationBox();
+        this.maxCurrentTimeSeen = currentTime;
+        this.showPosition(currentTime, duration);
+
+        // If we have metadata, check if this is a <video> without
+        // video data, or a video with no audio track.
+        if (this.video.readyState >= this.video.HAVE_METADATA) {
+          if (this.video instanceof HTMLVideoElement &&
+              (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
+            this.isAudioOnly = true;
+          }
 
-      <method name="valueChanged">
-          <parameter name="which"/>
-          <parameter name="newValue"/>
-          <parameter name="userChanged"/>
-          <body>
-          <![CDATA[
-            // This method is a copy of the base binding's valueChanged(), except that it does
-            // not dispatch a |change| event (to avoid exposing the event to web content), and
-            // just calls the videocontrol's seekToPosition() method directly.
-            switch (which) {
-              case "curpos":
-                if (this.type == "scrubber") {
-                    // Update the time shown in the thumb.
-                    this.thumb.setTime(newValue);
-                    this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
-                    // Update the value bar to match the thumb position.
-                    let percent = newValue / this.max;
-                    if (!isNaN(percent) && percent != Infinity) {
-                      this.valueBar.value = Math.round(percent * 10000); // has max=10000
-                    } else {
-                      this.valueBar.removeAttribute("value");
-                    }
+          // We have to check again if the media has audio here,
+          // because of bug 718107: switching to fullscreen may
+          // cause the bindings to detach and reattach, hence
+          // unsetting the attribute.
+          if (!this.isAudioOnly && !this.video.mozHasAudio) {
+            this.muteButton.setAttribute("noAudio", "true");
+            this.muteButton.setAttribute("disabled", "true");
+          }
+        }
+
+        if (this.isAudioOnly) {
+          this.clickToPlay.hidden = true;
+        }
+
+        // If the first frame hasn't loaded, kick off a throbber fade-in.
+        if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) {
+          this.firstFrameShown = true;
+        }
+
+        // We can't determine the exact buffering status, but do know if it's
+        // fully loaded. (If it's still loading, it will fire a progress event
+        // and we'll figure out the exact state then.)
+        this.bufferBar.max = 100;
+        if (this.video.readyState >= this.video.HAVE_METADATA) {
+          this.showBuffered();
+        } else {
+          this.bufferBar.value = 0;
+        }
+
+        // Set the current status icon.
+        if (this.hasError()) {
+          this.clickToPlay.hidden = true;
+          this.statusIcon.setAttribute("type", "error");
+          this.updateErrorText();
+          this.setupStatusFader(true);
+        }
+
+        // An event handler for |onresize| should be added when bug 227495 is fixed.
+        this.controlBar.hidden = false;
+
+        let layoutControls = [
+          ...this.controlBar.children,
+          this.durationSpan,
+          this.controlBar,
+          this.clickToPlay
+        ];
+
+        for (let control of layoutControls) {
+          if (!control) {
+            break;
+          }
+
+          Object.defineProperties(control, {
+            minWidth: {
+              value: control.clientWidth,
+              writable: true
+            },
+            minHeight: {
+              value: control.clientHeight,
+              writable: true
+            },
+            isAdjustableControl: {
+              value: true
+            },
+            isWanted: {
+              value: true,
+              writable: true
+            },
+            hideByAdjustment: {
+              set: (v) => {
+                if (v) {
+                  control.setAttribute("hidden", "true");
+                } else {
+                  control.removeAttribute("hidden");
                 }
 
-                // The value of userChanged is true when changing the position with the mouse,
-                // but not when pressing an arrow key. However, the base binding sets
-                // ._userChanged in its keypress handlers, so we just need to check both.
-                if (!userChanged && !this._userChanged)
-                  return;
-                this.setAttribute("value", newValue);
-
-                if (this.type == "scrubber")
-                    this.Utils.seekToPosition(newValue);
-                else if (this.type == "volumeControl")
-                    this.Utils.setVolume(newValue / 100);
-                break;
+                control._isHiddenByAdjustment = v;
+              },
+              get: () => control._isHiddenByAdjustment
+            },
+            _isHiddenByAdjustment: {
+              value: false,
+              writable: true
+            }
+          });
+        }
+        // Cannot get minimal width of flexible scrubber and clickToPlay.
+        // Rewrite to empirical value for now.
+        this.controlBar.minHeight = 40;
+        this.scrubberStack.minWidth = 64;
+        this.volumeControl.minWidth = 48;
+        this.clickToPlay.minWidth = 48;
 
-              case "minpos":
-                this.setAttribute("min", newValue);
-                break;
+        if (this.positionDurationBox) {
+          this.positionDurationBox.minWidth -= this.durationSpan.minWidth;
+        }
 
-              case "maxpos":
-                if (this.type == "scrubber") {
-                    // Update the value bar to match the thumb position.
-                    let percent = this.value / newValue;
-                    this.valueBar.value = Math.round(percent * 10000); // has max=10000
-                }
-                this.setAttribute("max", newValue);
-                break;
-            }
-          ]]>
-          </body>
-      </method>
+        this.adjustControlSize();
+        this.controlBar.hidden = true;
+
+        // Can only update the volume controls once we've computed
+        // _volumeControlWidth, since the volume slider implementation
+        // depends on it.
+        this.updateVolumeControls();
+      },
 
-      <method name="dragStateChanged">
-        <parameter name="isDragging"/>
-          <body>
-          <![CDATA[
-            if (this.type == "scrubber") {
-                this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
-                this.isDragging = isDragging;
-                if (this.isPausedByDragging && !isDragging) {
-                    // After the drag ends, resume playing.
-                    this.Utils.video.play();
-                    this.isPausedByDragging = false;
-                }
-            }
-          ]]>
-          </body>
-      </method>
+      setupNewLoadState() {
+        // videocontrols.css hides the control bar by default, because if script
+        // is disabled our binding's script is disabled too (bug 449358). Thus,
+        // the controls are broken and we don't want them shown. But if script is
+        // enabled, the code here will run and can explicitly unhide the controls.
+        //
+        // For videos with |autoplay| set, we'll leave the controls initially hidden,
+        // so that they don't get in the way of the playing video. Otherwise we'll
+        // go ahead and reveal the controls now, so they're an obvious user cue.
+        //
+        // (Note: the |controls| attribute is already handled via layout/style/html.css)
+        var shouldShow = !this.dynamicControls ||
+                         (this.video.paused &&
+                         !(this.video.autoplay && this.video.mozAutoplayEnabled));
+        // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
+        let shouldClickToPlayShow = shouldShow && !this.isAudioOnly &&
+                                    this.video.currentTime == 0 && !this.hasError();
+        this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
+        this.startFade(this.controlsSpacer, shouldClickToPlayShow, true);
+        this.startFade(this.controlBar, shouldShow, true);
+      },
+
+      get dynamicControls() {
+        // Don't fade controls for <audio> elements.
+        var enabled = !this.isAudioOnly;
+
+        // Allow tests to explicitly suppress the fading of controls.
+        if (this.video.hasAttribute("mozNoDynamicControls")) {
+          enabled = false;
+        }
 
-      <method name="pauseVideoDuringDragging">
-          <body>
-          <![CDATA[
-            if (this.isDragging &&
-                !this.Utils.video.paused && !this.isPausedByDragging) {
-                this.isPausedByDragging = true;
-                this.Utils.video.pause();
-            }
-          ]]>
-          </body>
-      </method>
+        // If the video hits an error, suppress controls if it
+        // hasn't managed to do anything else yet.
+        if (!this.firstFrameShown && this.hasError()) {
+          enabled = false;
+        }
 
-      </implementation>
-  </binding>
+        return enabled;
+      },
 
-  <binding id="videoControls">
+      updateVolume() {
+        const volume = this.volumeControl.value;
+        this.setVolume(volume / 100);
+      },
 
-    <resources>
-        <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
-        <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
-    </resources>
+      updateVolumeControls() {
+        var volume = this.video.muted ? 0 : this.video.volume;
+        var volumePercentage = Math.round(volume * 100);
+        this.updateMuteButtonState();
+        this.volumeControl.value = volumePercentage;
+      },
 
-    <xbl:content xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-    xmlns="http://www.w3.org/1999/xhtml" class="mediaControlsFrame">
-        <div anonid="controlsContainer" class="controlsContainer" role="none">
-            <div anonid="statusOverlay" class="statusOverlay stackItem" hidden="true">
-                <div anonid="statusIcon" class="statusIcon"></div>
-                <span class="errorLabel" anonid="errorAborted">&error.aborted;</span>
-                <span class="errorLabel" anonid="errorNetwork">&error.network;</span>
-                <span class="errorLabel" anonid="errorDecode">&error.decode;</span>
-                <span class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</span>
-                <span class="errorLabel" anonid="errorNoSource">&error.noSource2;</span>
-                <span class="errorLabel" anonid="errorGeneric">&error.generic;</span>
-            </div>
+      handleEvent(aEvent) {
+        this.log("Got media event ----> " + aEvent.type);
+
+        // If the binding is detached (or has been replaced by a
+        // newer instance of the binding), nuke our event-listeners.
+        if (this.videocontrols.randomID != this.randomID) {
+          this.terminateEventListeners();
+          return;
+        }
 
-            <div anonid="controlsOverlay" class="controlsOverlay stackItem">
-                <div class="controlsSpacerStack" aria-hideen="true">
-                  <div anonid="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
-                  <div anonid="clickToPlay" class="clickToPlay" hidden="true"></div>
-                </div>
-                <div anonid="controlBar" class="controlBar" hidden="true">
-                    <button anonid="playButton"
-                            class="playButton"
-                            playlabel="&playButton.playLabel;"
-                            pauselabel="&playButton.pauseLabel;"/>
-                    <div anonid="scrubberStack" class="scrubberStack progressContainer" role="none">
-                      <div class="progressBackgroundBar stackItem" role="none">
-                        <div class="progressStack" role="none">
-                          <progress anonid="bufferBar" class="bufferBar" value="0" max="100"></progress>
-                          <progress anonid="progressBar" class="progressBar" value="0" max="100"></progress>
-                        </div>
-                      </div>
-                      <input type="range" anonid="scrubber" class="scrubber"/>
-                    </div>
-                    <span anonid="positionLabel" class="positionLabel" role="presentation"></span>
-                    <span anonid="durationLabel" class="durationLabel" role="presentation"></span>
-                    <span anonid="positionDurationBox" class="positionDurationBox" aria-hidden="true">
-                      &positionAndDuration.nameFormat;
-                    </span>
-                    <div anonid="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div>
-                    <button anonid="muteButton"
-                            class="muteButton"
-                            mutelabel="&muteButton.muteLabel;"
-                            unmutelabel="&muteButton.unmuteLabel;"/>
-                    <div anonid="volumeStack" class="volumeStack progressContainer" role="none">
-                      <input type="range" anonid="volumeControl" class="volumeControl" min="0" max="100" step="1"/>
-                    </div>
-                    <button anonid="closedCaptionButton" class="closedCaptionButton"/>
-                    <button anonid="fullscreenButton"
-                            class="fullscreenButton"
-                            enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
-                            exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
-                </div>
-                <div anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></div>
-            </div>
-        </div>
-    </xbl:content>
-
-    <implementation>
-
-        <constructor>
-            <![CDATA[
-            this.isTouchControls = false;
-            this.randomID = 0;
-
-            this.Utils = {
-                debug : false,
-                video : null,
-                videocontrols : null,
-                controlBar : null,
-                playButton : null,
-                muteButton : null,
-                volumeControl  : null,
-                durationLabel  : null,
-                positionLabel  : null,
-                scrubber       : null,
-                progressBar    : null,
-                bufferBar      : null,
-                statusOverlay  : null,
-                controlsSpacer : null,
-                clickToPlay    : null,
-                controlsOverlay : null,
-                fullscreenButton : null,
-                currentTextTrackIndex: 0,
-
-                textTracksCount: 0,
-                randomID : 0,
-                videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
-                               "loadstart", "timeupdate", "progress",
-                               "playing", "waiting", "canplay", "canplaythrough",
-                               "seeking", "seeked", "emptied", "loadedmetadata",
-                               "error", "suspend", "stalled",
-                               "mozinterruptbegin", "mozinterruptend" ],
+        switch (aEvent.type) {
+          case "play":
+            this.setPlayButtonState(false);
+            this.setupStatusFader();
+            if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControls) {
+              this.startFadeOut(this.controlBar);
+            }
+            if (!this._triggeredByControls) {
+              this.clickToPlay.hidden = true;
+              this.controlsSpacer.setAttribute("fadeout", "true");
+            }
+            this._triggeredByControls = false;
+            break;
+          case "pause":
+            // Little white lie: if we've internally paused the video
+            // while dragging the scrubber, don't change the button state.
+            if (!this.scrubber.isDragging) {
+              this.setPlayButtonState(true);
+            }
+            this.setupStatusFader();
+            break;
+          case "ended":
+            this.setPlayButtonState(true);
+            // We throttle timechange events, so the thumb might not be
+            // exactly at the end when the video finishes.
+            this.showPosition(Math.round(this.video.currentTime * 1000),
+            Math.round(this.video.duration * 1000));
+            this.startFadeIn(this.controlBar);
+            this.setupStatusFader();
+            break;
+          case "volumechange":
+            this.updateVolumeControls();
+            // Show the controls to highlight the changing volume,
+            // but only if the click-to-play overlay has already
+            // been hidden (we don't hide controls when the overlay is visible).
+            if (this.clickToPlay.hidden && !this.isAudioOnly) {
+              this.startFadeIn(this.controlBar);
+              clearTimeout(this._hideControlsTimeout);
+              this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+            }
+            break;
+          case "loadedmetadata":
+            this.adjustControlSize();
+            // If a <video> doesn't have any video data, treat it as <audio>
+            // and show the controls (they won't fade back out)
+            if (this.video instanceof HTMLVideoElement &&
+                (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
+              this.isAudioOnly = true;
+              this.clickToPlay.hidden = true;
+              this.startFadeIn(this.controlBar);
+              this.setFullscreenButtonState();
+            }
+            this.showDuration(Math.round(this.video.duration * 1000));
+            if (!this.isAudioOnly && !this.video.mozHasAudio) {
+              this.muteButton.setAttribute("noAudio", "true");
+              this.muteButton.setAttribute("disabled", "true");
+            }
+            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);
+            this.setupNewLoadState();
+            this.setupStatusFader();
+            break;
+          case "progress":
+            this.statusIcon.removeAttribute("stalled");
+            this.showBuffered();
+            this.setupStatusFader();
+            break;
+          case "stalled":
+            this.statusIcon.setAttribute("stalled", "true");
+            this.statusIcon.setAttribute("type", "throbber");
+            this.setupStatusFader();
+            break;
+          case "suspend":
+            this.setupStatusFader();
+            break;
+          case "timeupdate":
+            var currentTime = Math.round(this.video.currentTime * 1000); // in ms
+            var duration = Math.round(this.video.duration * 1000); // in ms
 
-                firstFrameShown : false,
-                timeUpdateCount : 0,
-                maxCurrentTimeSeen : 0,
-                isPausedByDragging: false,
-                _isAudioOnly : false,
-                get isAudioOnly() { return this._isAudioOnly; },
-                set isAudioOnly(val) {
-                    this._isAudioOnly = val;
-                    this.setFullscreenButtonState();
-
-                    if (!this.isTopLevelSyntheticDocument)
-                        return;
-                    if (this._isAudioOnly) {
-                        this.video.style.height = this.controlBar.minHeight + "px";
-                        this.video.style.width = "66%";
-                    } else {
-                        this.video.style.removeProperty("height");
-                        this.video.style.removeProperty("width");
-                    }
-                },
-                suppressError : false,
-
-                setupStatusFader : function(immediate) {
-                    // Since the play button will be showing, we don't want to
-                    // show the throbber behind it. The throbber here will
-                    // only show if needed after the play button has been pressed.
-                    if (!this.clickToPlay.hidden) {
-                        this.startFadeOut(this.statusOverlay, true);
-                        return;
-                    }
-
-                    var show = false;
-                    if (this.video.seeking ||
-                        (this.video.error && !this.suppressError) ||
-                        this.video.networkState == this.video.NETWORK_NO_SOURCE ||
-                        (this.video.networkState == this.video.NETWORK_LOADING &&
-                         (this.video.paused || this.video.ended
-                           ? this.video.readyState < this.video.HAVE_CURRENT_DATA
-                           : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
-                        (this.timeUpdateCount <= 1 && !this.video.ended &&
-                         this.video.readyState < this.video.HAVE_FUTURE_DATA &&
-                         this.video.networkState == this.video.NETWORK_LOADING))
-                        show = true;
+            // If playing/seeking after the video ended, we won't get a "play"
+            // event, so update the button state here.
+            if (!this.video.paused) {
+              this.setPlayButtonState(false);
+            }
 
-                    // Explicitly hide the status fader if this
-                    // is audio only until bug 619421 is fixed.
-                    if (this.isAudioOnly)
-                        show = false;
-
-                    this.log("Status overlay: seeking=" + this.video.seeking +
-                             " error=" + this.video.error + " readyState=" + this.video.readyState +
-                             " paused=" + this.video.paused + " ended=" + this.video.ended +
-                             " networkState=" + this.video.networkState +
-                             " timeUpdateCount=" + this.timeUpdateCount +
-                             " --> " + (show ? "SHOW" : "HIDE"));
-                    this.startFade(this.statusOverlay, show, immediate);
-                },
-
-                /*
-                 * Set the initial state of the controls. The binding is normally created along
-                 * with video element, but could be attached at any point (eg, if the video is
-                 * removed from the document and then reinserted). Thus, some one-time events may
-                 * have already fired, and so we'll need to explicitly check the initial state.
-                 */
-                setupInitialState : function() {
-                    this.randomID = Math.random();
-                    this.videocontrols.randomID = this.randomID;
-
-                    this.setPlayButtonState(this.video.paused);
-
-                    this.setFullscreenButtonState();
-
-                    var duration = Math.round(this.video.duration * 1000); // in ms
-                    var currentTime = Math.round(this.video.currentTime * 1000); // in ms
-                    this.log("Initial playback position is at " + currentTime + " of " + duration);
-                    // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
-                    // to determine if the media source changed while we were detached.
-                    this.initPositionDurationBox();
-                    this.maxCurrentTimeSeen = currentTime;
-                    this.showPosition(currentTime, duration);
-
-                    // If we have metadata, check if this is a <video> without
-                    // video data, or a video with no audio track.
-                    if (this.video.readyState >= this.video.HAVE_METADATA) {
-                        if (this.video instanceof HTMLVideoElement &&
-                            (this.video.videoWidth == 0 || this.video.videoHeight == 0))
-                            this.isAudioOnly = true;
+            this.timeUpdateCount++;
+            // Whether we show the statusOverlay sometimes depends
+            // on whether we've seen more than one timeupdate
+            // event (if we haven't, there hasn't been any
+            // "playback activity" and we may wish to show the
+            // statusOverlay while we wait for HAVE_ENOUGH_DATA).
+            // If we've seen more than 2 timeupdate events,
+            // the count is no longer relevant to setupStatusFader.
+            if (this.timeUpdateCount <= 2) {
+              this.setupStatusFader();
+            }
 
-                        // We have to check again if the media has audio here,
-                        // because of bug 718107: switching to fullscreen may
-                        // cause the bindings to detach and reattach, hence
-                        // unsetting the attribute.
-                        if (!this.isAudioOnly && !this.video.mozHasAudio) {
-                          this.muteButton.setAttribute("noAudio", "true");
-                          this.muteButton.setAttribute("disabled", "true");
-                        }
-                    }
-
-                    if (this.isAudioOnly)
-                        this.clickToPlay.hidden = true;
-
-                    // If the first frame hasn't loaded, kick off a throbber fade-in.
-                    if (this.video.readyState >= this.video.HAVE_CURRENT_DATA)
-                        this.firstFrameShown = true;
-
-                    // We can't determine the exact buffering status, but do know if it's
-                    // fully loaded. (If it's still loading, it will fire a progress event
-                    // and we'll figure out the exact state then.)
-                    this.bufferBar.max = 100;
-                    if (this.video.readyState >= this.video.HAVE_METADATA)
-                        this.showBuffered();
-                    else
-                        this.bufferBar.value = 0;
-
-                    // Set the current status icon.
-                    if (this.hasError()) {
-                        this.clickToPlay.hidden = true;
-                        this.statusIcon.setAttribute("type", "error");
-                        this.updateErrorText();
-                        this.setupStatusFader(true);
-                    }
-
-                    // An event handler for |onresize| should be added when bug 227495 is fixed.
-                    this.controlBar.hidden = false;
-
-                    let layoutControls = [
-                      ...this.controlBar.children,
-                      this.durationSpan,
-                      this.controlBar,
-                      this.clickToPlay
-                    ];
-
-                    for (let control of layoutControls) {
-                      if (!control) {
-                        break;
-                      }
+            // If the user is dragging the scrubber ignore the delayed seek
+            // responses (don't yank the thumb away from the user)
+            if (this.scrubber.isDragging || this.scrubber.startToDrag) {
+              return;
+            }
+            this.showPosition(currentTime, duration);
+            this.showBuffered();
+            break;
+          case "emptied":
+            this.bufferBar.value = 0;
+            this.showPosition(0, 0);
+            break;
+          case "seeking":
+            this.showBuffered();
+            this.statusIcon.setAttribute("type", "throbber");
+            this.setupStatusFader();
+            break;
+          case "waiting":
+            this.statusIcon.setAttribute("type", "throbber");
+            this.setupStatusFader();
+            break;
+          case "seeked":
+          case "playing":
+          case "canplay":
+          case "canplaythrough":
+            this.setupStatusFader();
+            break;
+          case "error":
+            // We'll show the error status icon when we receive an error event
+            // 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.hasError()) {
+              this.suppressError = false;
+              this.clickToPlay.hidden = true;
+              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);
+              }
+              this.controlsSpacer.removeAttribute("hideCursor");
+            }
+            break;
+          case "mozinterruptbegin":
+          case "mozinterruptend":
+            // Nothing to do...
+            break;
+          default:
+            this.log("!!! event " + aEvent.type + " not handled!");
+        }
+      },
 
-                      Object.defineProperties(control, {
-                        minWidth: {
-                          value: control.clientWidth,
-                          writable: true
-                        },
-                        minHeight: {
-                          value: control.clientHeight,
-                          writable: true
-                        },
-                        isAdjustableControl: {
-                          value: true
-                        },
-                        isWanted: {
-                          value: true,
-                          writable: true
-                        },
-                        hideByAdjustment: {
-                          set: (v) => {
-                            if (v) {
-                              control.setAttribute("hidden", "true");
-                            } else {
-                              control.removeAttribute("hidden");
-                            }
+      terminateEventListeners() {
+        if (this.videoEvents) {
+          for (let event of this.videoEvents) {
+            this.video.removeEventListener(event, this, {
+              capture: true,
+              mozSystemGroup: true
+            });
+          }
+        }
+
+        if (this.controlListeners) {
+          for (let element of this.controlListeners) {
+            element.item.removeEventListener(element.event, element.func,
+              { mozSystemGroup: true });
+          }
+
+          delete this.controlListeners;
+        }
+
+        this.log("--- videocontrols terminated ---");
+      },
 
-                            control._isHiddenByAdjustment = v;
-                          },
-                          get: () => control._isHiddenByAdjustment
-                        },
-                        _isHiddenByAdjustment: {
-                          value: false,
-                          writable: true
-                        }
-                      });
-                    }
-                    // Cannot get minimal width of flexible scrubber and clickToPlay.
-                    // Rewrite to empirical value for now.
-                    this.controlBar.minHeight = 40;
-                    this.scrubberStack.minWidth = 64;
-                    this.volumeControl.minWidth = 48;
-                    this.clickToPlay.minWidth = 48;
-
-                    if (this.positionDurationBox) {
-                      this.positionDurationBox.minWidth -= this.durationSpan.minWidth;
-                    }
-
-                    this.adjustControlSize();
-                    this.controlBar.hidden = true;
+      hasError() {
+        // We either have an explicit error, or the resource selection
+        // algorithm is running and we've tried to load something and failed.
+        // Note: we don't consider the case where we've tried to load but
+        // there's no sources to load as an error condition, as sites may
+        // do this intentionally to work around requires-user-interaction to
+        // play restrictions, and we don't want to display a debug message
+        // if that's the case.
+        return this.video.error != null ||
+               (this.video.networkState == this.video.NETWORK_NO_SOURCE &&
+               this.hasSources());
+      },
 
-                    // Can only update the volume controls once we've computed
-                    // _volumeControlWidth, since the volume slider implementation
-                    // depends on it.
-                    this.updateVolumeControls();
-                },
-
-                setupNewLoadState : function() {
-                    // videocontrols.css hides the control bar by default, because if script
-                    // is disabled our binding's script is disabled too (bug 449358). Thus,
-                    // the controls are broken and we don't want them shown. But if script is
-                    // enabled, the code here will run and can explicitly unhide the controls.
-                    //
-                    // For videos with |autoplay| set, we'll leave the controls initially hidden,
-                    // so that they don't get in the way of the playing video. Otherwise we'll
-                    // go ahead and reveal the controls now, so they're an obvious user cue.
-                    //
-                    // (Note: the |controls| attribute is already handled via layout/style/html.css)
-                    var shouldShow = !this.dynamicControls ||
-                      (this.video.paused &&
-                       !(this.video.autoplay && this.video.mozAutoplayEnabled));
-                    // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
-                    let shouldClickToPlayShow = shouldShow && !this.isAudioOnly &&
-                                                this.video.currentTime == 0 && !this.hasError();
-                    this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
-                    this.startFade(this.controlsSpacer, shouldClickToPlayShow, true);
-                    this.startFade(this.controlBar, shouldShow, true);
-                },
+      hasSources() {
+        if (this.video.hasAttribute('src') &&
+            this.video.getAttribute('src') !== "") {
+          return true;
+        }
+        for (var child = this.video.firstChild;
+             child !== null;
+             child = child.nextElementSibling) {
+          if (child instanceof HTMLSourceElement) {
+            return true;
+          }
+        }
+        return false;
+      },
 
-                get dynamicControls() {
-                    // Don't fade controls for <audio> elements.
-                    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.hasError())
-                        enabled = false;
-
-                    return enabled;
-                },
+      updateErrorText() {
+        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.
+        }
 
-                updateVolume() {
-                  const volume = this.volumeControl.value;
-                  this.setVolume(volume / 100);
-                },
+        let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
+        this.controlsSpacer.setAttribute("aria-label", label.textContent);
+        this.statusOverlay.setAttribute("error", error);
+      },
 
-                updateVolumeControls() {
-                    var volume = this.video.muted ? 0 : this.video.volume;
-                    var volumePercentage = Math.round(volume * 100);
-                    this.updateMuteButtonState();
-                    this.volumeControl.value = volumePercentage;
-                },
+      formatTime(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) {
+          secs = "0" + secs;
+        }
+        if (hours) {
+          if (mins < 10) {
+            mins = "0" + mins;
+          }
+          timeString = hours + ":" + mins + ":" + secs;
+        } else {
+          timeString = mins + ":" + secs;
+        }
+        return timeString;
+      },
 
-                handleEvent : function(aEvent) {
-                    this.log("Got media event ----> " + aEvent.type);
+      initPositionDurationBox() {
+        if (this.videocontrols.isTouchControls) {
+          return;
+        }
 
-                    // If the binding is detached (or has been replaced by a
-                    // newer instance of the binding), nuke our event-listeners.
-                    if (this.videocontrols.randomID != this.randomID) {
-                        this.terminateEventListeners();
-                        return;
-                    }
+        const positionTextNode = Array.prototype.find.call(
+          this.positionDurationBox.childNodes, (n) => !!~n.textContent.search("#1"));
+        const durationSpan = this.durationSpan;
+        const durationFormat = durationSpan.textContent;
+        const positionFormat = positionTextNode.textContent;
 
-                    switch (aEvent.type) {
-                        case "play":
-                            this.setPlayButtonState(false);
-                            this.setupStatusFader();
-                            if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControls)
-                                this.startFadeOut(this.controlBar);
-                            if (!this._triggeredByControls) {
-                                this.clickToPlay.hidden = true;
-                                this.controlsSpacer.setAttribute("fadeout", "true");
-                            }
-                            this._triggeredByControls = false;
-                            break;
-                        case "pause":
-                            // Little white lie: if we've internally paused the video
-                            // while dragging the scrubber, don't change the button state.
-                            if (!this.scrubber.isDragging)
-                                this.setPlayButtonState(true);
-                            this.setupStatusFader();
-                            break;
-                        case "ended":
-                            this.setPlayButtonState(true);
-                            // We throttle timechange events, so the thumb might not be
-                            // exactly at the end when the video finishes.
-                            this.showPosition(Math.round(this.video.currentTime * 1000),
-                                              Math.round(this.video.duration * 1000));
-                            this.startFadeIn(this.controlBar);
-                            this.setupStatusFader();
-                            break;
-                        case "volumechange":
-                            this.updateVolumeControls();
-                            // Show the controls to highlight the changing volume,
-                            // but only if the click-to-play overlay has already
-                            // been hidden (we don't hide controls when the overlay is visible).
-                            if (this.clickToPlay.hidden && !this.isAudioOnly) {
-                                this.startFadeIn(this.controlBar);
-                                clearTimeout(this._hideControlsTimeout);
-                                this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
-                            }
-                            break;
-                        case "loadedmetadata":
-                            this.adjustControlSize();
-                            // If a <video> doesn't have any video data, treat it as <audio>
-                            // and show the controls (they won't fade back out)
-                            if (this.video instanceof HTMLVideoElement &&
-                                (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
-                                this.isAudioOnly = true;
-                                this.clickToPlay.hidden = true;
-                                this.startFadeIn(this.controlBar);
-                                this.setFullscreenButtonState();
-                            }
-                            this.showDuration(Math.round(this.video.duration * 1000));
-                            if (!this.isAudioOnly && !this.video.mozHasAudio) {
-                              this.muteButton.setAttribute("noAudio", "true");
-                              this.muteButton.setAttribute("disabled", "true");
-                            }
-                            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);
-                            this.setupNewLoadState();
-                            this.setupStatusFader();
-                            break;
-                        case "progress":
-                            this.statusIcon.removeAttribute("stalled");
-                            this.showBuffered();
-                            this.setupStatusFader();
-                            break;
-                        case "stalled":
-                            this.statusIcon.setAttribute("stalled", "true");
-                            this.statusIcon.setAttribute("type", "throbber");
-                            this.setupStatusFader();
-                            break;
-                        case "suspend":
-                            this.setupStatusFader();
-                            break;
-                        case "timeupdate":
-                            var currentTime = Math.round(this.video.currentTime * 1000); // in ms
-                            var duration = Math.round(this.video.duration * 1000); // in ms
+        durationSpan.classList.add("duration");
+        durationSpan.setAttribute("role", "none");
+
+        Object.defineProperties(this.positionDurationBox, {
+          durationSpan: {
+            value: durationSpan
+          },
+          position: {
+            set: (v) => {
+              positionTextNode.textContent = positionFormat.replace("#1", v);
+            }
+          },
+          duration: {
+            set: (v) => {
+              durationSpan.textContent = v ? durationFormat.replace("#2", v) : "";
+            }
+          }
+        });
+      },
+
+      showDuration(duration) {
+        let isInfinite = (duration == Infinity);
+        this.log("Duration is " + duration + "ms.\n");
+
+        if (isNaN(duration) || isInfinite) {
+          duration = this.maxCurrentTimeSeen;
+        }
 
-                            // If playing/seeking after the video ended, we won't get a "play"
-                            // event, so update the button state here.
-                            if (!this.video.paused)
-                                this.setPlayButtonState(false);
+        // Format the duration as "h:mm:ss" or "m:ss"
+        let timeString = isInfinite ? "" : this.formatTime(duration);
+        if (this.videocontrols.isTouchControls) {
+          this.durationLabel.setAttribute("value", timeString);
+        } else {
+          this.positionDurationBox.duration = timeString;
+        }
+
+        // "durationValue" property is used by scale binding to
+        // generate accessible name.
+        this.scrubber.durationValue = timeString;
+
+        // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+
+        this.scrubber.max = duration;
+        // XXX Can't set increment here, due to bug 473103. Also, doing so causes
+        // snapping when dragging with the mouse, so we can't just set a value for
+        // the arrow-keys.
+        this.scrubber.pageIncrement = Math.round(duration / 10);
+      },
+
+      pauseVideoDuringDragging() {
+        if (!this.video.paused &&
+            !this.isPausedByDragging &&
+            this.scrubber.isDragging) {
+          this.isPausedByDragging = true;
+          this.video.pause();
+        }
+      },
+
+      onScrubberInput(e) {
+        const duration = Math.round(this.video.duration * 1000); // in ms
+        let time = this.scrubber.value;
+
+        if (!this.scrubber.startToDrag || this.scrubber.isDragging) {
+          this.seekToPosition(time);
+          this.showPosition(time, duration);
+        }
 
-                            this.timeUpdateCount++;
-                            // Whether we show the statusOverlay sometimes depends
-                            // on whether we've seen more than one timeupdate
-                            // event (if we haven't, there hasn't been any
-                            // "playback activity" and we may wish to show the
-                            // statusOverlay while we wait for HAVE_ENOUGH_DATA).
-                            // If we've seen more than 2 timeupdate events,
-                            // the count is no longer relevant to setupStatusFader.
-                            if (this.timeUpdateCount <= 2)
-                                this.setupStatusFader();
+        this.scrubber.startToDrag = true;
+      },
+
+      onScrubberChange(e) {
+        this.scrubber.startToDrag = false;
+        this.scrubber.isDragging = false;
+
+        if (this.isPausedByDragging) {
+          this.video.play();
+          this.isPausedByDragging = false;
+        }
+      },
+
+      updateScrubberProgress() {
+        if (this.videocontrols.isTouchControls) {
+          return;
+        }
+
+        const positionPercent = this.scrubber.value / this.scrubber.max * 100;
 
-                            // If the user is dragging the scrubber ignore the delayed seek
-                            // responses (don't yank the thumb away from the user)
-                            if (this.scrubber.isDragging || this.scrubber.startToDrag)
-                                return;
+        if (!isNaN(positionPercent) && positionPercent != Infinity) {
+          this.progressBar.value = positionPercent;
+        } else {
+          this.progressBar.value = 0;
+        }
+      },
+
+      seekToPosition(newPosition) {
+        newPosition /= 1000; // convert from ms
+        this.log("+++ seeking to " + newPosition);
+        if (this.videocontrols.isGonk) {
+          // We use fastSeek() on B2G, and an accurate (but slower)
+          // seek on other platforms (that are likely to be higher
+          // perf).
+          this.video.fastSeek(newPosition);
+        } else {
+          this.video.currentTime = newPosition;
+        }
+      },
+
+      setVolume(newVolume) {
+        this.log("*** setting volume to " + newVolume);
+        this.video.volume = newVolume;
+        this.video.muted = false;
+      },
 
-                            this.showPosition(currentTime, duration);
-                            this.showBuffered();
-                            break;
-                        case "emptied":
-                            this.bufferBar.value = 0;
-                            this.showPosition(0, 0);
-                            break;
-                        case "seeking":
-                            this.showBuffered();
-                            this.statusIcon.setAttribute("type", "throbber");
-                            this.setupStatusFader();
-                            break;
-                        case "waiting":
-                            this.statusIcon.setAttribute("type", "throbber");
-                            this.setupStatusFader();
-                            break;
-                        case "seeked":
-                        case "playing":
-                        case "canplay":
-                        case "canplaythrough":
-                            this.setupStatusFader();
-                            break;
-                        case "error":
-                            // We'll show the error status icon when we receive an error event
-                            // 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.hasError()) {
-                                this.suppressError = false;
-                                this.clickToPlay.hidden = true;
-                                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);
-                                this.controlsSpacer.removeAttribute("hideCursor");
-                            }
-                            break;
-                        case "mozinterruptbegin":
-                        case "mozinterruptend":
-                            // Nothing to do...
-                            break;
-                        default:
-                            this.log("!!! event " + aEvent.type + " not handled!");
-                    }
-                },
+      showPosition(currentTime, duration) {
+        // If the duration is unknown (because the server didn't provide
+        // it, or the video is a stream), then we want to fudge the duration
+        // by using the maximum playback position that's been seen.
+        if (currentTime > this.maxCurrentTimeSeen) {
+          this.maxCurrentTimeSeen = currentTime;
+        }
+        this.showDuration(duration);
+
+        this.log("time update @ " + currentTime + "ms of " + duration + "ms");
+
+        let positionTime = this.formatTime(currentTime);
+
+        this.scrubber.value = currentTime;
+        if (this.videocontrols.isTouchControls) {
+          this.positionLabel.setAttribute("value", positionTime);
+        } else {
+          this.positionDurationBox.position = positionTime;
+          this.updateScrubberProgress();
+        }
+      },
 
-                terminateEventListeners : function() {
-                    if (this.videoEvents) {
-                      for (let event of this.videoEvents) {
-                          this.video.removeEventListener(event, this, {
-                              capture: true,
-                              mozSystemGroup: true
-                          });
-                      }
-                    }
-
-                    if (this.controlListeners) {
-                      for (let element of this.controlListeners) {
-                          element.item.removeEventListener(element.event, element.func,
-                                                           { mozSystemGroup: true });
-                      }
-
-                      delete this.controlListeners;
-                    }
-
-                    this.log("--- videocontrols terminated ---");
-                },
+      showBuffered() {
+        function bsearch(haystack, needle, cmp) {
+          var length = haystack.length;
+          var low = 0;
+          var high = length;
+          while (low < high) {
+            var probe = low + ((high - low) >> 1);
+            var r = cmp(haystack, probe, needle);
+            if (r == 0) {
+              return probe;
+            } else if (r > 0) {
+              low = probe + 1;
+            } else {
+              high = probe;
+            }
+          }
+          return -1;
+        }
 
-                hasError : function() {
-                    // We either have an explicit error, or the resource selection
-                    // algorithm is running and we've tried to load something and failed.
-                    // Note: we don't consider the case where we've tried to load but
-                    // there's no sources to load as an error condition, as sites may
-                    // do this intentionally to work around requires-user-interaction to
-                    // play restrictions, and we don't want to display a debug message
-                    // if that's the case.
-                    return this.video.error != null ||
-                          (this.video.networkState == this.video.NETWORK_NO_SOURCE &&
-                           this.hasSources());
-                },
+        function bufferedCompare(buffered, i, time) {
+          if (time > buffered.end(i)) {
+            return 1;
+          } else if (time >= buffered.start(i)) {
+            return 0;
+          }
+          return -1;
+        }
 
-                hasSources : function() {
-                    if (this.video.hasAttribute('src') &&
-                        this.video.getAttribute('src') !== "") {
-                        return true;
-                    }
-                    for (var child = this.video.firstChild;
-                         child !== null;
-                         child = child.nextElementSibling) {
-                        if (child instanceof HTMLSourceElement) {
-                            return true;
-                        }
-                    }
-                    return false;
-                },
+        var duration = Math.round(this.video.duration * 1000);
+        if (isNaN(duration) || duration == Infinity) {
+          duration = this.maxCurrentTimeSeen;
+        }
 
-                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.
-                    }
+        // Find the range that the current play position is in and use that
+        // range for bufferBar.  At some point we may support multiple ranges
+        // displayed in the bar.
+        var currentTime = this.video.currentTime;
+        var buffered = this.video.buffered;
+        var index = bsearch(buffered, currentTime, bufferedCompare);
+        var endTime = 0;
+        if (index >= 0) {
+          endTime = Math.round(buffered.end(index) * 1000);
+        }
+        this.bufferBar.max = duration;
+        this.bufferBar.value = endTime;
+      },
+
+      _controlsHiddenByTimeout : false,
+      _showControlsTimeout : 0,
+      SHOW_CONTROLS_TIMEOUT_MS: 500,
+      _showControlsFn() {
+        if (Utils.video.matches("video:hover")) {
+          Utils.startFadeIn(Utils.controlBar, false);
+          Utils._showControlsTimeout = 0;
+          Utils._controlsHiddenByTimeout = false;
+        }
+      },
 
-                    let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
-                    this.controlsSpacer.setAttribute("aria-label", label.textContent);
-                    this.statusOverlay.setAttribute("error", error);
-                },
+      _hideControlsTimeout : 0,
+      _hideControlsFn() {
+        if (!Utils.scrubber.isDragging) {
+          Utils.startFade(Utils.controlBar, false);
+          Utils._hideControlsTimeout = 0;
+          Utils._controlsHiddenByTimeout = true;
+        }
+      },
+      HIDE_CONTROLS_TIMEOUT_MS : 2000,
+      onMouseMove(event) {
+        // Pause playing video when the mouse is dragging over the control bar.
+        if (this.scrubber.startToDrag) {
+          this.scrubber.isDragging = true;
+          this.pauseVideoDuringDragging();
+        }
 
-                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)
-                        secs = "0" + secs;
-                    if (hours) {
-                        if (mins < 10)
-                            mins = "0" + mins;
-                        timeString = hours + ":" + mins + ":" + secs;
-                    } else {
-                        timeString = mins + ":" + secs;
-                    }
-                    return timeString;
-                },
+        // If the controls are static, don't change anything.
+        if (!this.dynamicControls) {
+          return;
+        }
+
+        clearTimeout(this._hideControlsTimeout);
 
-                initPositionDurationBox : function() {
-                  if (this.videocontrols.isTouchControls) {
-                    return;
-                  }
-
-                  const positionTextNode = Array.prototype.find.call(
-                    this.positionDurationBox.childNodes, (n) => !!~n.textContent.search("#1"));
-                  const durationSpan = this.durationSpan;
-                  const durationFormat = durationSpan.textContent;
-                  const positionFormat = positionTextNode.textContent;
-
-                  durationSpan.classList.add("duration");
-                  durationSpan.setAttribute("role", "none");
+        // 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;
+        }
 
-                  Object.defineProperties(this.positionDurationBox, {
-                    durationSpan: {
-                      value: durationSpan
-                    },
-                    position: {
-                      set: (v) => {
-                        positionTextNode.textContent = positionFormat.replace("#1", v);
-                      }
-                    },
-                    duration: {
-                      set: (v) => {
-                        durationSpan.textContent = v ? durationFormat.replace("#2", v) : "";
-                      }
-                    }
-                  });
-                },
+        if (this._controlsHiddenByTimeout) {
+          this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
+        } else {
+          this.startFade(this.controlBar, true);
+        }
 
-                showDuration : function(duration) {
-                    let isInfinite = (duration == Infinity);
-                    this.log("Duration is " + duration + "ms.\n");
-
-                    if (isNaN(duration) || isInfinite)
-                        duration = this.maxCurrentTimeSeen;
+        // Hide the controls if the mouse cursor is left on top of the video
+        // but above the control bar and if the click-to-play overlay is hidden.
+        if ((this._controlsHiddenByTimeout ||
+            event.clientY < this.controlBar.getBoundingClientRect().top) &&
+            this.clickToPlay.hidden) {
+          this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+        }
+      },
 
-                    // Format the duration as "h:mm:ss" or "m:ss"
-                    let timeString = isInfinite ? "" : this.formatTime(duration);
-                    if (this.videocontrols.isTouchControls) {
-                      this.durationLabel.setAttribute("value", timeString);
-                    } else {
-                      this.positionDurationBox.duration = timeString;
-                    }
+      onMouseInOut(event) {
+        // If the controls are static, don't change anything.
+        if (!this.dynamicControls) {
+          return;
+        }
+
+        clearTimeout(this._hideControlsTimeout);
 
-                    // "durationValue" property is used by scale binding to
-                    // generate accessible name.
-                    this.scrubber.durationValue = timeString;
-
-                    // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
+        // 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;
+        }
 
-                    this.scrubber.max = duration;
-                    // XXX Can't set increment here, due to bug 473103. Also, doing so causes
-                    // snapping when dragging with the mouse, so we can't just set a value for
-                    // the arrow-keys.
-                    this.scrubber.pageIncrement = Math.round(duration / 10);
-                },
+        var isMouseOver = (event.type == "mouseover");
+
+        var controlRect = this.controlBar.getBoundingClientRect();
+        var isMouseInControls = event.clientY > controlRect.top &&
+        event.clientY < controlRect.bottom &&
+        event.clientX > controlRect.left &&
+        event.clientX < controlRect.right;
 
-                pauseVideoDuringDragging: function() {
-                  if (!this.video.paused &&
-                      !this.isPausedByDragging &&
-                      this.scrubber.isDragging) {
-                    this.isPausedByDragging = true;
-                    this.video.pause();
-                  }
-                },
+        // 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 && !isMouseInControls) {
+          this.adjustControlSize();
 
-                onScrubberInput: function(e) {
-                  const duration = Math.round(this.video.duration * 1000); // in ms
-                  let time = this.scrubber.value;
+          // Keep the controls visible if the click-to-play is visible.
+          if (!this.clickToPlay.hidden) {
+            return;
+          }
 
-                  if (!this.scrubber.startToDrag || this.scrubber.isDragging) {
-                    this.seekToPosition(time);
-                    this.showPosition(time, duration);
-                  }
-
-                  this.scrubber.startToDrag = true;
-                },
-
-                onScrubberChange: function(e) {
-                  this.scrubber.startToDrag = false;
-                  this.scrubber.isDragging = false;
+          this.startFadeOut(this.controlBar, false);
+          this.textTrackList.setAttribute("hidden", "true");
+          clearTimeout(this._showControlsTimeout);
+          Utils._controlsHiddenByTimeout = false;
+        }
+      },
 
-                  if (this.isPausedByDragging) {
-                    this.video.play();
-                    this.isPausedByDragging = false;
-                  }
-                },
+      startFadeIn(element, immediate) {
+        this.startFade(element, true, immediate);
+      },
+
+      startFadeOut(element, immediate) {
+        this.startFade(element, false, immediate);
+      },
 
-                updateScrubberProgress() {
-                  if (this.videocontrols.isTouchControls) {
-                    return;
-                  }
-
-                  const positionPercent = this.scrubber.value / this.scrubber.max * 100;
-
-                  if (!isNaN(positionPercent) && positionPercent != Infinity) {
-                    this.progressBar.value = positionPercent;
-                  } else {
-                    this.progressBar.value = 0;
-                  }
-                },
+      startFade(element, fadeIn, immediate) {
+        if (element.classList.contains("controlBar") && fadeIn) {
+          // 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) {
+            if (this.videocontrols.isTouchControls) {
+              this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
+            } else {
+              this.scrubber.value = this.video.currentTime * 1000;
+            }
+          }
+        }
 
-                seekToPosition : function(newPosition) {
-                    newPosition /= 1000; // convert from ms
-                    this.log("+++ seeking to " + newPosition);
-                    if (this.videocontrols.isGonk) {
-                        // We use fastSeek() on B2G, and an accurate (but slower)
-                        // seek on other platforms (that are likely to be higher
-                        // perf).
-                        this.video.fastSeek(newPosition);
-                    } else {
-                        this.video.currentTime = newPosition;
-                    }
-                },
+        if (immediate) {
+          element.setAttribute("immediate", true);
+        } else {
+          element.removeAttribute("immediate");
+        }
 
-                setVolume : function(newVolume) {
-                    this.log("*** setting volume to " + newVolume);
-                    this.video.volume = newVolume;
-                    this.video.muted = false;
-                },
+        if (fadeIn && !(element.isAdjustableControl && element.hideByAdjustment)) {
+          element.hidden = false;
+          // force style resolution, so that transition begins
+          // when we remove the attribute.
+          element.clientTop;
+          element.removeAttribute("fadeout");
+          if (element.classList.contains("controlBar")) {
+            this.controlsSpacer.removeAttribute("hideCursor");
+          }
+        } else {
+          element.setAttribute("fadeout", true);
+          if (element.classList.contains("controlBar") && !this.hasError() &&
+              document.mozFullScreenElement == this.video) {
+            this.controlsSpacer.setAttribute("hideCursor", true);
+          }
+        }
+      },
 
-                showPosition : function(currentTime, duration) {
-                    // If the duration is unknown (because the server didn't provide
-                    // it, or the video is a stream), then we want to fudge the duration
-                    // by using the maximum playback position that's been seen.
-                    if (currentTime > this.maxCurrentTimeSeen)
-                        this.maxCurrentTimeSeen = currentTime;
-                    this.showDuration(duration);
+      onTransitionEnd(event) {
+        // Ignore events for things other than opacity changes.
+        if (event.propertyName != "opacity") {
+          return;
+        }
 
-                    this.log("time update @ " + currentTime + "ms of " + duration + "ms");
-
-                    let positionTime = this.formatTime(currentTime);
+        var element = event.originalTarget;
 
-                    this.scrubber.value = currentTime;
-                    if (this.videocontrols.isTouchControls) {
-                      this.positionLabel.setAttribute("value", positionTime);
-                    } else {
-                      this.positionDurationBox.position = positionTime;
-                      this.updateScrubberProgress();
-                    }
-                },
+        // Nothing to do when a fade *in* finishes.
+        if (!element.hasAttribute("fadeout")) {
+          return;
+        }
+
+        if (this.videocontrols.isTouchControls) {
+          this.scrubber.dragStateChanged(false);
+        }
+        element.hidden = true;
+      },
+
+      _triggeredByControls: false,
 
-                showBuffered : function() {
-                    function bsearch(haystack, needle, cmp) {
-                        var length = haystack.length;
-                        var low = 0;
-                        var high = length;
-                        while (low < high) {
-                            var probe = low + ((high - low) >> 1);
-                            var r = cmp(haystack, probe, needle);
-                            if (r == 0) {
-                                return probe;
-                            } else if (r > 0) {
-                                low = probe + 1;
-                            } else {
-                                high = probe;
-                            }
-                        }
-                        return -1;
-                    }
+      startPlay() {
+        this._triggeredByControls = true;
+        this.hideClickToPlay();
+        this.video.play();
+      },
+
+      togglePause() {
+        if (this.video.paused || this.video.ended) {
+          this.startPlay();
+        } else {
+          this.video.pause();
+        }
 
-                    function bufferedCompare(buffered, i, time) {
-                        if (time > buffered.end(i)) {
-                            return 1;
-                        } else if (time >= buffered.start(i)) {
-                            return 0;
-                        }
-                        return -1;
-                    }
+        // We'll handle style changes in the event listener for
+        // the "play" and "pause" events, same as if content
+        // script was controlling video playback.
+      },
 
-                    var duration = Math.round(this.video.duration * 1000);
-                    if (isNaN(duration) || duration == Infinity) {
-                        duration = this.maxCurrentTimeSeen;
-                    }
+      isVideoWithoutAudioTrack() {
+        return this.video.readyState >= this.video.HAVE_METADATA &&
+               !this.isAudioOnly &&
+               !this.video.mozHasAudio;
+      },
 
-                    // Find the range that the current play position is in and use that
-                    // range for bufferBar.  At some point we may support multiple ranges
-                    // displayed in the bar.
-                    var currentTime = this.video.currentTime;
-                    var buffered = this.video.buffered;
-                    var index = bsearch(buffered, currentTime, bufferedCompare);
-                    var endTime = 0;
-                    if (index >= 0) {
-                        endTime = Math.round(buffered.end(index) * 1000);
-                    }
-                    this.bufferBar.max = duration;
-                    this.bufferBar.value = endTime;
-                },
+      toggleMute() {
+        if (this.isVideoWithoutAudioTrack()) {
+          return;
+        }
+        this.video.muted = !this.isEffectivelyMuted();
+        if (this.video.volume === 0) {
+          this.video.volume = 0.5;
+        }
 
-                _controlsHiddenByTimeout : false,
-                _showControlsTimeout : 0,
-                SHOW_CONTROLS_TIMEOUT_MS: 500,
-                _showControlsFn : function() {
-                    if (Utils.video.matches("video:hover")) {
-                        Utils.startFadeIn(Utils.controlBar, false);
-                        Utils._showControlsTimeout = 0;
-                        Utils._controlsHiddenByTimeout = false;
-                    }
-                },
+        // We'll handle style changes in the event listener for
+        // the "volumechange" event, same as if content script was
+        // controlling volume.
+      },
+
+      isVideoInFullScreen() {
+        return document.mozFullScreenElement == this.video;
+      },
 
-                _hideControlsTimeout : 0,
-                _hideControlsFn : function() {
-                    if (!Utils.scrubber.isDragging) {
-                        Utils.startFade(Utils.controlBar, false);
-                        Utils._hideControlsTimeout = 0;
-                        Utils._controlsHiddenByTimeout = true;
-                    }
-                },
-                HIDE_CONTROLS_TIMEOUT_MS : 2000,
-                onMouseMove : function(event) {
-                    // Pause playing video when the mouse is dragging over the control bar.
-                    if (this.scrubber.startToDrag) {
-                      this.scrubber.isDragging = true;
-                      this.pauseVideoDuringDragging();
-                    }
+      toggleFullscreen() {
+        this.isVideoInFullScreen() ?
+          document.mozCancelFullScreen() :
+          this.video.mozRequestFullScreen();
+      },
 
-                    // 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;
-
-                    if (this._controlsHiddenByTimeout)
-                        this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
-                    else
-                        this.startFade(this.controlBar, true);
+      setFullscreenButtonState() {
+        if (this.isAudioOnly || !document.mozFullScreenEnabled) {
+          this.controlBar.setAttribute("fullscreen-unavailable", true);
+          this.adjustControlSize();
+          return;
+        }
+        this.controlBar.removeAttribute("fullscreen-unavailable");
+        this.adjustControlSize();
 
-                    // Hide the controls if the mouse cursor is left on top of the video
-                    // but above the control bar and if the click-to-play overlay is hidden.
-                    if ((this._controlsHiddenByTimeout ||
-                         event.clientY < this.controlBar.getBoundingClientRect().top) &&
-                        this.clickToPlay.hidden) {
-                        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);
+        var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
+        var value = this.fullscreenButton.getAttribute(attrName);
+        this.fullscreenButton.setAttribute("aria-label", value);
 
-                    // 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;
-
-                    var isMouseOver = (event.type == "mouseover");
+        if (this.isVideoInFullScreen()) {
+          this.fullscreenButton.setAttribute("fullscreened", "true");
+        } else {
+          this.fullscreenButton.removeAttribute("fullscreened");
+        }
+      },
 
-                    var controlRect = this.controlBar.getBoundingClientRect();
-                    var isMouseInControls = event.clientY > controlRect.top &&
-                                            event.clientY < controlRect.bottom &&
-                                            event.clientX > controlRect.left &&
-                                            event.clientX < controlRect.right;
-
-                    // 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;
+      onFullscreenChange() {
+        if (this.isVideoInFullScreen()) {
+          Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
+        }
+        this.setFullscreenButtonState();
+      },
 
-                    if (!isMouseOver && !isMouseInControls) {
-                        this.adjustControlSize();
-
-                        // Keep the controls visible if the click-to-play is visible.
-                        if (!this.clickToPlay.hidden)
-                            return;
-
-                        this.startFadeOut(this.controlBar, false);
-                        this.textTrackList.setAttribute("hidden", "true");
-                        clearTimeout(this._showControlsTimeout);
-                        Utils._controlsHiddenByTimeout = false;
-                    }
-                },
+      clickToPlayClickHandler(e) {
+        if (e.button != 0) {
+          return;
+        }
+        if (this.hasError() && !this.suppressError) {
+          // Errors that can be dismissed should be placed here as we discover them.
+          if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED) {
+            return;
+          }
+          this.statusOverlay.hidden = true;
+          this.suppressError = true;
+          return;
+        }
+        if (e.defaultPrevented) {
+          return;
+        }
+        if (this.playButton.hasAttribute("paused")) {
+          this.startPlay();
+        } else {
+          this.video.pause();
+        }
+      },
+      hideClickToPlay() {
+        let videoHeight = this.video.clientHeight;
+        let videoWidth = this.video.clientWidth;
 
-                startFadeIn : function(element, immediate) {
-                    this.startFade(element, true, immediate);
-                },
-
-                startFadeOut : function(element, immediate) {
-                    this.startFade(element, false, immediate);
-                },
-
-                startFade : function(element, fadeIn, immediate) {
-                    if (element.classList.contains("controlBar") && fadeIn) {
-                        // 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)
-                          if (this.videocontrols.isTouchControls) {
-                            this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
-                          } else {
-                            this.scrubber.value = this.video.currentTime * 1000;
-                          }
-                    }
+        // The play button will animate to 3x its size. This
+        // shows the animation unless the video is too small
+        // to show 2/3 of the animation.
+        let animationScale = 2;
+        let animationMinSize = this.clickToPlay.minWidth * animationScale;
 
-                    if (immediate)
-                        element.setAttribute("immediate", true);
-                    else
-                        element.removeAttribute("immediate");
+        if (animationMinSize > videoWidth ||
+            animationMinSize > (videoHeight - this.controlBar.minHeight)) {
+          this.clickToPlay.setAttribute("immediate", "true");
+          this.clickToPlay.hidden = true;
+        } else {
+          this.clickToPlay.removeAttribute("immediate");
+        }
+        this.clickToPlay.setAttribute("fadeout", "true");
+        this.controlsSpacer.setAttribute("fadeout", "true");
+      },
 
-                    if (fadeIn && !(element.isAdjustableControl && element.hideByAdjustment)) {
-                        element.hidden = false;
-                        // force style resolution, so that transition begins
-                        // when we remove the attribute.
-                        element.clientTop;
-                        element.removeAttribute("fadeout");
-                        if (element.classList.contains("controlBar"))
-                            this.controlsSpacer.removeAttribute("hideCursor");
-                    } else {
-                        element.setAttribute("fadeout", true);
-                        if (element.classList.contains("controlBar") && !this.hasError() &&
-                            document.mozFullScreenElement == this.video)
-                            this.controlsSpacer.setAttribute("hideCursor", true);
+      setPlayButtonState(aPaused) {
+        if (aPaused) {
+          this.playButton.setAttribute("paused", "true");
+        } else {
+          this.playButton.removeAttribute("paused");
+        }
+
+        var attrName = aPaused ? "playlabel" : "pauselabel";
+        var value = this.playButton.getAttribute(attrName);
+        this.playButton.setAttribute("aria-label", value);
+      },
 
-                    }
-                },
+      isEffectivelyMuted() {
+        return this.video.muted || !this.video.volume;
+      },
 
-                onTransitionEnd : function(event) {
-                    // Ignore events for things other than opacity changes.
-                    if (event.propertyName != "opacity")
-                        return;
+      updateMuteButtonState() {
+        var muted = this.isEffectivelyMuted();
 
-                    var element = event.originalTarget;
+        if (muted) {
+          this.muteButton.setAttribute("muted", "true");
+        } else {
+          this.muteButton.removeAttribute("muted");
+        }
 
-                    // Nothing to do when a fade *in* finishes.
-                    if (!element.hasAttribute("fadeout"))
-                        return;
+        var attrName = muted ? "unmutelabel" : "mutelabel";
+        var value = this.muteButton.getAttribute(attrName);
+        this.muteButton.setAttribute("aria-label", value);
+      },
 
-                    if (this.videocontrols.isTouchControls) {
-                      this.scrubber.dragStateChanged(false);
-                    }
-                    element.hidden = true;
-                },
+      _getComputedPropertyValueAsInt(element, property) {
+        let value = getComputedStyle(element, null).getPropertyValue(property);
+        return parseInt(value, 10);
+      },
 
-                _triggeredByControls: false,
+      keyHandler(event) {
+        // Ignore keys when content might be providing its own.
+        if (!this.video.hasAttribute("controls")) {
+          return;
+        }
 
-                startPlay : function() {
-                    this._triggeredByControls = true;
-                    this.hideClickToPlay();
-                    this.video.play();
-                },
-
-                togglePause : function() {
-                    if (this.video.paused || this.video.ended) {
-                        this.startPlay();
-                    } else {
-                        this.video.pause();
-                    }
-
-                    // We'll handle style changes in the event listener for
-                    // the "play" and "pause" events, same as if content
-                    // script was controlling video playback.
-                },
-
-                isVideoWithoutAudioTrack : function() {
-                  return this.video.readyState >= this.video.HAVE_METADATA &&
-                         !this.isAudioOnly &&
-                         !this.video.mozHasAudio;
-                },
+        var keystroke = "";
+        if (event.altKey) {
+          keystroke += "alt-";
+        }
+        if (event.shiftKey) {
+          keystroke += "shift-";
+        }
+        if (navigator.platform.startsWith("Mac")) {
+          if (event.metaKey) {
+            keystroke += "accel-";
+          }
+          if (event.ctrlKey) {
+            keystroke += "control-";
+          }
+        } else {
+          if (event.metaKey) {
+            keystroke += "meta-";
+          }
+          if (event.ctrlKey) {
+            keystroke += "accel-";
+          }
+        }
+        switch (event.keyCode) {
+          case KeyEvent.DOM_VK_UP:
+            keystroke += "upArrow";
+            break;
+          case KeyEvent.DOM_VK_DOWN:
+            keystroke += "downArrow";
+            break;
+          case KeyEvent.DOM_VK_LEFT:
+            keystroke += "leftArrow";
+            break;
+          case KeyEvent.DOM_VK_RIGHT:
+            keystroke += "rightArrow";
+            break;
+          case KeyEvent.DOM_VK_HOME:
+            keystroke += "home";
+            break;
+          case KeyEvent.DOM_VK_END:
+            keystroke += "end";
+            break;
+        }
 
-                toggleMute : function() {
-                    if (this.isVideoWithoutAudioTrack()) {
-                        return;
-                    }
-                    this.video.muted = !this.isEffectivelyMuted();
-                    if (this.video.volume === 0) {
-                      this.video.volume = 0.5;
-                    }
-
-                    // We'll handle style changes in the event listener for
-                    // the "volumechange" event, same as if content script was
-                    // controlling volume.
-                },
-
-                isVideoInFullScreen : function() {
-                    return document.mozFullScreenElement == this.video;
-                },
+        if (String.fromCharCode(event.charCode) == ' ') {
+          keystroke += "space";
+        }
 
-                toggleFullscreen : function() {
-                    this.isVideoInFullScreen() ?
-                        document.mozCancelFullScreen() :
-                        this.video.mozRequestFullScreen();
-                },
-
-                setFullscreenButtonState : function() {
-                    if (this.isAudioOnly || !document.mozFullScreenEnabled) {
-                        this.controlBar.setAttribute("fullscreen-unavailable", true);
-                        this.adjustControlSize();
-                        return;
-                    }
-                    this.controlBar.removeAttribute("fullscreen-unavailable");
-                    this.adjustControlSize();
-
-                    var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
-                    var value = this.fullscreenButton.getAttribute(attrName);
-                    this.fullscreenButton.setAttribute("aria-label", value);
+        this.log("Got keystroke: " + keystroke);
+        var oldval, newval;
 
-                    if (this.isVideoInFullScreen())
-                        this.fullscreenButton.setAttribute("fullscreened", "true");
-                    else
-                        this.fullscreenButton.removeAttribute("fullscreened");
-                },
-
-                onFullscreenChange: function() {
-                    if (this.isVideoInFullScreen()) {
-                        Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
-                    }
-                    this.setFullscreenButtonState();
-                },
+        try {
+          switch (keystroke) {
+            case "space": /* Play */
+              this.togglePause();
+              break;
+            case "downArrow": /* Volume decrease */
+              oldval = this.video.volume;
+              this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
+              this.video.muted = false;
+              break;
+            case "upArrow": /* Volume increase */
+              oldval = this.video.volume;
+              this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
+              this.video.muted = false;
+              break;
+            case "accel-downArrow": /* Mute */
+              this.video.muted = true;
+              break;
+            case "accel-upArrow": /* Unmute */
+              this.video.muted = false;
+              break;
+            case "leftArrow": /* Seek back 15 seconds */
+            case "accel-leftArrow": /* Seek back 10% */
+              oldval = this.video.currentTime;
+              if (keystroke == "leftArrow") {
+                newval = oldval - 15;
+              } else {
+                newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
+              }
+              this.video.currentTime = (newval >= 0 ? newval : 0);
+              break;
+            case "rightArrow": /* Seek forward 15 seconds */
+            case "accel-rightArrow": /* Seek forward 10% */
+              oldval = this.video.currentTime;
+              var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+              if (keystroke == "rightArrow") {
+                newval = oldval + 15;
+              } else {
+                newval = oldval + maxtime / 10;
+              }
+              this.video.currentTime = (newval <= maxtime ? newval : maxtime);
+              break;
+            case "home": /* Seek to beginning */
+              this.video.currentTime = 0;
+              break;
+            case "end": /* Seek to end */
+              if (this.video.currentTime != this.video.duration) {
+                this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+              }
+              break;
+            default:
+              return;
+          }
+        } catch (e) { /* ignore any exception from setting .currentTime */ }
 
-                clickToPlayClickHandler : function(e) {
-                    if (e.button != 0)
-                        return;
-                    if (this.hasError() && !this.suppressError) {
-                        // Errors that can be dismissed should be placed here as we discover them.
-                        if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED)
-                            return;
-                        this.statusOverlay.hidden = true;
-                        this.suppressError = true;
-                        return;
-                    }
-                    if (e.defaultPrevented)
-                        return;
-                    if (this.playButton.hasAttribute("paused")) {
-                        this.startPlay();
-                    } else {
-                        this.video.pause();
-                    }
-                },
-                hideClickToPlay : function() {
-                    let videoHeight = this.video.clientHeight;
-                    let videoWidth = this.video.clientWidth;
-
-                    // The play button will animate to 3x its size. This
-                    // shows the animation unless the video is too small
-                    // to show 2/3 of the animation.
-                    let animationScale = 2;
-                    let animationMinSize = this.clickToPlay.minWidth * animationScale;
-
-                    if (animationMinSize > videoWidth ||
-                        animationMinSize > (videoHeight - this.controlBar.minHeight)) {
-                      this.clickToPlay.setAttribute("immediate", "true");
-                      this.clickToPlay.hidden = true;
-                    } else {
-                        this.clickToPlay.removeAttribute("immediate");
-                    }
-                    this.clickToPlay.setAttribute("fadeout", "true");
-                    this.controlsSpacer.setAttribute("fadeout", "true");
-                },
-
-                setPlayButtonState : function(aPaused) {
-                  if (aPaused)
-                      this.playButton.setAttribute("paused", "true");
-                  else
-                      this.playButton.removeAttribute("paused");
-
-                  var attrName = aPaused ? "playlabel" : "pauselabel";
-                  var value = this.playButton.getAttribute(attrName);
-                  this.playButton.setAttribute("aria-label", value);
-                },
+        event.preventDefault(); // Prevent page scrolling
+      },
 
-                isEffectivelyMuted : function() {
-                  return this.video.muted || !this.video.volume;
-                },
+      isSupportedTextTrack(textTrack) {
+        return textTrack.kind == "subtitles" ||
+               textTrack.kind == "captions";
+      },
 
-                updateMuteButtonState : function() {
-                  var muted = this.isEffectivelyMuted();
-
-                  if (muted)
-                      this.muteButton.setAttribute("muted", "true");
-                  else
-                      this.muteButton.removeAttribute("muted");
+      get isClosedCaptionAvailable() {
+        return this.overlayableTextTracks.length && !this.videocontrols.isTouchControls;
+      },
 
-                  var attrName = muted ? "unmutelabel" : "mutelabel";
-                  var value = this.muteButton.getAttribute(attrName);
-                  this.muteButton.setAttribute("aria-label", value);
-                },
+      get overlayableTextTracks() {
+        return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
+      },
 
-                _getComputedPropertyValueAsInt : function(element, property) {
-                  let value = getComputedStyle(element, null).getPropertyValue(property);
-                  return parseInt(value, 10);
-                },
+      isClosedCaptionOn() {
+        for (let tt of this.overlayableTextTracks) {
+          if (tt.mode === "showing") {
+            return true;
+          }
+        }
 
-                keyHandler : function(event) {
-                    // Ignore keys when content might be providing its own.
-                    if (!this.video.hasAttribute("controls"))
-                        return;
+        return false;
+      },
 
-                    var keystroke = "";
-                    if (event.altKey)
-                        keystroke += "alt-";
-                    if (event.shiftKey)
-                        keystroke += "shift-";
-                    if (navigator.platform.startsWith("Mac")) {
-                        if (event.metaKey)
-                            keystroke += "accel-";
-                        if (event.ctrlKey)
-                            keystroke += "control-";
-                    } else {
-                        if (event.metaKey)
-                            keystroke += "meta-";
-                        if (event.ctrlKey)
-                            keystroke += "accel-";
-                    }
-                    switch (event.keyCode) {
-                        case KeyEvent.DOM_VK_UP:
-                            keystroke += "upArrow";
-                            break;
-                        case KeyEvent.DOM_VK_DOWN:
-                            keystroke += "downArrow";
-                            break;
-                        case KeyEvent.DOM_VK_LEFT:
-                            keystroke += "leftArrow";
-                            break;
-                        case KeyEvent.DOM_VK_RIGHT:
-                            keystroke += "rightArrow";
-                            break;
-                        case KeyEvent.DOM_VK_HOME:
-                            keystroke += "home";
-                            break;
-                        case KeyEvent.DOM_VK_END:
-                            keystroke += "end";
-                            break;
-                    }
+      setClosedCaptionButtonState() {
+        if (!this.isClosedCaptionAvailable) {
+          this.closedCaptionButton.setAttribute("hidden", "true");
+          return;
+        }
+
+        this.closedCaptionButton.removeAttribute("hidden");
+
+        if (this.isClosedCaptionOn()) {
+          this.closedCaptionButton.setAttribute("enabled", "true");
+        } else {
+          this.closedCaptionButton.removeAttribute("enabled");
+        }
+        this.adjustControlSize();
+
+        let ttItems = this.textTrackList.childNodes;
+
+        for (let tti of ttItems) {
+          const idx = +tti.getAttribute("index");
+
+          if (idx == this.currentTextTrackIndex) {
+            tti.setAttribute("on", "true");
+          } else {
+            tti.removeAttribute("on");
+          }
+        }
+      },
+
+      addNewTextTrack(tt) {
+        if (!this.isSupportedTextTrack(tt)) {
+          return;
+        }
 
-                    if (String.fromCharCode(event.charCode) == ' ')
-                        keystroke += "space";
+        if (tt.index && tt.index < this.textTracksCount) {
+          return;
+        }
+
+        tt.index = this.textTracksCount++;
+
+        const label = tt.label || "";
+        const ttText = document.createTextNode(label);
+        const ttBtn = document.createElement("button");
+
+        ttBtn.classList.add("textTrackItem");
+        ttBtn.setAttribute("index", tt.index);
 
-                    this.log("Got keystroke: " + keystroke);
-                    var oldval, newval;
+        ttBtn.addEventListener("click", function(event) {
+          event.stopPropagation();
+
+          this.changeTextTrack(tt.index);
+        }.bind(this));
+
+        ttBtn.appendChild(ttText);
+
+        this.textTrackList.appendChild(ttBtn);
+
+        if (tt.mode === "showing" && tt.index) {
+          this.changeTextTrack(tt.index);
+        }
+      },
 
-                    try {
-                        switch (keystroke) {
-                            case "space": /* Play */
-                                this.togglePause();
-                                break;
-                            case "downArrow": /* Volume decrease */
-                                oldval = this.video.volume;
-                                this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
-                                this.video.muted = false;
-                                break;
-                            case "upArrow": /* Volume increase */
-                                oldval = this.video.volume;
-                                this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
-                                this.video.muted = false;
-                                break;
-                            case "accel-downArrow": /* Mute */
-                                this.video.muted = true;
-                                break;
-                            case "accel-upArrow": /* Unmute */
-                                this.video.muted = false;
-                                break;
-                            case "leftArrow": /* Seek back 15 seconds */
-                            case "accel-leftArrow": /* Seek back 10% */
-                                oldval = this.video.currentTime;
-                                if (keystroke == "leftArrow")
-                                    newval = oldval - 15;
-                                else
-                                    newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
-                                this.video.currentTime = (newval >= 0 ? newval : 0);
-                                break;
-                            case "rightArrow": /* Seek forward 15 seconds */
-                            case "accel-rightArrow": /* Seek forward 10% */
-                                oldval = this.video.currentTime;
-                                var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
-                                if (keystroke == "rightArrow")
-                                    newval = oldval + 15;
-                                else
-                                    newval = oldval + maxtime / 10;
-                                this.video.currentTime = (newval <= maxtime ? newval : maxtime);
-                                break;
-                            case "home": /* Seek to beginning */
-                                this.video.currentTime = 0;
-                                break;
-                            case "end": /* Seek to end */
-                                if (this.video.currentTime != this.video.duration)
-                                    this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
-                                break;
-                            default:
-                                return;
-                        }
-                    } catch (e) { /* ignore any exception from setting .currentTime */ }
+      changeTextTrack(index) {
+        for (let tt of this.overlayableTextTracks) {
+          if (tt.index === index) {
+            tt.mode = "showing";
+
+            this.currentTextTrackIndex = tt.index;
+          } else {
+            tt.mode = "disabled";
+          }
+        }
+
+        // should fallback to off
+        if (this.currentTextTrackIndex !== index) {
+          this.currentTextTrackIndex = 0;
+        }
 
-                    event.preventDefault(); // Prevent page scrolling
-                },
+        this.textTrackList.setAttribute("hidden", "true");
+        this.setClosedCaptionButtonState();
+      },
 
-                isSupportedTextTrack : function(textTrack) {
-                  return textTrack.kind == "subtitles" ||
-                         textTrack.kind == "captions";
-                },
+      onControlBarTransitioned() {
+        this.textTrackList.setAttribute("hidden", "true");
+        this.video.dispatchEvent(new CustomEvent("controlbarchange"));
+      },
+
+      toggleClosedCaption() {
+        if (this.textTrackList.hasAttribute("hidden")) {
+          this.textTrackList.removeAttribute("hidden");
+        } else {
+          this.textTrackList.setAttribute("hidden", "true");
+        }
+      },
 
-                get isClosedCaptionAvailable() {
-                  return this.overlayableTextTracks.length && !this.videocontrols.isTouchControls;
-                },
+      onTextTrackAdd(trackEvent) {
+        this.addNewTextTrack(trackEvent.track);
+        this.setClosedCaptionButtonState();
+      },
 
-                get overlayableTextTracks() {
-                  return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
-                },
+      onTextTrackRemove(trackEvent) {
+        const toRemoveIndex = trackEvent.track.index;
+        const ttItems = this.textTrackList.childNodes;
 
-                isClosedCaptionOn : function() {
-                  for (let tt of this.overlayableTextTracks) {
-                    if (tt.mode === "showing") {
-                      return true;
-                    }
-                  }
+        if (!ttItems) {
+          return;
+        }
+
+        for (let tti of ttItems) {
+          const idx = +tti.getAttribute("index");
 
-                  return false;
-                },
+          if (idx === toRemoveIndex) {
+            tti.remove();
+            this.textTracksCount--;
+          }
 
-                setClosedCaptionButtonState : function() {
-                  if (!this.isClosedCaptionAvailable) {
-                    this.closedCaptionButton.setAttribute("hidden", "true");
-                    return;
-                  }
-
-                  this.closedCaptionButton.removeAttribute("hidden");
+          if (idx === this.currentTextTrackIndex) {
+            this.currentTextTrackIndex = 0;
 
-                  if (this.isClosedCaptionOn()) {
-                    this.closedCaptionButton.setAttribute("enabled", "true");
-                  } else {
-                    this.closedCaptionButton.removeAttribute("enabled");
-                  }
-                  this.adjustControlSize();
+            this.video.dispatchEvent(new CustomEvent("texttrackchange"));
+          }
+        }
 
-                  let ttItems = this.textTrackList.childNodes;
+        this.setClosedCaptionButtonState();
+      },
 
-                  for (let tti of ttItems) {
-                    const idx = +tti.getAttribute("index");
+      initTextTracks() {
+        if (!this.isClosedCaptionAvailable) {
+          this.closedCaptionButton.setAttribute("hidden", "true");
+          return;
+        }
 
-                    if (idx == this.currentTextTrackIndex) {
-                      tti.setAttribute("on", "true");
-                    } else {
-                      tti.removeAttribute("on");
-                    }
-                  }
-                },
+        const offLabel = this.textTrackList.getAttribute("offlabel");
 
-                addNewTextTrack : function(tt) {
-                  if (!this.isSupportedTextTrack(tt)) {
-                    return;
-                  }
+        this.addNewTextTrack({
+          label: offLabel,
+          kind: "subtitles"
+        });
 
-                  if (tt.index && tt.index < this.textTracksCount) {
-                    return;
-                  }
-
-                  tt.index = this.textTracksCount++;
+        for (let tt of this.overlayableTextTracks) {
+          this.addNewTextTrack(tt);
+        }
 
-                  const label = tt.label || "";
-                  const ttText = document.createTextNode(label);
-                  const ttBtn = document.createElement("button");
-
-                  ttBtn.classList.add("textTrackItem");
-                  ttBtn.setAttribute("index", tt.index);
+        this.setClosedCaptionButtonState();
+      },
 
-                  ttBtn.addEventListener("click", function(event) {
-                    event.stopPropagation();
-
-                    this.changeTextTrack(tt.index);
-                  }.bind(this));
-
-                  ttBtn.appendChild(ttText);
-
-                  this.textTrackList.appendChild(ttBtn);
-
-                  if (tt.mode === "showing" && tt.index) {
-                    this.changeTextTrack(tt.index);
-                  }
-                },
+      isEventWithin(event, parent1, parent2) {
+        function isDescendant(node) {
+          while (node) {
+            if (node == parent1 || node == parent2) {
+              return true;
+            }
+            node = node.parentNode;
+          }
+          return false;
+        }
+        return isDescendant(event.target) && isDescendant(event.relatedTarget);
+      },
 
-                changeTextTrack : function(index) {
-                  for (let tt of this.overlayableTextTracks) {
-                    if (tt.index === index) {
-                      tt.mode = "showing";
+      log(msg) {
+        if (this.debug) {
+          console.log("videoctl: " + msg + "\n");
+        }
+      },
 
-                      this.currentTextTrackIndex = tt.index;
-                    } else {
-                      tt.mode = "disabled";
-                    }
-                  }
-
-                  // should fallback to off
-                  if (this.currentTextTrackIndex !== index) {
-                    this.currentTextTrackIndex = 0;
-                  }
+      get isTopLevelSyntheticDocument() {
+        let doc = this.video.ownerDocument;
+        let win = doc.defaultView;
+        return doc.mozSyntheticDocument && win === win.top;
+      },
 
-                  this.textTrackList.setAttribute("hidden", "true");
-                  this.setClosedCaptionButtonState();
-                },
+      adjustControlSize() {
+        if (!this.controlBar.minWidth || this.videocontrols.isTouchControls) {
+          return;
+        }
 
-                onControlBarTransitioned : function() {
-                  this.textTrackList.setAttribute("hidden", "true");
-                  this.video.dispatchEvent(new CustomEvent("controlbarchange"));
-                },
+        let videoWidth = this.video.clientWidth;
+        let videoHeight = this.video.clientHeight;
+        const minControlBarPaddingWidth = 18;
 
-                toggleClosedCaption : function() {
-                  if (this.textTrackList.hasAttribute("hidden")) {
-                    this.textTrackList.removeAttribute("hidden");
-                  } else {
-                    this.textTrackList.setAttribute("hidden", "true");
-                  }
-                },
+        // Hide and show control in order.
+        const prioritizedControls = [
+          this.playButton,
+          this.muteButton,
+          this.fullscreenButton,
+          this.closedCaptionButton,
+          this.positionDurationBox,
+          this.scrubberStack,
+          this.durationSpan,
+          this.volumeStack
+        ];
 
-                onTextTrackAdd : function(trackEvent) {
-                  this.addNewTextTrack(trackEvent.track);
-                  this.setClosedCaptionButtonState();
-                },
+        if (this.controlBar.hasAttribute("fullscreen-unavailable")) {
+          this.fullscreenButton.isWanted = false;
+        }
+        if (!this.isClosedCaptionAvailable) {
+          this.closedCaptionButton.isWanted = false;
+        }
+        if (this.muteButton.hasAttribute("noAudio")) {
+          this.volumeStack.isWanted = false;
+        }
 
-                onTextTrackRemove : function(trackEvent) {
-                  const toRemoveIndex = trackEvent.track.index;
-                  const ttItems = this.textTrackList.childNodes;
-
-                  if (!ttItems) {
-                    return;
-                  }
-
-                  for (let tti of ttItems) {
-                    const idx = +tti.getAttribute("index");
+        let widthUsed = minControlBarPaddingWidth;
+        let preventAppendControl = false;
 
-                    if (idx === toRemoveIndex) {
-                      tti.remove();
-                      this.textTracksCount--;
-                    }
+        for (let control of prioritizedControls) {
+          if (!control.isWanted) {
+            control.hideByAdjustment = true;
+            continue;
+          }
 
-                    if (idx === this.currentTextTrackIndex) {
-                      this.currentTextTrackIndex = 0;
-
-                      this.video.dispatchEvent(new CustomEvent("texttrackchange"));
-                    }
-                  }
-
-                  this.setClosedCaptionButtonState();
-                },
+          control.hideByAdjustment = preventAppendControl ||
+          widthUsed + control.minWidth > videoWidth;
 
-                initTextTracks : function() {
-                  if (!this.isClosedCaptionAvailable) {
-                    this.closedCaptionButton.setAttribute("hidden", "true");
-                    return;
-                  }
-
-                  const offLabel = this.textTrackList.getAttribute("offlabel");
+          if (control.hideByAdjustment) {
+            preventAppendControl = true;
+          } else {
+            widthUsed += control.minWidth;
+          }
+        }
 
-                  this.addNewTextTrack({
-                    label: offLabel,
-                    kind: "subtitles"
-                  });
-
-                  for (let tt of this.overlayableTextTracks) {
-                    this.addNewTextTrack(tt);
-                  }
+        if (this.durationSpan.hideByAdjustment) {
+          this.positionDurationBox.setAttribute("positionOnly", "true");
+        } else {
+          this.positionDurationBox.removeAttribute("positionOnly");
+        }
 
-                  this.setClosedCaptionButtonState();
-                },
+        if (videoHeight < this.controlBar.minHeight ||
+            widthUsed === minControlBarPaddingWidth) {
+          this.controlBar.setAttribute("size", "hidden");
+          this.controlBar.hideByAdjustment = true;
+        } else {
+          this.controlBar.removeAttribute("size");
+          this.controlBar.hideByAdjustment = false;
+        }
 
-                isEventWithin : function(event, parent1, parent2) {
-                    function isDescendant(node) {
-                        while (node) {
-                            if (node == parent1 || node == parent2)
-                                return true;
-                            node = node.parentNode;
-                        }
-                        return false;
-                    }
-                    return isDescendant(event.target) && isDescendant(event.relatedTarget);
-                },
+        // Use flexible spacer to separate controls when scrubber is hidden.
+        // As long as muteButton hidden, which means only play button presents,
+        // hide spacer and make playButton centered.
+        this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
 
-                log : function(msg) {
-                    if (this.debug)
-                        console.log("videoctl: " + msg + "\n");
-                },
+        // Adjust clickToPlayButton size.
+        const minVideoSideLength = Math.min(videoWidth, videoHeight);
+        const clickToPlayViewRatio = 0.15;
+        const clickToPlayScaledSize = Math.max(
+        this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
 
-                get isTopLevelSyntheticDocument() {
-                  let doc = this.video.ownerDocument;
-                  let win = doc.defaultView;
-                  return doc.mozSyntheticDocument && win === win.top;
-                },
-
-                adjustControlSize : function adjustControlSize() {
-                  if (!this.controlBar.minWidth || this.videocontrols.isTouchControls) {
-                    return;
-                  }
-
-                  let videoWidth = this.video.clientWidth;
-                  let videoHeight = this.video.clientHeight;
-                  const minControlBarPaddingWidth = 18;
+        if (clickToPlayScaledSize >= videoWidth ||
+            (clickToPlayScaledSize + this.controlBar.minHeight / 2 >= videoHeight / 2 )) {
+          this.clickToPlay.hideByAdjustment = true;
+        } else {
+          if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
+            this.clickToPlay.hideByAdjustment = false;
+          }
+          this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
+          this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
+        }
+      },
 
-                  // Hide and show control in order.
-                  const prioritizedControls = [
-                    this.playButton,
-                    this.muteButton,
-                    this.fullscreenButton,
-                    this.closedCaptionButton,
-                    this.positionDurationBox,
-                    this.scrubberStack,
-                    this.durationSpan,
-                    this.volumeStack
-                  ];
+      init(binding) {
+        this.video = binding.parentNode;
+        this.videocontrols = binding;
 
-                  if (this.controlBar.hasAttribute("fullscreen-unavailable")) {
-                    this.fullscreenButton.isWanted = false;
-                  }
-                  if (!this.isClosedCaptionAvailable) {
-                    this.closedCaptionButton.isWanted = false;
-                  }
-                  if (this.muteButton.hasAttribute("noAudio")) {
-                    this.volumeStack.isWanted = false;
-                  }
-
-                  let widthUsed = minControlBarPaddingWidth;
-                  let preventAppendControl = false;
-
-                  for (let control of prioritizedControls) {
-                    if (!control.isWanted) {
-                      control.hideByAdjustment = true;
-                      continue;
-                    }
+        this.controlsContainer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
+        this.statusIcon    = document.getAnonymousElementByAttribute(binding, "anonid", "statusIcon");
+        this.controlBar    = document.getAnonymousElementByAttribute(binding, "anonid", "controlBar");
+        this.playButton    = document.getAnonymousElementByAttribute(binding, "anonid", "playButton");
+        this.controlBarSpacer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlBarSpacer");
+        this.muteButton    = document.getAnonymousElementByAttribute(binding, "anonid", "muteButton");
+        this.volumeStack   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeStack");
+        this.volumeControl = document.getAnonymousElementByAttribute(binding, "anonid", "volumeControl");
+        this.progressBar   = document.getAnonymousElementByAttribute(binding, "anonid", "progressBar");
+        this.bufferBar     = document.getAnonymousElementByAttribute(binding, "anonid", "bufferBar");
+        this.scrubberStack = document.getAnonymousElementByAttribute(binding, "anonid", "scrubberStack");
+        this.scrubber      = document.getAnonymousElementByAttribute(binding, "anonid", "scrubber");
+        this.durationLabel = document.getAnonymousElementByAttribute(binding, "anonid", "durationLabel");
+        this.positionLabel = document.getAnonymousElementByAttribute(binding, "anonid", "positionLabel");
+        this.positionDurationBox   = document.getAnonymousElementByAttribute(binding, "anonid", "positionDurationBox");
+        this.statusOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "statusOverlay");
+        this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "controlsOverlay");
+        this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "anonid", "controlsSpacer");
+        this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "anonid", "clickToPlay");
+        this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "anonid", "fullscreenButton");
+        this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
+        this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
 
-                    control.hideByAdjustment = preventAppendControl ||
-                                               widthUsed + control.minWidth > videoWidth;
+        if (this.positionDurationBox) {
+          this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
+        }
 
-                    if (control.hideByAdjustment) {
-                      preventAppendControl = true;
-                    } else {
-                      widthUsed += control.minWidth;
-                    }
-                  }
-
-                  if (this.durationSpan.hideByAdjustment) {
-                    this.positionDurationBox.setAttribute("positionOnly", "true");
-                  } else {
-                    this.positionDurationBox.removeAttribute("positionOnly");
-                  }
+        // XXX controlsContainer is a desktop only element. To determine whether
+        // isTouchControls or not during the whole initialization process, get
+        // this state overridden here.
+        this.videocontrols.isTouchControls = !this.controlsContainer;
+        this.isAudioOnly = (this.video instanceof HTMLAudioElement);
+        this.setupInitialState();
+        this.setupNewLoadState();
+        this.initTextTracks();
 
-                  if (videoHeight < this.controlBar.minHeight ||
-                      widthUsed === minControlBarPaddingWidth) {
-                    this.controlBar.setAttribute("size", "hidden");
-                    this.controlBar.hideByAdjustment = true;
-                  } else {
-                    this.controlBar.removeAttribute("size");
-                    this.controlBar.hideByAdjustment = false;
-                  }
+        // Use the handleEvent() callback for all media events.
+        // Only the "error" event listener must capture, so that it can trap error
+        // events from <source> children, which don't bubble. But we use capture
+        // for all events in order to simplify the event listener add/remove.
+        for (let event of this.videoEvents) {
+          this.video.addEventListener(event, this, {
+            capture: true,
+            mozSystemGroup: true
+          });
+        }
 
-                  // Use flexible spacer to separate controls when scrubber is hidden.
-                  // As long as muteButton hidden, which means only play button presents,
-                  // hide spacer and make playButton centered.
-                  this.controlBarSpacer.hidden = !this.scrubberStack.hidden || this.muteButton.hidden;
+        var self = this;
+        this.controlListeners = [];
 
-                  // Adjust clickToPlayButton size.
-                  const minVideoSideLength = Math.min(videoWidth, videoHeight);
-                  const clickToPlayViewRatio = 0.15;
-                  const clickToPlayScaledSize = Math.max(
-                    this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
+        // Helper function to add an event listener to the given element
+        function addListener(elem, eventName, func) {
+          let boundFunc = func.bind(self);
+          self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+          elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
+        }
 
-                  if (clickToPlayScaledSize >= videoWidth ||
-                    (clickToPlayScaledSize + this.controlBar.minHeight / 2 >= videoHeight / 2 )) {
+        addListener(this.muteButton, "click", this.toggleMute);
+        addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
+        addListener(this.fullscreenButton, "click", this.toggleFullscreen);
+        addListener(this.playButton, "click", this.clickToPlayClickHandler);
+        addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+        addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
+        addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
 
-                    this.clickToPlay.hideByAdjustment = true;
-                  } else {
-                    if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
-                      this.clickToPlay.hideByAdjustment = false;
-                    }
-                    this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
-                    this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
-                  }
-                },
-
-                init : function(binding) {
-                    this.video = binding.parentNode;
-                    this.videocontrols = binding;
+        addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
+        addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
+        addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
+        addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
+        addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
+        addListener(this.video, "keypress", this.keyHandler);
 
-                    this.controlsContainer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlsContainer");
-                    this.statusIcon    = document.getAnonymousElementByAttribute(binding, "anonid", "statusIcon");
-                    this.controlBar    = document.getAnonymousElementByAttribute(binding, "anonid", "controlBar");
-                    this.playButton    = document.getAnonymousElementByAttribute(binding, "anonid", "playButton");
-                    this.controlBarSpacer    = document.getAnonymousElementByAttribute(binding, "anonid", "controlBarSpacer");
-                    this.muteButton    = document.getAnonymousElementByAttribute(binding, "anonid", "muteButton");
-                    this.volumeStack   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeStack");
-                    this.volumeControl = document.getAnonymousElementByAttribute(binding, "anonid", "volumeControl");
-                    this.progressBar   = document.getAnonymousElementByAttribute(binding, "anonid", "progressBar");
-                    this.bufferBar     = document.getAnonymousElementByAttribute(binding, "anonid", "bufferBar");
-                    this.scrubberStack = document.getAnonymousElementByAttribute(binding, "anonid", "scrubberStack");
-                    this.scrubber      = document.getAnonymousElementByAttribute(binding, "anonid", "scrubber");
-                    this.durationLabel = document.getAnonymousElementByAttribute(binding, "anonid", "durationLabel");
-                    this.positionLabel = document.getAnonymousElementByAttribute(binding, "anonid", "positionLabel");
-                    this.positionDurationBox   = document.getAnonymousElementByAttribute(binding, "anonid", "positionDurationBox");
-                    this.statusOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "statusOverlay");
-                    this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "anonid", "controlsOverlay");
-                    this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "anonid", "controlsSpacer");
-                    this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "anonid", "clickToPlay");
-                    this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "anonid", "fullscreenButton");
-                    this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "anonid", "closedCaptionButton");
-                    this.textTrackList = document.getAnonymousElementByAttribute(binding, "anonid", "textTrackList");
+        addListener(this.videocontrols, "dragstart", function(event) {
+          event.preventDefault(); // prevent dragging of controls image (bug 517114)
+        });
 
-                    if (this.positionDurationBox) {
-                      this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
-                    }
+        if (!this.videocontrols.isTouchControls) {
+          addListener(this.scrubber, "input", this.onScrubberInput);
+          addListener(this.scrubber, "change", this.onScrubberChange);
+          addListener(this.volumeControl, "input", this.updateVolume);
+          addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
+          addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
+        }
 
-                    // XXX controlsContainer is a desktop only element. To determine whether
-                    // isTouchControls or not during the whole initialization process, get
-                    // this state overridden here.
-                    this.videocontrols.isTouchControls = !this.controlsContainer;
-                    this.isAudioOnly = (this.video instanceof HTMLAudioElement);
-                    this.setupInitialState();
-                    this.setupNewLoadState();
-                    this.initTextTracks();
+        this.log("--- videocontrols initialized ---");
+      }
+    };
 
-                    // Use the handleEvent() callback for all media events.
-                    // Only the "error" event listener must capture, so that it can trap error
-                    // events from <source> children, which don't bubble. But we use capture
-                    // for all events in order to simplify the event listener add/remove.
-                    for (let event of this.videoEvents) {
-                        this.video.addEventListener(event, this, {
-                            capture: true,
-                            mozSystemGroup: true
-                        });
-                    }
-
-                    var self = this;
-                    this.controlListeners = [];
+    this.Utils.init(this);
+    ]]>
+  </constructor>
+  <destructor>
+    <![CDATA[
+    this.Utils.terminateEventListeners();
+    // randomID used to be a <field>, which meant that the XBL machinery
+    // undefined the property when the element was unbound. The code in
+    // this file actually depends on this, so now that randomID is an
+    // expando, we need to make sure to explicitly delete it.
+    delete this.randomID;
+    ]]>
+  </destructor>
 
-                    // Helper function to add an event listener to the given element
-                    function addListener(elem, eventName, func) {
-                      let boundFunc = func.bind(self);
-                      self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
-                      elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
-                    }
-
-                    addListener(this.muteButton, "click", this.toggleMute);
-                    addListener(this.closedCaptionButton, "click", this.toggleClosedCaption);
-                    addListener(this.fullscreenButton, "click", this.toggleFullscreen);
-                    addListener(this.playButton, "click", this.clickToPlayClickHandler);
-                    addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
-                    addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
-                    addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
+  </implementation>
 
-                    addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
-                    addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
-                    addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
-                    addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
-                    addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
-                    addListener(this.video, "keypress", this.keyHandler);
-
-                    addListener(this.videocontrols, "dragstart", function(event) {
-                        event.preventDefault(); // prevent dragging of controls image (bug 517114)
-                    });
-
-                    if (!this.videocontrols.isTouchControls) {
-                      addListener(this.scrubber, "input", this.onScrubberInput);
-                      addListener(this.scrubber, "change", this.onScrubberChange);
-                      addListener(this.volumeControl, "input", this.updateVolume);
-                      addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
-                      addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);
-                    }
+  <handlers>
+    <handler event="mouseover">
+      if (!this.isTouchControls) {
+        this.Utils.onMouseInOut(event);
+      }
+    </handler>
+    <handler event="mouseout">
+      if (!this.isTouchControls) {
+        this.Utils.onMouseInOut(event);
+      }
+    </handler>
+    <handler event="mousemove">
+      if (!this.isTouchControls) {
+        this.Utils.onMouseMove(event);
+      }
+    </handler>
+  </handlers>
+</binding>
 
-                    this.log("--- videocontrols initialized ---");
-                }
-            };
+<binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
 
-            this.Utils.init(this);
-            ]]>
-        </constructor>
-        <destructor>
-            <![CDATA[
-            this.Utils.terminateEventListeners();
-            // randomID used to be a <field>, which meant that the XBL machinery
-            // undefined the property when the element was unbound. The code in
-            // this file actually depends on this, so now that randomID is an
-            // expando, we need to make sure to explicitly delete it.
-            delete this.randomID;
-            ]]>
-        </destructor>
-
-    </implementation>
+  <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
+    <stack flex="1">
+      <vbox anonid="statusOverlay" flex="1" class="statusOverlay" hidden="true">
+        <box anonid="statusIcon" 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.noSource2;</label>
+        <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
+      </vbox>
 
-    <handlers>
-        <handler event="mouseover">
-            if (!this.isTouchControls)
-                this.Utils.onMouseInOut(event);
-        </handler>
-        <handler event="mouseout">
-            if (!this.isTouchControls)
-                this.Utils.onMouseInOut(event);
-        </handler>
-        <handler event="mousemove">
-            if (!this.isTouchControls)
-                this.Utils.onMouseMove(event);
-        </handler>
-    </handlers>
-  </binding>
+      <vbox anonid="controlsOverlay" class="controlsOverlay">
+        <spacer anonid="controlsSpacer" class="controlsSpacer" flex="1"/>
+        <box flex="1" hidden="true">
+          <box anonid="clickToPlay" class="clickToPlay" hidden="true" flex="1"/>
+          <vbox anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
+        </box>
+        <vbox anonid="controlBar" class="controlBar" hidden="true">
+          <hbox class="buttonsBar">
+            <button anonid="playButton"
+                    class="playButton"
+                    playlabel="&playButton.playLabel;"
+                    pauselabel="&playButton.pauseLabel;"/>
+            <label anonid="positionLabel" class="positionLabel" role="presentation"/>
+            <stack anonid="scrubberStack" class="scrubberStack">
+              <box class="backgroundBar"/>
+              <progressmeter class="flexibleBar" value="100"/>
+              <progressmeter anonid="bufferBar" class="bufferBar"/>
+              <progressmeter anonid="progressBar" class="progressBar" max="10000"/>
+              <scale anonid="scrubber" class="scrubber" movetoclick="true"/>
+            </stack>
+            <label anonid="durationLabel" class="durationLabel" role="presentation"/>
+            <button anonid="muteButton"
+                    class="muteButton"
+                    mutelabel="&muteButton.muteLabel;"
+                    unmutelabel="&muteButton.unmuteLabel;"/>
+            <stack anonid="volumeStack" class="volumeStack">
+              <box anonid="volumeBackground" class="volumeBackground"/>
+              <box anonid="volumeForeground" class="volumeForeground"/>
+              <scale anonid="volumeControl" class="volumeControl" movetoclick="true"/>
+            </stack>
+            <button anonid="castingButton" class="castingButton" hidden="true"
+                    aria-label="&castingButton.castingLabel;"/>
+            <button anonid="closedCaptionButton" class="closedCaptionButton" hidden="true"/>
+            <button anonid="fullscreenButton"
+                    class="fullscreenButton"
+                    enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
+                    exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
+          </hbox>
+        </vbox>
+      </vbox>
+    </stack>
+  </xbl:content>
 
-  <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
+  <implementation>
+  <constructor>
+    <![CDATA[
+    this.isTouchControls = true;
+    this.TouchUtils = {
+      videocontrols: null,
+      video: null,
+      controlsTimer: null,
+      controlsTimeout: 5000,
+      positionLabel: null,
+      castingButton: null,
 
-    <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
-        <stack flex="1">
-            <vbox anonid="statusOverlay" flex="1" class="statusOverlay" hidden="true">
-                <box anonid="statusIcon" 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.noSource2;</label>
-                <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
-            </vbox>
+      get Utils() {
+        return this.videocontrols.Utils;
+      },
+
+      get visible() {
+        return !this.Utils.controlBar.hasAttribute("fadeout") &&
+               !(this.Utils.controlBar.getAttribute("hidden") == "true");
+      },
+
+      _firstShow: false,
+      get firstShow() { return this._firstShow; },
+      set firstShow(val) {
+        this._firstShow = val;
+        this.Utils.controlBar.setAttribute("firstshow", val);
+      },
 
-            <vbox anonid="controlsOverlay" class="controlsOverlay">
-                <spacer anonid="controlsSpacer" class="controlsSpacer" flex="1"/>
-                <box flex="1" hidden="true">
-                    <box anonid="clickToPlay" class="clickToPlay" hidden="true" flex="1"/>
-                    <vbox anonid="textTrackList" class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
-                </box>
-                <vbox anonid="controlBar" class="controlBar" hidden="true">
-                    <hbox class="buttonsBar">
-                        <button anonid="playButton"
-                                class="playButton"
-                                playlabel="&playButton.playLabel;"
-                                pauselabel="&playButton.pauseLabel;"/>
-                        <label anonid="positionLabel" class="positionLabel" role="presentation"/>
-                        <stack anonid="scrubberStack" class="scrubberStack">
-                            <box class="backgroundBar"/>
-                            <progressmeter class="flexibleBar" value="100"/>
-                            <progressmeter anonid="bufferBar" class="bufferBar"/>
-                            <progressmeter anonid="progressBar" class="progressBar" max="10000"/>
-                            <scale anonid="scrubber" class="scrubber" movetoclick="true"/>
-                        </stack>
-                        <label anonid="durationLabel" class="durationLabel" role="presentation"/>
-                        <button anonid="muteButton"
-                                class="muteButton"
-                                mutelabel="&muteButton.muteLabel;"
-                                unmutelabel="&muteButton.unmuteLabel;"/>
-                        <stack anonid="volumeStack" class="volumeStack">
-                          <box anonid="volumeBackground" class="volumeBackground"/>
-                          <box anonid="volumeForeground" class="volumeForeground"/>
-                          <scale anonid="volumeControl" class="volumeControl" movetoclick="true"/>
-                        </stack>
-                        <button anonid="castingButton" class="castingButton" hidden="true"
-                                aria-label="&castingButton.castingLabel;"/>
-                        <button anonid="closedCaptionButton" class="closedCaptionButton" hidden="true"/>
-                        <button anonid="fullscreenButton"
-                            class="fullscreenButton"
-                            enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
-                            exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
-                    </hbox>
-                </vbox>
-            </vbox>
-        </stack>
-    </xbl:content>
+      toggleControls() {
+        if (!this.Utils.dynamicControls || !this.visible) {
+          this.showControls();
+        } else {
+          this.delayHideControls(0);
+        }
+      },
+
+      showControls() {
+        if (this.Utils.dynamicControls) {
+          this.Utils.startFadeIn(this.Utils.controlBar);
+          this.delayHideControls(this.controlsTimeout);
+        }
+      },
+
+      clearTimer() {
+        if (this.controlsTimer) {
+          clearTimeout(this.controlsTimer);
+          this.controlsTimer = null;
+        }
+      },
+
+      delayHideControls(aTimeout) {
+        this.clearTimer();
+        let self = this;
+        this.controlsTimer = setTimeout(function() {
+          self.hideControls();
+        }, aTimeout);
+      },
 
-    <implementation>
-        <constructor>
-          <![CDATA[
-          this.isTouchControls = true;
-          this.TouchUtils = {
-            videocontrols: null,
-            video: null,
-            controlsTimer: null,
-            controlsTimeout: 5000,
-            positionLabel: null,
-            castingButton: null,
+      hideControls() {
+        if (!this.Utils.dynamicControls) {
+          return;
+        }
+        this.Utils.startFadeOut(this.Utils.controlBar);
+        if (this.firstShow) {
+          this.videocontrols.addEventListener("transitionend", this, false);
+        }
+      },
 
-            get Utils() {
-              return this.videocontrols.Utils;
-            },
+      handleEvent(aEvent) {
+        if (aEvent.type == "transitionend") {
+          this.firstShow = false;
+          this.videocontrols.removeEventListener("transitionend", this, false);
+          return;
+        }
 
-            get visible() {
-              return !this.Utils.controlBar.hasAttribute("fadeout") &&
-                     !(this.Utils.controlBar.getAttribute("hidden") == "true");
-            },
-
-            _firstShow: false,
-            get firstShow() { return this._firstShow; },
-            set firstShow(val) {
-              this._firstShow = val;
-              this.Utils.controlBar.setAttribute("firstshow", val);
-            },
+        if (this.videocontrols.randomID != this.Utils.randomID) {
+          this.terminateEventListeners();
+        }
+      },
 
-            toggleControls: function() {
-              if (!this.Utils.dynamicControls || !this.visible)
-                this.showControls();
-              else
-                this.delayHideControls(0);
-            },
+      terminateEventListeners() {
+        for (var event of this.videoEvents) {
+          this.Utils.video.removeEventListener(event, this, false);
+        }
+      },
 
-            showControls : function() {
-              if (this.Utils.dynamicControls) {
-                this.Utils.startFadeIn(this.Utils.controlBar);
-                this.delayHideControls(this.controlsTimeout);
-              }
-            },
+      isVideoCasting() {
+        return this.video.mozIsCasting;
+      },
 
-            clearTimer: function() {
-              if (this.controlsTimer) {
-                clearTimeout(this.controlsTimer);
-                this.controlsTimer = null;
-              }
-            },
+      updateCasting(eventDetail) {
+        let castingData = JSON.parse(eventDetail);
+        if ("allow" in castingData) {
+          this.video.mozAllowCasting = !!castingData.allow;
+        }
 
-            delayHideControls : function(aTimeout) {
-              this.clearTimer();
-              let self = this;
-              this.controlsTimer = setTimeout(function() {
-                self.hideControls();
-              }, aTimeout);
-            },
-
-            hideControls : function() {
-              if (!this.Utils.dynamicControls)
-                  return;
-              this.Utils.startFadeOut(this.Utils.controlBar);
-              if (this.firstShow)
-                this.videocontrols.addEventListener("transitionend", this, false);
-            },
+        if ("active" in castingData) {
+          this.video.mozIsCasting = !!castingData.active;
+        }
+        this.setCastButtonState();
+      },
 
-            handleEvent : function(aEvent) {
-              if (aEvent.type == "transitionend") {
-                this.firstShow = false;
-                this.videocontrols.removeEventListener("transitionend", this, false);
-                return;
-              }
+      startCasting() {
+        this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
+      },
 
-              if (this.videocontrols.randomID != this.Utils.randomID)
-                this.terminateEventListeners();
-
-            },
+      setCastButtonState() {
+        if (this.isAudioOnly || !this.video.mozAllowCasting) {
+          this.castingButton.hidden = true;
+          return;
+        }
 
-            terminateEventListeners : function() {
-              for (var event of this.videoEvents)
-                this.Utils.video.removeEventListener(event, this, false);
-            },
+        if (this.video.mozIsCasting) {
+          this.castingButton.setAttribute("active", "true");
+        } else {
+          this.castingButton.removeAttribute("active");
+        }
 
-            isVideoCasting : function() {
-              if (this.video.mozIsCasting)
-                return true;
-              return false;
-            },
+        this.castingButton.hidden = false;
+      },
 
-            updateCasting : function(eventDetail) {
-              let castingData = JSON.parse(eventDetail);
-              if ("allow" in castingData) {
-                this.video.mozAllowCasting = !!castingData.allow;
-              }
+      init(binding) {
+        this.videocontrols = binding;
+        this.video = binding.parentNode;
 
-              if ("active" in castingData) {
-                this.video.mozIsCasting = !!castingData.active;
-              }
-              this.setCastButtonState();
-            },
-
-            startCasting : function() {
-              this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
-            },
+        let self = this;
+        this.Utils.playButton.addEventListener("command", function() {
+          if (!self.video.paused) {
+            self.delayHideControls(0);
+          } else {
+            self.showControls();
+          }
+        }, false);
+        this.Utils.scrubber.addEventListener("touchstart", function() {
+          self.clearTimer();
+        }, false);
+        this.Utils.scrubber.addEventListener("touchend", function() {
+          self.delayHideControls(self.controlsTimeout);
+        }, false);
+        this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
 
-            setCastButtonState : function() {
-              if (this.isAudioOnly || !this.video.mozAllowCasting) {
-                this.castingButton.hidden = true;
-                return;
-              }
+        this.castingButton = document.getAnonymousElementByAttribute(binding, "anonid", "castingButton");
+        this.castingButton.addEventListener("command", function() {
+          self.startCasting();
+        }, false);
 
-              if (this.video.mozIsCasting) {
-                this.castingButton.setAttribute("active", "true");
-              } else {
-                this.castingButton.removeAttribute("active");
-              }
-
-              this.castingButton.hidden = false;
-            },
-
-            init : function(binding) {
-              this.videocontrols = binding;
-              this.video = binding.parentNode;
+        this.video.addEventListener("media-videoCasting", function(e) {
+          if (!e.isTrusted) {
+            return;
+          }
+          self.updateCasting(e.detail);
+        }, false, true);
 
-              let self = this;
-              this.Utils.playButton.addEventListener("command", function() {
-                if (!self.video.paused)
-                    self.delayHideControls(0);
-                else
-                    self.showControls();
-              }, false);
-              this.Utils.scrubber.addEventListener("touchstart", function() {
-                self.clearTimer();
-              }, false);
-              this.Utils.scrubber.addEventListener("touchend", function() {
-                self.delayHideControls(self.controlsTimeout);
-              }, false);
-              this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
+        // The first time the controls appear we want to just display
+        // a play button that does not fade away. The firstShow property
+        // makes that happen. But because of bug 718107 this init() method
+        // may be called again when we switch in or out of fullscreen
+        // mode. So we only set firstShow if we're not autoplaying and
+        // if we are at the beginning of the video and not already playing
+        if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
+            this.video.currentTime === 0) {
+          this.firstShow = true;
+        }
 
-              this.castingButton = document.getAnonymousElementByAttribute(binding, "anonid", "castingButton");
-              this.castingButton.addEventListener("command", function() {
-                self.startCasting();
-              }, false);
-
-              this.video.addEventListener("media-videoCasting", function(e) {
-                if (!e.isTrusted)
-                  return;
-                self.updateCasting(e.detail);
-              }, false, true);
-
-              // The first time the controls appear we want to just display
-              // a play button that does not fade away. The firstShow property
-              // makes that happen. But because of bug 718107 this init() method
-              // may be called again when we switch in or out of fullscreen
-              // mode. So we only set firstShow if we're not autoplaying and
-              // if we are at the beginning of the video and not already playing
-              if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
-                  this.video.currentTime === 0)
-                this.firstShow = true;
+        // If the video is not at the start, then we probably just
+        // transitioned into or out of fullscreen mode, and we don't want
+        // the controls to remain visible. this.controlsTimeout is a full
+        // 5s, which feels too long after the transition.
+        if (this.video.currentTime !== 0) {
+          this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
+        }
+      }
+    };
 
-              // If the video is not at the start, then we probably just
-              // transitioned into or out of fullscreen mode, and we don't want
-              // the controls to remain visible. this.controlsTimeout is a full
-              // 5s, which feels too long after the transition.
-              if (this.video.currentTime !== 0) {
-                this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
-              }
-            }
-          };
+    this.TouchUtils.init(this)
+    this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
+    ]]>
+  </constructor>
+  <destructor>
+    <![CDATA[
+    // XBL destructors don't appear to be inherited properly, so we need
+    // to do this here in addition to the videoControls destructor. :-(
+    delete this.randomID;
+    ]]>
+  </destructor>
+
+  </implementation>
 
-          this.TouchUtils.init(this)
-          this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
-      ]]>
-      </constructor>
-      <destructor>
-          <![CDATA[
-          // XBL destructors don't appear to be inherited properly, so we need
-          // to do this here in addition to the videoControls destructor. :-(
-          delete this.randomID;
-          ]]>
-      </destructor>
+  <handlers>
+    <handler event="mouseup">
+      if (event.originalTarget.nodeName == "vbox") {
+        if (this.TouchUtils.firstShow) {
+          this.Utils.video.play();
+        }
+        this.TouchUtils.toggleControls();
+      }
+    </handler>
+  </handlers>
+
+</binding>
 
-    </implementation>
+<binding id="touchControlsGonk" extends="chrome://global/content/bindings/videocontrols.xml#touchControls">
+  <implementation>
+    <constructor>
+      this.isGonk = true;
+    </constructor>
+  </implementation>
+</binding>
 
-    <handlers>
-        <handler event="mouseup">
-          if (event.originalTarget.nodeName == "vbox") {
-            if (this.TouchUtils.firstShow)
-              this.Utils.video.play();
-            this.TouchUtils.toggleControls();
-          }
-        </handler>
-    </handlers>
+<binding id="noControls">
 
-  </binding>
+  <resources>
+    <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
+    <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
+  </resources>
 
-  <binding id="touchControlsGonk" extends="chrome://global/content/bindings/videocontrols.xml#touchControls">
-    <implementation>
-      <constructor>
-          this.isGonk = true;
-      </constructor>
-    </implementation>
-  </binding>
-
-  <binding id="noControls">
+  <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
+    <vbox flex="1" class="statusOverlay" hidden="true">
+      <box flex="1">
+        <box class="clickToPlay" flex="1"/>
+      </box>
+    </vbox>
+  </xbl:content>
 
-    <resources>
-        <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
-        <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
-    </resources>
-
-    <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
-        <vbox flex="1" class="statusOverlay" hidden="true">
-            <box flex="1">
-                <box class="clickToPlay" flex="1"/>
-            </box>
-        </vbox>
-    </xbl:content>
+  <implementation>
+  <constructor>
+    <![CDATA[
+    this.randomID = 0;
+    this.Utils = {
+      randomID : 0,
+      videoEvents : ["play",
+                     "playing"],
+      controlListeners: [],
+      terminateEventListeners() {
+        for (let event of this.videoEvents) {
+          this.video.removeEventListener(event, this, { mozSystemGroup: true });
+        }
 
-    <implementation>
-        <constructor>
-          <![CDATA[
-          this.randomID = 0;
-          this.Utils = {
-            randomID : 0,
-            videoEvents : ["play",
-                           "playing"],
-            controlListeners: [],
-            terminateEventListeners : function() {
-              for (let event of this.videoEvents)
-                this.video.removeEventListener(event, this, { mozSystemGroup: true });
+        for (let element of this.controlListeners) {
+          element.item.removeEventListener(element.event, element.func,
+            { mozSystemGroup: true });
+        }
 
-              for (let element of this.controlListeners) {
-                element.item.removeEventListener(element.event, element.func,
-                                                 { mozSystemGroup: true });
-              }
+        delete this.controlListeners;
+      },
 
-              delete this.controlListeners;
-            },
+      hasError() {
+        return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
+      },
 
-            hasError : function() {
-              return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
-            },
+      handleEvent(aEvent) {
+        // If the binding is detached (or has been replaced by a
+        // newer instance of the binding), nuke our event-listeners.
+        if (this.binding.randomID != this.randomID) {
+          this.terminateEventListeners();
+          return;
+        }
 
-            handleEvent : function(aEvent) {
-              // If the binding is detached (or has been replaced by a
-              // newer instance of the binding), nuke our event-listeners.
-              if (this.binding.randomID != this.randomID) {
-                this.terminateEventListeners();
-                return;
-              }
-
-              switch (aEvent.type) {
-                case "play":
-                  this.noControlsOverlay.hidden = true;
-                  break;
-                case "playing":
-                  this.noControlsOverlay.hidden = true;
-                  break;
-              }
-            },
+        switch (aEvent.type) {
+          case "play":
+            this.noControlsOverlay.hidden = true;
+            break;
+          case "playing":
+            this.noControlsOverlay.hidden = true;
+            break;
+        }
+      },
 
-            blockedVideoHandler : function() {
-              if (this.binding.randomID != this.randomID) {
-                this.terminateEventListeners();
-                return;
-              } else if (this.hasError()) {
-                this.noControlsOverlay.hidden = true;
-                return;
-              }
-              this.noControlsOverlay.hidden = false;
-            },
+      blockedVideoHandler() {
+        if (this.binding.randomID != this.randomID) {
+          this.terminateEventListeners();
+          return;
+        } else if (this.hasError()) {
+          this.noControlsOverlay.hidden = true;
+          return;
+        }
+        this.noControlsOverlay.hidden = false;
+      },
 
-            clickToPlayClickHandler : function(e) {
-              if (this.binding.randomID != this.randomID) {
-                  this.terminateEventListeners();
-                  return;
-              } else if (e.button != 0) {
-                  return;
-              }
+      clickToPlayClickHandler(e) {
+        if (this.binding.randomID != this.randomID) {
+          this.terminateEventListeners();
+          return;
+        } else if (e.button != 0) {
+          return;
+        }
 
-              this.noControlsOverlay.hidden = true;
-              this.video.play();
-            },
+        this.noControlsOverlay.hidden = true;
+        this.video.play();
+      },
 
-            init : function(binding) {
-              this.binding = binding;
-              this.randomID = Math.random();
-              this.binding.randomID = this.randomID;
-              this.video = binding.parentNode;
-              this.clickToPlay       = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
-              this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
+      init(binding) {
+        this.binding = binding;
+        this.randomID = Math.random();
+        this.binding.randomID = this.randomID;
+        this.video = binding.parentNode;
+        this.clickToPlay       = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
+        this.noControlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
 
-              let self = this;
-              function addListener(elem, eventName, func) {
-                let boundFunc = func.bind(self);
-                self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
-                elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
-              }
-              addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
-              addListener(this.video, "MozNoControlsBlockedVideo", this.blockedVideoHandler);
+        let self = this;
+        function addListener(elem, eventName, func) {
+          let boundFunc = func.bind(self);
+          self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+          elem.addEventListener(eventName, boundFunc, { mozSystemGroup: true });
+        }
+        addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+        addListener(this.video, "MozNoControlsBlockedVideo", this.blockedVideoHandler);
 
-              for (let event of this.videoEvents) {
-                this.video.addEventListener(event, this, { mozSystemGroup: true });
-              }
+        for (let event of this.videoEvents) {
+          this.video.addEventListener(event, this, { mozSystemGroup: true });
+        }
 
-              if (this.video.autoplay && !this.video.mozAutoplayEnabled) {
-                this.blockedVideoHandler();
-              }
-            }
-          };
-          this.Utils.init(this);
-          this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
-          ]]>
-      </constructor>
-      <destructor>
-          <![CDATA[
-          this.Utils.terminateEventListeners();
-          // randomID used to be a <field>, which meant that the XBL machinery
-          // undefined the property when the element was unbound. The code in
-          // this file actually depends on this, so now that randomID is an
-          // expando, we need to make sure to explicitly delete it.
-          delete this.randomID;
-          ]]>
-      </destructor>
-    </implementation>
-  </binding>
+        if (this.video.autoplay && !this.video.mozAutoplayEnabled) {
+          this.blockedVideoHandler();
+        }
+      }
+    };
+    this.Utils.init(this);
+    this.Utils.video.dispatchEvent(new CustomEvent("MozNoControlsVideoBindingAttached"));
+    ]]>
+  </constructor>
+  <destructor>
+    <![CDATA[
+    this.Utils.terminateEventListeners();
+    // randomID used to be a <field>, which meant that the XBL machinery
+    // undefined the property when the element was unbound. The code in
+    // this file actually depends on this, so now that randomID is an
+    // expando, we need to make sure to explicitly delete it.
+    delete this.randomID;
+    ]]>
+  </destructor>
+  </implementation>
+</binding>
 
 </bindings>