toolkit/content/widgets/videocontrols.xml

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 <?xml version="1.0"?>
     2 <!-- This Source Code Form is subject to the terms of the Mozilla Public
     3    - License, v. 2.0. If a copy of the MPL was not distributed with this
     4    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
     7 <!DOCTYPE bindings [
     8   <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
     9   %videocontrolsDTD;
    10 ]>
    12 <bindings id="videoControlBindings"
    13    xmlns="http://www.mozilla.org/xbl"
    14    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    15    xmlns:xbl="http://www.mozilla.org/xbl"
    16    xmlns:svg="http://www.w3.org/2000/svg"
    17    xmlns:html="http://www.w3.org/1999/xhtml">
    19   <binding id="timeThumb"
    20            extends="chrome://global/content/bindings/scale.xml#scalethumb">
    21       <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    22           <xbl:children/>
    23           <hbox class="timeThumb" xbl:inherits="showhours">
    24               <label class="timeLabel"/>
    25           </hbox>
    26       </xbl:content>
    27       <implementation>
    29       <constructor>
    30           <![CDATA[
    31           this.timeLabel = document.getAnonymousElementByAttribute(this, "class", "timeLabel");
    32           this.timeLabel.setAttribute("value", "0:00");
    33           ]]>
    34       </constructor>
    36       <property name="showHours">
    37         <getter>
    38           <![CDATA[
    39           return this.getAttribute("showhours") == "true";
    40           ]]>
    41         </getter>
    42         <setter>
    43           <![CDATA[
    44           this.setAttribute("showhours", val);
    45           // If the duration becomes known while we're still showing the value
    46           // for time=0, immediately update the value to show or hide the hours.
    47           // It's less intrusive to do it now than when the user clicks play and
    48           // is looking right next to the thumb.
    49           var displayedTime = this.timeLabel.getAttribute("value");
    50           if (val && displayedTime == "0:00")
    51               this.timeLabel.setAttribute("value", "0:00:00");
    52           else if (!val && displayedTime == "0:00:00")
    53               this.timeLabel.setAttribute("value", "0:00");
    54           ]]>
    55         </setter>
    56       </property>
    58       <method name="setTime">
    59           <parameter name="time"/>
    60           <body>
    61           <![CDATA[
    62               var timeString;
    63               time = Math.round(time / 1000);
    64               var hours = Math.floor(time / 3600);
    65               var mins  = Math.floor((time % 3600) / 60);
    66               var secs  = Math.floor(time % 60);
    67               if (secs < 10)
    68                   secs = "0" + secs;
    69               if (hours || this.showHours) {
    70                   if (mins < 10)
    71                       mins = "0" + mins;
    72                   timeString = hours + ":" + mins + ":" + secs;
    73               } else {
    74                   timeString = mins + ":" + secs;
    75               }
    77               this.timeLabel.setAttribute("value", timeString);
    78           ]]>
    79           </body>
    80       </method>
    81       </implementation>
    82   </binding>
    84   <binding id="suppressChangeEvent"
    85             extends="chrome://global/content/bindings/scale.xml#scale">
    86     <implementation implements="nsIXBLAccessible">
    87       <!-- nsIXBLAccessible -->
    88       <property name="accessibleName" readonly="true">
    89         <getter>
    90           if (this.type != "scrubber")
    91               return "";
    93           var currTime = this.thumb.timeLabel.getAttribute("value");
    94           var totalTime = this.durationValue;
    96           return this.scrubberNameFormat.replace(/#1/, currTime).
    97               replace(/#2/, totalTime);
    98         </getter>
    99       </property>
   101       <constructor>
   102           <![CDATA[
   103           this.scrubberNameFormat = ]]>"&scrubberScale.nameFormat;"<![CDATA[;
   104           this.durationValue = "";
   105           this.valueBar = null;
   106           this.isDragging = false;
   107           this.wasPausedBeforeDrag = true;
   109           this.thumb = document.getAnonymousElementByAttribute(this, "class", "scale-thumb");
   110           this.type = this.getAttribute("class");
   111           this.Utils = document.getBindingParent(this.parentNode).Utils;
   112           if (this.type == "scrubber")
   113               this.valueBar = this.Utils.progressBar;
   114           ]]>
   115       </constructor>
   117       <method name="valueChanged">
   118           <parameter name="which"/>
   119           <parameter name="newValue"/>
   120           <parameter name="userChanged"/>
   121           <body>
   122           <![CDATA[
   123             // This method is a copy of the base binding's valueChanged(), except that it does
   124             // not dispatch a |change| event (to avoid exposing the event to web content), and
   125             // just calls the videocontrol's seekToPosition() method directly.
   126             switch (which) {
   127               case "curpos":
   128                 if (this.type == "scrubber") {
   129                     // Update the time shown in the thumb.
   130                     this.thumb.setTime(newValue);
   131                     this.Utils.positionLabel.setAttribute("value", this.thumb.timeLabel.value);
   132                     // Update the value bar to match the thumb position.
   133                     var percent = newValue / this.max;
   134                     this.valueBar.value = Math.round(percent * 10000); // has max=10000
   135                 }
   137                 // The value of userChanged is true when changing the position with the mouse,
   138                 // but not when pressing an arrow key. However, the base binding sets
   139                 // ._userChanged in its keypress handlers, so we just need to check both.
   140                 if (!userChanged && !this._userChanged)
   141                   return;
   142                 this.setAttribute("value", newValue);
   144                 if (this.type == "scrubber")
   145                     this.Utils.seekToPosition(newValue);
   146                 else if (this.type == "volumeControl")
   147                     this.Utils.setVolume(newValue / 100);
   148                 break;
   150               case "minpos":
   151                 this.setAttribute("min", newValue);
   152                 break;
   154               case "maxpos":
   155                 if (this.type == "scrubber") {
   156                     // Update the value bar to match the thumb position.
   157                     var percent = this.value / newValue;
   158                     this.valueBar.value = Math.round(percent * 10000); // has max=10000
   159                 }
   160                 this.setAttribute("max", newValue);
   161                 break;
   162             }
   163           ]]>
   164           </body>
   165       </method>
   167       <method name="dragStateChanged">
   168         <parameter name="isDragging"/>
   169           <body>
   170           <![CDATA[
   171             if (this.type == "scrubber") {
   172                 this.Utils.log("--- dragStateChanged: " + isDragging + " ---");
   173                 this.isDragging = isDragging;
   174                 if (isDragging) {
   175                     this.wasPausedBeforeDrag = this.Utils.video.paused;
   176                     this.previousPlaybackRate = this.Utils.video.playbackRate;
   177                     this.Utils.video.pause();
   178                 } else if (!this.wasPausedBeforeDrag) {
   179                     // After the drag ends, resume playing.
   180                     this.Utils.video.playbackRate = this.previousPlaybackRate;
   181                     this.Utils.video.play();
   182                 }
   183             }
   184           ]]>
   185           </body>
   186       </method>
   188       </implementation>
   189   </binding>
   191   <binding id="videoControls">
   193     <resources>
   194         <stylesheet src="chrome://global/content/bindings/videocontrols.css"/>
   195         <stylesheet src="chrome://global/skin/media/videocontrols.css"/>
   196     </resources>
   198     <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   199                  class="mediaControlsFrame">
   200         <stack flex="1">
   201             <vbox flex="1" class="statusOverlay" hidden="true">
   202                 <box class="statusIcon"/>
   203                 <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
   204                 <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
   205                 <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
   206                 <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
   207                 <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
   208                 <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
   209             </vbox>
   211             <vbox class="statsOverlay" hidden="true">
   212                 <html:div class="statsDiv" xmlns="http://www.w3.org/1999/xhtml">
   213                     <table class="statsTable">
   214                         <tr>
   215                             <td class="statLabel">&stats.media;</td>
   216                             <td class="statValue filename"><span class="statFilename"/></td>
   217                         </tr>
   218                         <tr>
   219                             <td class="statLabel">&stats.size;</td>
   220                             <td class="statValue size"><span class="statSize"/></td>
   221                         </tr>
   222                         <tr style="height: 1em;"/>
   224                         <tr>
   225                             <td class="statLabel">&stats.activity;</td>
   226                             <td class="statValue activity">
   227                               <span class="statActivity">
   228                                 <span class="statActivityPaused">&stats.activityPaused;</span>
   229                                 <span class="statActivityPlaying">&stats.activityPlaying;</span>
   230                                 <span class="statActivityEnded">&stats.activityEnded;</span>
   231                                 <span class="statActivitySeeking">&stats.activitySeeking;</span>
   232                               </span>
   233                             </td>
   234                         </tr>
   235                         <tr>
   236                             <td class="statLabel">&stats.volume;</td> <td class="statValue"><span class="statVolume"/></td>
   237                         </tr>
   238                         <tr>
   239                             <!-- Localization note: readyState is a HTML5 API MediaElement-specific attribute and should not be localized. -->
   240                             <td class="statLabel">readyState</td> <td class="statValue"><span class="statReadyState"/></td>
   241                         </tr>
   242                         <tr>
   243                           <!-- Localization note: networkState is a HTML5 API MediaElement-specific attribute and should not be localized. -->
   244                           <td class="statLabel">networkState</td> <td class="statValue"><span class="statNetState"/></td>
   245                         </tr>
   246                         <tr style="height: 1em;"/>
   248                         <tr>
   249                             <td class="statLabel">&stats.framesParsed;</td>
   250                             <td class="statValue"><span class="statFramesParsed"/></td>
   251                         </tr>
   252                         <tr>
   253                             <td class="statLabel">&stats.framesDecoded;</td>
   254                             <td class="statValue"><span class="statFramesDecoded"/></td>
   255                         </tr>
   256                         <tr>
   257                             <td class="statLabel">&stats.framesPresented;</td>
   258                             <td class="statValue"><span class="statFramesPresented"/></td>
   259                         </tr>
   260                         <tr>
   261                             <td class="statLabel">&stats.framesPainted;</td>
   262                             <td class="statValue"><span class="statFramesPainted"/></td>
   263                         </tr>
   264                     </table>
   265                 </html:div>
   266             </vbox>
   268             <vbox class="controlsOverlay">
   269                 <stack flex="1">
   270                     <spacer class="controlsSpacer" flex="1"/>
   271                     <box class="clickToPlay" hidden="true" flex="1"/>
   272                 </stack>
   273                 <hbox class="controlBar" hidden="true">
   274                     <button class="playButton"
   275                             playlabel="&playButton.playLabel;"
   276                             pauselabel="&playButton.pauseLabel;"/>
   277                     <stack class="scrubberStack" flex="1">
   278                         <box class="backgroundBar"/>
   279                         <progressmeter class="bufferBar"/>
   280                         <progressmeter class="progressBar" max="10000"/>
   281                         <scale class="scrubber" movetoclick="true"/>
   282                     </stack>
   283                     <vbox class="durationBox">
   284                         <label class="positionLabel" role="presentation"/>
   285                         <label class="durationLabel" role="presentation"/>
   286                     </vbox>
   287                     <button class="muteButton"
   288                             mutelabel="&muteButton.muteLabel;"
   289                             unmutelabel="&muteButton.unmuteLabel;"/>
   290                     <stack class="volumeStack">
   291                       <box class="volumeBackground"/>
   292                       <box class="volumeForeground" anonid="volumeForeground"/>
   293                       <scale class="volumeControl" movetoclick="true"/>
   294                     </stack>
   295                     <button class="fullscreenButton"
   296                             enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
   297                             exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
   298                 </hbox>
   299             </vbox>
   300         </stack>
   301     </xbl:content>
   303     <implementation>
   305         <constructor>
   306             <![CDATA[
   307             this.isTouchControl = false;
   308             this.randomID = 0;
   310             this.Utils = {
   311                 debug : false,
   312                 video : null,
   313                 videocontrols : null,
   314                 controlBar : null,
   315                 playButton : null,
   316                 muteButton : null,
   317                 volumeControl  : null,
   318                 durationLabel  : null,
   319                 positionLabel  : null,
   320                 scrubberThumb  : null,
   321                 scrubber       : null,
   322                 progressBar    : null,
   323                 bufferBar      : null,
   324                 statusOverlay  : null,
   325                 controlsSpacer : null,
   326                 clickToPlay    : null,
   327                 stats          : {},
   328                 fullscreenButton : null,
   330                 randomID : 0,
   331                 videoEvents : ["play", "pause", "ended", "volumechange", "loadeddata",
   332                                "loadstart", "timeupdate", "progress",
   333                                "playing", "waiting", "canplay", "canplaythrough",
   334                                "seeking", "seeked", "emptied", "loadedmetadata",
   335                                "error", "suspend", "stalled",
   336                                "mozinterruptbegin", "mozinterruptend" ],
   338                 firstFrameShown : false,
   339                 timeUpdateCount : 0,
   340                 maxCurrentTimeSeen : 0,
   341                 _isAudioOnly : false,
   342                 get isAudioOnly() { return this._isAudioOnly; },
   343                 set isAudioOnly(val) {
   344                     this._isAudioOnly = val;
   345                     if (this._isAudioOnly) {
   346                         this.controlBar.setAttribute("audio-only", true);
   347                     } else {
   348                         this.controlBar.removeAttribute("audio-only");
   349                     }
   350                     this.adjustControlSize();
   352                     if (!this.isTopLevelSyntheticDocument)
   353                         return;
   354                     if (this._isAudioOnly) {
   355                         this.video.style.height = this._controlBarHeight + "px";
   356                         this.video.style.width = "66%";
   357                     } else {
   358                         this.video.style.removeProperty("height");
   359                         this.video.style.removeProperty("width");
   360                     }
   361                 },
   362                 suppressError : false,
   364                 setupStatusFader : function(immediate) {
   365                     // Since the play button will be showing, we don't want to
   366                     // show the throbber behind it. The throbber here will
   367                     // only show if needed after the play button has been pressed.
   368                     if (!this.clickToPlay.hidden) {
   369                         this.startFadeOut(this.statusOverlay, true);
   370                         return;
   371                     }
   373                     var show = false;
   374                     if (this.video.seeking ||
   375                         (this.video.error && !this.suppressError) ||
   376                         this.video.networkState == this.video.NETWORK_NO_SOURCE ||
   377                         (this.video.networkState == this.video.NETWORK_LOADING &&
   378                          (this.video.paused || this.video.ended
   379                            ? this.video.readyState < this.video.HAVE_CURRENT_DATA
   380                            : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
   381                         (this.timeUpdateCount <= 1 && !this.video.ended &&
   382                          this.video.readyState < this.video.HAVE_ENOUGH_DATA &&
   383                          this.video.networkState == this.video.NETWORK_LOADING))
   384                         show = true;
   386                     // Explicitly hide the status fader if this
   387                     // is audio only until bug 619421 is fixed.
   388                     if (this.isAudioOnly)
   389                         show = false;
   391                     this.log("Status overlay: seeking=" + this.video.seeking +
   392                              " error=" + this.video.error + " readyState=" + this.video.readyState +
   393                              " paused=" + this.video.paused + " ended=" + this.video.ended +
   394                              " networkState=" + this.video.networkState +
   395                              " timeUpdateCount=" + this.timeUpdateCount +
   396                              " --> " + (show ? "SHOW" : "HIDE"));
   397                     this.startFade(this.statusOverlay, show, immediate);
   398                 },
   400                 /*
   401                  * Set the initial state of the controls. The binding is normally created along
   402                  * with video element, but could be attached at any point (eg, if the video is
   403                  * removed from the document and then reinserted). Thus, some one-time events may
   404                  * have already fired, and so we'll need to explicitly check the initial state.
   405                  */
   406                 setupInitialState : function() {
   407                     this.randomID = Math.random();
   408                     this.videocontrols.randomID = this.randomID;
   410                     this.setPlayButtonState(this.video.paused);
   411                     this.setMuteButtonState(this.video.muted);
   413                     this.setFullscreenButtonState();
   415                     var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
   416                     this.volumeControl.value = volume;
   418                     var duration = Math.round(this.video.duration * 1000); // in ms
   419                     var currentTime = Math.round(this.video.currentTime * 1000); // in ms
   420                     this.log("Initial playback position is at " + currentTime + " of " + duration);
   421                     // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
   422                     // to determine if the media source changed while we were detached.
   423                     this.maxCurrentTimeSeen = currentTime;
   424                     this.showPosition(currentTime, duration);
   426                     // If we have metadata, check if this is a <video> without
   427                     // video data, or a video with no audio track.
   428                     if (this.video.readyState >= this.video.HAVE_METADATA) {
   429                         if (this.video instanceof HTMLVideoElement &&
   430                             (this.video.videoWidth == 0 || this.video.videoHeight == 0))
   431                             this.isAudioOnly = true;
   433                         // We have to check again if the media has audio here,
   434                         // because of bug 718107: switching to fullscreen may
   435                         // cause the bindings to detach and reattach, hence
   436                         // unsetting the attribute.
   437                         if (!this.isAudioOnly && !this.video.mozHasAudio) {
   438                           this.muteButton.setAttribute("noAudio", "true");
   439                           this.muteButton.setAttribute("disabled", "true");
   440                         }
   441                     }
   443                     if (this.isAudioOnly)
   444                         this.clickToPlay.hidden = true;
   446                     // If the first frame hasn't loaded, kick off a throbber fade-in.
   447                     if (this.video.readyState >= this.video.HAVE_CURRENT_DATA)
   448                         this.firstFrameShown = true;
   450                     // We can't determine the exact buffering status, but do know if it's
   451                     // fully loaded. (If it's still loading, it will fire a progress event
   452                     // and we'll figure out the exact state then.)
   453                     this.bufferBar.setAttribute("max", 100);
   454                     if (this.video.readyState >= this.video.HAVE_METADATA)
   455                         this.showBuffered();
   456                     else
   457                         this.bufferBar.setAttribute("value", 0);
   459                     // Set the current status icon.
   460                     if (this.hasError()) {
   461                         this.clickToPlay.hidden = true;
   462                         this.statusIcon.setAttribute("type", "error");
   463                         this.updateErrorText();
   464                         this.setupStatusFader(true);
   465                     }
   467                     // An event handler for |onresize| should be added when bug 227495 is fixed.
   468                     this.controlBar.hidden = false;
   469                     this._playButtonWidth = this.playButton.clientWidth;
   470                     this._durationLabelWidth = this.durationLabel.clientWidth;
   471                     this._muteButtonWidth = this.muteButton.clientWidth;
   472                     this._volumeControlWidth = this.volumeControl.clientWidth;
   473                     this._fullscreenButtonWidth = this.fullscreenButton.clientWidth;
   474                     this._controlBarHeight = this.controlBar.clientHeight;
   475                     this.controlBar.hidden = true;
   476                     this.adjustControlSize();
   478                     // Preserve Statistics when toggling fullscreen mode due to bug 714071.
   479                     if (this.video.mozMediaStatisticsShowing)
   480                         this.showStatistics(true);
   482                     this._handleCustomEventsBound = this.handleCustomEvents.bind(this);
   483                     this.video.addEventListener("media-showStatistics", this._handleCustomEventsBound, false, true);
   484                 },
   486                 setupNewLoadState : function() {
   487                     // videocontrols.css hides the control bar by default, because if script
   488                     // is disabled our binding's script is disabled too (bug 449358). Thus,
   489                     // the controls are broken and we don't want them shown. But if script is
   490                     // enabled, the code here will run and can explicitly unhide the controls.
   491                     //
   492                     // For videos with |autoplay| set, we'll leave the controls initially hidden,
   493                     // so that they don't get in the way of the playing video. Otherwise we'll
   494                     // go ahead and reveal the controls now, so they're an obvious user cue.
   495                     //
   496                     // (Note: the |controls| attribute is already handled via layout/style/html.css)
   497                     var shouldShow = !this.dynamicControls ||
   498                       (this.video.paused &&
   499                        !(this.video.autoplay && this.video.mozAutoplayEnabled));
   500                     // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
   501                     this.startFade(this.clickToPlay, shouldShow && !this.isAudioOnly &&
   502                                    this.video.currentTime == 0 && !this.hasError(), true);
   503                     this.startFade(this.controlBar, shouldShow, true);
   504                 },
   506                 handleCustomEvents : function (e) {
   507                     if (!e.isTrusted)
   508                         return;
   509                     this.showStatistics(e.detail);
   510                 },
   512                 get dynamicControls() {
   513                     // Don't fade controls for <audio> elements.
   514                     var enabled = !this.isAudioOnly;
   516                     // Allow tests to explicitly suppress the fading of controls.
   517                     if (this.video.hasAttribute("mozNoDynamicControls"))
   518                         enabled = false;
   520                     // If the video hits an error, suppress controls if it
   521                     // hasn't managed to do anything else yet.
   522                     if (!this.firstFrameShown && this.hasError())
   523                         enabled = false;
   525                     return enabled;
   526                 },
   528                 handleEvent : function (aEvent) {
   529                     this.log("Got media event ----> " + aEvent.type);
   531                     // If the binding is detached (or has been replaced by a
   532                     // newer instance of the binding), nuke our event-listeners.
   533                     if (this.videocontrols.randomID != this.randomID) {
   534                         this.terminateEventListeners();
   535                         return;
   536                     }
   538                     switch (aEvent.type) {
   539                         case "play":
   540                             this.setPlayButtonState(false);
   541                             this.setupStatusFader();
   542                             if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl)
   543                                 this.startFadeOut(this.controlBar);
   544                             if (!this._triggeredByControls)
   545                                 this.clickToPlay.hidden = true;
   546                             this._triggeredByControls = false;
   547                             break;
   548                         case "pause":
   549                             // Little white lie: if we've internally paused the video
   550                             // while dragging the scrubber, don't change the button state.
   551                             if (!this.scrubber.isDragging)
   552                                 this.setPlayButtonState(true);
   553                             this.setupStatusFader();
   554                             break;
   555                         case "ended":
   556                             this.setPlayButtonState(true);
   557                             // We throttle timechange events, so the thumb might not be
   558                             // exactly at the end when the video finishes.
   559                             this.showPosition(Math.round(this.video.currentTime * 1000),
   560                                               Math.round(this.video.duration * 1000));
   561                             this.startFadeIn(this.controlBar);
   562                             this.setupStatusFader();
   563                             break;
   564                         case "volumechange":
   565                             var volume = this.video.muted ? 0 : this.video.volume;
   566                             var volumePercentage = Math.round(volume * 100);
   567                             this.setMuteButtonState(this.video.muted);
   568                             this.volumeControl.value = volumePercentage;
   569                             this.volumeForeground.style.paddingRight = (1 - volume) * this._volumeControlWidth + "px";
   570                             break;
   571                         case "loadedmetadata":
   572                             this.adjustControlSize();
   573                             // If a <video> doesn't have any video data, treat it as <audio>
   574                             // and show the controls (they won't fade back out)
   575                             if (this.video instanceof HTMLVideoElement &&
   576                                 (this.video.videoWidth == 0 || this.video.videoHeight == 0)) {
   577                                 this.isAudioOnly = true;
   578                                 this.clickToPlay.hidden = true;
   579                                 this.startFadeIn(this.controlBar);
   580                                 this.setFullscreenButtonState();
   581                             }
   582                             this.showDuration(Math.round(this.video.duration * 1000));
   583                             if (!this.isAudioOnly && !this.video.mozHasAudio) {
   584                               this.muteButton.setAttribute("noAudio", "true");
   585                               this.muteButton.setAttribute("disabled", "true");
   586                             }
   587                             break;
   588                         case "loadeddata":
   589                             this.firstFrameShown = true;
   590                             this.setupStatusFader();
   591                             break;
   592                         case "loadstart":
   593                             this.maxCurrentTimeSeen = 0;
   594                             this.controlsSpacer.removeAttribute("aria-label");
   595                             this.statusOverlay.removeAttribute("error");
   596                             this.statusIcon.setAttribute("type", "throbber");
   597                             this.isAudioOnly = (this.video instanceof HTMLAudioElement);
   598                             this.setPlayButtonState(true);
   599                             this.setupNewLoadState();
   600                             this.setupStatusFader();
   601                             break;
   602                         case "progress":
   603                             this.statusIcon.removeAttribute("stalled");
   604                             this.showBuffered();
   605                             this.setupStatusFader();
   606                             break;
   607                         case "stalled":
   608                             this.statusIcon.setAttribute("stalled", "true");
   609                             this.statusIcon.setAttribute("type", "throbber");
   610                             this.setupStatusFader();
   611                             break;
   612                         case "suspend":
   613                             this.setupStatusFader();
   614                             break;
   615                         case "timeupdate":
   616                             var currentTime = Math.round(this.video.currentTime * 1000); // in ms
   617                             var duration = Math.round(this.video.duration * 1000); // in ms
   619                             // If playing/seeking after the video ended, we won't get a "play"
   620                             // event, so update the button state here.
   621                             if (!this.video.paused)
   622                                 this.setPlayButtonState(false);
   624                             this.timeUpdateCount++;
   625                             // Whether we show the statusOverlay sometimes depends
   626                             // on whether we've seen more than one timeupdate
   627                             // event (if we haven't, there hasn't been any
   628                             // "playback activity" and we may wish to show the
   629                             // statusOverlay while we wait for HAVE_ENOUGH_DATA).
   630                             // If we've seen more than 2 timeupdate events,
   631                             // the count is no longer relevant to setupStatusFader.
   632                             if (this.timeUpdateCount <= 2)
   633                                 this.setupStatusFader();
   635                             // If the user is dragging the scrubber ignore the delayed seek
   636                             // responses (don't yank the thumb away from the user)
   637                             if (this.scrubber.isDragging)
   638                                 return;
   640                             this.showPosition(currentTime, duration);
   641                             break;
   642                         case "emptied":
   643                             this.bufferBar.value = 0;
   644                             this.showPosition(0, 0);
   645                             break;
   646                         case "seeking":
   647                             this.showBuffered();
   648                             this.statusIcon.setAttribute("type", "throbber");
   649                             this.setupStatusFader();
   650                             break;
   651                         case "waiting":
   652                             this.statusIcon.setAttribute("type", "throbber");
   653                             this.setupStatusFader();
   654                             break;
   655                         case "seeked":
   656                         case "playing":
   657                         case "canplay":
   658                         case "canplaythrough":
   659                             this.setupStatusFader();
   660                             break;
   661                         case "error":
   662                             // We'll show the error status icon when we receive an error event
   663                             // under either of the following conditions:
   664                             // 1. The video has its error attribute set; this means we're loading
   665                             //    from our src attribute, and the load failed, or we we're loading
   666                             //    from source children and the decode or playback failed after we
   667                             //    determined our selected resource was playable.
   668                             // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
   669                             //    loading from child source elements, but we were unable to select
   670                             //    any of the child elements for playback during resource selection.
   671                             if (this.hasError()) {
   672                                 this.suppressError = false;
   673                                 this.clickToPlay.hidden = true;
   674                                 this.statusIcon.setAttribute("type", "error");
   675                                 this.updateErrorText();
   676                                 this.setupStatusFader(true);
   677                                 // If video hasn't shown anything yet, disable the controls.
   678                                 if (!this.firstFrameShown)
   679                                     this.startFadeOut(this.controlBar);
   680                                 this.controlsSpacer.removeAttribute("hideCursor");
   681                             }
   682                             break;
   683                         case "mozinterruptbegin":
   684                         case "mozinterruptend":
   685                             // Nothing to do...
   686                             break;
   687                         default:
   688                             this.log("!!! event " + aEvent.type + " not handled!");
   689                     }
   690                 },
   692                 terminateEventListeners : function () {
   693                     if (this.statsInterval) {
   694                         clearInterval(this.statsInterval);
   695                         this.statsInterval = null;
   696                     }
   697                     for each (let event in this.videoEvents)
   698                         this.video.removeEventListener(event, this, false);
   700                     for each(let element in this.controlListeners)
   701                         element.item.removeEventListener(element.event, element.func, false);
   703                     delete this.controlListeners;
   705                     this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false);
   706                     delete this._handleCustomEventsBound;
   708                     this.log("--- videocontrols terminated ---");
   709                 },
   711                 hasError : function () {
   712                     return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
   713                 },
   715                 updateErrorText : function () {
   716                     let error;
   717                     let v = this.video;
   718                     // It is possible to have both v.networkState == NETWORK_NO_SOURCE
   719                     // as well as v.error being non-null. In this case, we will show
   720                     // the v.error.code instead of the v.networkState error.
   721                     if (v.error) {
   722                         switch (v.error.code) {
   723                           case v.error.MEDIA_ERR_ABORTED:
   724                               error = "errorAborted";
   725                               break;
   726                           case v.error.MEDIA_ERR_NETWORK:
   727                               error = "errorNetwork";
   728                               break;
   729                           case v.error.MEDIA_ERR_DECODE:
   730                               error = "errorDecode";
   731                               break;
   732                           case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
   733                               error = "errorSrcNotSupported";
   734                               break;
   735                           default:
   736                               error = "errorGeneric";
   737                               break;
   738                          }
   739                     } else if (v.networkState == v.NETWORK_NO_SOURCE) {
   740                         error = "errorNoSource";
   741                     } else {
   742                         return; // No error found.
   743                     }
   745                     let label = document.getAnonymousElementByAttribute(this.videocontrols, "anonid", error);
   746                     this.controlsSpacer.setAttribute("aria-label", label.textContent);
   747                     this.statusOverlay.setAttribute("error", error);
   748                 },
   750                 formatTime : function(aTime) {
   751                     // Format the duration as "h:mm:ss" or "m:ss"
   752                     aTime = Math.round(aTime / 1000);
   753                     let hours = Math.floor(aTime / 3600);
   754                     let mins  = Math.floor((aTime % 3600) / 60);
   755                     let secs  = Math.floor(aTime % 60);
   756                     let timeString;
   757                     if (secs < 10)
   758                         secs = "0" + secs;
   759                     if (hours) {
   760                         if (mins < 10)
   761                             mins = "0" + mins;
   762                         timeString = hours + ":" + mins + ":" + secs;
   763                     } else {
   764                         timeString = mins + ":" + secs;
   765                     }
   766                     return timeString;
   767                 },
   769                 showDuration : function (duration) {
   770                     let isInfinite = (duration == Infinity);
   771                     this.log("Duration is " + duration + "ms.\n");
   773                     if (isNaN(duration) || isInfinite)
   774                         duration = this.maxCurrentTimeSeen;
   776                     // Format the duration as "h:mm:ss" or "m:ss"
   777                     let timeString = isInfinite ? "" : this.formatTime(duration);
   778                     this.durationLabel.setAttribute("value", timeString);
   780                     // "durationValue" property is used by scale binding to
   781                     // generate accessible name.
   782                     this.scrubber.durationValue = timeString;
   784                     // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss
   785                     this.scrubberThumb.showHours = (duration >= 3600000);
   787                     this.scrubber.max = duration;
   788                     // XXX Can't set increment here, due to bug 473103. Also, doing so causes
   789                     // snapping when dragging with the mouse, so we can't just set a value for
   790                     // the arrow-keys.
   791                     //this.scrubber.increment = duration / 50;
   792                     this.scrubber.pageIncrement = Math.round(duration / 10);
   793                 },
   795                 seekToPosition : function(newPosition) {
   796                     newPosition /= 1000; // convert from ms
   797                     this.log("+++ seeking to " + newPosition);
   798 #ifdef MOZ_WIDGET_GONK
   799                     // We use fastSeek() on B2G, and an accurate (but slower)
   800                     // seek on other platforms (that are likely to be higher
   801                     // perf).
   802                     this.video.fastSeek(newPosition);
   803 #else
   804                     this.video.currentTime = newPosition;
   805 #endif
   806                 },
   808                 setVolume : function(newVolume) {
   809                     this.log("*** setting volume to " + newVolume);
   810                     this.video.volume = newVolume;
   811                     this.video.muted = false;
   812                 },
   814                 showPosition : function(currentTime, duration) {
   815                     // If the duration is unknown (because the server didn't provide
   816                     // it, or the video is a stream), then we want to fudge the duration
   817                     // by using the maximum playback position that's been seen.
   818                     if (currentTime > this.maxCurrentTimeSeen)
   819                         this.maxCurrentTimeSeen = currentTime;
   820                     this.showDuration(duration);
   822                     this.log("time update @ " + currentTime + "ms of " + duration + "ms");
   824                     this.positionLabel.setAttribute("value", this.formatTime(currentTime));
   825                     this.scrubber.value = currentTime;
   826                 },
   828                 showBuffered : function() {
   829                     function bsearch(haystack, needle, cmp) {
   830                         var length = haystack.length;
   831                         var low = 0;
   832                         var high = length;
   833                         while (low < high) {
   834                             var probe = low + ((high - low) >> 1);
   835                             var r = cmp(haystack, probe, needle);
   836                             if (r == 0) {
   837                                 return probe;
   838                             } else if (r > 0) {
   839                                 low = probe + 1;
   840                             } else {
   841                                 high = probe;
   842                             }
   843                         }
   844                         return -1;
   845                     }
   847                     function bufferedCompare(buffered, i, time) {
   848                         if (time > buffered.end(i)) {
   849                             return 1;
   850                         } else if (time >= buffered.start(i)) {
   851                             return 0;
   852                         }
   853                         return -1;
   854                     }
   856                     var duration = Math.round(this.video.duration * 1000);
   857                     if (isNaN(duration))
   858                         duration = this.maxCurrentTimeSeen;
   860                     // Find the range that the current play position is in and use that
   861                     // range for bufferBar.  At some point we may support multiple ranges
   862                     // displayed in the bar.
   863                     var currentTime = this.video.currentTime;
   864                     var buffered = this.video.buffered;
   865                     var index = bsearch(buffered, currentTime, bufferedCompare);
   866                     var endTime = 0;
   867                     if (index >= 0) {
   868                         endTime = Math.round(buffered.end(index) * 1000);
   869                     }
   870                     this.bufferBar.max = duration;
   871                     this.bufferBar.value = endTime;
   872                 },
   874                 _controlsHiddenByTimeout : false,
   875                 _showControlsTimeout : 0,
   876                 SHOW_CONTROLS_TIMEOUT_MS: 500,
   877                 _showControlsFn : function () {
   878                     if (Utils.video.mozMatchesSelector("video:hover")) {
   879                         Utils.startFadeIn(Utils.controlBar, false);
   880                         Utils._showControlsTimeout = 0;
   881                         Utils._controlsHiddenByTimeout = false;
   882                     }
   883                 },
   885                 _hideControlsTimeout : 0,
   886                 _hideControlsFn : function () {
   887                     if (!Utils.scrubber.isDragging) {
   888                         Utils.startFade(Utils.controlBar, false);
   889                         Utils._hideControlsTimeout = 0;
   890                         Utils._controlsHiddenByTimeout = true;
   891                     }
   892                 },
   893                 HIDE_CONTROLS_TIMEOUT_MS : 2000,
   894                 onMouseMove : function (event) {
   895                     // If the controls are static, don't change anything.
   896                     if (!this.dynamicControls)
   897                         return;
   899                     clearTimeout(this._hideControlsTimeout);
   901                     // Suppress fading out the controls until the video has rendered
   902                     // its first frame. But since autoplay videos start off with no
   903                     // controls, let them fade-out so the controls don't get stuck on.
   904                     if (!this.firstFrameShown &&
   905                         !(this.video.autoplay && this.video.mozAutoplayEnabled))
   906                         return;
   908                     if (this._controlsHiddenByTimeout)
   909                         this._showControlsTimeout = setTimeout(this._showControlsFn, this.SHOW_CONTROLS_TIMEOUT_MS);
   910                     else
   911                         this.startFade(this.controlBar, true);
   913                     // Hide the controls if the mouse cursor is left on top of the video
   914                     // but above the control bar and if the click-to-play overlay is hidden.
   915                     if ((this._controlsHiddenByTimeout ||
   916                          event.clientY < this.controlBar.getBoundingClientRect().top) &&
   917                         this.clickToPlay.hidden) {
   918                         this._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
   919                     }
   920                 },
   922                 onMouseInOut : function (event) {
   923                     // If the controls are static, don't change anything.
   924                     if (!this.dynamicControls)
   925                         return;
   927                     clearTimeout(this._hideControlsTimeout);
   929                     // Ignore events caused by transitions between child nodes.
   930                     // Note that the videocontrols element is the same
   931                     // size as the *content area* of the video element,
   932                     // but this is not the same as the video element's
   933                     // border area if the video has border or padding.
   934                     if (this.isEventWithin(event, this.videocontrols))
   935                         return;
   937                     var isMouseOver = (event.type == "mouseover");
   939                     var isMouseInControls = event.clientY > this.controlBar.getBoundingClientRect().top &&
   940                                             event.clientY < this.controlBar.getBoundingClientRect().bottom;
   942                     // Suppress fading out the controls until the video has rendered
   943                     // its first frame. But since autoplay videos start off with no
   944                     // controls, let them fade-out so the controls don't get stuck on.
   945                     if (!this.firstFrameShown && !isMouseOver &&
   946                         !(this.video.autoplay && this.video.mozAutoplayEnabled))
   947                         return;
   949                     if (!isMouseOver && !isMouseInControls) {
   950                         this.adjustControlSize();
   952                         // Keep the controls visible if the click-to-play is visible.
   953                         if (!this.clickToPlay.hidden)
   954                             return;
   956                         this.startFadeOut(this.controlBar, false);
   957                         clearTimeout(this._showControlsTimeout);
   958                         Utils._controlsHiddenByTimeout = false;
   959                     }
   960                 },
   962                 startFadeIn : function (element, immediate) {
   963                     this.startFade(element, true, immediate);
   964                 },
   966                 startFadeOut : function (element, immediate) {
   967                     this.startFade(element, false, immediate);
   968                 },
   970                 startFade : function (element, fadeIn, immediate) {
   971                     if (element.classList.contains("controlBar") && fadeIn) {
   972                         // Bug 493523, the scrubber doesn't call valueChanged while hidden,
   973                         // so our dependent state (eg, timestamp in the thumb) will be stale.
   974                         // As a workaround, update it manually when it first becomes unhidden.
   975                         if (element.hidden)
   976                             this.scrubber.valueChanged("curpos", this.video.currentTime * 1000, false);
   977                     }
   979                     if (immediate)
   980                         element.setAttribute("immediate", true);
   981                     else
   982                         element.removeAttribute("immediate");
   984                     if (fadeIn) {
   985                         element.hidden = false;
   986                         // force style resolution, so that transition begins
   987                         // when we remove the attribute.
   988                         element.clientTop;
   989                         element.removeAttribute("fadeout");
   990                         if (element.classList.contains("controlBar"))
   991                             this.controlsSpacer.removeAttribute("hideCursor");
   992                     } else {
   993                         element.setAttribute("fadeout", true);
   994                         if (element.classList.contains("controlBar") && !this.hasError() &&
   995                             document.mozFullScreenElement == this.video)
   996                             this.controlsSpacer.setAttribute("hideCursor", true);
   998                     }
   999                 },
  1001                 onTransitionEnd : function (event) {
  1002                     // Ignore events for things other than opacity changes.
  1003                     if (event.propertyName != "opacity")
  1004                         return;
  1006                     var element = event.originalTarget;
  1008                     // Nothing to do when a fade *in* finishes.
  1009                     if (!element.hasAttribute("fadeout"))
  1010                         return;
  1012                     element.hidden = true;
  1013                 },
  1015                 _triggeredByControls: false,
  1017                 togglePause : function () {
  1018                     if (this.video.paused || this.video.ended) {
  1019                         this._triggeredByControls = true;
  1020                         this.hideClickToPlay();
  1021                         this.video.playbackRate = this.video.defaultPlaybackRate;
  1022                         this.video.play();
  1023                     } else {
  1024                         this.video.pause();
  1027                     // We'll handle style changes in the event listener for
  1028                     // the "play" and "pause" events, same as if content
  1029                     // script was controlling video playback.
  1030                 },
  1032                 isVideoWithoutAudioTrack : function() {
  1033                   return this.video.readyState >= this.video.HAVE_METADATA &&
  1034                          !this.isAudioOnly &&
  1035                          !this.video.mozHasAudio;
  1036                 },
  1038                 toggleMute : function () {
  1039                     if (this.isVideoWithoutAudioTrack()) {
  1040                         return;
  1042                     this.video.muted = !this.video.muted;
  1044                     // We'll handle style changes in the event listener for
  1045                     // the "volumechange" event, same as if content script was
  1046                     // controlling volume.
  1047                 },
  1049                 isVideoInFullScreen : function () {
  1050                     return document.mozFullScreenElement == this.video;
  1051                 },
  1053                 toggleFullscreen : function () {
  1054                     this.isVideoInFullScreen() ?
  1055                         document.mozCancelFullScreen() :
  1056                         this.video.mozRequestFullScreen();
  1057                 },
  1059                 setFullscreenButtonState : function () {
  1060                     if (this.isAudioOnly || !document.mozFullScreenEnabled) {
  1061                         this.fullscreenButton.hidden = true;
  1062                         return;
  1065                     var attrName = this.isVideoInFullScreen() ? "exitfullscreenlabel" : "enterfullscreenlabel";
  1066                     var value = this.fullscreenButton.getAttribute(attrName);
  1067                     this.fullscreenButton.setAttribute("aria-label", value);
  1069                     if (this.isVideoInFullScreen())
  1070                         this.fullscreenButton.setAttribute("fullscreened", "true");
  1071                     else
  1072                         this.fullscreenButton.removeAttribute("fullscreened");
  1073                 },
  1075                 onFullscreenChange: function () {
  1076                     if (this.isVideoInFullScreen()) {
  1077                         Utils._hideControlsTimeout = setTimeout(this._hideControlsFn, this.HIDE_CONTROLS_TIMEOUT_MS);
  1079                     this.setFullscreenButtonState();
  1080                 },
  1082                 clickToPlayClickHandler : function(e) {
  1083                     if (e.button != 0)
  1084                         return;
  1085                     if (this.hasError() && !this.suppressError) {
  1086                         // Errors that can be dismissed should be placed here as we discover them.
  1087                         if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED)
  1088                             return;
  1089                         this.statusOverlay.hidden = true;
  1090                         this.suppressError = true;
  1091                         return;
  1094                     // Read defaultPrevented asynchronously, since Web content
  1095                     // may want to consume the "click" event but will only
  1096                     // receive it after us.
  1097                     let self = this;
  1098                     setTimeout(function clickToPlayCallback() {
  1099                         if (!e.defaultPrevented)
  1100                             self.togglePause();
  1101                     }, 0);
  1102                 },
  1103                 hideClickToPlay : function () {
  1104                     let videoHeight = this.video.clientHeight;
  1105                     let videoWidth = this.video.clientWidth;
  1107                     // The play button will animate to 3x its size. This
  1108                     // shows the animation unless the video is too small
  1109                     // to show 2/3 of the animation.
  1110                     let animationScale = 2;
  1111                     if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)||
  1112                         this._overlayPlayButtonWidth * animationScale > videoWidth) {
  1113                         this.clickToPlay.setAttribute("immediate", "true");
  1114                         this.clickToPlay.hidden = true;
  1115                     } else {
  1116                         this.clickToPlay.removeAttribute("immediate");
  1118                     this.clickToPlay.setAttribute("fadeout", "true");
  1119                 },
  1121                 setPlayButtonState : function(aPaused) {
  1122                   if (aPaused)
  1123                       this.playButton.setAttribute("paused", "true");
  1124                   else
  1125                       this.playButton.removeAttribute("paused");
  1127                   var attrName = aPaused ? "playlabel" : "pauselabel";
  1128                   var value = this.playButton.getAttribute(attrName);
  1129                   this.playButton.setAttribute("aria-label", value);
  1130                 },
  1132                 setMuteButtonState : function(aMuted) {
  1133                   if (aMuted)
  1134                       this.muteButton.setAttribute("muted", "true");
  1135                   else
  1136                       this.muteButton.removeAttribute("muted");
  1138                   var attrName = aMuted ? "unmutelabel" : "mutelabel";
  1139                   var value = this.muteButton.getAttribute(attrName);
  1140                   this.muteButton.setAttribute("aria-label", value);
  1141                 },
  1143                 _getComputedPropertyValueAsInt : function(element, property) {
  1144                   let value = window.getComputedStyle(element, null).getPropertyValue(property);
  1145                   return parseInt(value, 10);
  1146                 },
  1148                 STATS_INTERVAL_MS : 500,
  1149                 statsInterval : null,
  1151                 showStatistics : function(shouldShow) {
  1152                     if (this.statsInterval) {
  1153                         clearInterval(this.statsInterval);
  1154                         this.statsInterval = null;
  1157                     if (shouldShow) {
  1158                         this.video.mozMediaStatisticsShowing = true;
  1159                         this.statsOverlay.hidden = false;
  1160                         this.statsInterval = setInterval(this.updateStats.bind(this), this.STATS_INTERVAL_MS);
  1161                         this.updateStats();
  1162                     } else {
  1163                         this.video.mozMediaStatisticsShowing = false;
  1164                         this.statsOverlay.hidden = true;
  1166                 },
  1168                 updateStats : function() {
  1169                     if (this.videocontrols.randomID != this.randomID) {
  1170                         this.terminateEventListeners();
  1171                         return;
  1174                     let v = this.video;
  1175                     let s = this.stats;
  1177                     let src = v.currentSrc || v.src || "(no source found)";
  1178                     let srcParts = src.split('/');
  1179                     let srcIdx = srcParts.length - 1;
  1180                     if (src.lastIndexOf('/') == src.length - 1)
  1181                         srcIdx--;
  1182                     s.filename.textContent = decodeURI(srcParts[srcIdx]);
  1184                     let size = v.videoWidth + "x" + v.videoHeight;
  1185                     if (this._getComputedPropertyValueAsInt(this.video, "width") != v.videoWidth || this._getComputedPropertyValueAsInt(this.video, "height") != v.videoHeight)
  1186                         size += " scaled to " + this._getComputedPropertyValueAsInt(this.video, "width") + "x" + this._getComputedPropertyValueAsInt(this.video, "height");
  1187                     s.size.textContent = size;
  1189                     let activity;
  1190                     if (v.paused)
  1191                         activity = "paused";
  1192                     else
  1193                         activity = "playing";
  1194                     if (v.ended)
  1195                         activity = "ended";
  1196                     if (s.activity.getAttribute("activity") != activity)
  1197                         s.activity.setAttribute("activity", activity);
  1198                     if (v.seeking && !s.activity.hasAttribute("seeking"))
  1199                         s.activity.setAttribute("seeking", true);
  1200                     else if (s.activity.hasAttribute("seeking"))
  1201                         s.activity.removeAttribute("seeking");
  1203                     let readyState = v.readyState;
  1204                     switch (readyState) {
  1205                         case v.HAVE_NOTHING:      readyState = "HAVE_NOTHING";      break;
  1206                         case v.HAVE_METADATA:     readyState = "HAVE_METADATA";     break;
  1207                         case v.HAVE_CURRENT_DATA: readyState = "HAVE_CURRENT_DATA"; break;
  1208                         case v.HAVE_FUTURE_DATA:  readyState = "HAVE_FUTURE_DATA";  break;
  1209                         case v.HAVE_ENOUGH_DATA:  readyState = "HAVE_ENOUGH_DATA";  break;
  1211                     s.readyState.textContent = readyState;
  1213                     let networkState = v.networkState;
  1214                     switch (networkState) {
  1215                         case v.NETWORK_EMPTY:     networkState = "NETWORK_EMPTY";     break;
  1216                         case v.NETWORK_IDLE:      networkState = "NETWORK_IDLE";      break;
  1217                         case v.NETWORK_LOADING:   networkState = "NETWORK_LOADING";   break;
  1218                         case v.NETWORK_NO_SOURCE: networkState = "NETWORK_NO_SOURCE"; break;
  1220                     s.netState.textContent = networkState;
  1222                     s.framesParsed.textContent = v.mozParsedFrames;
  1223                     s.framesDecoded.textContent = v.mozDecodedFrames;
  1224                     s.framesPresented.textContent = v.mozPresentedFrames;
  1225                     s.framesPainted.textContent = v.mozPaintedFrames;
  1227                     let volume = Math.round(v.volume * 100) + "%";
  1228                     if (v.muted)
  1229                         volume += " (muted)";
  1230                     s.volume.textContent = volume;
  1231                 },
  1233                 keyHandler : function(event) {
  1234                     // Ignore keys when content might be providing its own.
  1235                     if (!this.video.hasAttribute("controls"))
  1236                         return;
  1238                     var keystroke = "";
  1239                     if (event.altKey)
  1240                         keystroke += "alt-";
  1241                     if (event.shiftKey)
  1242                         keystroke += "shift-";
  1243 #ifdef XP_MACOSX
  1244                     if (event.metaKey)
  1245                         keystroke += "accel-";
  1246                     if (event.ctrlKey)
  1247                         keystroke += "control-";
  1248 #else
  1249                     if (event.metaKey)
  1250                         keystroke += "meta-";
  1251                     if (event.ctrlKey)
  1252                         keystroke += "accel-";
  1253 #endif
  1255                     switch (event.keyCode) {
  1256                         case KeyEvent.DOM_VK_UP:
  1257                             keystroke += "upArrow";
  1258                             break;
  1259                         case KeyEvent.DOM_VK_DOWN:
  1260                             keystroke += "downArrow";
  1261                             break;
  1262                         case KeyEvent.DOM_VK_LEFT:
  1263                             keystroke += "leftArrow";
  1264                             break;
  1265                         case KeyEvent.DOM_VK_RIGHT:
  1266                             keystroke += "rightArrow";
  1267                             break;
  1268                         case KeyEvent.DOM_VK_HOME:
  1269                             keystroke += "home";
  1270                             break;
  1271                         case KeyEvent.DOM_VK_END:
  1272                             keystroke += "end";
  1273                             break;
  1276                     if (String.fromCharCode(event.charCode) == ' ')
  1277                         keystroke += "space";
  1279                     this.log("Got keystroke: " + keystroke);
  1280                     var oldval, newval;
  1282                     try {
  1283                         switch (keystroke) {
  1284                             case "space": /* Play */
  1285                                 this.togglePause();
  1286                                 break;
  1287                             case "downArrow": /* Volume decrease */
  1288                                 oldval = this.video.volume;
  1289                                 this.video.volume = (oldval < 0.1 ? 0 : oldval - 0.1);
  1290                                 this.video.muted = false;
  1291                                 break;
  1292                             case "upArrow": /* Volume increase */
  1293                                 oldval = this.video.volume;
  1294                                 this.video.volume = (oldval > 0.9 ? 1 : oldval + 0.1);
  1295                                 this.video.muted = false;
  1296                                 break;
  1297                             case "accel-downArrow": /* Mute */
  1298                                 this.video.muted = true;
  1299                                 break;
  1300                             case "accel-upArrow": /* Unmute */
  1301                                 this.video.muted = false;
  1302                                 break;
  1303                             case "leftArrow": /* Seek back 15 seconds */
  1304                             case "accel-leftArrow": /* Seek back 10% */
  1305                                 oldval = this.video.currentTime;
  1306                                 if (keystroke == "leftArrow")
  1307                                     newval = oldval - 15;
  1308                                 else
  1309                                     newval = oldval - (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
  1310                                 this.video.currentTime = (newval >= 0 ? newval : 0);
  1311                                 break;
  1312                             case "rightArrow": /* Seek forward 15 seconds */
  1313                             case "accel-rightArrow": /* Seek forward 10% */
  1314                                 oldval = this.video.currentTime;
  1315                                 var maxtime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
  1316                                 if (keystroke == "rightArrow")
  1317                                     newval = oldval + 15;
  1318                                 else
  1319                                     newval = oldval + maxtime / 10;
  1320                                 this.video.currentTime = (newval <= maxtime ? newval : maxtime);
  1321                                 break;
  1322                             case "home": /* Seek to beginning */
  1323                                 this.video.currentTime = 0;
  1324                                 break;
  1325                             case "end": /* Seek to end */
  1326                                 if (this.video.currentTime != this.video.duration)
  1327                                     this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
  1328                                 break;
  1329                             default:
  1330                                 return;
  1332                     } catch(e) { /* ignore any exception from setting .currentTime */ }
  1334                     event.preventDefault(); // Prevent page scrolling
  1335                 },
  1337                 isEventWithin : function (event, parent1, parent2) {
  1338                     function isDescendant (node) {
  1339                         while (node) {
  1340                             if (node == parent1 || node == parent2)
  1341                                 return true;
  1342                             node = node.parentNode;
  1344                         return false;
  1346                     return isDescendant(event.target) && isDescendant(event.relatedTarget);
  1347                 },
  1349                 log : function (msg) {
  1350                     if (this.debug)
  1351                         dump("videoctl: " + msg + "\n");
  1352                 },
  1354                 get isTopLevelSyntheticDocument() {
  1355                   let doc = this.video.ownerDocument;
  1356                   let win = doc.defaultView;
  1357                   return doc.mozSyntheticDocument && win === win.top;
  1358                 },
  1360                 _playButtonWidth : 0,
  1361                 _durationLabelWidth : 0,
  1362                 _muteButtonWidth : 0,
  1363                 _volumeControlWidth : 0,
  1364                 _fullscreenButtonWidth : 0,
  1365                 _controlBarHeight : 0,
  1366                 _overlayPlayButtonHeight : 64,
  1367                 _overlayPlayButtonWidth : 64,
  1368                 _volumeStackMarginEnd : 8,
  1369                 adjustControlSize : function adjustControlSize() {
  1370                     let doc = this.video.ownerDocument;
  1372                     // The scrubber has |flex=1|, therefore |minScrubberWidth|
  1373                     // was generated by empirical testing.
  1374                     let minScrubberWidth = 25;
  1375                     let minWidthAllControls = this._playButtonWidth +
  1376                                               minScrubberWidth +
  1377                                               this._durationLabelWidth +
  1378                                               this._muteButtonWidth +
  1379                                               this._volumeControlWidth +
  1380                                               this._fullscreenButtonWidth;
  1382                     let isAudioOnly = this.isAudioOnly;
  1383                     if (isAudioOnly) {
  1384                         // When the fullscreen button is hidden we add margin-end to the volume stack.
  1385                         minWidthAllControls -= this._fullscreenButtonWidth - this._volumeStackMarginEnd;
  1388                     let minHeightForControlBar = this._controlBarHeight;
  1389                     let minWidthOnlyPlayPause = this._playButtonWidth + this._muteButtonWidth;
  1391                     let videoHeight = isAudioOnly ? minHeightForControlBar : this.video.clientHeight;
  1392                     let videoWidth = isAudioOnly ? minWidthAllControls : this.video.clientWidth;
  1394                     if ((this._overlayPlayButtonHeight + this._controlBarHeight) > videoHeight ||
  1395                         this._overlayPlayButtonWidth > videoWidth) {
  1396                         this.clickToPlay.hidden = true;
  1397                     } else if (this.clickToPlay.hidden &&
  1398                                !this.video.played.length &&
  1399                                this.video.paused) {
  1400                         // Check this.video.paused to handle when a video is
  1401                         // playing but hasn't processed any frames yet
  1402                         this.clickToPlay.hidden = false;
  1405                     let size = "normal";
  1406                     if (videoHeight < minHeightForControlBar)
  1407                         size = "hidden";
  1408                     else if (videoWidth < minWidthOnlyPlayPause)
  1409                         size = "hidden";
  1410                     else if (videoWidth < minWidthAllControls)
  1411                         size = "small";
  1412                     this.controlBar.setAttribute("size", size);
  1413                 },
  1415                 init : function (binding) {
  1416                     this.video = binding.parentNode;
  1417                     this.videocontrols = binding;
  1419                     this.statusIcon    = document.getAnonymousElementByAttribute(binding, "class", "statusIcon");
  1420                     this.controlBar    = document.getAnonymousElementByAttribute(binding, "class", "controlBar");
  1421                     this.playButton    = document.getAnonymousElementByAttribute(binding, "class", "playButton");
  1422                     this.muteButton    = document.getAnonymousElementByAttribute(binding, "class", "muteButton");
  1423                     this.volumeControl = document.getAnonymousElementByAttribute(binding, "class", "volumeControl");
  1424                     this.progressBar   = document.getAnonymousElementByAttribute(binding, "class", "progressBar");
  1425                     this.bufferBar     = document.getAnonymousElementByAttribute(binding, "class", "bufferBar");
  1426                     this.scrubber      = document.getAnonymousElementByAttribute(binding, "class", "scrubber");
  1427                     this.scrubberThumb = document.getAnonymousElementByAttribute(this.scrubber, "class", "scale-thumb");
  1428                     this.durationLabel = document.getAnonymousElementByAttribute(binding, "class", "durationLabel");
  1429                     this.positionLabel = document.getAnonymousElementByAttribute(binding, "class", "positionLabel");
  1430                     this.statusOverlay = document.getAnonymousElementByAttribute(binding, "class", "statusOverlay");
  1431                     this.statsOverlay  = document.getAnonymousElementByAttribute(binding, "class", "statsOverlay");
  1432                     this.controlsSpacer     = document.getAnonymousElementByAttribute(binding, "class", "controlsSpacer");
  1433                     this.clickToPlay        = document.getAnonymousElementByAttribute(binding, "class", "clickToPlay");
  1434                     this.fullscreenButton   = document.getAnonymousElementByAttribute(binding, "class", "fullscreenButton");
  1435                     this.volumeForeground   = document.getAnonymousElementByAttribute(binding, "anonid", "volumeForeground");
  1437                     this.statsTable       = document.getAnonymousElementByAttribute(binding, "class", "statsTable");
  1438                     this.stats.filename   = document.getAnonymousElementByAttribute(binding, "class", "statFilename");
  1439                     this.stats.size       = document.getAnonymousElementByAttribute(binding, "class", "statSize");
  1440                     this.stats.activity   = document.getAnonymousElementByAttribute(binding, "class", "statActivity");
  1441                     this.stats.volume     = document.getAnonymousElementByAttribute(binding, "class", "statVolume");
  1442                     this.stats.readyState = document.getAnonymousElementByAttribute(binding, "class", "statReadyState");
  1443                     this.stats.netState   = document.getAnonymousElementByAttribute(binding, "class", "statNetState");
  1444                     this.stats.framesParsed    = document.getAnonymousElementByAttribute(binding, "class", "statFramesParsed");
  1445                     this.stats.framesDecoded   = document.getAnonymousElementByAttribute(binding, "class", "statFramesDecoded");
  1446                     this.stats.framesPresented = document.getAnonymousElementByAttribute(binding, "class", "statFramesPresented");
  1447                     this.stats.framesPainted   = document.getAnonymousElementByAttribute(binding, "class", "statFramesPainted");
  1449                     this.isAudioOnly = (this.video instanceof HTMLAudioElement);
  1450                     this.setupInitialState();
  1451                     this.setupNewLoadState();
  1453                     // Use the handleEvent() callback for all media events.
  1454                     // The "error" event listener must capture, so that it can trap error events
  1455                     // from the <source> children, which don't bubble.
  1456                     for each (let event in this.videoEvents)
  1457                         this.video.addEventListener(event, this, (event == "error") ? true : false);
  1459                     var self = this;
  1461                     this.controlListeners = [];
  1463                     // Helper function to add an event listener to the given element
  1464                     function addListener(elem, eventName, func) {
  1465                       let boundFunc = func.bind(self);
  1466                       self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
  1467                       elem.addEventListener(eventName, boundFunc, false);
  1470                     addListener(this.muteButton, "command", this.toggleMute);
  1471                     addListener(this.playButton, "command", this.togglePause);
  1472                     addListener(this.fullscreenButton, "command", this.toggleFullscreen);
  1473                     addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
  1474                     addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
  1475                     addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
  1477                     addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
  1478                     addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
  1479                     addListener(this.video.ownerDocument, "mozfullscreenchange", this.onFullscreenChange);
  1480                     addListener(this.video, "keypress", this.keyHandler);
  1482                     this.log("--- videocontrols initialized ---");
  1484             };
  1485             this.Utils.init(this);
  1486             ]]>
  1487         </constructor>
  1488         <destructor>
  1489             <![CDATA[
  1490             // randomID used to be a <field>, which meant that the XBL machinery
  1491             // undefined the property when the element was unbound. The code in
  1492             // this file actually depends on this, so now that randomID is an
  1493             // expando, we need to make sure to explicitly delete it.
  1494             delete this.randomID;
  1495             ]]>
  1496         </destructor>
  1498     </implementation>
  1500     <handlers>
  1501         <handler event="mouseover">
  1502             if (!this.isTouchControl)
  1503                 this.Utils.onMouseInOut(event);
  1504         </handler>
  1505         <handler event="mouseout">
  1506             if (!this.isTouchControl)
  1507                 this.Utils.onMouseInOut(event);
  1508         </handler>
  1509         <handler event="mousemove">
  1510             if (!this.isTouchControl)
  1511                 this.Utils.onMouseMove(event);
  1512         </handler>
  1513     </handlers>
  1514   </binding>
  1516   <binding id="touchControls" extends="chrome://global/content/bindings/videocontrols.xml#videoControls">
  1518     <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="mediaControlsFrame">
  1519         <stack flex="1">
  1520             <vbox flex="1" class="statusOverlay" hidden="true">
  1521                 <box class="statusIcon"/>
  1522                 <label class="errorLabel" anonid="errorAborted">&error.aborted;</label>
  1523                 <label class="errorLabel" anonid="errorNetwork">&error.network;</label>
  1524                 <label class="errorLabel" anonid="errorDecode">&error.decode;</label>
  1525                 <label class="errorLabel" anonid="errorSrcNotSupported">&error.srcNotSupported;</label>
  1526                 <label class="errorLabel" anonid="errorNoSource">&error.noSource2;</label>
  1527                 <label class="errorLabel" anonid="errorGeneric">&error.generic;</label>
  1528             </vbox>
  1530             <vbox class="controlsOverlay">
  1531                 <spacer class="controlsSpacer" flex="1"/>
  1532                 <box flex="1" hidden="true">
  1533                     <box class="clickToPlay" hidden="true" flex="1"/>
  1534                 </box>
  1535                 <vbox class="controlBar" hidden="true">
  1536                     <hbox class="buttonsBar">
  1537                         <button class="castingButton" hidden="true"
  1538                                 aria-label="&castingButton.castingLabel;"/>
  1539                         <button class="fullscreenButton"
  1540                             enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
  1541                             exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
  1542                         <spacer flex="1"/>
  1543                         <button class="playButton"
  1544                                 playlabel="&playButton.playLabel;"
  1545                                 pauselabel="&playButton.pauseLabel;"/>
  1546                         <spacer flex="1"/>
  1547                         <button class="muteButton"
  1548                                 mutelabel="&muteButton.muteLabel;"
  1549                                 unmutelabel="&muteButton.unmuteLabel;"/>
  1550                         <stack class="volumeStack">
  1551                           <box class="volumeBackground"/>
  1552                           <box class="volumeForeground" anonid="volumeForeground"/>
  1553                           <scale class="volumeControl" movetoclick="true"/>
  1554                         </stack>
  1555                       </hbox>
  1556                     <stack class="scrubberStack" flex="1">
  1557                         <box class="backgroundBar"/>
  1558                         <progressmeter class="bufferBar"/>
  1559                         <progressmeter class="progressBar" max="10000"/>
  1560                         <scale class="scrubber" movetoclick="true"/>
  1561                     </stack>
  1562                     <vbox class="durationBox">
  1563                         <label class="positionLabel" role="presentation"/>
  1564                         <label class="durationLabel" role="presentation"/>
  1565                     </vbox>
  1566                 </vbox>
  1567             </vbox>
  1568         </stack>
  1569     </xbl:content>
  1571     <implementation>
  1573         <constructor>
  1574           <![CDATA[
  1575           this.isTouchControl = true;
  1576           this.TouchUtils = {
  1577             videocontrols: null,
  1578             video: null,
  1579             controlsTimer: null,
  1580             controlsTimeout: 5000,
  1581             positionLabel: null,
  1582             castingButton: null,
  1584             get Utils() {
  1585               return this.videocontrols.Utils;
  1586             },
  1588             get visible() {
  1589               return !this.Utils.controlBar.hasAttribute("fadeout") &&
  1590                      !(this.Utils.controlBar.getAttribute("hidden") == "true");
  1591             },
  1593             _firstShow: false,
  1594             get firstShow() { return this._firstShow; },
  1595             set firstShow(val) {
  1596               this._firstShow = val;
  1597               this.Utils.controlBar.setAttribute("firstshow", val);
  1598             },
  1600             toggleControls: function() {
  1601               if (!this.Utils.dynamicControls || !this.visible)
  1602                 this.showControls();
  1603               else
  1604                 this.delayHideControls(0);
  1605             },
  1607             showControls : function() {
  1608               if (this.Utils.dynamicControls) {
  1609                 this.Utils.startFadeIn(this.Utils.controlBar);
  1610                 this.delayHideControls(this.controlsTimeout);
  1612             },
  1614             clearTimer: function() {
  1615               if (this.controlsTimer) {
  1616                 clearTimeout(this.controlsTimer);
  1617                 this.controlsTimer = null;
  1619             },
  1621             delayHideControls : function(aTimeout) {
  1622               this.clearTimer();
  1623               let self = this;
  1624               this.controlsTimer = setTimeout(function() {
  1625                 self.hideControls();
  1626               }, aTimeout);
  1627             },
  1629             hideControls : function() {
  1630               if (!this.Utils.dynamicControls)
  1631                   return;
  1632               this.Utils.startFadeOut(this.Utils.controlBar);
  1633               if (this.firstShow)
  1634                 this.videocontrols.addEventListener("transitionend", this, false);
  1635             },
  1637             handleEvent : function (aEvent) {
  1638               if (aEvent.type == "transitionend") {
  1639                 this.firstShow = false;
  1640                 this.videocontrols.removeEventListener("transitionend", this, false);
  1641                 return;
  1644               if (this.videocontrols.randomID != this.Utils.randomID)
  1645                 this.terminateEventListeners();
  1647             },
  1649             terminateEventListeners : function () {
  1650               for each (var event in this.videoEvents)
  1651                 this.Utils.video.removeEventListener(event, this, false);
  1652             },
  1654             isVideoCasting : function () {
  1655               if (this.video.mozIsCasting)
  1656                 return true;
  1657               return false;
  1658             },
  1660             updateCasting : function (eventDetail) {
  1661               let castingData = JSON.parse(eventDetail);
  1662               if ("allow" in castingData) {
  1663                 this.video.mozAllowCasting = !!castingData.allow;
  1666               if ("active" in castingData) {
  1667                 this.video.mozIsCasting = !!castingData.active;
  1669               this.setCastButtonState();
  1670             },
  1672             startCasting : function () {
  1673               this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
  1674             },
  1676             setCastButtonState : function () {
  1677               if (this.isAudioOnly || !this.video.mozAllowCasting) {
  1678                 this.castingButton.hidden = true;
  1679                 return;
  1682               if (this.video.mozIsCasting) {
  1683                 this.castingButton.setAttribute("active", "true");
  1684               } else {
  1685                 this.castingButton.removeAttribute("active");
  1688               this.castingButton.hidden = false;
  1689             },
  1691             init : function (binding) {
  1692               this.videocontrols = binding;
  1693               this.video = binding.parentNode;
  1695               let self = this;
  1696               this.Utils.playButton.addEventListener("command", function() {
  1697                 if (!self.video.paused)
  1698                     self.delayHideControls(0);
  1699                 else
  1700                     self.showControls();
  1701               }, false);
  1702               this.Utils.scrubber.addEventListener("touchstart", function() {
  1703                 self.clearTimer();
  1704               }, false);
  1705               this.Utils.scrubber.addEventListener("touchend", function() {
  1706                 self.delayHideControls(self.controlsTimeout);
  1707               }, false);
  1708               this.Utils.muteButton.addEventListener("click", function() { self.delayHideControls(self.controlsTimeout); }, false);
  1710               this.castingButton = document.getAnonymousElementByAttribute(binding, "class", "castingButton");
  1711               this.castingButton.addEventListener("command", function() {
  1712                 self.startCasting();
  1713               }, false);
  1715               this.video.addEventListener("media-videoCasting", function (e) {
  1716                 if (!e.isTrusted)
  1717                   return;
  1718                 self.updateCasting(e.detail);
  1719               }, false, true);
  1721               // The first time the controls appear we want to just display
  1722               // a play button that does not fade away. The firstShow property
  1723               // makes that happen. But because of bug 718107 this init() method
  1724               // may be called again when we switch in or out of fullscreen
  1725               // mode. So we only set firstShow if we're not autoplaying and
  1726               // if we are at the beginning of the video and not already playing
  1727               if (!this.video.autoplay && this.Utils.dynamicControls && this.video.paused &&
  1728                   this.video.currentTime === 0)
  1729                 this.firstShow = true;
  1731               // If the video is not at the start, then we probably just
  1732               // transitioned into or out of fullscreen mode, and we don't want
  1733               // the controls to remain visible. this.controlsTimeout is a full
  1734               // 5s, which feels too long after the transition.
  1735               if (this.video.currentTime !== 0) {
  1736                 this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
  1739           };
  1740           this.TouchUtils.init(this);
  1741           this.dispatchEvent(new CustomEvent("VideoBindingAttached"));
  1742       ]]>
  1743       </constructor>
  1744       <destructor>
  1745           <![CDATA[
  1746           // XBL destructors don't appear to be inherited properly, so we need
  1747           // to do this here in addition to the videoControls destructor. :-(
  1748           delete this.randomID;
  1749           ]]>
  1750       </destructor>
  1752     </implementation>
  1754     <handlers>
  1755         <handler event="mouseup">
  1756           if(event.originalTarget.nodeName == "vbox") {
  1757             if (this.TouchUtils.firstShow)
  1758               this.Utils.video.play();
  1759             this.TouchUtils.toggleControls();
  1761         </handler>
  1762     </handlers>
  1764   </binding>
  1765 </bindings>

mercurial