<?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;
]>

<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">

  <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.
          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>

      <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;
              }

              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;

          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;
                    this.valueBar.value = Math.round(percent * 10000); // has max=10000
                }

                // 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="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
                 class="mediaControlsFrame">
        <stack flex="1">
            <vbox flex="1" class="statusOverlay" hidden="true">
                <box class="statusIcon"/>
                <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
                <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
                <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
                <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
                <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
                <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
            </vbox>

            <vbox class="controlsOverlay">
                <stack flex="1">
                    <spacer class="controlsSpacer" flex="1"/>
                    <box class="clickToPlay" hidden="true" flex="1"/>
                    <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
                </stack>
                <hbox class="controlBar" hidden="true">
                    <button class="playButton"
                            playlabel="&playButton.playLabel;"
                            pauselabel="&playButton.pauseLabel;"/>
                    <stack class="scrubberStack" flex="1">
                        <box class="backgroundBar"/>
                        <progressmeter class="bufferBar"/>
                        <progressmeter class="progressBar" max="10000"/>
                        <scale class="scrubber" movetoclick="true"/>
                    </stack>
                    <vbox class="durationBox">
                        <label class="positionLabel" role="presentation"/>
                        <label class="durationLabel" role="presentation"/>
                    </vbox>
                    <button class="muteButton"
                            mutelabel="&muteButton.muteLabel;"
                            unmutelabel="&muteButton.unmuteLabel;"/>
                    <stack class="volumeStack">
                      <box class="volumeBackground"/>
                      <box class="volumeForeground" anonid="volumeForeground"/>
                      <scale class="volumeControl" movetoclick="true"/>
                    </stack>
                    <button class="closedCaptionButton"/>
                    <button class="fullscreenButton"
                            enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
                            exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
                </hbox>
            </vbox>
        </stack>
    </xbl:content>

    <implementation>

        <constructor>
            <![CDATA[
            this.isTouchControl = false;
            this.randomID = 0;

            this.Utils = {
                debug : false,
                video : null,
                videocontrols : null,
                controlBar : null,
                playButton : null,
                muteButton : null,
                volumeControl  : null,
                durationLabel  : null,
                positionLabel  : null,
                scrubberThumb  : 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" ],

                firstFrameShown : false,
                timeUpdateCount : 0,
                maxCurrentTimeSeen : 0,
                _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._controlBarHeight + "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;

                    // 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.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;

                        // 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.setAttribute("max", 100);
                    if (this.video.readyState >= this.video.HAVE_METADATA)
                        this.showBuffered();
                    else
                        this.bufferBar.setAttribute("value", 0);

                    // Set the current status icon.
                    if (this.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;
                    this._playButtonWidth = this.playButton.clientWidth;
                    this._durationLabelWidth = this.durationLabel.clientWidth;
                    this._muteButtonWidth = this.muteButton.clientWidth;
                    this._volumeControlWidth = this.volumeControl.clientWidth;
                    this._closedCaptionButtonWidth = this.closedCaptionButton.clientWidth;
                    this._fullscreenButtonWidth = this.fullscreenButton.clientWidth;
                    this._controlBarHeight = this.controlBar.clientHeight;
                    this.controlBar.hidden = true;
                    this.adjustControlSize();

                    // 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.
                    this.startFade(this.clickToPlay, shouldShow && !this.isAudioOnly &&
                                   this.video.currentTime == 0 && !this.hasError(), 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;

                    // 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;
                },

                updateVolumeControls() {
                    var volume = this.video.muted ? 0 : this.video.volume;
                    var volumePercentage = Math.round(volume * 100);
                    this.updateMuteButtonState();
                    this.volumeControl.value = volumePercentage;
                    this.volumeForeground.style.paddingRight = (1 - volume) * this._volumeControlWidth + "px";
                },

                handleEvent : function (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;
                    }

                    switch (aEvent.type) {
                        case "play":
                            this.setPlayButtonState(false);
                            this.setupStatusFader();
                            if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl)
                                this.startFadeOut(this.controlBar);
                            if (!this._triggeredByControls)
                                this.clickToPlay.hidden = 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

                            // 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);

                            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();

                            // 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)
                                return;

                            this.showPosition(currentTime, duration);
                            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!");
                    }
                },

                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 ---");
                },

                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());
                },

                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;
                },

                updateErrorText : function () {
                    let error;
                    let v = this.video;
                    // It is possible to have both v.networkState == NETWORK_NO_SOURCE
                    // as well as v.error being non-null. In this case, we will show
                    // the v.error.code instead of the v.networkState error.
                    if (v.error) {
                        switch (v.error.code) {
                          case v.error.MEDIA_ERR_ABORTED:
                              error = "errorAborted";
                              break;
                          case v.error.MEDIA_ERR_NETWORK:
                              error = "errorNetwork";
                              break;
                          case v.error.MEDIA_ERR_DECODE:
                              error = "errorDecode";
                              break;
                          case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
                              error = "errorSrcNotSupported";
                              break;
                          default:
                              error = "errorGeneric";
                              break;
                         }
                    } else if (v.networkState == v.NETWORK_NO_SOURCE) {
                        error = "errorNoSource";
                    } else {
                        return; // No error found.
                    }

                    let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
                    this.controlsSpacer.setAttribute("aria-label", label.textContent);
                    this.statusOverlay.setAttribute("error", error);
                },

                formatTime : function(aTime) {
                    // Format the duration as "h:mm:ss" or "m:ss"
                    aTime = Math.round(aTime / 1000);
                    let hours = Math.floor(aTime / 3600);
                    let mins  = Math.floor((aTime % 3600) / 60);
                    let secs  = Math.floor(aTime % 60);
                    let timeString;
                    if (secs < 10)
                        secs = "0" + secs;
                    if (hours) {
                        if (mins < 10)
                            mins = "0" + mins;
                        timeString = hours + ":" + mins + ":" + secs;
                    } else {
                        timeString = mins + ":" + secs;
                    }
                    return timeString;
                },

                showDuration : function (duration) {
                    let isInfinite = (duration == Infinity);
                    this.log("Duration is " + duration + "ms.\n");

                    if (isNaN(duration) || isInfinite)
                        duration = this.maxCurrentTimeSeen;

                    // Format the duration as "h:mm:ss" or "m:ss"
                    let timeString = isInfinite ? "" : this.formatTime(duration);
                    this.durationLabel.setAttribute("value", 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.scrubberThumb.showHours = (duration >= 3600000);

                    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);
                },

                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;
                    }
                },

                setVolume : function(newVolume) {
                    this.log("*** setting volume to " + newVolume);
                    this.video.volume = newVolume;
                    this.video.muted = false;
                },

                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);

                    this.log("time update @ " + currentTime + "ms of " + duration + "ms");

                    this.positionLabel.setAttribute("value", this.formatTime(currentTime));
                    this.scrubber.value = currentTime;
                },

                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;
                    }

                    function bufferedCompare(buffered, i, time) {
                        if (time > buffered.end(i)) {
                            return 1;
                        } else if (time >= buffered.start(i)) {
                            return 0;
                        }
                        return -1;
                    }

                    var duration = Math.round(this.video.duration * 1000);
                    if (isNaN(duration))
                        duration = this.maxCurrentTimeSeen;

                    // 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 : function () {
                    if (Utils.video.matches("video:hover")) {
                        Utils.startFadeIn(Utils.controlBar, false);
                        Utils._showControlsTimeout = 0;
                        Utils._controlsHiddenByTimeout = false;
                    }
                },

                _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.isDragging) {
                      this.scrubber.pauseVideoDuringDragging();
                    }

                    // 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);

                    // 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);

                    // 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");

                    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;

                    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;
                    }
                },

                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)
                            this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
                    }

                    if (immediate)
                        element.setAttribute("immediate", true);
                    else
                        element.removeAttribute("immediate");

                    if (fadeIn) {
                        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);

                    }
                },

                onTransitionEnd : function (event) {
                    // Ignore events for things other than opacity changes.
                    if (event.propertyName != "opacity")
                        return;

                    var element = event.originalTarget;

                    // Nothing to do when a fade *in* finishes.
                    if (!element.hasAttribute("fadeout"))
                        return;

                    this.scrubber.dragStateChanged(false);
                    element.hidden = true;
                },

                _triggeredByControls: false,

                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;
                },

                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;
                },

                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);

                    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();
                },

                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;
                    if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)||
                        this._overlayPlayButtonWidth * animationScale > videoWidth) {
                        this.clickToPlay.setAttribute("immediate", "true");
                        this.clickToPlay.hidden = true;
                    } else {
                        this.clickToPlay.removeAttribute("immediate");
                    }
                    this.clickToPlay.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);
                },

                isEffectivelyMuted : function() {
                  return this.video.muted || !this.video.volume;
                },

                updateMuteButtonState : function() {
                  var muted = this.isEffectivelyMuted();

                  if (muted)
                      this.muteButton.setAttribute("muted", "true");
                  else
                      this.muteButton.removeAttribute("muted");

                  var attrName = muted ? "unmutelabel" : "mutelabel";
                  var value = this.muteButton.getAttribute(attrName);
                  this.muteButton.setAttribute("aria-label", value);
                },

                _getComputedPropertyValueAsInt : function(element, property) {
                  let value = getComputedStyle(element, null).getPropertyValue(property);
                  return parseInt(value, 10);
                },

                keyHandler : function(event) {
                    // Ignore keys when content might be providing its own.
                    if (!this.video.hasAttribute("controls"))
                        return;

                    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;
                    }

                    if (String.fromCharCode(event.charCode) == ' ')
                        keystroke += "space";

                    this.log("Got keystroke: " + keystroke);
                    var oldval, newval;

                    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 */ }

                    event.preventDefault(); // Prevent page scrolling
                },

                isSupportedTextTrack : function(textTrack) {
                  return textTrack.kind == "subtitles" ||
                         textTrack.kind == "captions";
                },

                get overlayableTextTracks() {
                  return Array.prototype.filter.call(this.video.textTracks, this.isSupportedTextTrack);
                },

                isClosedCaptionOn : function () {
                  for (let tt of this.overlayableTextTracks) {
                    if (tt.mode === "showing") {
                      return true;
                    }
                  }

                  return false;
                },

                setClosedCaptionButtonState : function () {
                  if (!this.overlayableTextTracks.length || this.videocontrols.isTouchControl) {
                    this.closedCaptionButton.setAttribute("hidden", "true");
                    return;
                  }

                  this.closedCaptionButton.removeAttribute("hidden");

                  if (this.isClosedCaptionOn()) {
                    this.closedCaptionButton.setAttribute("enabled", "true");
                  } else {
                    this.closedCaptionButton.removeAttribute("enabled");
                  }

                  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 : function (tt) {
                  if (!this.isSupportedTextTrack(tt)) {
                    return;
                  }

                  if (tt.index && tt.index < this.textTracksCount) {
                    // Don't create items for initialized tracks. However, we
                    // still need to care about mode since TextTrackManager would
                    // turn on the first available track automatically.
                    if (tt.mode === "showing") {
                      this.changeTextTrack(tt.index);
                    }
                    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);

                  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);
                  }
                },

                changeTextTrack : function (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;
                  }

                  this.textTrackList.setAttribute("hidden", "true");
                  this.setClosedCaptionButtonState();
                },

                onControlBarTransitioned : function () {
                  this.textTrackList.setAttribute("hidden", "true");
                  this.video.dispatchEvent(new CustomEvent("controlbarchange"));
                },

                toggleClosedCaption : function () {
                  if (this.overlayableTextTracks.length === 1) {
                    const lastTTIdx = this.overlayableTextTracks[0].index;
                    this.changeTextTrack(this.isClosedCaptionOn() ? 0 : lastTTIdx);
                    return;
                  }

                  if (this.textTrackList.hasAttribute("hidden")) {
                    this.textTrackList.removeAttribute("hidden");
                  } else {
                    this.textTrackList.setAttribute("hidden", "true");
                  }

                  let maxButtonWidth = 0;

                  for (let tti of this.textTrackList.childNodes) {
                    if (tti.clientWidth > maxButtonWidth) {
                      maxButtonWidth = tti.clientWidth;
                    }
                  }

                  if (maxButtonWidth > this.video.clientWidth) {
                    maxButtonWidth = this.video.clientWidth;
                  }

                  for (let tti of this.textTrackList.childNodes) {
                    tti.style.width = maxButtonWidth + "px";
                  }
                },

                onTextTrackAdd : function (trackEvent) {
                  this.addNewTextTrack(trackEvent.track);
                  this.setClosedCaptionButtonState();
                },

                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");

                    if (idx === toRemoveIndex) {
                      tti.remove();
                      this.textTracksCount--;
                    }

                    if (idx === this.currentTextTrackIndex) {
                      this.currentTextTrackIndex = 0;

                      this.video.dispatchEvent(new CustomEvent("texttrackchange"));
                    }
                  }

                  this.setClosedCaptionButtonState();
                },

                initTextTracks : function () {
                  // add 'off' button anyway as new text track might be
                  // dynamically added after initialization.
                  const offLabel = this.textTrackList.getAttribute("offlabel");

                  this.addNewTextTrack({
                    label: offLabel,
                    kind: "subtitles"
                  });

                  for (let tt of this.overlayableTextTracks) {
                    this.addNewTextTrack(tt);
                  }

                  this.setClosedCaptionButtonState();
                },

                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);
                },

                log : function (msg) {
                    if (this.debug)
                        console.log("videoctl: " + msg + "\n");
                },

                get isTopLevelSyntheticDocument() {
                  let doc = this.video.ownerDocument;
                  let win = doc.defaultView;
                  return doc.mozSyntheticDocument && win === win.top;
                },

                _playButtonWidth : 0,
                _durationLabelWidth : 0,
                _muteButtonWidth : 0,
                _volumeControlWidth : 0,
                _closedCaptionButtonWidth : 0,
                _fullscreenButtonWidth : 0,
                _controlBarHeight : 0,
                _overlayPlayButtonHeight : 64,
                _overlayPlayButtonWidth : 64,
                _controlBarPaddingEnd: 8,
                adjustControlSize : function adjustControlSize() {
                    let doc = this.video.ownerDocument;

                    // The scrubber has |flex=1|, therefore |minScrubberWidth|
                    // was generated by empirical testing.
                    let minScrubberWidth = 25;
                    let minWidthAllControls = this._playButtonWidth +
                                              minScrubberWidth +
                                              this._durationLabelWidth +
                                              this._muteButtonWidth +
                                              this._volumeControlWidth +
                                              this._closedCaptionButtonWidth +
                                              this._fullscreenButtonWidth;

                    let isFullscreenUnavailable = this.controlBar.hasAttribute("fullscreen-unavailable");
                    if (isFullscreenUnavailable) {
                        // When the fullscreen button is hidden we add margin-end to the volume stack.
                        minWidthAllControls -= this._fullscreenButtonWidth - this._controlBarPaddingEnd;
                    }

                    let minHeightForControlBar = this._controlBarHeight;
                    let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;

                    let isAudioOnly = this.isAudioOnly;
                    let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
                    let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;

                    // Adapt the size of the controls to the size of the video
                    if (this.video.readyState >= this.video.HAVE_METADATA) {
                      if (!this.isAudioOnly && this.video.videoWidth && this.video.videoHeight) {
                        var rect = this.video.getBoundingClientRect();
                        var widthRatio = rect.width / this.video.videoWidth;
                        var heightRatio = rect.height / this.video.videoHeight;
                        var width = this.video.videoWidth * Math.min(widthRatio, heightRatio);

                        this.controlsOverlay.setAttribute("scaled", true);
                        this.controlsOverlay.style.width = width + "px";
                        this.controlsSpacer.style.width = width + "px";
                        this.controlBar.style.width = width + "px";
                      } else {
                        this.controlsOverlay.removeAttribute("scaled");
                        this.controlsOverlay.style.width = "";
                        this.controlsSpacer.style.width = "";
                        this.controlBar.style.width = "";
                      }
                    }

                    if ((this._overlayPlayButtonHeight + this._controlBarHeight) > videoHeight ||
                        this._overlayPlayButtonWidth > videoWidth) {
                        this.clickToPlay.hidden = true;
                    } else if (this.clickToPlay.hidden &&
                               !this.video.played.length &&
                               this.video.paused) {
                        // Check this.video.paused to handle when a video is
                        // playing but hasn't processed any frames yet
                        this.clickToPlay.hidden = false;
                    }

                    let size = "normal";
                    if (videoHeight < minHeightForControlBar)
                        size = "hidden";
                    else if (videoWidth < minWidthOnlyPlayPause)
                        size = "hidden";
                    else if (videoWidth < minWidthAllControls)
                        size = "small";
                    this.controlBar.setAttribute("size", size);
                },

                init : function (binding) {
                    this.video = binding.parentNode;
                    this.videocontrols = binding;

                    this.statusIcon    = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
                    this.controlBar    = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
                    this.playButton    = document.getAnonymousElementByAttribute(binding, "class", "playButton");
                    this.muteButton    = document.getAnonymousElementByAttribute(binding, "class", "muteButton");
                    this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl");
                    this.progressBar   = document.getAnonymousElementByAttribute(binding, "class", "progressBar");
                    this.bufferBar     = document.getAnonymousElementByAttribute(binding, "class", "bufferBar");
                    this.scrubber      = document.getAnonymousElementByAttribute(binding, "class", "scrubber");
                    this.scrubberThumb = document.getAnonymousElementByAttribute(this.scrubber, "class", "scale-thumb");
                    this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel");
                    this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel");
                    this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
                    this.controlsOverlay = document.getAnonymousElementByAttribute(binding, "class", "controlsOverlay");
                    this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
                    this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
                    this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
                    this.volumeForeground   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground");
                    this.closedCaptionButton = document.getAnonymousElementByAttribute(binding, "class", "closedCaptionButton");
                    this.textTrackList = document.getAnonymousElementByAttribute(binding, "class", "textTrackList");

                    this.isAudioOnly = (this.video instanceof HTMLAudioElement);
                    this.setupInitialState();
                    this.setupNewLoadState();
                    this.initTextTracks();

                    // 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 = [];

                    // 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, "command", this.toggleMute);
                    addListener(this.closedCaptionButton, "command", this.toggleClosedCaption);
                    addListener(this.playButton, "click", this.clickToPlayClickHandler);
                    addListener(this.fullscreenButton, "command", this.toggleFullscreen);
                    addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
                    addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
                    addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);

                    addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
                    addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
                    addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
                    addListener(this.videocontrols, "transitionend", this.onControlBarTransitioned);
                    addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
                    addListener(this.video, "keypress", this.keyHandler);
                    addListener(this.video.textTracks, "addtrack", this.onTextTrackAdd);
                    addListener(this.video.textTracks, "removetrack", this.onTextTrackRemove);

                    addListener(this.videocontrols, "dragstart", function(event) {
                        event.preventDefault(); // prevent dragging of controls image (bug 517114)
                    });

                    this.log("--- videocontrols initialized ---");
                }
            };
            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>

    <handlers>
        <handler event="mouseover">
            if (!this.isTouchControl)
                this.Utils.onMouseInOut(event);
        </handler>
        <handler event="mouseout">
            if (!this.isTouchControl)
                this.Utils.onMouseInOut(event);
        </handler>
        <handler event="mousemove">
            if (!this.isTouchControl)
                this.Utils.onMouseMove(event);
        </handler>
    </handlers>
  </binding>

  <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">

    <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
        <stack flex="1">
            <vbox flex="1" class="statusOverlay" hidden="true">
                <box class="statusIcon"/>
                <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
                <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
                <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
                <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
                <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
                <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
            </vbox>

            <vbox class="controlsOverlay">
                <spacer class="controlsSpacer" flex="1"/>
                <box flex="1" hidden="true">
                    <box class="clickToPlay" hidden="true" flex="1"/>
                    <vbox class="textTrackList" hidden="true" offlabel="&closedCaption.off;"></vbox>
                </box>
                <vbox class="controlBar" hidden="true">
                    <hbox class="buttonsBar">
                        <button class="playButton"
                                playlabel="&playButton.playLabel;"
                                pauselabel="&playButton.pauseLabel;"/>
                        <label class="positionLabel" role="presentation"/>
                        <stack class="scrubberStack">
                            <box class="backgroundBar"/>
                            <progressmeter class="flexibleBar" value="100"/>
                            <progressmeter class="bufferBar"/>
                            <progressmeter class="progressBar" max="10000"/>
                            <scale class="scrubber" movetoclick="true"/>
                        </stack>
                        <label class="durationLabel" role="presentation"/>
                        <button class="muteButton"
                                mutelabel="&muteButton.muteLabel;"
                                unmutelabel="&muteButton.unmuteLabel;"/>
                        <stack class="volumeStack">
                          <box class="volumeBackground"/>
                          <box class="volumeForeground" anonid="volumeForeground"/>
                          <scale class="volumeControl" movetoclick="true"/>
                        </stack>
                        <button class="castingButton" hidden="true"
                                aria-label="&castingButton.castingLabel;"/>
                        <button class="closedCaptionButton" hidden="true"/>
                        <button class="fullscreenButton"
                            enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
                            exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
                    </hbox>
                </vbox>
            </vbox>
        </stack>
    </xbl:content>

    <implementation>

        <constructor>
          <![CDATA[
          this.isTouchControl = true;
          this.TouchUtils = {
            videocontrols: null,
            video: null,
            controlsTimer: null,
            controlsTimeout: 5000,
            positionLabel: null,
            castingButton: null,

            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);
            },

            toggleControls: function() {
              if (!this.Utils.dynamicControls || !this.visible)
                this.showControls();
              else
                this.delayHideControls(0);
            },

            showControls : function() {
              if (this.Utils.dynamicControls) {
                this.Utils.startFadeIn(this.Utils.controlBar);
                this.delayHideControls(this.controlsTimeout);
              }
            },

            clearTimer: function() {
              if (this.controlsTimer) {
                clearTimeout(this.controlsTimer);
                this.controlsTimer = null;
              }
            },

            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);
            },

            handleEvent : function (aEvent) {
              if (aEvent.type == "transitionend") {
                this.firstShow = false;
                this.videocontrols.removeEventListener("transitionend", this, false);
                return;
              }

              if (this.videocontrols.randomID != this.Utils.randomID)
                this.terminateEventListeners();

            },

            terminateEventListeners : function () {
              for (var event of this.videoEvents)
                this.Utils.video.removeEventListener(event, this, false);
            },

            isVideoCasting : function () {
              if (this.video.mozIsCasting)
                return true;
              return false;
            },

            updateCasting : function (eventDetail) {
              let castingData = JSON.parse(eventDetail);
              if ("allow" in castingData) {
                this.video.mozAllowCasting = !!castingData.allow;
              }

              if ("active" in castingData) {
                this.video.mozIsCasting = !!castingData.active;
              }
              this.setCastButtonState();
            },

            startCasting : function () {
              this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
            },

            setCastButtonState : function () {
              if (this.isAudioOnly || !this.video.mozAllowCasting) {
                this.castingButton.hidden = true;
                return;
              }

              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;

              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);

              this.castingButton = document.getAnonymousElementByAttribute(binding, "class", "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);
              }
            }
          };
          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>

    <handlers>
        <handler event="mouseup">
          if (event.originalTarget.nodeName == "vbox") {
            if (this.TouchUtils.firstShow)
              this.Utils.video.play();
            this.TouchUtils.toggleControls();
          }
        </handler>
    </handlers>

  </binding>

  <binding id="touchControlsGonk" extends="chrome://global/content/bindings/videoControls.xml#touchControls">
    <implementation>
      <constructor>
          this.isGonk = true;
      </constructor>
    </implementation>
  </binding>

  <binding id="noControls">

    <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 : 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 });
              }

              delete this.controlListeners;
            },

            hasError : function () {
              return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
            },

            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;
              }
            },

            blockedVideoHandler : function () {
              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;
              }

              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");

              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 });
              }

              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>