[GTK][WPE] Media controls numeric position value is not automatically updated during...
[WebKit-https.git] / Source / WebCore / Modules / mediacontrols / mediaControlsAdwaita.js
1 function createControls(root, video, host)
2 {
3     return new Controller(root, video, host);
4 };
5
6 function Controller(root, video, host)
7 {
8     this.video = video;
9     this.root = root;
10     this.host = host;
11     this.controls = {};
12     this.listeners = {};
13     this.isLive = false;
14     this.hasVisualMedia = false;
15
16     this.addVideoListeners();
17     this.createBase();
18     this.createControls();
19     this.updateBase();
20     this.updateControls();
21     this.updateDuration();
22     this.updateTime();
23     this.updatePlaying();
24     this.updateCaptionButton();
25     this.updateCaptionContainer();
26     this.updateFullscreenButton();
27     this.updateVolume();
28     this.updateHasAudio();
29     this.updateHasVideo();
30 };
31
32 Controller.prototype = {
33
34     /* Constants */
35     HandledVideoEvents: {
36         emptied: 'handleReadyStateChange',
37         loadedmetadata: 'handleReadyStateChange',
38         loadeddata: 'handleReadyStateChange',
39         canplay: 'handleReadyStateChange',
40         canplaythrough: 'handleReadyStateChange',
41         timeupdate: 'handleTimeUpdate',
42         durationchange: 'handleDurationChange',
43         playing: 'handlePlay',
44         pause: 'handlePause',
45         volumechange: 'handleVolumeChange',
46         webkitfullscreenchange: 'handleFullscreenChange',
47         webkitbeginfullscreen: 'handleFullscreenChange',
48         webkitendfullscreen: 'handleFullscreenChange',
49     },
50     HideControlsDelay: 4 * 1000,
51     ClassNames: {
52         exit: 'exit',
53         hidden: 'hidden',
54         hiding: 'hiding',
55         muteBox: 'mute-box',
56         muted: 'muted',
57         paused: 'paused',
58         selected: 'selected',
59         show: 'show',
60         noVideo: 'no-video',
61         noDuration: 'no-duration',
62         down: 'down',
63         out: 'out',
64     },
65     KeyCodes: {
66         enter: 13,
67         escape: 27,
68         space: 32,
69         left: 37,
70         up: 38,
71         right: 39,
72         down: 40
73     },
74
75     listenFor: function(element, eventName, handler, useCapture)
76     {
77         if (typeof useCapture === 'undefined')
78             useCapture = false;
79
80         if (!(this.listeners[eventName] instanceof Array))
81             this.listeners[eventName] = [];
82         this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
83         element.addEventListener(eventName, this, useCapture);
84     },
85
86     stopListeningFor: function(element, eventName, handler, useCapture)
87     {
88         if (typeof useCapture === 'undefined')
89             useCapture = false;
90
91         if (!(this.listeners[eventName] instanceof Array))
92             return;
93
94         this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
95             return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
96         });
97         element.removeEventListener(eventName, this, useCapture);
98     },
99
100     addVideoListeners: function()
101     {
102         for (var name in this.HandledVideoEvents) {
103             this.listenFor(this.video, name, this.HandledVideoEvents[name]);
104         };
105
106         /* text tracks */
107         this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
108         this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
109         this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
110
111         /* audio tracks */
112         this.listenFor(this.video.audioTracks, 'change', this.updateHasAudio);
113         this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
114         this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
115
116         /* video tracks */
117         this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
118         this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
119         this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
120
121         /* controls attribute */
122         this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
123         this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
124     },
125
126     removeVideoListeners: function()
127     {
128         for (var name in this.HandledVideoEvents) {
129             this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
130         };
131
132         /* text tracks */
133         this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
134         this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
135         this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
136
137         /* audio tracks */
138         this.stopListeningFor(this.video.audioTracks, 'change', this.updateHasAudio);
139         this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
140         this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
141
142         /* video tracks */
143         this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
144         this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
145         this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
146
147         /* controls attribute */
148         this.controlsObserver.disconnect();
149         delete(this.controlsObserver);
150     },
151
152     handleEvent: function(event)
153     {
154         var preventDefault = false;
155
156         try {
157             if (event.target === this.video) {
158                 var handlerName = this.HandledVideoEvents[event.type];
159                 var handler = this[handlerName];
160                 if (handler && handler instanceof Function)
161                     handler.call(this, event);
162             }
163
164             if (!(this.listeners[event.type] instanceof Array))
165                 return;
166
167             this.listeners[event.type].forEach(function(entry) {
168                 if (entry.element === event.currentTarget && entry.handler instanceof Function)
169                     preventDefault |= entry.handler.call(this, event);
170             }, this);
171         } catch(e) {
172             if (window.console)
173                 console.error(e);
174         }
175
176         if (preventDefault) {
177             event.stopPropagation();
178             event.preventDefault();
179         }
180     },
181
182     createBase: function()
183     {
184         var base = this.base = document.createElement('div');
185         base.setAttribute('pseudo', '-webkit-media-controls');
186         this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
187         this.listenFor(base, 'mouseout', this.handleWrapperMouseOut);
188         if (this.host.textTrackContainer)
189             base.appendChild(this.host.textTrackContainer);
190     },
191
192     isAudio: function()
193     {
194         return this.video instanceof HTMLAudioElement;
195     },
196
197     isFullScreen: function()
198     {
199         return this.video.webkitDisplayingFullscreen;
200     },
201
202     shouldHaveControls: function()
203     {
204         if (!this.isAudio() && !this.host.allowsInlineMediaPlayback)
205             return true;
206
207         return this.video.controls || this.isFullScreen();
208     },
209
210     updateBase: function()
211     {
212         if (this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length)) {
213             if (!this.base.parentNode) {
214                 this.root.appendChild(this.base);
215             }
216         } else {
217             if (this.base.parentNode) {
218                 this.base.parentNode.removeChild(this.base);
219             }
220         }
221     },
222
223     createControls: function()
224     {
225         var enclosure = this.controls.enclosure = document.createElement('div');
226         enclosure.setAttribute('pseudo', '-webkit-media-controls-enclosure');
227
228         var panel = this.controls.panel = document.createElement('div');
229         panel.setAttribute('pseudo', '-webkit-media-controls-panel');
230         panel.setAttribute('aria-label', (this.isAudio() ? 'Audio Playback' : 'Video Playback'));
231         panel.setAttribute('role', 'toolbar');
232         this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
233         this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
234         this.listenFor(panel, 'click', this.handlePanelClick);
235         this.listenFor(panel, 'dblclick', this.handlePanelClick);
236
237         var playButton = this.controls.playButton = document.createElement('button');
238         playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
239         playButton.setAttribute('aria-label', 'Play');
240         this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
241
242         var timelineBox = this.controls.timelineBox = document.createElement('div');
243         timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
244
245         var currentTime = this.controls.currentTime = document.createElement('div');
246         currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
247         currentTime.setAttribute('aria-label', 'Elapsed');
248         currentTime.setAttribute('role', 'timer');
249
250         var timeline = this.controls.timeline = document.createElement('input');
251         timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
252         timeline.setAttribute('aria-label', 'Duration');
253         timeline.type = 'range';
254         timeline.value = 0;
255         timeline.step = .01;
256         this.listenFor(timeline, 'input', this.handleTimelineChange);
257         this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
258
259         var muteBox = this.controls.muteBox = document.createElement('div');
260         muteBox.classList.add(this.ClassNames.muteBox);
261         this.listenFor(muteBox, 'mouseout', this.handleVolumeBoxMouseOut);
262
263         var muteButton = this.controls.muteButton = document.createElement('button');
264         muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
265         muteButton.setAttribute('aria-label', 'Mute');
266         this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
267         this.listenFor(muteButton, 'mouseover', this.handleMuteButtonMouseOver);
268
269         var volumeBox = this.controls.volumeBox = document.createElement('div');
270         volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
271         volumeBox.classList.add(this.ClassNames.hiding);
272         this.listenFor(volumeBox, 'mouseover', this.handleMuteButtonMouseOver);
273
274         var volume = this.controls.volume = document.createElement('input');
275         volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
276         volume.setAttribute('aria-label', 'Volume');
277         volume.type = 'range';
278         volume.min = 0;
279         volume.max = 1;
280         volume.step = .01;
281         this.listenFor(volume, 'input', this.handleVolumeSliderInput);
282         this.listenFor(volume, 'mouseover', this.handleMuteButtonMouseOver);
283
284         var captionButton = this.controls.captionButton = document.createElement('button');
285         captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
286         captionButton.setAttribute('aria-label', 'Captions');
287         captionButton.setAttribute('aria-haspopup', 'true');
288         captionButton.setAttribute('aria-owns', 'audioTrackMenu');
289         this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
290         this.listenFor(captionButton, 'mouseover', this.handleCaptionButtonMouseOver);
291         this.listenFor(captionButton, 'mouseout', this.handleCaptionButtonMouseOut);
292
293         var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
294         fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
295         fullscreenButton.setAttribute('aria-label', 'Display Full Screen');
296         fullscreenButton.disabled = true;
297         this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
298     },
299
300     configureControls: function()
301     {
302         this.controls.panel.appendChild(this.controls.playButton);
303         this.controls.panel.appendChild(this.controls.timeline);
304         this.controls.panel.appendChild(this.controls.currentTime);
305         this.controls.panel.appendChild(this.controls.muteBox);
306         this.controls.muteBox.appendChild(this.controls.muteButton);
307         this.controls.muteBox.appendChild(this.controls.volumeBox);
308         this.controls.volumeBox.appendChild(this.controls.volume);
309         this.controls.panel.appendChild(this.controls.captionButton);
310         if (!this.isAudio())
311             this.controls.panel.appendChild(this.controls.fullscreenButton);
312         this.controls.enclosure.appendChild(this.controls.panel);
313     },
314
315     disconnectControls: function(event)
316     {
317         for (var item in this.controls) {
318             var control = this.controls[item];
319             if (control && control.parentNode)
320                 control.parentNode.removeChild(control);
321        }
322     },
323
324     reconnectControls: function()
325     {
326         this.disconnectControls();
327         this.configureControls();
328
329         if (this.shouldHaveControls())
330             this.addControls();
331     },
332
333     showControls: function()
334     {
335         this.controls.panel.classList.remove(this.ClassNames.hidden);
336         this.controls.panel.classList.add(this.ClassNames.show);
337
338         this.updateTime();
339     },
340
341     hideControls: function()
342     {
343         this.controls.panel.classList.remove(this.ClassNames.show);
344     },
345
346     resetHideControlsTimer: function()
347     {
348         if (this.hideTimer)
349             clearTimeout(this.hideTimer);
350         this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
351     },
352
353     clearHideControlsTimer: function()
354     {
355         if (this.hideTimer)
356             clearTimeout(this.hideTimer);
357         this.hideTimer = null;
358     },
359
360     controlsAreAlwaysVisible: function()
361     {
362         return this.isAudio() || this.controls.panel.classList.contains(this.ClassNames.noVideo);
363     },
364
365     controlsAreHidden: function()
366     {
367         if (this.controlsAreAlwaysVisible())
368             return false;
369
370         var panel = this.controls.panel;
371         return (!panel.classList.contains(this.ClassNames.show) || panel.classList.contains(this.ClassNames.hidden))
372             && (panel.parentElement.querySelector(':hover') !== panel);
373     },
374
375     addControls: function()
376     {
377         this.base.appendChild(this.controls.enclosure);
378     },
379
380     removeControls: function()
381     {
382         if (this.controls.enclosure.parentNode)
383             this.controls.enclosure.parentNode.removeChild(this.controls.enclosure);
384         this.hideCaptionMenu();
385     },
386
387     updateControls: function()
388     {
389         this.reconnectControls();
390     },
391
392     setIsLive: function(live)
393     {
394         if (live === this.isLive)
395             return;
396         this.isLive = live;
397
398         this.reconnectControls();
399         this.controls.timeline.disabled = this.isLive;
400     },
401
402     updateDuration: function()
403     {
404         var duration = this.video.duration;
405         this.setIsLive(duration === Number.POSITIVE_INFINITY);
406         this.controls.timeline.min = 0;
407         this.controls.timeline.max = this.isLive ? 0 : duration;
408         if (this.isLive || isNaN(duration))
409             this.timeDigitsCount = 4;
410         else if (duration < 10 * 60) /* Ten minutes */
411             this.timeDigitsCount = 3;
412         else if (duration < 60 * 60) /* One hour */
413             this.timeDigitsCount = 4;
414         else if (duration < 10 * 60 * 60) /* Ten hours */
415             this.timeDigitsCount = 5;
416         else
417             this.timeDigitsCount = 6;
418     },
419
420     formatTime: function(time)
421     {
422         if (isNaN(time))
423             return '00:00';
424
425         const absTime = Math.abs(time);
426         const seconds = Math.floor(absTime % 60).toFixed(0);
427         const minutes = Math.floor((absTime / 60) % 60).toFixed(0);
428         const hours = Math.floor(absTime / (60 * 60)).toFixed(0);
429
430         function prependZeroIfNeeded(value) {
431             if (value < 10)
432                 return `0${value}`;
433             return `${value}`;
434         }
435
436         switch (this.timeDigitsCount) {
437         case 3:
438             return minutes + ':' + prependZeroIfNeeded(seconds);
439         case 4:
440             return prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds);
441         case 5:
442             return hours + ':' + prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds);
443         case 6:
444             return prependZeroIfNeeded(hours) + ':' + prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds);
445         }
446     },
447
448     updateTime: function(forceUpdate)
449     {
450         if (!forceUpdate && this.controlsAreHidden())
451             return;
452
453         var currentTime = this.video.currentTime;
454         this.controls.timeline.value = currentTime;
455         this.controls.currentTime.innerText = this.formatTime(currentTime);
456         if (!this.isLive) {
457             var duration = this.video.duration;
458             this.controls.currentTime.innerText += " / " + this.formatTime(duration);
459             this.controls.currentTime.classList.toggle(this.ClassNames.noDuration, !duration);
460             this.controls.timeline.disabled = !duration;
461         } else
462             this.controls.currentTime.classList.remove(this.ClassNames.noDuration);
463     },
464
465     setPlaying: function(isPlaying)
466     {
467         if (this.isPlaying === isPlaying)
468             return;
469         this.isPlaying = isPlaying;
470
471         if (!isPlaying) {
472             this.controls.panel.classList.add(this.ClassNames.paused);
473             this.controls.playButton.classList.add(this.ClassNames.paused);
474             this.controls.playButton.setAttribute('aria-label', 'Play');
475             this.showControls();
476         } else {
477             this.controls.panel.classList.remove(this.ClassNames.paused);
478             this.controls.playButton.classList.remove(this.ClassNames.paused);
479             this.controls.playButton.setAttribute('aria-label', 'Pause');
480
481             this.hideControls();
482             this.resetHideControlsTimer();
483         }
484     },
485
486     updatePlaying: function()
487     {
488         this.setPlaying(!this.canPlay());
489         if (!this.canPlay())
490             this.showControls();
491     },
492
493     updateCaptionButton: function()
494     {
495         if (this.video.webkitHasClosedCaptions)
496             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
497         else
498             this.controls.captionButton.classList.add(this.ClassNames.hidden);
499     },
500
501     updateCaptionContainer: function()
502     {
503         if (!this.host.textTrackContainer)
504             return;
505
506         var hasClosedCaptions = this.video.webkitHasClosedCaptions;
507         var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
508
509         if (hasClosedCaptions && hasHiddenClass)
510             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
511         else if (!hasClosedCaptions && !hasHiddenClass)
512             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
513
514         this.updateBase();
515         this.host.updateTextTrackContainer();
516     },
517
518     updateFullscreenButton: function()
519     {
520         if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.hasVisualMedia) {
521             this.controls.fullscreenButton.classList.add(this.ClassNames.hidden);
522             return;
523         }
524
525         this.controls.fullscreenButton.disabled = !this.host.supportsFullscreen;
526     },
527
528     updateVolume: function()
529     {
530         if (this.video.muted || !this.video.volume) {
531             this.controls.muteButton.classList.add(this.ClassNames.muted);
532             this.controls.volume.value = 0;
533         } else {
534             this.controls.muteButton.classList.remove(this.ClassNames.muted);
535             this.controls.volume.value = this.video.volume;
536         }
537     },
538
539     updateHasAudio: function()
540     {
541         this.controls.muteButton.disabled = this.video.audioTracks.length == 0;
542     },
543
544     updateHasVideo: function()
545     {
546         if (this.video.videoTracks.length)
547             this.controls.panel.classList.remove(this.ClassNames.noVideo);
548         else
549             this.controls.panel.classList.add(this.ClassNames.noVideo);
550     },
551
552     handleReadyStateChange: function(event)
553     {
554         this.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0;
555         this.updateVolume();
556         this.updateDuration();
557         this.updateCaptionButton();
558         this.updateCaptionContainer();
559         this.updateFullscreenButton();
560     },
561
562     handleTimeUpdate: function(event)
563     {
564         this.updateTime();
565     },
566
567     handleDurationChange: function(event)
568     {
569         this.updateDuration();
570         this.updateTime(true);
571     },
572
573     handlePlay: function(event)
574     {
575         this.setPlaying(true);
576     },
577
578     handlePause: function(event)
579     {
580         this.setPlaying(false);
581     },
582
583     handleVolumeChange: function(event)
584     {
585         this.updateVolume();
586     },
587
588     handleFullscreenChange: function(event)
589     {
590         this.updateBase();
591         this.updateControls();
592
593         if (this.isFullScreen()) {
594             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
595             this.controls.fullscreenButton.setAttribute('aria-label', 'Exit Full Screen');
596             this.host.enteredFullscreen();
597         } else {
598             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
599             this.controls.fullscreenButton.setAttribute('aria-label', 'Display Full Screen');
600             this.host.exitedFullscreen();
601         }
602     },
603
604     handleTextTrackChange: function(event)
605     {
606         this.updateCaptionContainer();
607     },
608
609     handleTextTrackAdd: function(event)
610     {
611         this.updateCaptionButton();
612         this.updateCaptionContainer();
613     },
614
615     handleTextTrackRemove: function(event)
616     {
617         this.updateCaptionButton();
618         this.updateCaptionContainer();
619     },
620
621     handleControlsChange: function()
622     {
623         try {
624             this.updateBase();
625
626             if (this.shouldHaveControls())
627                 this.addControls();
628             else
629                 this.removeControls();
630         } catch(e) {
631             if (window.console)
632                 console.error(e);
633         }
634     },
635
636     handleWrapperMouseMove: function(event)
637     {
638         this.showControls();
639         this.resetHideControlsTimer();
640
641         if (!this.isDragging)
642             return;
643         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
644         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
645         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
646         event.stopPropagation()
647     },
648
649     handleWrapperMouseOut: function(event)
650     {
651         this.hideControls();
652         this.clearHideControlsTimer();
653     },
654
655     handleWrapperMouseUp: function(event)
656     {
657         this.isDragging = false;
658         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
659     },
660
661     handlePanelMouseDown: function(event)
662     {
663         if (event.target != this.controls.panel)
664             return;
665
666         if (!this.isFullScreen())
667             return;
668
669         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
670         this.isDragging = true;
671         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
672         this.initialOffset = new WebKitPoint(
673             parseInt(this.controls.panel.style.left) | 0,
674             parseInt(this.controls.panel.style.top) | 0
675         );
676     },
677
678     handlePanelTransitionEnd: function(event)
679     {
680         var opacity = window.getComputedStyle(this.controls.panel).opacity;
681         if (parseInt(opacity) > 0)
682             this.controls.panel.classList.remove(this.ClassNames.hidden);
683         else
684             this.controls.panel.classList.add(this.ClassNames.hidden);
685     },
686
687     handlePanelClick: function(event)
688     {
689         /* Prevent clicks in the panel from playing or pausing the video in a MediaDocument. */
690         event.preventDefault();
691     },
692
693     canPlay: function()
694     {
695         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
696     },
697
698     handlePlayButtonClicked: function(event)
699     {
700         if (this.canPlay())
701             this.video.play();
702         else
703             this.video.pause();
704         return true;
705     },
706
707     handleTimelineChange: function(event)
708     {
709         this.video.fastSeek(this.controls.timeline.value);
710     },
711
712     handleTimelineMouseUp: function(event)
713     {
714         /* Do a precise seek when we lift the mouse. */
715         this.video.currentTime = this.controls.timeline.value;
716     },
717
718     handleMuteButtonClicked: function(event)
719     {
720         this.video.muted = !this.video.muted;
721         if (this.video.muted)
722             this.controls.muteButton.setAttribute('aria-label', 'Unmute');
723         return true;
724     },
725
726     handleMuteButtonMouseOver: function(event)
727     {
728         if (this.video.offsetTop + this.controls.enclosure.offsetTop < 105) {
729             this.controls.volumeBox.classList.add(this.ClassNames.down);
730             this.controls.panel.classList.add(this.ClassNames.down);
731         } else {
732             this.controls.volumeBox.classList.remove(this.ClassNames.down);
733             this.controls.panel.classList.remove(this.ClassNames.down);
734         }
735         this.controls.volumeBox.classList.remove(this.ClassNames.hiding);
736
737         return true;
738     },
739
740     handleVolumeBoxMouseOut: function(event)
741     {
742         this.controls.volumeBox.classList.add(this.ClassNames.hiding);
743         return true;
744     },
745
746     handleVolumeSliderInput: function(event)
747     {
748         if (this.video.muted) {
749             this.video.muted = false;
750             this.controls.muteButton.setAttribute('aria-label', 'Mute');
751         }
752         this.video.volume = this.controls.volume.value;
753     },
754
755     handleFullscreenButtonClicked: function(event)
756     {
757         if (this.isFullScreen())
758             this.video.webkitExitFullscreen();
759         else
760             this.video.webkitEnterFullscreen();
761         return true;
762     },
763
764     buildCaptionMenu: function()
765     {
766         var tracks = this.host.sortedTrackListForMenu(this.video.textTracks);
767         if (!tracks || !tracks.length)
768             return;
769
770         this.captionMenu = document.createElement('div');
771         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
772         this.captionMenu.setAttribute('id', 'audioTrackMenu');
773         this.listenFor(this.captionMenu, 'mouseout', this.handleCaptionMenuMouseOut);
774         this.listenFor(this.captionMenu, 'transitionend', this.captionMenuTransitionEnd);
775         this.base.appendChild(this.captionMenu);
776         this.captionMenu.captionMenuTreeElements = [];
777         this.captionMenuItems = [];
778
779         var offItem = this.host.captionMenuOffItem;
780         var automaticItem = this.host.captionMenuAutomaticItem;
781         var displayMode = this.host.captionDisplayMode;
782
783         var list = document.createElement('div');
784         this.captionMenu.appendChild(list);
785         this.captionMenu.captionMenuTreeElements.push(list);
786
787         var heading = document.createElement('h3');
788         heading.id = 'webkitMediaControlsClosedCaptionsHeading';
789         list.appendChild(heading);
790         heading.innerText = 'Subtitles';
791         this.captionMenu.captionMenuTreeElements.push(heading);
792
793         var ul = document.createElement('ul');
794         ul.setAttribute('role', 'menu');
795         ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
796         list.appendChild(ul);
797         this.captionMenu.captionMenuTreeElements.push(ul);
798
799         for (var i = 0; i < tracks.length; ++i) {
800             var menuItem = document.createElement('li');
801             menuItem.setAttribute('role', 'menuitemradio');
802             menuItem.setAttribute('tabindex', '-1');
803             this.captionMenuItems.push(menuItem);
804             this.listenFor(menuItem, 'click', this.captionItemSelected);
805             this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
806             ul.appendChild(menuItem);
807
808             var track = tracks[i];
809             menuItem.innerText = this.host.displayNameForTrack(track);
810             menuItem.track = track;
811
812             if (track === offItem) {
813                 var offMenu = menuItem;
814                 continue;
815             }
816
817             if (track === automaticItem) {
818                 if (displayMode === 'automatic') {
819                     menuItem.classList.add(this.ClassNames.selected);
820                     menuItem.setAttribute('tabindex', '0');
821                     menuItem.setAttribute('aria-checked', 'true');
822                 }
823                 continue;
824             }
825
826             if (displayMode != 'automatic' && track.mode === 'showing') {
827                 var trackMenuItemSelected = true;
828                 menuItem.classList.add(this.ClassNames.selected);
829                 menuItem.setAttribute('tabindex', '0');
830                 menuItem.setAttribute('aria-checked', 'true');
831             }
832         }
833
834         if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
835             offMenu.classList.add(this.ClassNames.selected);
836             menuItem.setAttribute('tabindex', '0');
837             menuItem.setAttribute('aria-checked', 'true');
838         }
839
840         /* Focus first selected menuitem. */
841         for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
842             var item = this.captionMenuItems[i];
843             if (item.classList.contains(this.ClassNames.selected)) {
844                 item.focus();
845                 break;
846             }
847         }
848
849         /* Caption menu has to be centered to the caption button. */
850         var captionButtonCenter =  this.controls.panel.offsetLeft + this.controls.captionButton.offsetLeft +
851             this.controls.captionButton.offsetWidth / 2;
852         var captionMenuLeft = (captionButtonCenter - this.captionMenu.offsetWidth / 2);
853         if (captionMenuLeft + this.captionMenu.offsetWidth > this.controls.panel.offsetLeft + this.controls.panel.offsetWidth)
854             this.captionMenu.classList.add(this.ClassNames.out);
855         this.captionMenu.style.left = captionMenuLeft + 'px';
856         /* As height is not in the css, it needs to be specified to animate it. */
857         this.captionMenu.height = this.captionMenu.offsetHeight;
858         this.captionMenu.style.height = 0;
859     },
860
861     captionItemSelected: function(event)
862     {
863         this.host.setSelectedTextTrack(event.target.track);
864         this.hideCaptionMenu();
865     },
866
867     focusSiblingCaptionItem: function(event)
868     {
869         var currentItem = event.target;
870         var pendingItem = false;
871         switch(event.keyCode) {
872         case this.KeyCodes.left:
873         case this.KeyCodes.up:
874             pendingItem = currentItem.previousSibling;
875             break;
876         case this.KeyCodes.right:
877         case this.KeyCodes.down:
878             pendingItem = currentItem.nextSibling;
879             break;
880         }
881         if (pendingItem) {
882             currentItem.setAttribute('tabindex', '-1');
883             pendingItem.setAttribute('tabindex', '0');
884             pendingItem.focus();
885         }
886     },
887
888     handleCaptionItemKeyUp: function(event)
889     {
890         switch (event.keyCode) {
891         case this.KeyCodes.enter:
892         case this.KeyCodes.space:
893             this.captionItemSelected(event);
894             break;
895         case this.KeyCodes.escape:
896             this.hideCaptionMenu();
897             break;
898         case this.KeyCodes.left:
899         case this.KeyCodes.up:
900         case this.KeyCodes.right:
901         case this.KeyCodes.down:
902             this.focusSiblingCaptionItem(event);
903             break;
904         default:
905             return;
906         }
907
908         event.stopPropagation();
909         event.preventDefault();
910     },
911
912     showCaptionMenu: function()
913     {
914         if (!this.captionMenu)
915             this.buildCaptionMenu();
916         this.captionMenu.style.height = this.captionMenu.height + 'px';
917     },
918
919     hideCaptionMenu: function()
920     {
921         if (!this.captionMenu)
922             return;
923         this.captionMenu.style.height = 0;
924     },
925
926     captionMenuTransitionEnd: function(event)
927     {
928         if (this.captionMenu.offsetHeight !== 0)
929             return;
930
931         this.captionMenuItems.forEach(function(item) {
932             this.stopListeningFor(item, 'click', this.captionItemSelected);
933             this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
934         }, this);
935
936         /* FKA and AX: focus the trigger before destroying the element with focus. */
937         if (this.controls.captionButton)
938             this.controls.captionButton.focus();
939
940         if (this.captionMenu.parentNode)
941             this.captionMenu.parentNode.removeChild(this.captionMenu);
942         delete this.captionMenu;
943         delete this.captionMenuItems;
944     },
945
946     captionMenuContainsNode: function(node)
947     {
948         return this.captionMenu.captionMenuTreeElements.find((item) => item == node)
949             || this.captionMenuItems.find((item) => item == node);
950     },
951
952     handleCaptionButtonClicked: function(event)
953     {
954         this.showCaptionMenu();
955         return true;
956     },
957
958     handleCaptionButtonMouseOver: function(event)
959     {
960         this.showCaptionMenu();
961         return true;
962     },
963
964     handleCaptionButtonMouseOut: function(event)
965     {
966         if (this.captionMenu && !this.captionMenuContainsNode(event.relatedTarget))
967             this.hideCaptionMenu();
968         return true;
969     },
970
971     handleCaptionMenuMouseOut: function(event)
972     {
973         if (event.relatedTarget != this.controls.captionButton && !this.captionMenuContainsNode(event.relatedTarget))
974             this.hideCaptionMenu();
975         return true;
976     },
977 };