toolkit/content/widgets/videocontrols.xml
author Benjamin Smedberg <benjamin@smedbergs.us>
Fri, 22 May 2009 15:49:16 -0400
changeset 25704 0e0202a65953707c570a8afbe5880bb893e88f0f
parent 25698 fc4da433b431034541988a303b93743602cf1d29
child 25712 496518af5d5634527a28311954356d8047369d4e
permissions -rw-r--r--
Backed out changeset fc4da433b431, bug 494346, due to media keyboard conflicts.

<?xml version="1.0"?>

<!DOCTYPE bindings [
  <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
  %videocontrolsDTD;
]>

<bindings id="videoContolBindings"
   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">

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

      <field name="timeLabel">null</field>
      <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;
              var hours = Math.floor(time / 3600000);
              var mins  = Math.floor(time % 3600000 / 60000);
              var secs  = Math.floor(time % 60000 / 1000);
              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>

      <!-- Public -->
      <field name="scrubberNameFormat">"&scrubberScale.nameFormat;"</field>
      <field name="durationValue">""</field>

      <field name="thumb">null</field>
      <field name="valueBar">null</field>
      <field name="isDragging">false</field>
      <field name="wasPausedBeforeDrag">true</field>
      <field name="type">null</field>
      <field name="Utils">null</field>
      <constructor>
          <![CDATA[
          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);
                    // Update the value bar to match the thumb position.
                    var 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.
                    var 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 (isDragging) {
                    this.wasPausedBeforeDrag = this.Utils.video.paused;
                    this.Utils.video.pause();
                } else if (!this.wasPausedBeforeDrag) {
                    // After the drag ends, resume playing.
                    this.Utils.video.play();
                }
            }
          ]]>
          </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 class="statusOverlay" hidden="true">
                <box class="statusIcon" flex="1"/>
            </vbox>

            <vbox>
                <spacer flex="1"/>
                <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="durationLabel" role="presentation"/>
                    </vbox>
                    <button class="muteButton"
                            mutelabel="&muteButton.muteLabel;"
                            unmutelabel="&muteButton.unmuteLabel;"/>
                    <stack class="volumeStack" hidden="true">
                        <box class="volumeBackgroundBar"/>
                        <scale class="volumeControl" orient="vertical" dir="reverse" movetoclick="true"/>
                    </stack>
                </hbox>
            </vbox>
        </stack>
    </xbl:content>

    <implementation implements="nsISecurityCheckedComponent">
        <!-- nsISecurityCheckedComponent -->
        <method name="canCreateWrapper">
            <parameter name="aIID"/>
            <body>
                return "AllAccess";
            </body>
        </method> 

        <method name="canCallMethod">
            <parameter name="aIID"/>
            <parameter name="aMethodName"/> 
            <body>
                return "AllAccess";
            </body>
        </method> 

        <method name="canGetProperty">
            <parameter name="aIID"/>
            <parameter name="aPropertyName"/> 
            <body>
                return "AllAccess";
            </body>
        </method> 

        <method name="canSetProperty">
            <parameter name="aIID"/>
            <parameter name="aPropertyName"/>
            <body>
                return "AllAccess";
            </body>
        </method> 

        <method name="QueryInterface">
            <parameter name="aIID"/>
            <body>
            <![CDATA[
            if (!iid.equals(Components.interfaces.nsISecurityCheckedComponent))
                throw Components.results.NS_ERROR_NO_INTERFACE;
            return this;
            ]]>
            </body>
        </method>

        <constructor>
            <![CDATA[
            this.Utils.init(this);
            ]]>
        </constructor>

        <field name="randomID">0</field>

        <field name="Utils">
            <![CDATA[ ({
                debug : false,
                video : null,
                videocontrols : null,
                playButton : null,
                muteButton : null,
                volumeStack    : null,
                volumeControl  : null,
                durationLabel  : null,
                scrubberThumb  : null,
                scrubber       : null,
                progressBar    : null,
                bufferBar      : null,

                randomID : 0,
                videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
                               "loadstart", "durationchange", "timeupdate", "progress",
                               "playing", "waiting", "canplay", "canplaythrough",
                               "seeking", "seeked", "emptied", "loadedmetadata",
                               "error", "suspend"],

                // controlFader holds the fade state for the control bar.
                controlFader  : {
                                    name : "controls", // fader identifier
                                    element : null,    // the element to fade in/out
                                    runtime : 0,       // duration of active animation
                                    fadingIn : false,  // are we fading in, or fading out?
                                    isVisible : false, // is it at all visible?
                                    timer : null,      // handle from setInterval()
                                    delayTimer : null, // handle from setTimeout()
                                    START_DELAY : 0,   // ms, delay before fading in
                                    RUNTIME_MAX : 200, // ms
                                    RUNTIME_STEP : 30  // ms
                                },

                // statusFader holds the fade state for the status overlay (inc. throbber)
                statusFader : {
                                    name : "status",
                                    element : null,
                                    runtime : 0,
                                    fadingIn : false,
                                    isVisible : false,
                                    timer : null,
                                    delayTimer : null,
                                    START_DELAY : 750,
                                    RUNTIME_MAX : 300,
                                    RUNTIME_STEP : 20
                                },

                // volumeFader holds the fade state for the volume <scale>.
                volumeFader : {
                                    name : "volume",
                                    element : null,
                                    maxSlide : null, // height when extended, set in init()
                                    runtime : 0,
                                    fadingIn : false,
                                    isVisible : false,
                                    timer : null,
                                    delayTimer : null,
                                    START_DELAY : 0,
                                    RUNTIME_MAX : 200,
                                    RUNTIME_STEP : 15
                                },

                firstFrameShown : false,
                timeUpdateCount : 0,
                lastTimeUpdate : 0,
                maxCurrentTimeSeen : 0,
                isAudioOnly : false,

                setupStatusFader : function(immediate) {
                    if (this.video.seeking || this.video.error ||
                        (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.readyState < this.video.HAVE_ENOUGH_DATA &&
                         this.video.networkState >= this.video.NETWORK_LOADING))
                        this.startFadeIn(this.statusFader, immediate);
                    else
                        this.startFadeOut(this.statusFader, immediate);

                    this.log("Status fader: 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 +
                             " --> " + (this.statusFader.fadingIn ? "SHOW" : "HIDE"));
                },

                /*
                 * 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.setMuteButtonState(this.video.muted);

                    var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
                    this.volumeControl.value = volume;

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

                    // If we have metadata, check if this is a <video> without video data.
                    if (this.video.readyState >= this.video.HAVE_METADATA) {
                        if (this.video instanceof HTMLVideoElement &&
                            (this.video.videoWidth == 0 || this.videoHeight == 0))
                            this.isAudioOnly = 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.networkState == this.video.NETWORK_LOADED)
                        this.bufferBar.setAttribute("value", 100);
                    else
                        this.bufferBar.setAttribute("value", 0);

                    // Set the current status icon.
                    if (this.video.error) {
                        this.statusIcon.setAttribute("type", "error");
                        this.setupStatusFader(true);
                    } else {
                        this.statusIcon.setAttribute("type", "throbber");
                        this.setupStatusFader();
                    }
                },

                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.video.error)
                        enabled = false;

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

                    switch (aEvent.type) {
                        case "play":
                            this.setPlayButtonState(false);
                            this.setupStatusFader();
                            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.setupStatusFader();
                            break;
                        case "volumechange":
                            var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
                            this.setMuteButtonState(this.video.muted);
                            this.volumeControl.value = volume;
                            break;
                        case "loadedmetadata":
                            // 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.startFadeIn(this.controlFader);
                            }
                            break;
                        case "loadeddata":
                            this.firstFrameShown = true;
                            this.setupStatusFader();
                            break;
                        case "loadstart":
                            this.maxCurrentTimeSeen = 0;
                            this.statusIcon.setAttribute("type", "throbber");
                            this.isAudioOnly = (this.video instanceof HTMLAudioElement);
                            break;
                        case "durationchange":
                            var duration = Math.round(this.video.duration * 1000); // in ms
                            this.durationChange(duration);
                            break;
                        case "progress":
                            var loaded = aEvent.loaded;
                            var total = aEvent.total;
                            this.log("+++ load, " + loaded + " of " + total);
                            // When the source is streaming, the value of .total is -1. Set the
                            // progress bar to the maximum, since it's not useful.
                            if (total == -1)
                                total = loaded;
                            this.bufferBar.max = total;
                            this.bufferBar.value = loaded;
                            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

                            this.timeUpdateCount++;
                            // Whether we show the statusFader 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
                            // statusFader 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;

                            // Timeupdate events are dispatched *every frame*. Reduce workload by
                            // ignoring position changes that are within 333ms of the current position.
                            if (Math.abs(currentTime - this.lastTimeUpdate) < 333)
                                return;
                            this.lastTimeUpdate = currentTime;
                            this.showPosition(currentTime, duration);
                            break;
                        case "emptied":
                            this.bufferBar.value = 0;
                            break;
                        case "seeking":
                        case "waiting":
                            this.statusIcon.setAttribute("type", "throbber");
                            this.setupStatusFader();
                            break;
                        case "seeked":
                        case "playing":
                        case "canplay":
                        case "canplaythrough":
                            this.setupStatusFader();
                            break;
                        case "error":
                            this.statusIcon.setAttribute("type", "error");
                            this.setupStatusFader(true);
                            // If video hasn't shown anything yet, disable the controls.
                            if (!this.firstFrameShown)
                                this.startFadeOut(this.controlFader);
                            break;
                        default:
                            this.log("!!! event " + aEvent.type + " not handled!");
                    }
                },

                terminateEventListeners : function () {
                    for each (var event in this.videoEvents)
                        this.video.removeEventListener(event, this, false);

                    if (this.controlFader.timer)
                        clearInterval(this.controlFader.timer);
                    if (this.controlFader.delayTimer)
                        clearInterval(this.controlFader.delayTimer);
                    if (this.statusFader.timer)
                        clearInterval(this.statusFader.timer);
                    if (this.statusFader.delayTimer)
                        clearTimeout(this.statusFader.delayTimer);
                    this.log("--- videocontrols terminated ---");
                },

                durationChange : function (duration) {
                    if (isNaN(duration))
                        duration = this.maxCurrentTimeSeen;
                    this.log("Duration is " + duration + "ms");

                    // Format the duration as "h:mm:ss" or "m:ss"
                    var hours = Math.floor(duration / 3600000);
                    var mins  = Math.floor(duration % 3600000 / 60000);
                    var secs  = Math.floor(duration % 60000 / 1000);
                    var timeString;
                    if (secs < 10)
                        secs = "0" + secs;
                    if (hours) {
                        if (mins < 10)
                            mins = "0" + mins;
                        timeString = hours + ":" + mins + ":" + secs;
                    } else {
                        timeString = mins + ":" + secs;
                    }
                    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.increment = duration / 50;
                    this.scrubber.pageIncrement = Math.round(duration / 10);
                },

                seekToPosition : function(newPosition) {
                    newPosition /= 1000; // convert from ms
                    this.log("+++ seeking to " + newPosition);
                    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;
                    if (isNaN(duration)) {
                        duration = this.maxCurrentTimeSeen;
                        this.durationChange(duration);
                    }

                    this.log("time update @ " + currentTime + "ms of " + duration + "ms");
                    this.scrubber.value = currentTime;
                },

                onVolumeMouseInOut : function (event) {
                    // Ignore events caused by transitions between mute button and volumeStack,
                    // or between nodes inside these two elements.
                    if (this.isEventWithin(event, this.muteButton, this.volumeStack))
                        return;
                    var isMouseOver = (event.type == "mouseover");
                    this.startFade(this.volumeFader, isMouseOver);
                },

                onMouseInOut : function (event) {
                    // If the controls are static, don't change anything.
                    if (!this.dynamicControls)
                        return;

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

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

                    this.startFade(this.controlFader, isMouseOver);
                },

                startFadeIn : function (fader, immediate) {
                    this.startFade(fader, true, immediate);
                },

                startFadeOut : function (fader, immediate) {
                    this.startFade(fader, false, immediate);
                },

                startFade : function (fader, fadeIn, immediate) {
                    // If the fader specifies a start delay, don't immediately fade in...
                    // Unless there's already a fade underway, in which case we want to be
                    // able to immediately reverse it (eg, a seeking event right after seeked).
                    if (fadeIn && fader.START_DELAY && !immediate && !fader.timer) {
                        function delayedFadeStart(self, fader) {
                            self.log("Delated start timer fired.");
                            fader.delayTimer = null;
                            self.startFade(fader, true, true);
                        }

                        // If there's already a timer running, let it handle things.
                        if (fader.delayTimer)
                            return;

                        this.log("Delaying " + fader.name + " fade-in...");
                        fader.delayTimer = setTimeout(delayedFadeStart, fader.START_DELAY, this, fader);
                        return;
                    }

                    // Cancel any delay timer (eg, if we start fading-out before it fires)
                    if (fader.delayTimer) {
                        this.log("Canceling " + fader.name + " fade-in delay...");
                        clearTimeout(fader.delayTimer);
                        fader.delayTimer = null;
                    }


                    // If we're already fading towards the desired state (or are
                    // already there), then we don't need to do anything more.
                    var directionChange = (fader.fadingIn != fadeIn);
                    if (!directionChange)
                        return;

                    fader.fadingIn = fadeIn;
                    this.log("Fading " + fader.name + (fader.fadingIn ? " in" : " out"));

                    // When switching direction mid-fade, we want the reversed fade
                    // to complete in the same amount of time as the current fade has
                    // been running. So we invert the runtime.
                    //
                    // For example, if we're 20ms into a 100ms fade-in, then we want to
                    // fade-out over 20ms. This is done by setting the .runtime to 80ms
                    // (100-20), so that doFade will only animate for 20ms more.
                    if (fader.runtime)
                        fader.runtime = fader.RUNTIME_MAX - fader.runtime;

                    if (!fader.timer) {
                        fader.timer = setInterval(this.doFade, fader.RUNTIME_STEP, this, fader);
                        // Perform the first fade step now, notably to make a fade-in
                        // immediately activate the controls.
                        this.doFade(this, fader, -(fader.RUNTIME_STEP - 1));
                    }
                },

                doFade : function (self, fader, lateness) {
                    // Update elapsed time, and compute position as a percent
                    // of total. Last frame could run over, so clamp to 1.
                    fader.runtime += fader.RUNTIME_STEP + lateness;
                    var pos = fader.runtime / fader.RUNTIME_MAX;
                    if (pos > 1)
                        pos = 1;
                    
                    // 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 (fader.name == "controls" && fader.fadingIn && fader.element.hidden)
                        self.scrubber.valueChanged("curpos", self.video.currentTime * 1000, false);

                    // Calculate the opacity for our position in the animation.
                    var opacity;
                    if (fader.fadingIn)
                        opacity = Math.pow(pos, 0.5);
                    else
                        opacity = Math.pow(1 - pos, 0.5);
                    fader.isVisible = (opacity ? true : false);
                    fader.element.style.opacity = opacity;
                    // Hide the element to ignore mouse clicks and reduce throbber CPU usage.
                    fader.element.setAttribute("hidden", !fader.isVisible);

                    // If this fader also has a slide effect, change the CSS margin-top too.
                    if (fader.maxSlide) {
                        var marginTop;
                        if (fader.fadingIn)
                            marginTop = Math.round(fader.maxSlide * Math.pow(pos, 0.5));
                        else
                            marginTop = Math.round(fader.maxSlide * Math.pow(1 - pos, 0.5));

                        fader.element.style.marginTop = marginTop;
                    }

                    // Is the animation done?
                    if (pos == 1) {
                        clearInterval(fader.timer);
                        fader.timer = null;
                        fader.runtime = 0;
                    }
                },

                togglePause : function () {
                    if (this.video.paused || this.video.ended)
                        this.video.play();
                    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.
                },

                toggleMute : function () {
                    this.video.muted = !this.video.muted;

                    // We'll handle style changes in the event listener for
                    // the "volumechange" event, same as if content script was
                    // controlling volume.
                },

                setPlayButtonState: function(aPaused)
                {
                  this.playButton.setAttribute("paused", aPaused);

                  var attrName = aPaused ? "playlabel" : "pauselabel";
                  var value = this.playButton.getAttribute(attrName);
                  this.playButton.setAttribute("aria-label", value);
                },

                setMuteButtonState: function(aMuted)
                {
                  this.muteButton.setAttribute("muted", aMuted);

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

                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)
                        dump("videoctl: " + msg + "\n");
                },

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

                    this.controlFader.element = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
                    this.statusFader.element  = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
                    this.volumeFader.element  = document.getAnonymousElementByAttribute(binding, "class", "volumeStack");

                    this.statusIcon    = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
                    this.playButton    = document.getAnonymousElementByAttribute(binding, "class", "playButton");
                    this.muteButton    = document.getAnonymousElementByAttribute(binding, "class", "muteButton");
                    this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl");
                    this.volumeStack   = document.getAnonymousElementByAttribute(binding, "class", "volumeStack");
                    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.setupInitialState();

                    // 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)
                    if (!(this.video.autoplay && this.video.mozAutoplayEnabled) || !this.dynamicControls) {
                        var fader = this.controlFader;
                        fader.element.setAttribute("hidden", "false");
                        fader.isVisible = true;
                        fader.fadingIn = true;
                    }

                    // Use the handleEvent() callback for all media events.
                    for each (var event in this.videoEvents)
                        this.video.addEventListener(event, this, false);

                    // Determine the height of the volumeFader when extended (which is controlled by CSS).
                    // Its .clientHeight seems to be 0 here, so use the theme's initial value. (eg "-70px")
                    this.volumeFader.maxSlide = parseInt(window.getComputedStyle(this.volumeStack, null)
                                                               .getPropertyValue("margin-top"));
                    var self = this;
                    this.muteButton.addEventListener("command", function() { self.toggleMute(); }, false);
                    this.playButton.addEventListener("command", function() { self.togglePause(); }, false);
                    this.muteButton.addEventListener("mouseover",  function(e) { self.onVolumeMouseInOut(e); }, false);
                    this.muteButton.addEventListener("mouseout",   function(e) { self.onVolumeMouseInOut(e); }, false);
                    this.volumeStack.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false);
                    this.volumeStack.addEventListener("mouseout",  function(e) { self.onVolumeMouseInOut(e); }, false);

                    this.log("--- videocontrols initialized ---");
                }
            }) ]]>
        </field>


    </implementation>

    <handlers>
        <handler event="mouseover">
            this.Utils.onMouseInOut(event);
        </handler>
        <handler event="mouseout">
            this.Utils.onMouseInOut(event);
        </handler>
    </handlers>
  </binding>
</bindings>