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.

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

mercurial