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