toolkit/content/widgets/videocontrols.xml

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/content/widgets/videocontrols.xml	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1765 @@
     1.4 +<?xml version="1.0"?>
     1.5 +<!-- This Source Code Form is subject to the terms of the Mozilla Public
     1.6 +   - License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 +   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
     1.8 +
     1.9 +
    1.10 +<!DOCTYPE bindings [
    1.11 +  <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
    1.12 +  %videocontrolsDTD;
    1.13 +]>
    1.14 +
    1.15 +<bindings id="videoControlBindings"
    1.16 +   xmlns="http://www.mozilla.org/xbl"
    1.17 +   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    1.18 +   xmlns:xbl="http://www.mozilla.org/xbl"
    1.19 +   xmlns:svg="http://www.w3.org/2000/svg"
    1.20 +   xmlns:html="http://www.w3.org/1999/xhtml">
    1.21 +
    1.22 +  <binding id="timeThumb"
    1.23 +           extends="chrome://global/content/bindings/scale.xml#scalethumb">
    1.24 +      <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    1.25 +          <xbl:children/>
    1.26 +          <hbox class="timeThumb" xbl:inherits="showhours">
    1.27 +              <label class="timeLabel"/>
    1.28 +          </hbox>
    1.29 +      </xbl:content>
    1.30 +      <implementation>
    1.31 +
    1.32 +      <constructor>
    1.33 +          <![CDATA[
    1.34 +          this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
    1.35 +          this.timeLabel.setAttribute("value", "0:00");
    1.36 +          ]]>
    1.37 +      </constructor>
    1.38 +
    1.39 +      <property name="showHours">
    1.40 +        <getter>
    1.41 +          <![CDATA[
    1.42 +          return this.getAttribute("showhours") == "true";
    1.43 +          ]]>
    1.44 +        </getter>
    1.45 +        <setter>
    1.46 +          <![CDATA[
    1.47 +          this.setAttribute("showhours", val);
    1.48 +          // If the duration becomes known while we're still showing the value
    1.49 +          // for time=0, immediately update the value to show or hide the hours.
    1.50 +          // It's less intrusive to do it now than when the user clicks play and
    1.51 +          // is looking right next to the thumb.
    1.52 +          var displayedTime = this.timeLabel.getAttribute("value");
    1.53 +          if (val && displayedTime == "0:00")
    1.54 +              this.timeLabel.setAttribute("value", "0:00:00");
    1.55 +          else if (!val && displayedTime == "0:00:00")
    1.56 +              this.timeLabel.setAttribute("value", "0:00");
    1.57 +          ]]>
    1.58 +        </setter>
    1.59 +      </property>
    1.60 +
    1.61 +      <method name="setTime">
    1.62 +          <parameter name="time"/>
    1.63 +          <body>
    1.64 +          <![CDATA[
    1.65 +              var timeString;
    1.66 +              time = Math.round(time / 1000);
    1.67 +              var hours = Math.floor(time / 3600);
    1.68 +              var mins  = Math.floor((time % 3600) / 60);
    1.69 +              var secs  = Math.floor(time % 60);
    1.70 +              if (secs < 10)
    1.71 +                  secs = "0" + secs;
    1.72 +              if (hours || this.showHours) {
    1.73 +                  if (mins < 10)
    1.74 +                      mins = "0" + mins;
    1.75 +                  timeString = hours + ":" + mins + ":" + secs;
    1.76 +              } else {
    1.77 +                  timeString = mins + ":" + secs;
    1.78 +              }
    1.79 +
    1.80 +              this.timeLabel.setAttribute("value", timeString);
    1.81 +          ]]>
    1.82 +          </body>
    1.83 +      </method>
    1.84 +      </implementation>
    1.85 +  </binding>
    1.86 +
    1.87 +  <binding id="suppressChangeEvent"
    1.88 +            extends="chrome://global/content/bindings/scale.xml#scale">
    1.89 +    <implementation implements="nsIXBLAccessible">
    1.90 +      <!-- nsIXBLAccessible -->
    1.91 +      <property name="accessibleName" readonly="true">
    1.92 +        <getter>
    1.93 +          if (this.type != "scrubber")
    1.94 +              return "";
    1.95 +
    1.96 +          var currTime = this.thumb.timeLabel.getAttribute("value");
    1.97 +          var totalTime = this.durationValue;
    1.98 +
    1.99 +          return this.scrubberNameFormat.replace(/#1/, currTime).
   1.100 +              replace(/#2/, totalTime);
   1.101 +        </getter>
   1.102 +      </property>
   1.103 +
   1.104 +      <constructor>
   1.105 +          <![CDATA[
   1.106 +          this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
   1.107 +          this.durationValue = "";
   1.108 +          this.valueBar = null;
   1.109 +          this.isDragging = false;
   1.110 +          this.wasPausedBeforeDrag = true;
   1.111 +
   1.112 +          this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
   1.113 +          this.type = this.getAttribute("class");
   1.114 +          this.Utils = document.getBindingParent(this.parentNode).Utils;
   1.115 +          if (this.type == "scrubber")
   1.116 +              this.valueBar = this.Utils.progressBar;
   1.117 +          ]]>
   1.118 +      </constructor>
   1.119 +
   1.120 +      <method name="valueChanged">
   1.121 +          <parameter name="which"/>
   1.122 +          <parameter name="newValue"/>
   1.123 +          <parameter name="userChanged"/>
   1.124 +          <body>
   1.125 +          <![CDATA[
   1.126 +            // This method is a copy of the base binding's valueChanged(), except that it does
   1.127 +            // not dispatch a |change| event (to avoid exposing the event to web content), and
   1.128 +            // just calls the videocontrol's seekToPosition() method directly.
   1.129 +            switch (which) {
   1.130 +              case "curpos":
   1.131 +                if (this.type == "scrubber") {
   1.132 +                    // Update the time shown in the thumb.
   1.133 +                    this.thumb.setTime(newValue);
   1.134 +                    this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
   1.135 +                    // Update the value bar to match the thumb position.
   1.136 +                    var percent = newValue / this.max;
   1.137 +                    this.valueBar.value = Math.round(percent * 10000); // has max=10000
   1.138 +                }
   1.139 +
   1.140 +                // The value of userChanged is true when changing the position with the mouse,
   1.141 +                // but not when pressing an arrow key. However, the base binding sets
   1.142 +                // ._userChanged in its keypress handlers, so we just need to check both.
   1.143 +                if (!userChanged && !this._userChanged)
   1.144 +                  return;
   1.145 +                this.setAttribute("value", newValue);
   1.146 +
   1.147 +                if (this.type == "scrubber")
   1.148 +                    this.Utils.seekToPosition(newValue);
   1.149 +                else if (this.type == "volumeControl")
   1.150 +                    this.Utils.setVolume(newValue / 100);
   1.151 +                break;
   1.152 +
   1.153 +              case "minpos":
   1.154 +                this.setAttribute("min", newValue);
   1.155 +                break;
   1.156 +
   1.157 +              case "maxpos":
   1.158 +                if (this.type == "scrubber") {
   1.159 +                    // Update the value bar to match the thumb position.
   1.160 +                    var percent = this.value / newValue;
   1.161 +                    this.valueBar.value = Math.round(percent * 10000); // has max=10000
   1.162 +                }
   1.163 +                this.setAttribute("max", newValue);
   1.164 +                break;
   1.165 +            }
   1.166 +          ]]>
   1.167 +          </body>
   1.168 +      </method>
   1.169 +
   1.170 +      <method name="dragStateChanged">
   1.171 +        <parameter name="isDragging"/>
   1.172 +          <body>
   1.173 +          <![CDATA[
   1.174 +            if (this.type == "scrubber") {
   1.175 +                this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
   1.176 +                this.isDragging = isDragging;
   1.177 +                if (isDragging) {
   1.178 +                    this.wasPausedBeforeDrag = this.Utils.video.paused;
   1.179 +                    this.previousPlaybackRate = this.Utils.video.playbackRate;
   1.180 +                    this.Utils.video.pause();
   1.181 +                } else if (!this.wasPausedBeforeDrag) {
   1.182 +                    // After the drag ends, resume playing.
   1.183 +                    this.Utils.video.playbackRate = this.previousPlaybackRate;
   1.184 +                    this.Utils.video.play();
   1.185 +                }
   1.186 +            }
   1.187 +          ]]>
   1.188 +          </body>
   1.189 +      </method>
   1.190 +
   1.191 +      </implementation>
   1.192 +  </binding>
   1.193 +
   1.194 +  <binding id="videoControls">
   1.195 +
   1.196 +    <resources>
   1.197 +        <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
   1.198 +        <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
   1.199 +    </resources>
   1.200 +
   1.201 +    <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   1.202 +                 class="mediaControlsFrame">
   1.203 +        <stack flex="1">
   1.204 +            <vbox flex="1" class="statusOverlay" hidden="true">
   1.205 +                <box class="statusIcon"/>
   1.206 +                <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
   1.207 +                <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
   1.208 +                <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
   1.209 +                <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
   1.210 +                <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
   1.211 +                <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
   1.212 +            </vbox>
   1.213 +
   1.214 +            <vbox class="statsOverlay" hidden="true">
   1.215 +                <html:div class="statsDiv" xmlns="http://www.w3.org/1999/xhtml">
   1.216 +                    <table class="statsTable">
   1.217 +                        <tr>
   1.218 +                            <td class="statLabel">&stats.media;</td>
   1.219 +                            <td class="statValue filename"><span class="statFilename"/></td>
   1.220 +                        </tr>
   1.221 +                        <tr>
   1.222 +                            <td class="statLabel">&stats.size;</td>
   1.223 +                            <td class="statValue size"><span class="statSize"/></td>
   1.224 +                        </tr>
   1.225 +                        <tr style="height: 1em;"/>
   1.226 +
   1.227 +                        <tr>
   1.228 +                            <td class="statLabel">&stats.activity;</td>
   1.229 +                            <td class="statValue activity">
   1.230 +                              <span class="statActivity">
   1.231 +                                <span class="statActivityPaused">&stats.activityPaused;</span>
   1.232 +                                <span class="statActivityPlaying">&stats.activityPlaying;</span>
   1.233 +                                <span class="statActivityEnded">&stats.activityEnded;</span>
   1.234 +                                <span class="statActivitySeeking">&stats.activitySeeking;</span>
   1.235 +                              </span>
   1.236 +                            </td>
   1.237 +                        </tr>
   1.238 +                        <tr>
   1.239 +                            <td class="statLabel">&stats.volume;</td> <td class="statValue"><span class="statVolume"/></td>
   1.240 +                        </tr>
   1.241 +                        <tr>
   1.242 +                            <!-- Localization note: readyState is a HTML5 API MediaElement-specific attribute and should not be localized. -->
   1.243 +                            <td class="statLabel">readyState</td> <td class="statValue"><span class="statReadyState"/></td>
   1.244 +                        </tr>
   1.245 +                        <tr>
   1.246 +                          <!-- Localization note: networkState is a HTML5 API MediaElement-specific attribute and should not be localized. -->
   1.247 +                          <td class="statLabel">networkState</td> <td class="statValue"><span class="statNetState"/></td>
   1.248 +                        </tr>
   1.249 +                        <tr style="height: 1em;"/>
   1.250 +
   1.251 +                        <tr>
   1.252 +                            <td class="statLabel">&stats.framesParsed;</td>
   1.253 +                            <td class="statValue"><span class="statFramesParsed"/></td>
   1.254 +                        </tr>
   1.255 +                        <tr>
   1.256 +                            <td class="statLabel">&stats.framesDecoded;</td>
   1.257 +                            <td class="statValue"><span class="statFramesDecoded"/></td>
   1.258 +                        </tr>
   1.259 +                        <tr>
   1.260 +                            <td class="statLabel">&stats.framesPresented;</td>
   1.261 +                            <td class="statValue"><span class="statFramesPresented"/></td>
   1.262 +                        </tr>
   1.263 +                        <tr>
   1.264 +                            <td class="statLabel">&stats.framesPainted;</td>
   1.265 +                            <td class="statValue"><span class="statFramesPainted"/></td>
   1.266 +                        </tr>
   1.267 +                    </table>
   1.268 +                </html:div>
   1.269 +            </vbox>
   1.270 +
   1.271 +            <vbox class="controlsOverlay">
   1.272 +                <stack flex="1">
   1.273 +                    <spacer class="controlsSpacer" flex="1"/>
   1.274 +                    <box class="clickToPlay" hidden="true" flex="1"/>
   1.275 +                </stack>
   1.276 +                <hbox class="controlBar" hidden="true">
   1.277 +                    <button class="playButton"
   1.278 +                            playlabel="&playButton.playLabel;"
   1.279 +                            pauselabel="&playButton.pauseLabel;"/>
   1.280 +                    <stack class="scrubberStack" flex="1">
   1.281 +                        <box class="backgroundBar"/>
   1.282 +                        <progressmeter class="bufferBar"/>
   1.283 +                        <progressmeter class="progressBar" max="10000"/>
   1.284 +                        <scale class="scrubber" movetoclick="true"/>
   1.285 +                    </stack>
   1.286 +                    <vbox class="durationBox">
   1.287 +                        <label class="positionLabel" role="presentation"/>
   1.288 +                        <label class="durationLabel" role="presentation"/>
   1.289 +                    </vbox>
   1.290 +                    <button class="muteButton"
   1.291 +                            mutelabel="&muteButton.muteLabel;"
   1.292 +                            unmutelabel="&muteButton.unmuteLabel;"/>
   1.293 +                    <stack class="volumeStack">
   1.294 +                      <box class="volumeBackground"/>
   1.295 +                      <box class="volumeForeground" anonid="volumeForeground"/>
   1.296 +                      <scale class="volumeControl" movetoclick="true"/>
   1.297 +                    </stack>
   1.298 +                    <button class="fullscreenButton"
   1.299 +                            enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
   1.300 +                            exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
   1.301 +                </hbox>
   1.302 +            </vbox>
   1.303 +        </stack>
   1.304 +    </xbl:content>
   1.305 +
   1.306 +    <implementation>
   1.307 +
   1.308 +        <constructor>
   1.309 +            <![CDATA[
   1.310 +            this.isTouchControl = false;
   1.311 +            this.randomID = 0;
   1.312 +
   1.313 +            this.Utils = {
   1.314 +                debug : false,
   1.315 +                video : null,
   1.316 +                videocontrols : null,
   1.317 +                controlBar : null,
   1.318 +                playButton : null,
   1.319 +                muteButton : null,
   1.320 +                volumeControl  : null,
   1.321 +                durationLabel  : null,
   1.322 +                positionLabel  : null,
   1.323 +                scrubberThumb  : null,
   1.324 +                scrubber       : null,
   1.325 +                progressBar    : null,
   1.326 +                bufferBar      : null,
   1.327 +                statusOverlay  : null,
   1.328 +                controlsSpacer : null,
   1.329 +                clickToPlay    : null,
   1.330 +                stats          : {},
   1.331 +                fullscreenButton : null,
   1.332 +
   1.333 +                randomID : 0,
   1.334 +                videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
   1.335 +                               "loadstart", "timeupdate", "progress",
   1.336 +                               "playing", "waiting", "canplay", "canplaythrough",
   1.337 +                               "seeking", "seeked", "emptied", "loadedmetadata",
   1.338 +                               "error", "suspend", "stalled",
   1.339 +                               "mozinterruptbegin", "mozinterruptend" ],
   1.340 +
   1.341 +                firstFrameShown : false,
   1.342 +                timeUpdateCount : 0,
   1.343 +                maxCurrentTimeSeen : 0,
   1.344 +                _isAudioOnly : false,
   1.345 +                get isAudioOnly() { return this._isAudioOnly; },
   1.346 +                set isAudioOnly(val) {
   1.347 +                    this._isAudioOnly = val;
   1.348 +                    if (this._isAudioOnly) {
   1.349 +                        this.controlBar.setAttribute("audio-only", true);
   1.350 +                    } else {
   1.351 +                        this.controlBar.removeAttribute("audio-only");
   1.352 +                    }
   1.353 +                    this.adjustControlSize();
   1.354 +
   1.355 +                    if (!this.isTopLevelSyntheticDocument)
   1.356 +                        return;
   1.357 +                    if (this._isAudioOnly) {
   1.358 +                        this.video.style.height = this._controlBarHeight + "px";
   1.359 +                        this.video.style.width = "66%";
   1.360 +                    } else {
   1.361 +                        this.video.style.removeProperty("height");
   1.362 +                        this.video.style.removeProperty("width");
   1.363 +                    }
   1.364 +                },
   1.365 +                suppressError : false,
   1.366 +
   1.367 +                setupStatusFader : function(immediate) {
   1.368 +                    // Since the play button will be showing, we don't want to
   1.369 +                    // show the throbber behind it. The throbber here will
   1.370 +                    // only show if needed after the play button has been pressed.
   1.371 +                    if (!this.clickToPlay.hidden) {
   1.372 +                        this.startFadeOut(this.statusOverlay, true);
   1.373 +                        return;
   1.374 +                    }
   1.375 +
   1.376 +                    var show = false;
   1.377 +                    if (this.video.seeking ||
   1.378 +                        (this.video.error && !this.suppressError) ||
   1.379 +                        this.video.networkState == this.video.NETWORK_NO_SOURCE ||
   1.380 +                        (this.video.networkState == this.video.NETWORK_LOADING &&
   1.381 +                         (this.video.paused || this.video.ended
   1.382 +                           ? this.video.readyState < this.video.HAVE_CURRENT_DATA
   1.383 +                           : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
   1.384 +                        (this.timeUpdateCount <= 1 && !this.video.ended &&
   1.385 +                         this.video.readyState < this.video.HAVE_ENOUGH_DATA &&
   1.386 +                         this.video.networkState == this.video.NETWORK_LOADING))
   1.387 +                        show = true;
   1.388 +
   1.389 +                    // Explicitly hide the status fader if this
   1.390 +                    // is audio only until bug 619421 is fixed.
   1.391 +                    if (this.isAudioOnly)
   1.392 +                        show = false;
   1.393 +
   1.394 +                    this.log("Status overlay: seeking=" + this.video.seeking +
   1.395 +                             " error=" + this.video.error + " readyState=" + this.video.readyState +
   1.396 +                             " paused=" + this.video.paused + " ended=" + this.video.ended +
   1.397 +                             " networkState=" + this.video.networkState +
   1.398 +                             " timeUpdateCount=" + this.timeUpdateCount +
   1.399 +                             " --> " + (show ? "SHOW" : "HIDE"));
   1.400 +                    this.startFade(this.statusOverlay, show, immediate);
   1.401 +                },
   1.402 +
   1.403 +                /*
   1.404 +                 * Set the initial state of the controls. The binding is normally created along
   1.405 +                 * with video element, but could be attached at any point (eg, if the video is
   1.406 +                 * removed from the document and then reinserted). Thus, some one-time events may
   1.407 +                 * have already fired, and so we'll need to explicitly check the initial state.
   1.408 +                 */
   1.409 +                setupInitialState : function() {
   1.410 +                    this.randomID = Math.random();
   1.411 +                    this.videocontrols.randomID = this.randomID;
   1.412 +
   1.413 +                    this.setPlayButtonState(this.video.paused);
   1.414 +                    this.setMuteButtonState(this.video.muted);
   1.415 +
   1.416 +                    this.setFullscreenButtonState();
   1.417 +
   1.418 +                    var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
   1.419 +                    this.volumeControl.value = volume;
   1.420 +
   1.421 +                    var duration = Math.round(this.video.duration * 1000); // in ms
   1.422 +                    var currentTime = Math.round(this.video.currentTime * 1000); // in ms
   1.423 +                    this.log("Initial playback position is at " + currentTime + " of " + duration);
   1.424 +                    // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
   1.425 +                    // to determine if the media source changed while we were detached.
   1.426 +                    this.maxCurrentTimeSeen = currentTime;
   1.427 +                    this.showPosition(currentTime, duration);
   1.428 +
   1.429 +                    // If we have metadata, check if this is a <video> without
   1.430 +                    // video data, or a video with no audio track.
   1.431 +                    if (this.video.readyState >= this.video.HAVE_METADATA) {
   1.432 +                        if (this.video instanceof HTMLVideoElement &&
   1.433 +                            (this.video.videoWidth == 0 || this.video.videoHeight == 0))
   1.434 +                            this.isAudioOnly = true;
   1.435 +
   1.436 +                        // We have to check again if the media has audio here,
   1.437 +                        // because of bug 718107: switching to fullscreen may
   1.438 +                        // cause the bindings to detach and reattach, hence
   1.439 +                        // unsetting the attribute.
   1.440 +                        if (!this.isAudioOnly && !this.video.mozHasAudio) {
   1.441 +                          this.muteButton.setAttribute("noAudio", "true");
   1.442 +                          this.muteButton.setAttribute("disabled", "true");
   1.443 +                        }
   1.444 +                    }
   1.445 +
   1.446 +                    if (this.isAudioOnly)
   1.447 +                        this.clickToPlay.hidden = true;
   1.448 +
   1.449 +                    // If the first frame hasn't loaded, kick off a throbber fade-in.
   1.450 +                    if (this.video.readyState >= this.video.HAVE_CURRENT_DATA)
   1.451 +                        this.firstFrameShown = true;
   1.452 +
   1.453 +                    // We can't determine the exact buffering status, but do know if it's
   1.454 +                    // fully loaded. (If it's still loading, it will fire a progress event
   1.455 +                    // and we'll figure out the exact state then.)
   1.456 +                    this.bufferBar.setAttribute("max", 100);
   1.457 +                    if (this.video.readyState >= this.video.HAVE_METADATA)
   1.458 +                        this.showBuffered();
   1.459 +                    else
   1.460 +                        this.bufferBar.setAttribute("value", 0);
   1.461 +
   1.462 +                    // Set the current status icon.
   1.463 +                    if (this.hasError()) {
   1.464 +                        this.clickToPlay.hidden = true;
   1.465 +                        this.statusIcon.setAttribute("type", "error");
   1.466 +                        this.updateErrorText();
   1.467 +                        this.setupStatusFader(true);
   1.468 +                    }
   1.469 +
   1.470 +                    // An event handler for |onresize| should be added when bug 227495 is fixed.
   1.471 +                    this.controlBar.hidden = false;
   1.472 +                    this._playButtonWidth = this.playButton.clientWidth;
   1.473 +                    this._durationLabelWidth = this.durationLabel.clientWidth;
   1.474 +                    this._muteButtonWidth = this.muteButton.clientWidth;
   1.475 +                    this._volumeControlWidth = this.volumeControl.clientWidth;
   1.476 +                    this._fullscreenButtonWidth = this.fullscreenButton.clientWidth;
   1.477 +                    this._controlBarHeight = this.controlBar.clientHeight;
   1.478 +                    this.controlBar.hidden = true;
   1.479 +                    this.adjustControlSize();
   1.480 +
   1.481 +                    // Preserve Statistics when toggling fullscreen mode due to bug 714071.
   1.482 +                    if (this.video.mozMediaStatisticsShowing)
   1.483 +                        this.showStatistics(true);
   1.484 +
   1.485 +                    this._handleCustomEventsBound = this.handleCustomEvents.bind(this);
   1.486 +                    this.video.addEventListener("media-showStatistics", this._handleCustomEventsBound, false, true);
   1.487 +                },
   1.488 +
   1.489 +                setupNewLoadState : function() {
   1.490 +                    // videocontrols.css hides the control bar by default, because if script
   1.491 +                    // is disabled our binding's script is disabled too (bug 449358). Thus,
   1.492 +                    // the controls are broken and we don't want them shown. But if script is
   1.493 +                    // enabled, the code here will run and can explicitly unhide the controls.
   1.494 +                    //
   1.495 +                    // For videos with |autoplay| set, we'll leave the controls initially hidden,
   1.496 +                    // so that they don't get in the way of the playing video. Otherwise we'll
   1.497 +                    // go ahead and reveal the controls now, so they're an obvious user cue.
   1.498 +                    //
   1.499 +                    // (Note: the |controls| attribute is already handled via layout/style/html.css)
   1.500 +                    var shouldShow = !this.dynamicControls ||
   1.501 +                      (this.video.paused &&
   1.502 +                       !(this.video.autoplay && this.video.mozAutoplayEnabled));
   1.503 +                    // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
   1.504 +                    this.startFade(this.clickToPlay, shouldShow && !this.isAudioOnly &&
   1.505 +                                   this.video.currentTime == 0 && !this.hasError(), true);
   1.506 +                    this.startFade(this.controlBar, shouldShow, true);
   1.507 +                },
   1.508 +
   1.509 +                handleCustomEvents : function (e) {
   1.510 +                    if (!e.isTrusted)
   1.511 +                        return;
   1.512 +                    this.showStatistics(e.detail);
   1.513 +                },
   1.514 +
   1.515 +                get dynamicControls() {
   1.516 +                    // Don't fade controls for <audio> elements.
   1.517 +                    var enabled = !this.isAudioOnly;
   1.518 +
   1.519 +                    // Allow tests to explicitly suppress the fading of controls.
   1.520 +                    if (this.video.hasAttribute("mozNoDynamicControls"))
   1.521 +                        enabled = false;
   1.522 +
   1.523 +                    // If the video hits an error, suppress controls if it
   1.524 +                    // hasn't managed to do anything else yet.
   1.525 +                    if (!this.firstFrameShown && this.hasError())
   1.526 +                        enabled = false;
   1.527 +
   1.528 +                    return enabled;
   1.529 +                },
   1.530 +
   1.531 +                handleEvent : function (aEvent) {
   1.532 +                    this.log("Got media event ----> " + aEvent.type);
   1.533 +
   1.534 +                    // If the binding is detached (or has been replaced by a
   1.535 +                    // newer instance of the binding), nuke our event-listeners.
   1.536 +                    if (this.videocontrols.randomID != this.randomID) {
   1.537 +                        this.terminateEventListeners();
   1.538 +                        return;
   1.539 +                    }
   1.540 +
   1.541 +                    switch (aEvent.type) {
   1.542 +                        case "play":
   1.543 +                            this.setPlayButtonState(false);
   1.544 +                            this.setupStatusFader();
   1.545 +                            if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl)
   1.546 +                                this.startFadeOut(this.controlBar);
   1.547 +                            if (!this._triggeredByControls)
   1.548 +                                this.clickToPlay.hidden = true;
   1.549 +                            this._triggeredByControls = false;
   1.550 +                            break;
   1.551 +                        case "pause":
   1.552 +                            // Little white lie: if we've internally paused the video
   1.553 +                            // while dragging the scrubber, don't change the button state.
   1.554 +                            if (!this.scrubber.isDragging)
   1.555 +                                this.setPlayButtonState(true);
   1.556 +                            this.setupStatusFader();
   1.557 +                            break;
   1.558 +                        case "ended":
   1.559 +                            this.setPlayButtonState(true);
   1.560 +                            // We throttle timechange events, so the thumb might not be
   1.561 +                            // exactly at the end when the video finishes.
   1.562 +                            this.showPosition(Math.round(this.video.currentTime * 1000),
   1.563 +                                              Math.round(this.video.duration * 1000));
   1.564 +                            this.startFadeIn(this.controlBar);
   1.565 +                            this.setupStatusFader();
   1.566 +                            break;
   1.567 +                        case "volumechange":
   1.568 +                            var volume = this.video.muted ? 0 : this.video.volume;
   1.569 +                            var volumePercentage = Math.round(volume * 100);
   1.570 +                            this.setMuteButtonState(this.video.muted);
   1.571 +                            this.volumeControl.value = volumePercentage;
   1.572 +                            this.volumeForeground.style.paddingRight = (1 - volume) * this._volumeControlWidth + "px";
   1.573 +                            break;
   1.574 +                        case "loadedmetadata":
   1.575 +                            this.adjustControlSize();
   1.576 +                            // If a <video> doesn't have any video data, treat it as <audio>
   1.577 +                            // and show the controls (they won't fade back out)
   1.578 +                            if (this.video instanceof HTMLVideoElement &&
   1.579 +                                (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
   1.580 +                                this.isAudioOnly = true;
   1.581 +                                this.clickToPlay.hidden = true;
   1.582 +                                this.startFadeIn(this.controlBar);
   1.583 +                                this.setFullscreenButtonState();
   1.584 +                            }
   1.585 +                            this.showDuration(Math.round(this.video.duration * 1000));
   1.586 +                            if (!this.isAudioOnly && !this.video.mozHasAudio) {
   1.587 +                              this.muteButton.setAttribute("noAudio", "true");
   1.588 +                              this.muteButton.setAttribute("disabled", "true");
   1.589 +                            }
   1.590 +                            break;
   1.591 +                        case "loadeddata":
   1.592 +                            this.firstFrameShown = true;
   1.593 +                            this.setupStatusFader();
   1.594 +                            break;
   1.595 +                        case "loadstart":
   1.596 +                            this.maxCurrentTimeSeen = 0;
   1.597 +                            this.controlsSpacer.removeAttribute("aria-label");
   1.598 +                            this.statusOverlay.removeAttribute("error");
   1.599 +                            this.statusIcon.setAttribute("type", "throbber");
   1.600 +                            this.isAudioOnly = (this.video instanceof HTMLAudioElement);
   1.601 +                            this.setPlayButtonState(true);
   1.602 +                            this.setupNewLoadState();
   1.603 +                            this.setupStatusFader();
   1.604 +                            break;
   1.605 +                        case "progress":
   1.606 +                            this.statusIcon.removeAttribute("stalled");
   1.607 +                            this.showBuffered();
   1.608 +                            this.setupStatusFader();
   1.609 +                            break;
   1.610 +                        case "stalled":
   1.611 +                            this.statusIcon.setAttribute("stalled", "true");
   1.612 +                            this.statusIcon.setAttribute("type", "throbber");
   1.613 +                            this.setupStatusFader();
   1.614 +                            break;
   1.615 +                        case "suspend":
   1.616 +                            this.setupStatusFader();
   1.617 +                            break;
   1.618 +                        case "timeupdate":
   1.619 +                            var currentTime = Math.round(this.video.currentTime * 1000); // in ms
   1.620 +                            var duration = Math.round(this.video.duration * 1000); // in ms
   1.621 +
   1.622 +                            // If playing/seeking after the video ended, we won't get a "play"
   1.623 +                            // event, so update the button state here.
   1.624 +                            if (!this.video.paused)
   1.625 +                                this.setPlayButtonState(false);
   1.626 +
   1.627 +                            this.timeUpdateCount++;
   1.628 +                            // Whether we show the statusOverlay sometimes depends
   1.629 +                            // on whether we've seen more than one timeupdate
   1.630 +                            // event (if we haven't, there hasn't been any
   1.631 +                            // "playback activity" and we may wish to show the
   1.632 +                            // statusOverlay while we wait for HAVE_ENOUGH_DATA).
   1.633 +                            // If we've seen more than 2 timeupdate events,
   1.634 +                            // the count is no longer relevant to setupStatusFader.
   1.635 +                            if (this.timeUpdateCount <= 2)
   1.636 +                                this.setupStatusFader();
   1.637 +
   1.638 +                            // If the user is dragging the scrubber ignore the delayed seek
   1.639 +                            // responses (don't yank the thumb away from the user)
   1.640 +                            if (this.scrubber.isDragging)
   1.641 +                                return;
   1.642 +
   1.643 +                            this.showPosition(currentTime, duration);
   1.644 +                            break;
   1.645 +                        case "emptied":
   1.646 +                            this.bufferBar.value = 0;
   1.647 +                            this.showPosition(0, 0);
   1.648 +                            break;
   1.649 +                        case "seeking":
   1.650 +                            this.showBuffered();
   1.651 +                            this.statusIcon.setAttribute("type", "throbber");
   1.652 +                            this.setupStatusFader();
   1.653 +                            break;
   1.654 +                        case "waiting":
   1.655 +                            this.statusIcon.setAttribute("type", "throbber");
   1.656 +                            this.setupStatusFader();
   1.657 +                            break;
   1.658 +                        case "seeked":
   1.659 +                        case "playing":
   1.660 +                        case "canplay":
   1.661 +                        case "canplaythrough":
   1.662 +                            this.setupStatusFader();
   1.663 +                            break;
   1.664 +                        case "error":
   1.665 +                            // We'll show the error status icon when we receive an error event
   1.666 +                            // under either of the following conditions:
   1.667 +                            // 1. The video has its error attribute set; this means we're loading
   1.668 +                            //    from our src attribute, and the load failed, or we we're loading
   1.669 +                            //    from source children and the decode or playback failed after we
   1.670 +                            //    determined our selected resource was playable.
   1.671 +                            // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
   1.672 +                            //    loading from child source elements, but we were unable to select
   1.673 +                            //    any of the child elements for playback during resource selection.
   1.674 +                            if (this.hasError()) {
   1.675 +                                this.suppressError = false;
   1.676 +                                this.clickToPlay.hidden = true;
   1.677 +                                this.statusIcon.setAttribute("type", "error");
   1.678 +                                this.updateErrorText();
   1.679 +                                this.setupStatusFader(true);
   1.680 +                                // If video hasn't shown anything yet, disable the controls.
   1.681 +                                if (!this.firstFrameShown)
   1.682 +                                    this.startFadeOut(this.controlBar);
   1.683 +                                this.controlsSpacer.removeAttribute("hideCursor");
   1.684 +                            }
   1.685 +                            break;
   1.686 +                        case "mozinterruptbegin":
   1.687 +                        case "mozinterruptend":
   1.688 +                            // Nothing to do...
   1.689 +                            break;
   1.690 +                        default:
   1.691 +                            this.log("!!! event " + aEvent.type + " not handled!");
   1.692 +                    }
   1.693 +                },
   1.694 +
   1.695 +                terminateEventListeners : function () {
   1.696 +                    if (this.statsInterval) {
   1.697 +                        clearInterval(this.statsInterval);
   1.698 +                        this.statsInterval = null;
   1.699 +                    }
   1.700 +                    for each (let event in this.videoEvents)
   1.701 +                        this.video.removeEventListener(event, this, false);
   1.702 +
   1.703 +                    for each(let element in this.controlListeners)
   1.704 +                        element.item.removeEventListener(element.event, element.func, false);
   1.705 +
   1.706 +                    delete this.controlListeners;
   1.707 +
   1.708 +                    this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false);
   1.709 +                    delete this._handleCustomEventsBound;
   1.710 +
   1.711 +                    this.log("--- videocontrols terminated ---");
   1.712 +                },
   1.713 +
   1.714 +                hasError : function () {
   1.715 +                    return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
   1.716 +                },
   1.717 +
   1.718 +                updateErrorText : function () {
   1.719 +                    let error;
   1.720 +                    let v = this.video;
   1.721 +                    // It is possible to have both v.networkState == NETWORK_NO_SOURCE
   1.722 +                    // as well as v.error being non-null. In this case, we will show
   1.723 +                    // the v.error.code instead of the v.networkState error.
   1.724 +                    if (v.error) {
   1.725 +                        switch (v.error.code) {
   1.726 +                          case v.error.MEDIA_ERR_ABORTED:
   1.727 +                              error = "errorAborted";
   1.728 +                              break;
   1.729 +                          case v.error.MEDIA_ERR_NETWORK:
   1.730 +                              error = "errorNetwork";
   1.731 +                              break;
   1.732 +                          case v.error.MEDIA_ERR_DECODE:
   1.733 +                              error = "errorDecode";
   1.734 +                              break;
   1.735 +                          case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
   1.736 +                              error = "errorSrcNotSupported";
   1.737 +                              break;
   1.738 +                          default:
   1.739 +                              error = "errorGeneric";
   1.740 +                              break;
   1.741 +                         }
   1.742 +                    } else if (v.networkState == v.NETWORK_NO_SOURCE) {
   1.743 +                        error = "errorNoSource";
   1.744 +                    } else {
   1.745 +                        return; // No error found.
   1.746 +                    }
   1.747 +
   1.748 +                    let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
   1.749 +                    this.controlsSpacer.setAttribute("aria-label", label.textContent);
   1.750 +                    this.statusOverlay.setAttribute("error", error);
   1.751 +                },
   1.752 +
   1.753 +                formatTime : function(aTime) {
   1.754 +                    // Format the duration as "h:mm:ss" or "m:ss"
   1.755 +                    aTime = Math.round(aTime / 1000);
   1.756 +                    let hours = Math.floor(aTime / 3600);
   1.757 +                    let mins  = Math.floor((aTime % 3600) / 60);
   1.758 +                    let secs  = Math.floor(aTime % 60);
   1.759 +                    let timeString;
   1.760 +                    if (secs < 10)
   1.761 +                        secs = "0" + secs;
   1.762 +                    if (hours) {
   1.763 +                        if (mins < 10)
   1.764 +                            mins = "0" + mins;
   1.765 +                        timeString = hours + ":" + mins + ":" + secs;
   1.766 +                    } else {
   1.767 +                        timeString = mins + ":" + secs;
   1.768 +                    }
   1.769 +                    return timeString;
   1.770 +                },
   1.771 +
   1.772 +                showDuration : function (duration) {
   1.773 +                    let isInfinite = (duration == Infinity);
   1.774 +                    this.log("Duration is " + duration + "ms.\n");
   1.775 +
   1.776 +                    if (isNaN(duration) || isInfinite)
   1.777 +                        duration = this.maxCurrentTimeSeen;
   1.778 +
   1.779 +                    // Format the duration as "h:mm:ss" or "m:ss"
   1.780 +                    let timeString = isInfinite ? "" : this.formatTime(duration);
   1.781 +                    this.durationLabel.setAttribute("value", timeString);
   1.782 +
   1.783 +                    // "durationValue" property is used by scale binding to
   1.784 +                    // generate accessible name.
   1.785 +                    this.scrubber.durationValue = timeString;
   1.786 +
   1.787 +                    // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
   1.788 +                    this.scrubberThumb.showHours = (duration >= 3600000);
   1.789 +
   1.790 +                    this.scrubber.max = duration;
   1.791 +                    // XXX Can't set increment here, due to bug 473103. Also, doing so causes
   1.792 +                    // snapping when dragging with the mouse, so we can't just set a value for
   1.793 +                    // the arrow-keys.
   1.794 +                    //this.scrubber.increment = duration / 50;
   1.795 +                    this.scrubber.pageIncrement = Math.round(duration / 10);
   1.796 +                },
   1.797 +
   1.798 +                seekToPosition : function(newPosition) {
   1.799 +                    newPosition /= 1000; // convert from ms
   1.800 +                    this.log("+++ seeking to " + newPosition);
   1.801 +#ifdef MOZ_WIDGET_GONK
   1.802 +                    // We use fastSeek() on B2G, and an accurate (but slower)
   1.803 +                    // seek on other platforms (that are likely to be higher
   1.804 +                    // perf).
   1.805 +                    this.video.fastSeek(newPosition);
   1.806 +#else
   1.807 +                    this.video.currentTime = newPosition;
   1.808 +#endif
   1.809 +                },
   1.810 +
   1.811 +                setVolume : function(newVolume) {
   1.812 +                    this.log("*** setting volume to " + newVolume);
   1.813 +                    this.video.volume = newVolume;
   1.814 +                    this.video.muted = false;
   1.815 +                },
   1.816 +
   1.817 +                showPosition : function(currentTime, duration) {
   1.818 +                    // If the duration is unknown (because the server didn't provide
   1.819 +                    // it, or the video is a stream), then we want to fudge the duration
   1.820 +                    // by using the maximum playback position that's been seen.
   1.821 +                    if (currentTime > this.maxCurrentTimeSeen)
   1.822 +                        this.maxCurrentTimeSeen = currentTime;
   1.823 +                    this.showDuration(duration);
   1.824 +
   1.825 +                    this.log("time update @ " + currentTime + "ms of " + duration + "ms");
   1.826 +
   1.827 +                    this.positionLabel.setAttribute("value", this.formatTime(currentTime));
   1.828 +                    this.scrubber.value = currentTime;
   1.829 +                },
   1.830 +
   1.831 +                showBuffered : function() {
   1.832 +                    function bsearch(haystack, needle, cmp) {
   1.833 +                        var length = haystack.length;
   1.834 +                        var low = 0;
   1.835 +                        var high = length;
   1.836 +                        while (low < high) {
   1.837 +                            var probe = low + ((high - low) >> 1);
   1.838 +                            var r = cmp(haystack, probe, needle);
   1.839 +                            if (r == 0) {
   1.840 +                                return probe;
   1.841 +                            } else if (r > 0) {
   1.842 +                                low = probe + 1;
   1.843 +                            } else {
   1.844 +                                high = probe;
   1.845 +                            }
   1.846 +                        }
   1.847 +                        return -1;
   1.848 +                    }
   1.849 +
   1.850 +                    function bufferedCompare(buffered, i, time) {
   1.851 +                        if (time > buffered.end(i)) {
   1.852 +                            return 1;
   1.853 +                        } else if (time >= buffered.start(i)) {
   1.854 +                            return 0;
   1.855 +                        }
   1.856 +                        return -1;
   1.857 +                    }
   1.858 +
   1.859 +                    var duration = Math.round(this.video.duration * 1000);
   1.860 +                    if (isNaN(duration))
   1.861 +                        duration = this.maxCurrentTimeSeen;
   1.862 +
   1.863 +                    // Find the range that the current play position is in and use that
   1.864 +                    // range for bufferBar.  At some point we may support multiple ranges
   1.865 +                    // displayed in the bar.
   1.866 +                    var currentTime = this.video.currentTime;
   1.867 +                    var buffered = this.video.buffered;
   1.868 +                    var index = bsearch(buffered, currentTime, bufferedCompare);
   1.869 +                    var endTime = 0;
   1.870 +                    if (index >= 0) {
   1.871 +                        endTime = Math.round(buffered.end(index) * 1000);
   1.872 +                    }
   1.873 +                    this.bufferBar.max = duration;
   1.874 +                    this.bufferBar.value = endTime;
   1.875 +                },
   1.876 +
   1.877 +                _controlsHiddenByTimeout : false,
   1.878 +                _showControlsTimeout : 0,
   1.879 +                SHOW_CONTROLS_TIMEOUT_MS: 500,
   1.880 +                _showControlsFn : function () {
   1.881 +                    if (Utils.video.mozMatchesSelector("video:hover")) {
   1.882 +                        Utils.startFadeIn(Utils.controlBar, false);
   1.883 +                        Utils._showControlsTimeout = 0;
   1.884 +                        Utils._controlsHiddenByTimeout = false;
   1.885 +                    }
   1.886 +                },
   1.887 +
   1.888 +                _hideControlsTimeout : 0,
   1.889 +                _hideControlsFn : function () {
   1.890 +                    if (!Utils.scrubber.isDragging) {
   1.891 +                        Utils.startFade(Utils.controlBar, false);
   1.892 +                        Utils._hideControlsTimeout = 0;
   1.893 +                        Utils._controlsHiddenByTimeout = true;
   1.894 +                    }
   1.895 +                },
   1.896 +                HIDE_CONTROLS_TIMEOUT_MS : 2000,
   1.897 +                onMouseMove : function (event) {
   1.898 +                    // If the controls are static, don't change anything.
   1.899 +                    if (!this.dynamicControls)
   1.900 +                        return;
   1.901 +
   1.902 +                    clearTimeout(this._hideControlsTimeout);
   1.903 +
   1.904 +                    // Suppress fading out the controls until the video has rendered
   1.905 +                    // its first frame. But since autoplay videos start off with no
   1.906 +                    // controls, let them fade-out so the controls don't get stuck on.
   1.907 +                    if (!this.firstFrameShown &&
   1.908 +                        !(this.video.autoplay && this.video.mozAutoplayEnabled))
   1.909 +                        return;
   1.910 +
   1.911 +                    if (this._controlsHiddenByTimeout)
   1.912 +                        this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
   1.913 +                    else
   1.914 +                        this.startFade(this.controlBar, true);
   1.915 +
   1.916 +                    // Hide the controls if the mouse cursor is left on top of the video
   1.917 +                    // but above the control bar and if the click-to-play overlay is hidden.
   1.918 +                    if ((this._controlsHiddenByTimeout ||
   1.919 +                         event.clientY < this.controlBar.getBoundingClientRect().top) &&
   1.920 +                        this.clickToPlay.hidden) {
   1.921 +                        this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
   1.922 +                    }
   1.923 +                },
   1.924 +
   1.925 +                onMouseInOut : function (event) {
   1.926 +                    // If the controls are static, don't change anything.
   1.927 +                    if (!this.dynamicControls)
   1.928 +                        return;
   1.929 +
   1.930 +                    clearTimeout(this._hideControlsTimeout);
   1.931 +
   1.932 +                    // Ignore events caused by transitions between child nodes.
   1.933 +                    // Note that the videocontrols element is the same
   1.934 +                    // size as the *content area* of the video element,
   1.935 +                    // but this is not the same as the video element's
   1.936 +                    // border area if the video has border or padding.
   1.937 +                    if (this.isEventWithin(event, this.videocontrols))
   1.938 +                        return;
   1.939 +
   1.940 +                    var isMouseOver = (event.type == "mouseover");
   1.941 +
   1.942 +                    var isMouseInControls = event.clientY > this.controlBar.getBoundingClientRect().top &&
   1.943 +                                            event.clientY < this.controlBar.getBoundingClientRect().bottom;
   1.944 +
   1.945 +                    // Suppress fading out the controls until the video has rendered
   1.946 +                    // its first frame. But since autoplay videos start off with no
   1.947 +                    // controls, let them fade-out so the controls don't get stuck on.
   1.948 +                    if (!this.firstFrameShown && !isMouseOver &&
   1.949 +                        !(this.video.autoplay && this.video.mozAutoplayEnabled))
   1.950 +                        return;
   1.951 +
   1.952 +                    if (!isMouseOver && !isMouseInControls) {
   1.953 +                        this.adjustControlSize();
   1.954 +
   1.955 +                        // Keep the controls visible if the click-to-play is visible.
   1.956 +                        if (!this.clickToPlay.hidden)
   1.957 +                            return;
   1.958 +
   1.959 +                        this.startFadeOut(this.controlBar, false);
   1.960 +                        clearTimeout(this._showControlsTimeout);
   1.961 +                        Utils._controlsHiddenByTimeout = false;
   1.962 +                    }
   1.963 +                },
   1.964 +
   1.965 +                startFadeIn : function (element, immediate) {
   1.966 +                    this.startFade(element, true, immediate);
   1.967 +                },
   1.968 +
   1.969 +                startFadeOut : function (element, immediate) {
   1.970 +                    this.startFade(element, false, immediate);
   1.971 +                },
   1.972 +
   1.973 +                startFade : function (element, fadeIn, immediate) {
   1.974 +                    if (element.classList.contains("controlBar") && fadeIn) {
   1.975 +                        // Bug 493523, the scrubber doesn't call valueChanged while hidden,
   1.976 +                        // so our dependent state (eg, timestamp in the thumb) will be stale.
   1.977 +                        // As a workaround, update it manually when it first becomes unhidden.
   1.978 +                        if (element.hidden)
   1.979 +                            this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
   1.980 +                    }
   1.981 +
   1.982 +                    if (immediate)
   1.983 +                        element.setAttribute("immediate", true);
   1.984 +                    else
   1.985 +                        element.removeAttribute("immediate");
   1.986 +
   1.987 +                    if (fadeIn) {
   1.988 +                        element.hidden = false;
   1.989 +                        // force style resolution, so that transition begins
   1.990 +                        // when we remove the attribute.
   1.991 +                        element.clientTop;
   1.992 +                        element.removeAttribute("fadeout");
   1.993 +                        if (element.classList.contains("controlBar"))
   1.994 +                            this.controlsSpacer.removeAttribute("hideCursor");
   1.995 +                    } else {
   1.996 +                        element.setAttribute("fadeout", true);
   1.997 +                        if (element.classList.contains("controlBar") && !this.hasError() &&
   1.998 +                            document.mozFullScreenElement == this.video)
   1.999 +                            this.controlsSpacer.setAttribute("hideCursor", true);
  1.1000 +
  1.1001 +                    }
  1.1002 +                },
  1.1003 +
  1.1004 +                onTransitionEnd : function (event) {
  1.1005 +                    // Ignore events for things other than opacity changes.
  1.1006 +                    if (event.propertyName != "opacity")
  1.1007 +                        return;
  1.1008 +
  1.1009 +                    var element = event.originalTarget;
  1.1010 +
  1.1011 +                    // Nothing to do when a fade *in* finishes.
  1.1012 +                    if (!element.hasAttribute("fadeout"))
  1.1013 +                        return;
  1.1014 +
  1.1015 +                    element.hidden = true;
  1.1016 +                },
  1.1017 +
  1.1018 +                _triggeredByControls: false,
  1.1019 +
  1.1020 +                togglePause : function () {
  1.1021 +                    if (this.video.paused || this.video.ended) {
  1.1022 +                        this._triggeredByControls = true;
  1.1023 +                        this.hideClickToPlay();
  1.1024 +                        this.video.playbackRate = this.video.defaultPlaybackRate;
  1.1025 +                        this.video.play();
  1.1026 +                    } else {
  1.1027 +                        this.video.pause();
  1.1028 +                    }
  1.1029 +
  1.1030 +                    // We'll handle style changes in the event listener for
  1.1031 +                    // the "play" and "pause" events, same as if content
  1.1032 +                    // script was controlling video playback.
  1.1033 +                },
  1.1034 +
  1.1035 +                isVideoWithoutAudioTrack : function() {
  1.1036 +                  return this.video.readyState >= this.video.HAVE_METADATA &&
  1.1037 +                         !this.isAudioOnly &&
  1.1038 +                         !this.video.mozHasAudio;
  1.1039 +                },
  1.1040 +
  1.1041 +                toggleMute : function () {
  1.1042 +                    if (this.isVideoWithoutAudioTrack()) {
  1.1043 +                        return;
  1.1044 +                    }
  1.1045 +                    this.video.muted = !this.video.muted;
  1.1046 +
  1.1047 +                    // We'll handle style changes in the event listener for
  1.1048 +                    // the "volumechange" event, same as if content script was
  1.1049 +                    // controlling volume.
  1.1050 +                },
  1.1051 +
  1.1052 +                isVideoInFullScreen : function () {
  1.1053 +                    return document.mozFullScreenElement == this.video;
  1.1054 +                },
  1.1055 +
  1.1056 +                toggleFullscreen : function () {
  1.1057 +                    this.isVideoInFullScreen() ?
  1.1058 +                        document.mozCancelFullScreen() :
  1.1059 +                        this.video.mozRequestFullScreen();
  1.1060 +                },
  1.1061 +
  1.1062 +                setFullscreenButtonState : function () {
  1.1063 +                    if (this.isAudioOnly || !document.mozFullScreenEnabled) {
  1.1064 +                        this.fullscreenButton.hidden = true;
  1.1065 +                        return;
  1.1066 +                    }
  1.1067 +
  1.1068 +                    var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
  1.1069 +                    var value = this.fullscreenButton.getAttribute(attrName);
  1.1070 +                    this.fullscreenButton.setAttribute("aria-label", value);
  1.1071 +
  1.1072 +                    if (this.isVideoInFullScreen())
  1.1073 +                        this.fullscreenButton.setAttribute("fullscreened", "true");
  1.1074 +                    else
  1.1075 +                        this.fullscreenButton.removeAttribute("fullscreened");
  1.1076 +                },
  1.1077 +
  1.1078 +                onFullscreenChange: function () {
  1.1079 +                    if (this.isVideoInFullScreen()) {
  1.1080 +                        Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
  1.1081 +                    }
  1.1082 +                    this.setFullscreenButtonState();
  1.1083 +                },
  1.1084 +
  1.1085 +                clickToPlayClickHandler : function(e) {
  1.1086 +                    if (e.button != 0)
  1.1087 +                        return;
  1.1088 +                    if (this.hasError() && !this.suppressError) {
  1.1089 +                        // Errors that can be dismissed should be placed here as we discover them.
  1.1090 +                        if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED)
  1.1091 +                            return;
  1.1092 +                        this.statusOverlay.hidden = true;
  1.1093 +                        this.suppressError = true;
  1.1094 +                        return;
  1.1095 +                    }
  1.1096 +
  1.1097 +                    // Read defaultPrevented asynchronously, since Web content
  1.1098 +                    // may want to consume the "click" event but will only
  1.1099 +                    // receive it after us.
  1.1100 +                    let self = this;
  1.1101 +                    setTimeout(function clickToPlayCallback() {
  1.1102 +                        if (!e.defaultPrevented)
  1.1103 +                            self.togglePause();
  1.1104 +                    }, 0);
  1.1105 +                },
  1.1106 +                hideClickToPlay : function () {
  1.1107 +                    let videoHeight = this.video.clientHeight;
  1.1108 +                    let videoWidth = this.video.clientWidth;
  1.1109 +
  1.1110 +                    // The play button will animate to 3x its size. This
  1.1111 +                    // shows the animation unless the video is too small
  1.1112 +                    // to show 2/3 of the animation.
  1.1113 +                    let animationScale = 2;
  1.1114 +                    if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)||
  1.1115 +                        this._overlayPlayButtonWidth * animationScale > videoWidth) {
  1.1116 +                        this.clickToPlay.setAttribute("immediate", "true");
  1.1117 +                        this.clickToPlay.hidden = true;
  1.1118 +                    } else {
  1.1119 +                        this.clickToPlay.removeAttribute("immediate");
  1.1120 +                    }
  1.1121 +                    this.clickToPlay.setAttribute("fadeout", "true");
  1.1122 +                },
  1.1123 +
  1.1124 +                setPlayButtonState : function(aPaused) {
  1.1125 +                  if (aPaused)
  1.1126 +                      this.playButton.setAttribute("paused", "true");
  1.1127 +                  else
  1.1128 +                      this.playButton.removeAttribute("paused");
  1.1129 +
  1.1130 +                  var attrName = aPaused ? "playlabel" : "pauselabel";
  1.1131 +                  var value = this.playButton.getAttribute(attrName);
  1.1132 +                  this.playButton.setAttribute("aria-label", value);
  1.1133 +                },
  1.1134 +
  1.1135 +                setMuteButtonState : function(aMuted) {
  1.1136 +                  if (aMuted)
  1.1137 +                      this.muteButton.setAttribute("muted", "true");
  1.1138 +                  else
  1.1139 +                      this.muteButton.removeAttribute("muted");
  1.1140 +
  1.1141 +                  var attrName = aMuted ? "unmutelabel" : "mutelabel";
  1.1142 +                  var value = this.muteButton.getAttribute(attrName);
  1.1143 +                  this.muteButton.setAttribute("aria-label", value);
  1.1144 +                },
  1.1145 +
  1.1146 +                _getComputedPropertyValueAsInt : function(element, property) {
  1.1147 +                  let value = window.getComputedStyle(element, null).getPropertyValue(property);
  1.1148 +                  return parseInt(value, 10);
  1.1149 +                },
  1.1150 +
  1.1151 +                STATS_INTERVAL_MS : 500,
  1.1152 +                statsInterval : null,
  1.1153 +
  1.1154 +                showStatistics : function(shouldShow) {
  1.1155 +                    if (this.statsInterval) {
  1.1156 +                        clearInterval(this.statsInterval);
  1.1157 +                        this.statsInterval = null;
  1.1158 +                    }
  1.1159 +
  1.1160 +                    if (shouldShow) {
  1.1161 +                        this.video.mozMediaStatisticsShowing = true;
  1.1162 +                        this.statsOverlay.hidden = false;
  1.1163 +                        this.statsInterval = setInterval(this.updateStats.bind(this), this.STATS_INTERVAL_MS);
  1.1164 +                        this.updateStats();
  1.1165 +                    } else {
  1.1166 +                        this.video.mozMediaStatisticsShowing = false;
  1.1167 +                        this.statsOverlay.hidden = true;
  1.1168 +                    }
  1.1169 +                },
  1.1170 +
  1.1171 +                updateStats : function() {
  1.1172 +                    if (this.videocontrols.randomID != this.randomID) {
  1.1173 +                        this.terminateEventListeners();
  1.1174 +                        return;
  1.1175 +                    }
  1.1176 +
  1.1177 +                    let v = this.video;
  1.1178 +                    let s = this.stats;
  1.1179 +
  1.1180 +                    let src = v.currentSrc || v.src || "(no source found)";
  1.1181 +                    let srcParts = src.split('/');
  1.1182 +                    let srcIdx = srcParts.length - 1;
  1.1183 +                    if (src.lastIndexOf('/') == src.length - 1)
  1.1184 +                        srcIdx--;
  1.1185 +                    s.filename.textContent = decodeURI(srcParts[srcIdx]);
  1.1186 +
  1.1187 +                    let size = v.videoWidth + "x" + v.videoHeight;
  1.1188 +                    if (this._getComputedPropertyValueAsInt(this.video, "width") != v.videoWidth || this._getComputedPropertyValueAsInt(this.video, "height") != v.videoHeight)
  1.1189 +                        size += " scaled to " + this._getComputedPropertyValueAsInt(this.video, "width") + "x" + this._getComputedPropertyValueAsInt(this.video, "height");
  1.1190 +                    s.size.textContent = size;
  1.1191 +
  1.1192 +                    let activity;
  1.1193 +                    if (v.paused)
  1.1194 +                        activity = "paused";
  1.1195 +                    else
  1.1196 +                        activity = "playing";
  1.1197 +                    if (v.ended)
  1.1198 +                        activity = "ended";
  1.1199 +                    if (s.activity.getAttribute("activity") != activity)
  1.1200 +                        s.activity.setAttribute("activity", activity);
  1.1201 +                    if (v.seeking && !s.activity.hasAttribute("seeking"))
  1.1202 +                        s.activity.setAttribute("seeking", true);
  1.1203 +                    else if (s.activity.hasAttribute("seeking"))
  1.1204 +                        s.activity.removeAttribute("seeking");
  1.1205 +
  1.1206 +                    let readyState = v.readyState;
  1.1207 +                    switch (readyState) {
  1.1208 +                        case v.HAVE_NOTHING:      readyState = "HAVE_NOTHING";      break;
  1.1209 +                        case v.HAVE_METADATA:     readyState = "HAVE_METADATA";     break;
  1.1210 +                        case v.HAVE_CURRENT_DATA: readyState = "HAVE_CURRENT_DATA"; break;
  1.1211 +                        case v.HAVE_FUTURE_DATA:  readyState = "HAVE_FUTURE_DATA";  break;
  1.1212 +                        case v.HAVE_ENOUGH_DATA:  readyState = "HAVE_ENOUGH_DATA";  break;
  1.1213 +                    }
  1.1214 +                    s.readyState.textContent = readyState;
  1.1215 +
  1.1216 +                    let networkState = v.networkState;
  1.1217 +                    switch (networkState) {
  1.1218 +                        case v.NETWORK_EMPTY:     networkState = "NETWORK_EMPTY";     break;
  1.1219 +                        case v.NETWORK_IDLE:      networkState = "NETWORK_IDLE";      break;
  1.1220 +                        case v.NETWORK_LOADING:   networkState = "NETWORK_LOADING";   break;
  1.1221 +                        case v.NETWORK_NO_SOURCE: networkState = "NETWORK_NO_SOURCE"; break;
  1.1222 +                    }
  1.1223 +                    s.netState.textContent = networkState;
  1.1224 +
  1.1225 +                    s.framesParsed.textContent = v.mozParsedFrames;
  1.1226 +                    s.framesDecoded.textContent = v.mozDecodedFrames;
  1.1227 +                    s.framesPresented.textContent = v.mozPresentedFrames;
  1.1228 +                    s.framesPainted.textContent = v.mozPaintedFrames;
  1.1229 +
  1.1230 +                    let volume = Math.round(v.volume * 100) + "%";
  1.1231 +                    if (v.muted)
  1.1232 +                        volume += " (muted)";
  1.1233 +                    s.volume.textContent = volume;
  1.1234 +                },
  1.1235 +
  1.1236 +                keyHandler : function(event) {
  1.1237 +                    // Ignore keys when content might be providing its own.
  1.1238 +                    if (!this.video.hasAttribute("controls"))
  1.1239 +                        return;
  1.1240 +
  1.1241 +                    var keystroke = "";
  1.1242 +                    if (event.altKey)
  1.1243 +                        keystroke += "alt-";
  1.1244 +                    if (event.shiftKey)
  1.1245 +                        keystroke += "shift-";
  1.1246 +#ifdef XP_MACOSX
  1.1247 +                    if (event.metaKey)
  1.1248 +                        keystroke += "accel-";
  1.1249 +                    if (event.ctrlKey)
  1.1250 +                        keystroke += "control-";
  1.1251 +#else
  1.1252 +                    if (event.metaKey)
  1.1253 +                        keystroke += "meta-";
  1.1254 +                    if (event.ctrlKey)
  1.1255 +                        keystroke += "accel-";
  1.1256 +#endif
  1.1257 +
  1.1258 +                    switch (event.keyCode) {
  1.1259 +                        case KeyEvent.DOM_VK_UP:
  1.1260 +                            keystroke += "upArrow";
  1.1261 +                            break;
  1.1262 +                        case KeyEvent.DOM_VK_DOWN:
  1.1263 +                            keystroke += "downArrow";
  1.1264 +                            break;
  1.1265 +                        case KeyEvent.DOM_VK_LEFT:
  1.1266 +                            keystroke += "leftArrow";
  1.1267 +                            break;
  1.1268 +                        case KeyEvent.DOM_VK_RIGHT:
  1.1269 +                            keystroke += "rightArrow";
  1.1270 +                            break;
  1.1271 +                        case KeyEvent.DOM_VK_HOME:
  1.1272 +                            keystroke += "home";
  1.1273 +                            break;
  1.1274 +                        case KeyEvent.DOM_VK_END:
  1.1275 +                            keystroke += "end";
  1.1276 +                            break;
  1.1277 +                    }
  1.1278 +
  1.1279 +                    if (String.fromCharCode(event.charCode) == ' ')
  1.1280 +                        keystroke += "space";
  1.1281 +
  1.1282 +                    this.log("Got keystroke: " + keystroke);
  1.1283 +                    var oldval, newval;
  1.1284 +
  1.1285 +                    try {
  1.1286 +                        switch (keystroke) {
  1.1287 +                            case "space": /* Play */
  1.1288 +                                this.togglePause();
  1.1289 +                                break;
  1.1290 +                            case "downArrow": /* Volume decrease */
  1.1291 +                                oldval = this.video.volume;
  1.1292 +                                this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
  1.1293 +                                this.video.muted = false;
  1.1294 +                                break;
  1.1295 +                            case "upArrow": /* Volume increase */
  1.1296 +                                oldval = this.video.volume;
  1.1297 +                                this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
  1.1298 +                                this.video.muted = false;
  1.1299 +                                break;
  1.1300 +                            case "accel-downArrow": /* Mute */
  1.1301 +                                this.video.muted = true;
  1.1302 +                                break;
  1.1303 +                            case "accel-upArrow": /* Unmute */
  1.1304 +                                this.video.muted = false;
  1.1305 +                                break;
  1.1306 +                            case "leftArrow": /* Seek back 15 seconds */
  1.1307 +                            case "accel-leftArrow": /* Seek back 10% */
  1.1308 +                                oldval = this.video.currentTime;
  1.1309 +                                if (keystroke == "leftArrow")
  1.1310 +                                    newval = oldval - 15;
  1.1311 +                                else
  1.1312 +                                    newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
  1.1313 +                                this.video.currentTime = (newval >= 0 ? newval : 0);
  1.1314 +                                break;
  1.1315 +                            case "rightArrow": /* Seek forward 15 seconds */
  1.1316 +                            case "accel-rightArrow": /* Seek forward 10% */
  1.1317 +                                oldval = this.video.currentTime;
  1.1318 +                                var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
  1.1319 +                                if (keystroke == "rightArrow")
  1.1320 +                                    newval = oldval + 15;
  1.1321 +                                else
  1.1322 +                                    newval = oldval + maxtime / 10;
  1.1323 +                                this.video.currentTime = (newval <= maxtime ? newval : maxtime);
  1.1324 +                                break;
  1.1325 +                            case "home": /* Seek to beginning */
  1.1326 +                                this.video.currentTime = 0;
  1.1327 +                                break;
  1.1328 +                            case "end": /* Seek to end */
  1.1329 +                                if (this.video.currentTime != this.video.duration)
  1.1330 +                                    this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
  1.1331 +                                break;
  1.1332 +                            default:
  1.1333 +                                return;
  1.1334 +                        }
  1.1335 +                    } catch(e) { /* ignore any exception from setting .currentTime */ }
  1.1336 +
  1.1337 +                    event.preventDefault(); // Prevent page scrolling
  1.1338 +                },
  1.1339 +
  1.1340 +                isEventWithin : function (event, parent1, parent2) {
  1.1341 +                    function isDescendant (node) {
  1.1342 +                        while (node) {
  1.1343 +                            if (node == parent1 || node == parent2)
  1.1344 +                                return true;
  1.1345 +                            node = node.parentNode;
  1.1346 +                        }
  1.1347 +                        return false;
  1.1348 +                    }
  1.1349 +                    return isDescendant(event.target) && isDescendant(event.relatedTarget);
  1.1350 +                },
  1.1351 +
  1.1352 +                log : function (msg) {
  1.1353 +                    if (this.debug)
  1.1354 +                        dump("videoctl: " + msg + "\n");
  1.1355 +                },
  1.1356 +
  1.1357 +                get isTopLevelSyntheticDocument() {
  1.1358 +                  let doc = this.video.ownerDocument;
  1.1359 +                  let win = doc.defaultView;
  1.1360 +                  return doc.mozSyntheticDocument && win === win.top;
  1.1361 +                },
  1.1362 +
  1.1363 +                _playButtonWidth : 0,
  1.1364 +                _durationLabelWidth : 0,
  1.1365 +                _muteButtonWidth : 0,
  1.1366 +                _volumeControlWidth : 0,
  1.1367 +                _fullscreenButtonWidth : 0,
  1.1368 +                _controlBarHeight : 0,
  1.1369 +                _overlayPlayButtonHeight : 64,
  1.1370 +                _overlayPlayButtonWidth : 64,
  1.1371 +                _volumeStackMarginEnd : 8,
  1.1372 +                adjustControlSize : function adjustControlSize() {
  1.1373 +                    let doc = this.video.ownerDocument;
  1.1374 +
  1.1375 +                    // The scrubber has |flex=1|, therefore |minScrubberWidth|
  1.1376 +                    // was generated by empirical testing.
  1.1377 +                    let minScrubberWidth = 25;
  1.1378 +                    let minWidthAllControls = this._playButtonWidth +
  1.1379 +                                              minScrubberWidth +
  1.1380 +                                              this._durationLabelWidth +
  1.1381 +                                              this._muteButtonWidth +
  1.1382 +                                              this._volumeControlWidth +
  1.1383 +                                              this._fullscreenButtonWidth;
  1.1384 +
  1.1385 +                    let isAudioOnly = this.isAudioOnly;
  1.1386 +                    if (isAudioOnly) {
  1.1387 +                        // When the fullscreen button is hidden we add margin-end to the volume stack.
  1.1388 +                        minWidthAllControls -= this._fullscreenButtonWidth - this._volumeStackMarginEnd;
  1.1389 +                    }
  1.1390 +
  1.1391 +                    let minHeightForControlBar = this._controlBarHeight;
  1.1392 +                    let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;
  1.1393 +
  1.1394 +                    let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
  1.1395 +                    let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;
  1.1396 +
  1.1397 +                    if ((this._overlayPlayButtonHeight + this._controlBarHeight) > videoHeight ||
  1.1398 +                        this._overlayPlayButtonWidth > videoWidth) {
  1.1399 +                        this.clickToPlay.hidden = true;
  1.1400 +                    } else if (this.clickToPlay.hidden &&
  1.1401 +                               !this.video.played.length &&
  1.1402 +                               this.video.paused) {
  1.1403 +                        // Check this.video.paused to handle when a video is
  1.1404 +                        // playing but hasn't processed any frames yet
  1.1405 +                        this.clickToPlay.hidden = false;
  1.1406 +                    }
  1.1407 +
  1.1408 +                    let size = "normal";
  1.1409 +                    if (videoHeight < minHeightForControlBar)
  1.1410 +                        size = "hidden";
  1.1411 +                    else if (videoWidth < minWidthOnlyPlayPause)
  1.1412 +                        size = "hidden";
  1.1413 +                    else if (videoWidth < minWidthAllControls)
  1.1414 +                        size = "small";
  1.1415 +                    this.controlBar.setAttribute("size", size);
  1.1416 +                },
  1.1417 +
  1.1418 +                init : function (binding) {
  1.1419 +                    this.video = binding.parentNode;
  1.1420 +                    this.videocontrols = binding;
  1.1421 +
  1.1422 +                    this.statusIcon    = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
  1.1423 +                    this.controlBar    = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
  1.1424 +                    this.playButton    = document.getAnonymousElementByAttribute(binding, "class", "playButton");
  1.1425 +                    this.muteButton    = document.getAnonymousElementByAttribute(binding, "class", "muteButton");
  1.1426 +                    this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl");
  1.1427 +                    this.progressBar   = document.getAnonymousElementByAttribute(binding, "class", "progressBar");
  1.1428 +                    this.bufferBar     = document.getAnonymousElementByAttribute(binding, "class", "bufferBar");
  1.1429 +                    this.scrubber      = document.getAnonymousElementByAttribute(binding, "class", "scrubber");
  1.1430 +                    this.scrubberThumb = document.getAnonymousElementByAttribute(this.scrubber, "class", "scale-thumb");
  1.1431 +                    this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel");
  1.1432 +                    this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel");
  1.1433 +                    this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
  1.1434 +                    this.statsOverlay  = document.getAnonymousElementByAttribute(binding, "class", "statsOverlay");
  1.1435 +                    this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
  1.1436 +                    this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
  1.1437 +                    this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
  1.1438 +                    this.volumeForeground   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground");
  1.1439 +
  1.1440 +                    this.statsTable       = document.getAnonymousElementByAttribute(binding, "class", "statsTable");
  1.1441 +                    this.stats.filename   = document.getAnonymousElementByAttribute(binding, "class", "statFilename");
  1.1442 +                    this.stats.size       = document.getAnonymousElementByAttribute(binding, "class", "statSize");
  1.1443 +                    this.stats.activity   = document.getAnonymousElementByAttribute(binding, "class", "statActivity");
  1.1444 +                    this.stats.volume     = document.getAnonymousElementByAttribute(binding, "class", "statVolume");
  1.1445 +                    this.stats.readyState = document.getAnonymousElementByAttribute(binding, "class", "statReadyState");
  1.1446 +                    this.stats.netState   = document.getAnonymousElementByAttribute(binding, "class", "statNetState");
  1.1447 +                    this.stats.framesParsed    = document.getAnonymousElementByAttribute(binding, "class", "statFramesParsed");
  1.1448 +                    this.stats.framesDecoded   = document.getAnonymousElementByAttribute(binding, "class", "statFramesDecoded");
  1.1449 +                    this.stats.framesPresented = document.getAnonymousElementByAttribute(binding, "class", "statFramesPresented");
  1.1450 +                    this.stats.framesPainted   = document.getAnonymousElementByAttribute(binding, "class", "statFramesPainted");
  1.1451 +
  1.1452 +                    this.isAudioOnly = (this.video instanceof HTMLAudioElement);
  1.1453 +                    this.setupInitialState();
  1.1454 +                    this.setupNewLoadState();
  1.1455 +
  1.1456 +                    // Use the handleEvent() callback for all media events.
  1.1457 +                    // The "error" event listener must capture, so that it can trap error events
  1.1458 +                    // from the <source> children, which don't bubble.
  1.1459 +                    for each (let event in this.videoEvents)
  1.1460 +                        this.video.addEventListener(event, this, (event == "error") ? true : false);
  1.1461 +
  1.1462 +                    var self = this;
  1.1463 +
  1.1464 +                    this.controlListeners = [];
  1.1465 +
  1.1466 +                    // Helper function to add an event listener to the given element
  1.1467 +                    function addListener(elem, eventName, func) {
  1.1468 +                      let boundFunc = func.bind(self);
  1.1469 +                      self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
  1.1470 +                      elem.addEventListener(eventName, boundFunc, false);
  1.1471 +                    }
  1.1472 +
  1.1473 +                    addListener(this.muteButton, "command", this.toggleMute);
  1.1474 +                    addListener(this.playButton, "command", this.togglePause);
  1.1475 +                    addListener(this.fullscreenButton, "command", this.toggleFullscreen);
  1.1476 +                    addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
  1.1477 +                    addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
  1.1478 +                    addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
  1.1479 +
  1.1480 +                    addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
  1.1481 +                    addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
  1.1482 +                    addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
  1.1483 +                    addListener(this.video, "keypress", this.keyHandler);
  1.1484 +
  1.1485 +                    this.log("--- videocontrols initialized ---");
  1.1486 +                }
  1.1487 +            };
  1.1488 +            this.Utils.init(this);
  1.1489 +            ]]>
  1.1490 +        </constructor>
  1.1491 +        <destructor>
  1.1492 +            <![CDATA[
  1.1493 +            // randomID used to be a <field>, which meant that the XBL machinery
  1.1494 +            // undefined the property when the element was unbound. The code in
  1.1495 +            // this file actually depends on this, so now that randomID is an
  1.1496 +            // expando, we need to make sure to explicitly delete it.
  1.1497 +            delete this.randomID;
  1.1498 +            ]]>
  1.1499 +        </destructor>
  1.1500 +
  1.1501 +    </implementation>
  1.1502 +
  1.1503 +    <handlers>
  1.1504 +        <handler event="mouseover">
  1.1505 +            if (!this.isTouchControl)
  1.1506 +                this.Utils.onMouseInOut(event);
  1.1507 +        </handler>
  1.1508 +        <handler event="mouseout">
  1.1509 +            if (!this.isTouchControl)
  1.1510 +                this.Utils.onMouseInOut(event);
  1.1511 +        </handler>
  1.1512 +        <handler event="mousemove">
  1.1513 +            if (!this.isTouchControl)
  1.1514 +                this.Utils.onMouseMove(event);
  1.1515 +        </handler>
  1.1516 +    </handlers>
  1.1517 +  </binding>
  1.1518 +
  1.1519 +  <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
  1.1520 +
  1.1521 +    <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
  1.1522 +        <stack flex="1">
  1.1523 +            <vbox flex="1" class="statusOverlay" hidden="true">
  1.1524 +                <box class="statusIcon"/>
  1.1525 +                <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
  1.1526 +                <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
  1.1527 +                <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
  1.1528 +                <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
  1.1529 +                <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
  1.1530 +                <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
  1.1531 +            </vbox>
  1.1532 +
  1.1533 +            <vbox class="controlsOverlay">
  1.1534 +                <spacer class="controlsSpacer" flex="1"/>
  1.1535 +                <box flex="1" hidden="true">
  1.1536 +                    <box class="clickToPlay" hidden="true" flex="1"/>
  1.1537 +                </box>
  1.1538 +                <vbox class="controlBar" hidden="true">
  1.1539 +                    <hbox class="buttonsBar">
  1.1540 +                        <button class="castingButton" hidden="true"
  1.1541 +                                aria-label="&castingButton.castingLabel;"/>
  1.1542 +                        <button class="fullscreenButton"
  1.1543 +                            enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
  1.1544 +                            exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
  1.1545 +                        <spacer flex="1"/>
  1.1546 +                        <button class="playButton"
  1.1547 +                                playlabel="&playButton.playLabel;"
  1.1548 +                                pauselabel="&playButton.pauseLabel;"/>
  1.1549 +                        <spacer flex="1"/>
  1.1550 +                        <button class="muteButton"
  1.1551 +                                mutelabel="&muteButton.muteLabel;"
  1.1552 +                                unmutelabel="&muteButton.unmuteLabel;"/>
  1.1553 +                        <stack class="volumeStack">
  1.1554 +                          <box class="volumeBackground"/>
  1.1555 +                          <box class="volumeForeground" anonid="volumeForeground"/>
  1.1556 +                          <scale class="volumeControl" movetoclick="true"/>
  1.1557 +                        </stack>
  1.1558 +                      </hbox>
  1.1559 +                    <stack class="scrubberStack" flex="1">
  1.1560 +                        <box class="backgroundBar"/>
  1.1561 +                        <progressmeter class="bufferBar"/>
  1.1562 +                        <progressmeter class="progressBar" max="10000"/>
  1.1563 +                        <scale class="scrubber" movetoclick="true"/>
  1.1564 +                    </stack>
  1.1565 +                    <vbox class="durationBox">
  1.1566 +                        <label class="positionLabel" role="presentation"/>
  1.1567 +                        <label class="durationLabel" role="presentation"/>
  1.1568 +                    </vbox>
  1.1569 +                </vbox>
  1.1570 +            </vbox>
  1.1571 +        </stack>
  1.1572 +    </xbl:content>
  1.1573 +
  1.1574 +    <implementation>
  1.1575 +
  1.1576 +        <constructor>
  1.1577 +          <![CDATA[
  1.1578 +          this.isTouchControl = true;
  1.1579 +          this.TouchUtils = {
  1.1580 +            videocontrols: null,
  1.1581 +            video: null,
  1.1582 +            controlsTimer: null,
  1.1583 +            controlsTimeout: 5000,
  1.1584 +            positionLabel: null,
  1.1585 +            castingButton: null,
  1.1586 +
  1.1587 +            get Utils() {
  1.1588 +              return this.videocontrols.Utils;
  1.1589 +            },
  1.1590 +
  1.1591 +            get visible() {
  1.1592 +              return !this.Utils.controlBar.hasAttribute("fadeout") &&
  1.1593 +                     !(this.Utils.controlBar.getAttribute("hidden") == "true");
  1.1594 +            },
  1.1595 +
  1.1596 +            _firstShow: false,
  1.1597 +            get firstShow() { return this._firstShow; },
  1.1598 +            set firstShow(val) {
  1.1599 +              this._firstShow = val;
  1.1600 +              this.Utils.controlBar.setAttribute("firstshow", val);
  1.1601 +            },
  1.1602 +
  1.1603 +            toggleControls: function() {
  1.1604 +              if (!this.Utils.dynamicControls || !this.visible)
  1.1605 +                this.showControls();
  1.1606 +              else
  1.1607 +                this.delayHideControls(0);
  1.1608 +            },
  1.1609 +
  1.1610 +            showControls : function() {
  1.1611 +              if (this.Utils.dynamicControls) {
  1.1612 +                this.Utils.startFadeIn(this.Utils.controlBar);
  1.1613 +                this.delayHideControls(this.controlsTimeout);
  1.1614 +              }
  1.1615 +            },
  1.1616 +
  1.1617 +            clearTimer: function() {
  1.1618 +              if (this.controlsTimer) {
  1.1619 +                clearTimeout(this.controlsTimer);
  1.1620 +                this.controlsTimer = null;
  1.1621 +              }
  1.1622 +            },
  1.1623 +
  1.1624 +            delayHideControls : function(aTimeout) {
  1.1625 +              this.clearTimer();
  1.1626 +              let self = this;
  1.1627 +              this.controlsTimer = setTimeout(function() {
  1.1628 +                self.hideControls();
  1.1629 +              }, aTimeout);
  1.1630 +            },
  1.1631 +
  1.1632 +            hideControls : function() {
  1.1633 +              if (!this.Utils.dynamicControls)
  1.1634 +                  return;
  1.1635 +              this.Utils.startFadeOut(this.Utils.controlBar);
  1.1636 +              if (this.firstShow)
  1.1637 +                this.videocontrols.addEventListener("transitionend", this, false);
  1.1638 +            },
  1.1639 +
  1.1640 +            handleEvent : function (aEvent) {
  1.1641 +              if (aEvent.type == "transitionend") {
  1.1642 +                this.firstShow = false;
  1.1643 +                this.videocontrols.removeEventListener("transitionend", this, false);
  1.1644 +                return;
  1.1645 +              }
  1.1646 +
  1.1647 +              if (this.videocontrols.randomID != this.Utils.randomID)
  1.1648 +                this.terminateEventListeners();
  1.1649 +
  1.1650 +            },
  1.1651 +
  1.1652 +            terminateEventListeners : function () {
  1.1653 +              for each (var event in this.videoEvents)
  1.1654 +                this.Utils.video.removeEventListener(event, this, false);
  1.1655 +            },
  1.1656 +
  1.1657 +            isVideoCasting : function () {
  1.1658 +              if (this.video.mozIsCasting)
  1.1659 +                return true;
  1.1660 +              return false;
  1.1661 +            },
  1.1662 +
  1.1663 +            updateCasting : function (eventDetail) {
  1.1664 +              let castingData = JSON.parse(eventDetail);
  1.1665 +              if ("allow" in castingData) {
  1.1666 +                this.video.mozAllowCasting = !!castingData.allow;
  1.1667 +              }
  1.1668 +
  1.1669 +              if ("active" in castingData) {
  1.1670 +                this.video.mozIsCasting = !!castingData.active;
  1.1671 +              }
  1.1672 +              this.setCastButtonState();
  1.1673 +            },
  1.1674 +
  1.1675 +            startCasting : function () {
  1.1676 +              this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
  1.1677 +            },
  1.1678 +
  1.1679 +            setCastButtonState : function () {
  1.1680 +              if (this.isAudioOnly || !this.video.mozAllowCasting) {
  1.1681 +                this.castingButton.hidden = true;
  1.1682 +                return;
  1.1683 +              }
  1.1684 +
  1.1685 +              if (this.video.mozIsCasting) {
  1.1686 +                this.castingButton.setAttribute("active", "true");
  1.1687 +              } else {
  1.1688 +                this.castingButton.removeAttribute("active");
  1.1689 +              }
  1.1690 +
  1.1691 +              this.castingButton.hidden = false;
  1.1692 +            },
  1.1693 +
  1.1694 +            init : function (binding) {
  1.1695 +              this.videocontrols = binding;
  1.1696 +              this.video = binding.parentNode;
  1.1697 +
  1.1698 +              let self = this;
  1.1699 +              this.Utils.playButton.addEventListener("command", function() {
  1.1700 +                if (!self.video.paused)
  1.1701 +                    self.delayHideControls(0);
  1.1702 +                else
  1.1703 +                    self.showControls();
  1.1704 +              }, false);
  1.1705 +              this.Utils.scrubber.addEventListener("touchstart", function() {
  1.1706 +                self.clearTimer();
  1.1707 +              }, false);
  1.1708 +              this.Utils.scrubber.addEventListener("touchend", function() {
  1.1709 +                self.delayHideControls(self.controlsTimeout);
  1.1710 +              }, false);
  1.1711 +              this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
  1.1712 +
  1.1713 +              this.castingButton = document.getAnonymousElementByAttribute(binding, "class", "castingButton");
  1.1714 +              this.castingButton.addEventListener("command", function() {
  1.1715 +                self.startCasting();
  1.1716 +              }, false);
  1.1717 +
  1.1718 +              this.video.addEventListener("media-videoCasting", function (e) {
  1.1719 +                if (!e.isTrusted)
  1.1720 +                  return;
  1.1721 +                self.updateCasting(e.detail);
  1.1722 +              }, false, true);
  1.1723 +
  1.1724 +              // The first time the controls appear we want to just display
  1.1725 +              // a play button that does not fade away. The firstShow property
  1.1726 +              // makes that happen. But because of bug 718107 this init() method
  1.1727 +              // may be called again when we switch in or out of fullscreen
  1.1728 +              // mode. So we only set firstShow if we're not autoplaying and
  1.1729 +              // if we are at the beginning of the video and not already playing
  1.1730 +              if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
  1.1731 +                  this.video.currentTime === 0)
  1.1732 +                this.firstShow = true;
  1.1733 +
  1.1734 +              // If the video is not at the start, then we probably just
  1.1735 +              // transitioned into or out of fullscreen mode, and we don't want
  1.1736 +              // the controls to remain visible. this.controlsTimeout is a full
  1.1737 +              // 5s, which feels too long after the transition.
  1.1738 +              if (this.video.currentTime !== 0) {
  1.1739 +                this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
  1.1740 +              }
  1.1741 +            }
  1.1742 +          };
  1.1743 +          this.TouchUtils.init(this);
  1.1744 +          this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
  1.1745 +      ]]>
  1.1746 +      </constructor>
  1.1747 +      <destructor>
  1.1748 +          <![CDATA[
  1.1749 +          // XBL destructors don't appear to be inherited properly, so we need
  1.1750 +          // to do this here in addition to the videoControls destructor. :-(
  1.1751 +          delete this.randomID;
  1.1752 +          ]]>
  1.1753 +      </destructor>
  1.1754 +
  1.1755 +    </implementation>
  1.1756 +
  1.1757 +    <handlers>
  1.1758 +        <handler event="mouseup">
  1.1759 +          if(event.originalTarget.nodeName == "vbox") {
  1.1760 +            if (this.TouchUtils.firstShow)
  1.1761 +              this.Utils.video.play();
  1.1762 +            this.TouchUtils.toggleControls();
  1.1763 +          }
  1.1764 +        </handler>
  1.1765 +    </handlers>
  1.1766 +
  1.1767 +  </binding>
  1.1768 +</bindings>

mercurial