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