[iOS Media] incorrect front padding on time values
[WebKit-https.git] / Source / WebCore / Modules / mediacontrols / mediaControlsApple.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.statusHidden = true;
15     this.hasVisualMedia = false;
16
17     this.addVideoListeners();
18     this.createBase();
19     this.createControls();
20     this.updateBase();
21     this.updateControls();
22     this.updateDuration();
23     this.updateProgress();
24     this.updateTime();
25     this.updateReadyState();
26     this.updatePlaying();
27     this.updateThumbnail();
28     this.updateCaptionButton();
29     this.updateCaptionContainer();
30     this.updateFullscreenButtons();
31     this.updateVolume();
32     this.updateHasAudio();
33     this.updateHasVideo();
34 };
35
36 /* Enums */
37 Controller.InlineControls = 0;
38 Controller.FullScreenControls = 1;
39
40 Controller.PlayAfterSeeking = 0;
41 Controller.PauseAfterSeeking = 1;
42
43 Controller.prototype = {
44
45     /* Constants */
46     HandledVideoEvents: {
47         loadstart: 'handleLoadStart',
48         error: 'handleError',
49         abort: 'handleAbort',
50         suspend: 'handleSuspend',
51         stalled: 'handleStalled',
52         waiting: 'handleWaiting',
53         emptied: 'handleReadyStateChange',
54         loadedmetadata: 'handleReadyStateChange',
55         loadeddata: 'handleReadyStateChange',
56         canplay: 'handleReadyStateChange',
57         canplaythrough: 'handleReadyStateChange',
58         timeupdate: 'handleTimeUpdate',
59         durationchange: 'handleDurationChange',
60         playing: 'handlePlay',
61         pause: 'handlePause',
62         progress: 'handleProgress',
63         volumechange: 'handleVolumeChange',
64         webkitfullscreenchange: 'handleFullscreenChange',
65         webkitbeginfullscreen: 'handleFullscreenChange',
66         webkitendfullscreen: 'handleFullscreenChange',
67     },
68     HideControlsDelay: 4 * 1000,
69     RewindAmount: 30,
70     MaximumSeekRate: 8,
71     SeekDelay: 1500,
72     ClassNames: {
73         active: 'active',
74         exit: 'exit',
75         failed: 'failed',
76         hidden: 'hidden',
77         hiding: 'hiding',
78         hourLongTime: 'hour-long-time',
79         list: 'list',
80         muteBox: 'mute-box',
81         muted: 'muted',
82         paused: 'paused',
83         playing: 'playing',
84         selected: 'selected',
85         show: 'show',
86         thumbnail: 'thumbnail',
87         thumbnailImage: 'thumbnail-image',
88         thumbnailTrack: 'thumbnail-track',
89         volumeBox: 'volume-box',
90         noVideo: 'no-video',
91         down: 'down',
92         out: 'out',
93     },
94     KeyCodes: {
95         enter: 13,
96         escape: 27,
97         space: 32,
98         pageUp: 33,
99         pageDown: 34,
100         end: 35,
101         home: 36,
102         left: 37,
103         up: 38,
104         right: 39,
105         down: 40
106     },
107
108     extend: function(child)
109     {
110         for (var property in this) {
111             if (!child.hasOwnProperty(property))
112                 child[property] = this[property];
113         }
114     },
115
116     UIString: function(developmentString, replaceString, replacementString)
117     {
118         var localized = UIStringTable[developmentString];
119         if (replaceString && replacementString)
120             return localized.replace(replaceString, replacementString);
121
122         if (localized)
123             return localized;
124
125         console.error("Localization for string \"" + developmentString + "\" not found.");
126         return "LOCALIZED STRING NOT FOUND";
127     },
128
129     listenFor: function(element, eventName, handler, useCapture)
130     {
131         if (typeof useCapture === 'undefined')
132             useCapture = false;
133
134         if (!(this.listeners[eventName] instanceof Array))
135             this.listeners[eventName] = [];
136         this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
137         element.addEventListener(eventName, this, useCapture);
138     },
139
140     stopListeningFor: function(element, eventName, handler, useCapture)
141     {
142         if (typeof useCapture === 'undefined')
143             useCapture = false;
144
145         if (!(this.listeners[eventName] instanceof Array))
146             return;
147
148         this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
149             return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
150         });
151         element.removeEventListener(eventName, this, useCapture);
152     },
153
154     addVideoListeners: function()
155     {
156         for (var name in this.HandledVideoEvents) {
157             this.listenFor(this.video, name, this.HandledVideoEvents[name]);
158         };
159
160         /* text tracks */
161         this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
162         this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
163         this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
164
165         /* audio tracks */
166         this.listenFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
167         this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
168         this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
169
170         /* video tracks */
171         this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
172         this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
173         this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
174
175         /* controls attribute */
176         this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
177         this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
178     },
179
180     removeVideoListeners: function()
181     {
182         for (var name in this.HandledVideoEvents) {
183             this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
184         };
185
186         /* text tracks */
187         this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
188         this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
189         this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
190
191         /* audio tracks */
192         this.stopListeningFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
193         this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
194         this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
195
196         /* video tracks */
197         this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
198         this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
199         this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
200
201         /* controls attribute */
202         this.controlsObserver.disconnect();
203         delete(this.controlsObserver);
204     },
205
206     handleEvent: function(event)
207     {
208         var preventDefault = false;
209
210         try {
211             if (event.target === this.video) {
212                 var handlerName = this.HandledVideoEvents[event.type];
213                 var handler = this[handlerName];
214                 if (handler && handler instanceof Function)
215                     handler.call(this, event);
216             }
217
218             if (!(this.listeners[event.type] instanceof Array))
219                 return;
220
221             this.listeners[event.type].forEach(function(entry) {
222                 if (entry.element === event.currentTarget && entry.handler instanceof Function)
223                     preventDefault |= entry.handler.call(this, event);
224             }, this);
225         } catch(e) {
226             if (window.console)
227                 console.error(e);
228         }
229
230         if (preventDefault) {
231             event.stopPropagation();
232             event.preventDefault();
233         }
234     },
235
236     createBase: function()
237     {
238         var base = this.base = document.createElement('div');
239         base.setAttribute('pseudo', '-webkit-media-controls');
240         this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
241         this.listenFor(base, 'mouseout', this.handleWrapperMouseOut);
242         if (this.host.textTrackContainer)
243             base.appendChild(this.host.textTrackContainer);
244     },
245
246     shouldHaveAnyUI: function()
247     {
248         return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length);
249     },
250
251     shouldHaveControls: function()
252     {
253         if (!this.isAudio() && !this.host.mediaPlaybackAllowsInline)
254             return true;
255
256         return this.video.controls || this.isFullScreen();
257     },
258
259     setNeedsTimelineMetricsUpdate: function()
260     {
261         this.timelineMetricsNeedsUpdate = true;
262     },
263
264     updateTimelineMetricsIfNeeded: function()
265     {
266         if (this.timelineMetricsNeedsUpdate) {
267             this.timelineLeft = this.controls.timeline.offsetLeft;
268             this.timelineWidth = this.controls.timeline.offsetWidth;
269             this.timelineHeight = this.controls.timeline.offsetHeight;
270             this.timelineMetricsNeedsUpdate = false;
271         }
272     },
273
274     updateBase: function()
275     {
276         if (this.shouldHaveAnyUI()) {
277             if (!this.base.parentNode) {
278                 this.root.appendChild(this.base);
279             }
280         } else {
281             if (this.base.parentNode) {
282                 this.base.parentNode.removeChild(this.base);
283             }
284         }
285     },
286
287     createControls: function()
288     {
289         var panel = this.controls.panel = document.createElement('div');
290         panel.setAttribute('pseudo', '-webkit-media-controls-panel');
291         panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
292         panel.setAttribute('role', 'toolbar');
293         this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
294         this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
295         this.listenFor(panel, 'click', this.handlePanelClick);
296         this.listenFor(panel, 'dblclick', this.handlePanelClick);
297         this.listenFor(panel, 'dragstart', this.handlePanelDragStart);
298
299         var rewindButton = this.controls.rewindButton = document.createElement('button');
300         rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
301         rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
302         this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
303
304         var seekBackButton = this.controls.seekBackButton = document.createElement('button');
305         seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
306         seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
307         this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
308         this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
309
310         var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
311         seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
312         seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
313         this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
314         this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
315
316         var playButton = this.controls.playButton = document.createElement('button');
317         playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
318         playButton.setAttribute('aria-label', this.UIString('Play'));
319         this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
320
321         var statusDisplay = this.controls.statusDisplay = document.createElement('div');
322         statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
323         statusDisplay.classList.add(this.ClassNames.hidden);
324
325         var timelineBox = this.controls.timelineBox = document.createElement('div');
326         timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
327
328         var currentTime = this.controls.currentTime = document.createElement('div');
329         currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
330         currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
331         currentTime.setAttribute('role', 'timer');
332
333         var timeline = this.controls.timeline = document.createElement('input');
334         timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
335         timeline.setAttribute('aria-label', this.UIString('Duration'));
336         timeline.type = 'range';
337         timeline.value = 0;
338         this.listenFor(timeline, 'input', this.handleTimelineInput);
339         this.listenFor(timeline, 'change', this.handleTimelineChange);
340         this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
341         this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
342         this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
343         this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
344         this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
345         timeline.step = .01;
346
347         var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
348         thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
349
350         var thumbnail = this.controls.thumbnail = document.createElement('div');
351         thumbnail.classList.add(this.ClassNames.thumbnail);
352
353         var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
354         thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
355
356         var remainingTime = this.controls.remainingTime = document.createElement('div');
357         remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
358         remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
359         remainingTime.setAttribute('role', 'timer');
360
361         var muteBox = this.controls.muteBox = document.createElement('div');
362         muteBox.classList.add(this.ClassNames.muteBox);
363
364         var muteButton = this.controls.muteButton = document.createElement('button');
365         muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
366         muteButton.setAttribute('aria-label', this.UIString('Mute'));
367         this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
368
369         var minButton = this.controls.minButton = document.createElement('button');
370         minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
371         minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
372         this.listenFor(minButton, 'click', this.handleMinButtonClicked);
373
374         var maxButton = this.controls.maxButton = document.createElement('button');
375         maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
376         maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
377         this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
378
379         var volumeBox = this.controls.volumeBox = document.createElement('div');
380         volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
381         volumeBox.classList.add(this.ClassNames.volumeBox);
382
383         var volume = this.controls.volume = document.createElement('input');
384         volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
385         volume.setAttribute('aria-label', this.UIString('Volume'));
386         volume.type = 'range';
387         volume.min = 0;
388         volume.max = 1;
389         volume.step = .01;
390         this.listenFor(volume, 'input', this.handleVolumeSliderInput);
391
392         var captionButton = this.controls.captionButton = document.createElement('button');
393         captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
394         captionButton.setAttribute('aria-label', this.UIString('Captions'));
395         captionButton.setAttribute('aria-haspopup', 'true');
396         this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
397
398         var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
399         fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
400         fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
401         this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
402
403         var optimizedFullscreenButton = this.controls.optimizedFullscreenButton = document.createElement('button');
404         optimizedFullscreenButton.setAttribute('pseudo', '-webkit-media-controls-optimized-fullscreen-button');
405         optimizedFullscreenButton.setAttribute('aria-label', this.UIString('Display Optimized Full Screen'));
406         this.listenFor(optimizedFullscreenButton, 'click', this.handleOptimizedFullscreenButtonClicked);
407     },
408
409     setControlsType: function(type)
410     {
411         if (type === this.controlsType)
412             return;
413         this.controlsType = type;
414
415         this.reconnectControls();
416     },
417
418     setIsLive: function(live)
419     {
420         if (live === this.isLive)
421             return;
422         this.isLive = live;
423
424         this.updateStatusDisplay();
425
426         this.reconnectControls();
427     },
428
429     reconnectControls: function()
430     {
431         this.disconnectControls();
432
433         if (this.controlsType === Controller.InlineControls)
434             this.configureInlineControls();
435         else if (this.controlsType == Controller.FullScreenControls)
436             this.configureFullScreenControls();
437
438         if (this.shouldHaveControls())
439             this.addControls();
440     },
441
442     disconnectControls: function(event)
443     {
444         for (var item in this.controls) {
445             var control = this.controls[item];
446             if (control && control.parentNode)
447                 control.parentNode.removeChild(control);
448        }
449     },
450
451     configureInlineControls: function()
452     {
453         if (!this.isLive)
454             this.controls.panel.appendChild(this.controls.rewindButton);
455         this.controls.panel.appendChild(this.controls.playButton);
456         this.controls.panel.appendChild(this.controls.statusDisplay);
457         if (!this.isLive) {
458             this.controls.panel.appendChild(this.controls.timelineBox);
459             this.controls.timelineBox.appendChild(this.controls.currentTime);
460             this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
461             this.controls.thumbnailTrack.appendChild(this.controls.timeline);
462             this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
463             this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
464             this.controls.timelineBox.appendChild(this.controls.remainingTime);
465         }
466         this.controls.panel.appendChild(this.controls.muteBox);
467         this.controls.muteBox.appendChild(this.controls.volumeBox);
468         this.controls.volumeBox.appendChild(this.controls.volume);
469         this.controls.muteBox.appendChild(this.controls.muteButton);
470         this.controls.panel.appendChild(this.controls.captionButton);
471         if (!this.isAudio())
472             this.controls.panel.appendChild(this.controls.fullscreenButton);
473
474         this.controls.panel.style.removeProperty('left');
475         this.controls.panel.style.removeProperty('top');
476         this.controls.panel.style.removeProperty('bottom');
477     },
478
479     configureFullScreenControls: function()
480     {
481         this.controls.panel.appendChild(this.controls.volumeBox);
482         this.controls.volumeBox.appendChild(this.controls.minButton);
483         this.controls.volumeBox.appendChild(this.controls.volume);
484         this.controls.volumeBox.appendChild(this.controls.maxButton);
485         this.controls.panel.appendChild(this.controls.seekBackButton);
486         this.controls.panel.appendChild(this.controls.playButton);
487         this.controls.panel.appendChild(this.controls.seekForwardButton);
488         this.controls.panel.appendChild(this.controls.captionButton);
489         if (!this.isAudio())
490             this.controls.panel.appendChild(this.controls.fullscreenButton);
491         if (!this.isLive) {
492             this.controls.panel.appendChild(this.controls.timelineBox);
493             this.controls.timelineBox.appendChild(this.controls.currentTime);
494             this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
495             this.controls.thumbnailTrack.appendChild(this.controls.timeline);
496             this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
497             this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
498             this.controls.timelineBox.appendChild(this.controls.remainingTime);
499         } else
500             this.controls.panel.appendChild(this.controls.statusDisplay);
501     },
502
503     updateControls: function()
504     {
505         if (this.isFullScreen())
506             this.setControlsType(Controller.FullScreenControls);
507         else
508             this.setControlsType(Controller.InlineControls);
509
510         this.setNeedsTimelineMetricsUpdate();
511     },
512
513     updateStatusDisplay: function(event)
514     {
515         if (this.video.error !== null)
516             this.controls.statusDisplay.innerText = this.UIString('Error');
517         else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
518             this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
519         else if (this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
520             this.controls.statusDisplay.innerText = this.UIString('Loading');
521         else
522             this.controls.statusDisplay.innerText = '';
523
524         this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error);
525     },
526
527     handleLoadStart: function(event)
528     {
529         this.updateStatusDisplay();
530         this.updateProgress();
531     },
532
533     handleError: function(event)
534     {
535         this.updateStatusDisplay();
536     },
537
538     handleAbort: function(event)
539     {
540         this.updateStatusDisplay();
541     },
542
543     handleSuspend: function(event)
544     {
545         this.updateStatusDisplay();
546     },
547
548     handleStalled: function(event)
549     {
550         this.updateStatusDisplay();
551         this.updateProgress();
552     },
553
554     handleWaiting: function(event)
555     {
556         this.updateStatusDisplay();
557     },
558
559     handleReadyStateChange: function(event)
560     {
561         this.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0;
562         this.updateReadyState();
563         this.updateDuration();
564         this.updateCaptionButton();
565         this.updateCaptionContainer();
566         this.updateFullscreenButtons();
567         this.updateProgress();
568     },
569
570     handleTimeUpdate: function(event)
571     {
572         if (!this.scrubbing)
573             this.updateTime();
574     },
575
576     handleDurationChange: function(event)
577     {
578         this.updateDuration();
579         this.updateTime(true);
580         this.updateProgress(true);
581     },
582
583     handlePlay: function(event)
584     {
585         this.setPlaying(true);
586     },
587
588     handlePause: function(event)
589     {
590         this.setPlaying(false);
591     },
592
593     handleProgress: function(event)
594     {
595         this.updateProgress();
596     },
597
598     handleVolumeChange: function(event)
599     {
600         this.updateVolume();
601     },
602
603     handleTextTrackChange: function(event)
604     {
605         this.updateCaptionContainer();
606     },
607
608     handleTextTrackAdd: function(event)
609     {
610         var track = event.track;
611
612         if (this.trackHasThumbnails(track) && track.mode === 'disabled')
613             track.mode = 'hidden';
614
615         this.updateThumbnail();
616         this.updateCaptionButton();
617         this.updateCaptionContainer();
618     },
619
620     handleTextTrackRemove: function(event)
621     {
622         this.updateThumbnail();
623         this.updateCaptionButton();
624         this.updateCaptionContainer();
625     },
626
627     handleAudioTrackChange: function(event)
628     {
629         this.updateHasAudio();
630     },
631
632     handleAudioTrackAdd: function(event)
633     {
634         this.updateHasAudio();
635         this.updateCaptionButton();
636     },
637
638     handleAudioTrackRemove: function(event)
639     {
640         this.updateHasAudio();
641         this.updateCaptionButton();
642     },
643
644     isFullScreen: function()
645     {
646         return this.video.webkitDisplayingFullscreen;
647     },
648
649     handleFullscreenChange: function(event)
650     {
651         this.updateBase();
652         this.updateControls();
653
654         if (this.isFullScreen()) {
655             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
656             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
657             this.host.enteredFullscreen();
658         } else {
659             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
660             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
661             this.host.exitedFullscreen();
662         }
663     },
664
665     handleWrapperMouseMove: function(event)
666     {
667         this.showControls();
668         this.resetHideControlsTimer();
669
670         if (!this.isDragging)
671             return;
672         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
673         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
674         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
675         event.stopPropagation()
676     },
677
678     handleWrapperMouseOut: function(event)
679     {
680         this.hideControls();
681         this.clearHideControlsTimer();
682     },
683
684     handleWrapperMouseUp: function(event)
685     {
686         this.isDragging = false;
687         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
688     },
689
690     handlePanelMouseDown: function(event)
691     {
692         if (event.target != this.controls.panel)
693             return;
694
695         if (!this.isFullScreen())
696             return;
697
698         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
699         this.isDragging = true;
700         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
701         this.initialOffset = new WebKitPoint(
702             parseInt(this.controls.panel.style.left) | 0,
703             parseInt(this.controls.panel.style.top) | 0
704         );
705     },
706
707     handlePanelTransitionEnd: function(event)
708     {
709         var opacity = window.getComputedStyle(this.controls.panel).opacity;
710         if (parseInt(opacity) > 0)
711             this.controls.panel.classList.remove(this.ClassNames.hidden);
712         else
713             this.controls.panel.classList.add(this.ClassNames.hidden);
714     },
715
716     handlePanelClick: function(event)
717     {
718         // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
719         event.preventDefault();
720     },
721
722     handlePanelDragStart: function(event)
723     {
724         // Prevent drags in the panel from triggering a drag event on the <video> element.
725         event.preventDefault();
726     },
727
728     handleRewindButtonClicked: function(event)
729     {
730         var newTime = Math.max(
731                                this.video.currentTime - this.RewindAmount,
732                                this.video.seekable.start(0));
733         this.video.currentTime = newTime;
734         return true;
735     },
736
737     canPlay: function()
738     {
739         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
740     },
741
742     handlePlayButtonClicked: function(event)
743     {
744         if (this.canPlay())
745             this.video.play();
746         else
747             this.video.pause();
748         return true;
749     },
750
751     handleTimelineInput: function(event)
752     {
753         this.video.fastSeek(this.controls.timeline.value);
754     },
755
756     handleTimelineChange: function(event)
757     {
758         this.video.currentTime = this.controls.timeline.value;
759     },
760
761     handleTimelineDown: function(event)
762     {
763         this.controls.thumbnail.classList.add(this.ClassNames.show);
764     },
765
766     handleTimelineUp: function(event)
767     {
768         this.controls.thumbnail.classList.remove(this.ClassNames.show);
769     },
770
771     handleTimelineMouseOver: function(event)
772     {
773         this.controls.thumbnail.classList.add(this.ClassNames.show);
774     },
775
776     handleTimelineMouseOut: function(event)
777     {
778         this.controls.thumbnail.classList.remove(this.ClassNames.show);
779     },
780
781     handleTimelineMouseMove: function(event)
782     {
783         if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
784             return;
785
786         this.updateTimelineMetricsIfNeeded();
787         this.controls.thumbnail.classList.add(this.ClassNames.show);
788         var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
789         var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
790         percent = Math.max(Math.min(1, percent), 0);
791         this.controls.thumbnail.style.left = percent * 100 + '%';
792
793         var thumbnailTime = percent * this.video.duration;
794         for (var i = 0; i < this.video.textTracks.length; ++i) {
795             var track = this.video.textTracks[i];
796             if (!this.trackHasThumbnails(track))
797                 continue;
798
799             if (!track.cues)
800                 continue;
801
802             for (var j = 0; j < track.cues.length; ++j) {
803                 var cue = track.cues[j];
804                 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
805                     this.controls.thumbnailImage.src = cue.text;
806                     return;
807                 }
808             }
809         }
810     },
811
812     handleTimelineMouseDown: function(event)
813     {
814         this.scrubbing = true;
815     },
816
817     handleTimelineMouseUp: function(event)
818     {
819         this.scrubbing = false;
820     },
821
822     handleMuteButtonClicked: function(event)
823     {
824         this.video.muted = !this.video.muted;
825         if (this.video.muted)
826             this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute'));
827         return true;
828     },
829
830     handleMinButtonClicked: function(event)
831     {
832         if (this.video.muted) {
833             this.video.muted = false;
834             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
835         }
836         this.video.volume = 0;
837         return true;
838     },
839
840     handleMaxButtonClicked: function(event)
841     {
842         if (this.video.muted) {
843             this.video.muted = false;
844             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
845         }
846         this.video.volume = 1;
847     },
848
849     handleVolumeSliderInput: function(event)
850     {
851         if (this.video.muted) {
852             this.video.muted = false;
853             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
854         }
855         this.video.volume = this.controls.volume.value;
856     },
857
858     handleCaptionButtonClicked: function(event)
859     {
860         if (this.captionMenu)
861             this.destroyCaptionMenu();
862         else
863             this.buildCaptionMenu();
864         return true;
865     },
866
867     updateFullscreenButtons: function()
868     {
869         var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVisualMedia;
870         this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden);
871         this.controls.optimizedFullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden);
872     },
873
874     handleFullscreenButtonClicked: function(event)
875     {
876         if (this.isFullScreen())
877             this.video.webkitExitFullscreen();
878         else
879             this.video.webkitEnterFullscreen();
880         return true;
881     },
882
883     handleControlsChange: function()
884     {
885         try {
886             this.updateBase();
887
888             if (this.shouldHaveControls())
889                 this.addControls();
890             else
891                 this.removeControls();
892         } catch(e) {
893             if (window.console)
894                 console.error(e);
895         }
896     },
897
898     nextRate: function()
899     {
900         return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
901     },
902
903     handleSeekBackMouseDown: function(event)
904     {
905         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
906         this.video.play();
907         this.video.playbackRate = this.nextRate() * -1;
908         this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
909     },
910
911     seekBackFaster: function()
912     {
913         this.video.playbackRate = this.nextRate() * -1;
914     },
915
916     handleSeekBackMouseUp: function(event)
917     {
918         this.video.playbackRate = this.video.defaultPlaybackRate;
919         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
920             this.video.pause();
921         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
922             this.video.play();
923         if (this.seekInterval)
924             clearInterval(this.seekInterval);
925     },
926
927     handleSeekForwardMouseDown: function(event)
928     {
929         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
930         this.video.play();
931         this.video.playbackRate = this.nextRate();
932         this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
933     },
934
935     seekForwardFaster: function()
936     {
937         this.video.playbackRate = this.nextRate();
938     },
939
940     handleSeekForwardMouseUp: function(event)
941     {
942         this.video.playbackRate = this.video.defaultPlaybackRate;
943         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
944             this.video.pause();
945         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
946             this.video.play();
947         if (this.seekInterval)
948             clearInterval(this.seekInterval);
949     },
950
951     updateDuration: function()
952     {
953         var duration = this.video.duration;
954         this.controls.timeline.min = 0;
955         this.controls.timeline.max = duration;
956
957         this.setIsLive(duration === Number.POSITIVE_INFINITY);
958
959         this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
960         this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
961     },
962
963     progressFillStyle: function(context)
964     {
965         var height = this.timelineHeight;
966         var gradient = context.createLinearGradient(0, 0, 0, height);
967         gradient.addColorStop(0, 'rgb(2, 2, 2)');
968         gradient.addColorStop(1, 'rgb(23, 23, 23)');
969         return gradient;
970     },
971
972     updateProgress: function(forceUpdate)
973     {
974         if (!forceUpdate && this.controlsAreHidden())
975             return;
976
977         this.updateTimelineMetricsIfNeeded();
978         this.drawTimelineBackground();
979     },
980
981     drawTimelineBackground: function() {
982         var background = 'url(\'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="none"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(2, 2, 2)"/><stop offset="1" stop-color="rgb(23, 23, 23)"/></linearGradient><g style="fill:url(#gradient)">'
983
984         var duration = this.video.duration;
985         var buffered = this.video.buffered;
986         for (var i = 0, end = buffered.length; i < end; ++i) {
987             var startTime = buffered.start(i);
988             var endTime = buffered.end(i);
989
990             var startX = startTime / duration;
991             var widthX = (endTime - startTime) / duration;
992             background += '<rect x="' + startX + '" y="0" width="' + widthX + '" height="1"/>';
993         }
994
995         background += '</g></svg>\')'
996         this.controls.timeline.style.backgroundImage = background;
997     },
998
999     formatTime: function(time)
1000     {
1001         if (isNaN(time))
1002             time = 0;
1003         var absTime = Math.abs(time);
1004         var intSeconds = Math.floor(absTime % 60).toFixed(0);
1005         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
1006         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
1007         var sign = time < 0 ? '-' : String();
1008
1009         if (intHours > 0)
1010             return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
1011
1012         return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
1013     },
1014
1015     updatePlaying: function()
1016     {
1017         this.setPlaying(!this.canPlay());
1018     },
1019
1020     setPlaying: function(isPlaying)
1021     {
1022         if (this.isPlaying === isPlaying)
1023             return;
1024         this.isPlaying = isPlaying;
1025
1026         if (!isPlaying) {
1027             this.controls.panel.classList.add(this.ClassNames.paused);
1028             this.controls.playButton.classList.add(this.ClassNames.paused);
1029             this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
1030         } else {
1031             this.controls.panel.classList.remove(this.ClassNames.paused);
1032             this.controls.playButton.classList.remove(this.ClassNames.paused);
1033             this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
1034             this.resetHideControlsTimer();
1035         }
1036     },
1037
1038     showControls: function()
1039     {
1040         this.setNeedsTimelineMetricsUpdate();
1041
1042         this.updateTime();
1043         this.updateProgress(true);
1044
1045         this.controls.panel.classList.add(this.ClassNames.show);
1046         this.controls.panel.classList.remove(this.ClassNames.hidden);
1047     },
1048
1049     hideControls: function()
1050     {
1051         this.controls.panel.classList.remove(this.ClassNames.show);
1052     },
1053
1054     controlsAreHidden: function()
1055     {
1056         return !this.isAudio() && this.controls.panel.classList.contains(this.ClassNames.hidden);
1057     },
1058
1059     removeControls: function()
1060     {
1061         if (this.controls.panel.parentNode)
1062             this.controls.panel.parentNode.removeChild(this.controls.panel);
1063         this.destroyCaptionMenu();
1064     },
1065
1066     addControls: function()
1067     {
1068         this.base.appendChild(this.controls.panel);
1069         this.setNeedsTimelineMetricsUpdate();
1070     },
1071
1072     updateTime: function(forceUpdate)
1073     {
1074         if (!forceUpdate && this.controlsAreHidden())
1075             return;
1076
1077         var currentTime = this.video.currentTime;
1078         var timeRemaining = currentTime - this.video.duration;
1079         this.controls.currentTime.innerText = this.formatTime(currentTime);
1080         this.controls.timeline.value = this.video.currentTime;
1081         this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1082     },
1083
1084     updateReadyState: function()
1085     {
1086         this.updateStatusDisplay();
1087     },
1088
1089     setStatusHidden: function(hidden)
1090     {
1091         if (this.statusHidden === hidden)
1092             return;
1093
1094         this.statusHidden = hidden;
1095
1096         if (hidden) {
1097             this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
1098             this.controls.currentTime.classList.remove(this.ClassNames.hidden);
1099             this.controls.timeline.classList.remove(this.ClassNames.hidden);
1100             this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
1101             this.setNeedsTimelineMetricsUpdate();
1102         } else {
1103             this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
1104             this.controls.currentTime.classList.add(this.ClassNames.hidden);
1105             this.controls.timeline.classList.add(this.ClassNames.hidden);
1106             this.controls.remainingTime.classList.add(this.ClassNames.hidden);
1107         }
1108     },
1109
1110     trackHasThumbnails: function(track)
1111     {
1112         return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
1113     },
1114
1115     updateThumbnail: function()
1116     {
1117         for (var i = 0; i < this.video.textTracks.length; ++i) {
1118             var track = this.video.textTracks[i];
1119             if (this.trackHasThumbnails(track)) {
1120                 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
1121                 return;
1122             }
1123         }
1124
1125         this.controls.thumbnail.classList.add(this.ClassNames.hidden);
1126     },
1127
1128     updateCaptionButton: function()
1129     {
1130         if (this.video.webkitHasClosedCaptions || this.video.audioTracks.length > 1)
1131             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
1132         else
1133             this.controls.captionButton.classList.add(this.ClassNames.hidden);
1134     },
1135
1136     updateCaptionContainer: function()
1137     {
1138         if (!this.host.textTrackContainer)
1139             return;
1140
1141         var hasClosedCaptions = this.video.webkitHasClosedCaptions;
1142         var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
1143
1144         if (hasClosedCaptions && hasHiddenClass)
1145             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
1146         else if (!hasClosedCaptions && !hasHiddenClass)
1147             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
1148
1149         this.updateBase();
1150         this.host.updateTextTrackContainer();
1151     },
1152
1153     buildCaptionMenu: function()
1154     {
1155         var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
1156         var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1157
1158         if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
1159             return;
1160
1161         this.captionMenu = document.createElement('div');
1162         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
1163         this.base.appendChild(this.captionMenu);
1164         this.captionMenuItems = [];
1165
1166         var offItem = this.host.captionMenuOffItem;
1167         var automaticItem = this.host.captionMenuAutomaticItem;
1168         var displayMode = this.host.captionDisplayMode;
1169
1170         var list = document.createElement('div');
1171         this.captionMenu.appendChild(list);
1172         list.classList.add(this.ClassNames.list);
1173
1174         if (audioTracks && audioTracks.length > 1) {
1175             var heading = document.createElement('h3');
1176             heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
1177             list.appendChild(heading);
1178             heading.innerText = this.UIString('Audio');
1179
1180             var ul = document.createElement('ul');
1181             ul.setAttribute('role', 'menu');
1182             ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
1183             list.appendChild(ul);
1184
1185             for (var i = 0; i < audioTracks.length; ++i) {
1186                 var menuItem = document.createElement('li');
1187                 menuItem.setAttribute('role', 'menuitemradio');
1188                 menuItem.setAttribute('tabindex', '-1');
1189                 this.captionMenuItems.push(menuItem);
1190                 this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
1191                 this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
1192                 ul.appendChild(menuItem);
1193
1194                 var track = audioTracks[i];
1195                 menuItem.innerText = this.host.displayNameForTrack(track);
1196                 menuItem.track = track;
1197
1198                 if (track.enabled) {
1199                     var trackMenuItemSelected = true;
1200                     menuItem.classList.add(this.ClassNames.selected);
1201                     menuItem.setAttribute('tabindex', '0');
1202                     menuItem.setAttribute('aria-checked', 'true');
1203                 }
1204             }
1205
1206             if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
1207                 offMenu.classList.add(this.ClassNames.selected);
1208                 menuItem.setAttribute('tabindex', '0');
1209                 menuItem.setAttribute('aria-checked', 'true');
1210             }
1211         }
1212
1213         if (textTracks && textTracks.length > 2) {
1214             var heading = document.createElement('h3');
1215             heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
1216             list.appendChild(heading);
1217             heading.innerText = this.UIString('Subtitles');
1218
1219             var ul = document.createElement('ul');
1220             ul.setAttribute('role', 'menu');
1221             ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
1222             list.appendChild(ul);
1223
1224             for (var i = 0; i < textTracks.length; ++i) {
1225                 var menuItem = document.createElement('li');
1226                 menuItem.setAttribute('role', 'menuitemradio');
1227                 menuItem.setAttribute('tabindex', '-1');
1228                 this.captionMenuItems.push(menuItem);
1229                 this.listenFor(menuItem, 'click', this.captionItemSelected);
1230                 this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
1231                 ul.appendChild(menuItem);
1232
1233                 var track = textTracks[i];
1234                 menuItem.innerText = this.host.displayNameForTrack(track);
1235                 menuItem.track = track;
1236
1237                 if (track === offItem) {
1238                     var offMenu = menuItem;
1239                     continue;
1240                 }
1241
1242                 if (track === automaticItem) {
1243                     if (displayMode === 'automatic') {
1244                         menuItem.classList.add(this.ClassNames.selected);
1245                         menuItem.setAttribute('tabindex', '0');
1246                         menuItem.setAttribute('aria-checked', 'true');
1247                     }
1248                     continue;
1249                 }
1250
1251                 if (displayMode != 'automatic' && track.mode === 'showing') {
1252                     var trackMenuItemSelected = true;
1253                     menuItem.classList.add(this.ClassNames.selected);
1254                     menuItem.setAttribute('tabindex', '0');
1255                     menuItem.setAttribute('aria-checked', 'true');
1256                 }
1257
1258             }
1259
1260             if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
1261                 offMenu.classList.add(this.ClassNames.selected);
1262                 menuItem.setAttribute('tabindex', '0');
1263                 menuItem.setAttribute('aria-checked', 'true');
1264             }
1265         }
1266         
1267         // focus first selected menuitem
1268         for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
1269             var item = this.captionMenuItems[i];
1270             if (item.classList.contains(this.ClassNames.selected)) {
1271                 item.focus();
1272                 break;
1273             }
1274         }
1275         
1276     },
1277
1278     captionItemSelected: function(event)
1279     {
1280         this.host.setSelectedTextTrack(event.target.track);
1281         this.destroyCaptionMenu();
1282     },
1283
1284     focusSiblingCaptionItem: function(event)
1285     {
1286         var currentItem = event.target;
1287         var pendingItem = false;
1288         switch(event.keyCode) {
1289         case this.KeyCodes.left:
1290         case this.KeyCodes.up:
1291             pendingItem = currentItem.previousSibling;
1292             break;
1293         case this.KeyCodes.right:
1294         case this.KeyCodes.down:
1295             pendingItem = currentItem.nextSibling;
1296             break;
1297         }
1298         if (pendingItem) {
1299             currentItem.setAttribute('tabindex', '-1');
1300             pendingItem.setAttribute('tabindex', '0');
1301             pendingItem.focus();
1302         }
1303     },
1304
1305     handleCaptionItemKeyUp: function(event)
1306     {
1307         switch (event.keyCode) {
1308         case this.KeyCodes.enter:
1309         case this.KeyCodes.space:
1310             this.captionItemSelected(event);
1311             break;
1312         case this.KeyCodes.escape:
1313             this.destroyCaptionMenu();
1314             break;
1315         case this.KeyCodes.left:
1316         case this.KeyCodes.up:
1317         case this.KeyCodes.right:
1318         case this.KeyCodes.down:
1319             this.focusSiblingCaptionItem(event);
1320             break;
1321         default:
1322             return;
1323         }
1324         // handled
1325         event.stopPropagation();
1326         event.preventDefault();
1327     },
1328
1329     audioTrackItemSelected: function(event)
1330     {
1331         for (var i = 0; i < this.video.audioTracks.length; ++i) {
1332             var track = this.video.audioTracks[i];
1333             track.enabled = (track == event.target.track);
1334         }
1335
1336         this.destroyCaptionMenu();
1337     },
1338
1339     focusSiblingAudioTrackItem: function(event)
1340     {
1341         var currentItem = event.target;
1342         var pendingItem = false;
1343         switch(event.keyCode) {
1344             case this.KeyCodes.left:
1345             case this.KeyCodes.up:
1346                 pendingItem = currentItem.previousSibling;
1347                 break;
1348             case this.KeyCodes.right:
1349             case this.KeyCodes.down:
1350                 pendingItem = currentItem.nextSibling;
1351                 break;
1352         }
1353         if (pendingItem) {
1354             currentItem.setAttribute('tabindex', '-1');
1355             pendingItem.setAttribute('tabindex', '0');
1356             pendingItem.focus();
1357         }
1358     },
1359
1360     handleAudioTrackItemKeyUp: function(event)
1361     {
1362         switch (event.keyCode) {
1363             case this.KeyCodes.enter:
1364             case this.KeyCodes.space:
1365                 this.audioTrackItemSelected(event);
1366                 break;
1367             case this.KeyCodes.escape:
1368                 this.destroyCaptionMenu();
1369                 break;
1370             case this.KeyCodes.left:
1371             case this.KeyCodes.up:
1372             case this.KeyCodes.right:
1373             case this.KeyCodes.down:
1374                 this.focusSiblingAudioTrackItem(event);
1375                 break;
1376             default:
1377                 return;
1378         }
1379         // handled
1380         event.stopPropagation();
1381         event.preventDefault();
1382     },
1383
1384     destroyCaptionMenu: function()
1385     {
1386         if (!this.captionMenu)
1387             return;
1388
1389         this.captionMenuItems.forEach(function(item){
1390             this.stopListeningFor(item, 'click', this.captionItemSelected);
1391             this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
1392         }, this);
1393
1394         // FKA and AX: focus the trigger before destroying the element with focus
1395         if (this.controls.captionButton)
1396             this.controls.captionButton.focus();
1397
1398         if (this.captionMenu.parentNode)
1399             this.captionMenu.parentNode.removeChild(this.captionMenu);
1400         delete this.captionMenu;
1401         delete this.captionMenuItems;
1402     },
1403
1404     updateHasAudio: function()
1405     {
1406         if (this.video.audioTracks.length)
1407             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
1408         else
1409             this.controls.muteBox.classList.add(this.ClassNames.hidden);
1410     },
1411
1412     updateHasVideo: function()
1413     {
1414         if (this.video.videoTracks.length)
1415             this.controls.panel.classList.remove(this.ClassNames.noVideo);
1416         else
1417             this.controls.panel.classList.add(this.ClassNames.noVideo);
1418     },
1419
1420     updateVolume: function()
1421     {
1422         if (this.video.muted || !this.video.volume) {
1423             this.controls.muteButton.classList.add(this.ClassNames.muted);
1424             this.controls.volume.value = 0;
1425         } else {
1426             this.controls.muteButton.classList.remove(this.ClassNames.muted);
1427             this.controls.volume.value = this.video.volume;
1428         }
1429     },
1430
1431     isAudio: function()
1432     {
1433         return this.video instanceof HTMLAudioElement;
1434     },
1435
1436     clearHideControlsTimer: function()
1437     {
1438         if (this.hideTimer)
1439             clearTimeout(this.hideTimer);
1440         this.hideTimer = null;
1441     },
1442
1443     resetHideControlsTimer: function()
1444     {
1445         if (this.hideTimer)
1446             clearTimeout(this.hideTimer);
1447         this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
1448     },
1449 };