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