toolkit/content/widgets/videocontrols.xml

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:f1ae7c2fbe73
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>

mercurial