REGRESSION(r156546): media/video-no-audio.html broken
[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
14     this.addVideoListeners();
15     this.createBase();
16     this.createControls();
17     this.setControlsType(this.isFullScreen() ? Controller.FullScreenControls : Controller.InlineControls);
18
19     this.updateBase();
20     this.updateDuration();
21     this.updateTime();
22     this.updateReadyState();
23     this.updatePlaying();
24     this.updateThumbnail();
25     this.updateCaptionButton();
26     this.updateCaptionContainer();
27     this.updateVolume();
28     this.updateHasAudio();
29 };
30
31 /* Enums */
32 Controller.InlineControls = 0;
33 Controller.FullScreenControls = 1;
34
35 Controller.PlayAfterSeeking = 0;
36 Controller.PauseAfterSeeking = 1;
37
38 Controller.prototype = {
39
40     /* Constants */
41     HandledVideoEvents: {
42         loadstart: 'handleLoadStart',
43         error: 'handleError',
44         abort: 'handleAbort',
45         suspend: 'handleSuspend',
46         stalled: 'handleStalled',
47         waiting: 'handleWaiting',
48         emptied: 'handleReadyStateChange',
49         loadedmetadata: 'handleReadyStateChange',
50         loadeddata: 'handleReadyStateChange',
51         canplay: 'handleReadyStateChange',
52         canplaythrough: 'handleReadyStateChange',
53         timeupdate: 'handleTimeUpdate',
54         durationchange: 'handleDurationChange',
55         play: 'handlePlay',
56         pause: 'handlePause',
57         volumechange: 'handleVolumeChange',
58         webkitfullscreenchange: 'handleFullscreenChange',
59     },
60     HideContrtolsDelay: 4 * 1000,
61     RewindAmount: 30,
62     MaximumSeekRate: 8,
63     SeekDelay: 1500,
64     ClassNames: {
65         exit: 'exit',
66         hidden: 'hidden',
67         list: 'list',
68         muteBox: 'mute-box',
69         muted: 'muted',
70         paused: 'paused',
71         playing: 'playing',
72         selected: 'selected',
73         show: 'show',
74         thumbnail: 'thumbnail',
75         thumbnailImage: 'thumbnail-image',
76         thumbnailTrack: 'thumbnail-track',
77         volumeBox: 'volume-box',
78     },
79
80     listenFor: function(element, eventName, handler, useCapture)
81     {
82         if (typeof useCapture === 'undefined')
83             useCapture = false;
84
85         if (!(this.listeners[eventName] instanceof Array))
86             this.listeners[eventName] = [];
87         this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
88         element.addEventListener(eventName, this, useCapture);
89     },
90
91     stopListeningFor: function(element, eventName, handler, useCapture)
92     {
93         if (typeof useCapture === 'undefined')
94             useCapture = false;
95
96         if (!(this.listeners[eventName] instanceof Array))
97             return;
98
99         this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
100             return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
101         });
102         element.removeEventListener(eventName, this, useCapture);
103     },
104
105     addVideoListeners: function()
106     {
107         for (name in this.HandledVideoEvents) {
108             this.listenFor(this.video, name, this.HandledVideoEvents[name]);
109         };
110
111         /* text tracks */
112         this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
113         this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
114         this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
115
116         /* audio tracks */
117         this.listenFor(this.video.audioTracks, 'change', this.updateHasAudio);
118         this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
119         this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
120
121         /* controls attribute */
122         this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
123         this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
124     },
125
126     removeVideoListeners: function()
127     {
128         for (name in this.HandledVideoEvents) {
129             this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
130         };
131
132         /* text tracks */
133         this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
134         this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
135         this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
136
137         /* audio tracks */
138         this.stopListeningFor(this.video.audioTracks, 'change', this.updateHasAudio);
139         this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
140         this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
141
142         /* controls attribute */
143         this.controlsObserver.disconnect();
144         delete(this.controlsObserver);
145     },
146
147     handleEvent: function(event)
148     {
149         try {
150             if (event.target === this.video) {
151                 var handlerName = this.HandledVideoEvents[event.type];
152                 var handler = this[handlerName];
153                 if (handler && handler instanceof Function)
154                     handler.call(this, event);
155             } else {
156                 if (!(this.listeners[event.type] instanceof Array))
157                     return;
158
159                 this.listeners[event.type].forEach(function(entry) {
160                     if (entry.element === event.currentTarget && entry.handler instanceof Function)
161                         entry.handler.call(this, event);
162                 }, this);
163             }
164         } catch(e) {
165             if (window.console)
166                 console.error(e);
167         }
168     },
169
170     createBase: function()
171     {
172         var base = this.base = document.createElement('div');
173         base.setAttribute('pseudo', '-webkit-media-controls');
174         this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
175         this.listenFor(base, 'mouseout', this.handleWrapperMouseOut);
176         if (this.host.textTrackContainer)
177             base.appendChild(this.host.textTrackContainer);
178     },
179
180     shouldHaveControls: function()
181     {
182         return this.video.controls || this.isFullScreen();
183     },
184
185     updateBase: function()
186     {
187         if (this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length)) {
188             if (!this.base.parentNode)
189                 this.root.appendChild(this.base);
190         } else {
191             if (this.base.parentNode)
192                 this.base.parentNode.removeChild(this.base);
193         }
194     },
195
196     createControls: function()
197     {
198         var panel = this.controls.panel = document.createElement('div');
199         panel.setAttribute('pseudo', '-webkit-media-controls-panel');
200         this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
201         this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
202         this.listenFor(panel, 'click', this.handlePanelClick);
203         this.listenFor(panel, 'dblclick', this.handlePanelClick);
204
205         var rewindButton = this.controls.rewindButton = document.createElement('button');
206         rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
207         this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
208
209         var seekBackButton = this.controls.seekBackButton = document.createElement('button');
210         seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
211         this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
212         this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
213
214         var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
215         seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
216         this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
217         this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
218
219         var playButton = this.controls.playButton = document.createElement('button');
220         playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
221         this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
222
223         var statusDisplay = this.controls.statusDisplay = document.createElement('div');
224         statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
225         statusDisplay.classList.add(this.ClassNames.hidden);
226
227         var timelineBox = this.controls.timelineBox = document.createElement('div');
228         timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
229
230         var currentTime = this.controls.currentTime = document.createElement('div');
231         currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
232
233         var timeline = this.controls.timeline = document.createElement('input');
234         timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
235         timeline.type = 'range';
236         this.listenFor(timeline, 'change', this.handleTimelineChange);
237         this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
238         this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
239         this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
240         timeline.step = .01;
241
242         var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
243         thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
244
245         var thumbnail = this.controls.thumbnail = document.createElement('div');
246         thumbnail.classList.add(this.ClassNames.thumbnail);
247
248         var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
249         thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
250
251         var remainingTime = this.controls.remainingTime = document.createElement('div');
252         remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
253
254         var muteBox = this.controls.muteBox = document.createElement('div');
255         muteBox.classList.add(this.ClassNames.muteBox);
256
257         var muteButton = this.controls.muteButton = document.createElement('button');
258         muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
259         this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
260
261         var minButton = this.controls.minButton = document.createElement('button');
262         minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
263         this.listenFor(minButton, 'click', this.handleMinButtonClicked);
264
265         var maxButton = this.controls.maxButton = document.createElement('button');
266         maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
267         this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
268
269         var volumeBox = this.controls.volumeBox = document.createElement('div');
270         volumeBox.classList.add(this.ClassNames.volumeBox);
271
272         var volume = this.controls.volume = document.createElement('input');
273         volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
274         volume.type = 'range';
275         volume.min = 0;
276         volume.max = 1;
277         volume.step = .01;
278         this.listenFor(volume, 'change', this.handleVolumeSliderChange);
279
280         var captionButton = this.controls.captionButton = document.createElement('button');
281         captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
282         this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
283
284         var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
285         fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
286         this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
287     },
288
289     setControlsType: function(type)
290     {
291         if (type === this.controlsType)
292             return;
293
294         this.disconnectControls();
295
296         if (type === Controller.InlineControls)
297             this.configureInlineControls();
298         else
299             this.configureFullScreenControls();
300
301         if (this.shouldHaveControls())
302             this.addControls();
303     },
304
305     disconnectControls: function(event)
306     {
307         for (item in this.controls) {
308             var control = this.controls[item];
309             if (control && control.parentNode)
310                 control.parentNode.removeChild(control);
311        }
312     },
313
314     configureInlineControls: function()
315     {
316         this.controls.panel.appendChild(this.controls.rewindButton);
317         this.controls.panel.appendChild(this.controls.playButton);
318         this.controls.panel.appendChild(this.controls.statusDisplay);
319         this.controls.panel.appendChild(this.controls.timelineBox);
320         this.controls.timelineBox.appendChild(this.controls.currentTime);
321         this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
322         this.controls.thumbnailTrack.appendChild(this.controls.timeline);
323         this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
324         this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
325         this.controls.timelineBox.appendChild(this.controls.remainingTime);
326         this.controls.panel.appendChild(this.controls.muteBox);
327         this.controls.muteBox.appendChild(this.controls.volumeBox);
328         this.controls.volumeBox.appendChild(this.controls.volume);
329         this.controls.muteBox.appendChild(this.controls.muteButton);
330         this.controls.panel.appendChild(this.controls.captionButton);
331         if (!this.isAudio())
332             this.controls.panel.appendChild(this.controls.fullscreenButton);
333
334         this.controls.panel.style.removeProperty('left');
335         this.controls.panel.style.removeProperty('top');
336         this.controls.panel.style.removeProperty('bottom');
337     },
338
339     configureFullScreenControls: function()
340     {
341         this.controls.panel.appendChild(this.controls.volumeBox);
342         this.controls.volumeBox.appendChild(this.controls.minButton);
343         this.controls.volumeBox.appendChild(this.controls.volume);
344         this.controls.volumeBox.appendChild(this.controls.maxButton);
345         this.controls.panel.appendChild(this.controls.seekBackButton);
346         this.controls.panel.appendChild(this.controls.playButton);
347         this.controls.panel.appendChild(this.controls.seekForwardButton);
348         this.controls.panel.appendChild(this.controls.captionButton);
349         if (!this.isAudio())
350             this.controls.panel.appendChild(this.controls.fullscreenButton);
351         this.controls.panel.appendChild(this.controls.timelineBox);
352         this.controls.timelineBox.appendChild(this.controls.currentTime);
353         this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
354         this.controls.thumbnailTrack.appendChild(this.controls.timeline);
355         this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
356         this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
357         this.controls.timelineBox.appendChild(this.controls.remainingTime);
358     },
359
360     handleLoadStart: function(event)
361     {
362         // FIXME: Needs localization <http://webkit.org/b/120956>
363         this.controls.statusDisplay.innerText = 'Loading';
364     },
365
366     handleError: function(event)
367     {
368         // FIXME: Needs localization <http://webkit.org/b/120956>
369         this.controls.statusDisplay.innerText = 'Error';
370     },
371
372     handleAbort: function(event)
373     {
374         // FIXME: Needs localization <http://webkit.org/b/120956>
375         this.controls.statusDisplay.innerText = 'Aborted';
376     },
377
378     handleSuspend: function(event)
379     {
380         // FIXME: Needs localization <http://webkit.org/b/120956>
381         this.controls.statusDisplay.innerText = 'Suspended';
382     },
383
384     handleStalled: function(event)
385     {
386         // FIXME: Needs localization <http://webkit.org/b/120956>
387         this.controls.statusDisplay.innerText = 'Stalled';
388     },
389
390     handleWaiting: function(event)
391     {
392         // FIXME: Needs localization <http://webkit.org/b/120956>
393         this.controls.statusDisplay.innerText = 'Waiting';
394     },
395
396     handleReadyStateChange: function(event)
397     {
398         this.updateReadyState();
399         this.updateCaptionButton();
400         this.updateCaptionContainer();
401     },
402
403     handleTimeUpdate: function(event)
404     {
405         this.updateTime();
406     },
407
408     handleDurationChange: function(event)
409     {
410         this.updateDuration();
411         this.updateTime();
412     },
413
414     handlePlay: function(event)
415     {
416         this.updatePlaying();
417     },
418
419     handlePause: function(event)
420     {
421         this.updatePlaying();
422     },
423
424     handleVolumeChange: function(event)
425     {
426         this.updateVolume();
427     },
428
429     handleTextTrackChange: function(event)
430     {
431         this.updateCaptionContainer();
432     },
433
434     handleTextTrackAdd: function(event)
435     {
436         var track = event.track;
437         this.listenFor(track, 'cuechange', this.handleTextTrackCueChange);
438
439         if (this.trackHasThumbnails(track) && track.mode === 'disabled')
440             track.mode = 'hidden';
441
442         this.updateThumbnail();
443         this.updateCaptionButton();
444         this.updateCaptionContainer();
445     },
446
447     handleTextTrackRemove: function(event)
448     {
449         var track = event.track;
450         this.stopListeningFor(track, 'cuechange', this.handleTextTrackCueChange);
451         this.updateThumbnail();
452         this.updateCaptionButton();
453         this.updateCaptionContainer();
454     },
455
456     handleTextTrackCueChange: function(event)
457     {
458         this.updateCaptionContainer();
459     },
460
461     isFullScreen: function()
462     {
463         return document.webkitCurrentFullScreenElement === this.video;
464     },
465
466     handleFullscreenChange: function(event)
467     {
468         this.updateBase();
469
470         if (this.isFullScreen()) {
471             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
472             this.setControlsType(Controller.FullScreenControls);
473         } else {
474             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
475             this.setControlsType(Controller.InlineControls);
476         }
477     },
478
479     handleWrapperMouseMove: function(event)
480     {
481         this.showControls();
482         if (this.hideTimer)
483             clearTimeout(this.hideTimer);
484         this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideContrtolsDelay);
485
486         if (!this.isDragging)
487             return;
488         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
489         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
490         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
491         event.stopPropagation()
492     },
493
494     handleWrapperMouseOut: function(event)
495     {
496         this.controls.panel.classList.remove(this.ClassNames.show);
497         if (this.hideTimer)
498             clearTimeout(this.hideTimer);
499     },
500
501     handleWrapperMouseUp: function(event)
502     {
503         this.isDragging = false;
504         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
505     },
506
507     handlePanelMouseDown: function(event)
508     {
509         if (event.target != this.controls.panel)
510             return;
511
512         if (!this.isFullScreen())
513             return;
514
515         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
516         this.isDragging = true;
517         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
518         this.initialOffset = new WebKitPoint(
519             parseInt(this.controls.panel.style.left) | 0,
520             parseInt(this.controls.panel.style.top) | 0
521         );
522     },
523
524     handlePanelTransitionEnd: function(event)
525     {
526         var opacity = window.getComputedStyle(this.controls.panel).opacity;
527         if (parseInt(opacity) > 0)
528             this.controls.panel.classList.remove(this.ClassNames.hidden);
529         else
530             this.controls.panel.classList.add(this.ClassNames.hidden);
531     },
532
533     handlePanelClick: function(event)
534     {
535         // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
536         event.preventDefault();
537     },
538
539     handleRewindButtonClicked: function(event)
540     {
541         var newTime = Math.max(
542                                this.video.startTime,
543                                this.video.currentTime - this.RewindAmount,
544                                this.video.seekable.start(0));
545         this.video.currentTime = newTime;
546     },
547
548     canPlay: function()
549     {
550         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
551     },
552
553     handlePlayButtonClicked: function(event)
554     {
555         if (this.canPlay())
556             this.video.play();
557         else
558             this.video.pause();
559     },
560
561     handleTimelineChange: function(event)
562     {
563         this.video.currentTime = this.controls.timeline.value;
564     },
565
566     handleTimelineDown: function(event)
567     {
568         this.controls.thumbnail.classList.add(this.ClassNames.show);
569     },
570
571     handleTimelineUp: function(event)
572     {
573         this.controls.thumbnail.classList.remove(this.ClassNames.show);
574     },
575
576     handleTimelineMouseOver: function(event)
577     {
578         this.controls.thumbnail.classList.add(this.ClassNames.show);
579     },
580
581     handleTimelineMouseOut: function(event)
582     {
583         this.controls.thumbnail.classList.remove(this.ClassNames.show);
584     },
585
586     handleTimelineMouseMove: function(event)
587     {
588         if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
589             return;
590
591         this.controls.thumbnail.classList.add(this.ClassNames.show);
592         var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
593         var percent = (localPoint.x - this.controls.timeline.offsetLeft) / this.controls.timeline.offsetWidth;
594         percent = Math.max(Math.min(1, percent), 0);
595         this.controls.thumbnail.style.left = percent * 100 + '%';
596
597         var thumbnailTime = this.video.startTime + percent * this.video.duration;
598         for (var i = 0; i < this.video.textTracks.length; ++i) {
599             var track = this.video.textTracks[i];
600             if (!this.trackHasThumbnails(track))
601                 continue;
602
603             if (!track.cues)
604                 continue;
605
606             for (var j = 0; j < track.cues.length; ++j) {
607                 var cue = track.cues[j];
608                 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
609                     this.controls.thumbnailImage.src = cue.text;
610                     return;
611                 }
612             }
613         }
614     },
615
616     handleMuteButtonClicked: function(event)
617     {
618         this.video.muted = !this.video.muted;
619     },
620
621     handleMinButtonClicked: function(event)
622     {
623         if (this.video.muted)
624             this.video.muted = false;
625         this.video.volume = 0;
626     },
627
628     handleMaxButtonClicked: function(event)
629     {
630         if (this.video.muted)
631             this.video.muted = false;
632         this.video.volume = 1;
633     },
634
635     handleVolumeSliderChange: function(event)
636     {
637         if (this.video.muted)
638             this.video.muted = false;
639         this.video.volume = this.controls.volume.value;
640     },
641
642     handleCaptionButtonClicked: function(event)
643     {
644         if (this.captionMenu)
645             this.destroyCaptionMenu();
646         else
647             this.buildCaptionMenu();
648     },
649
650     handleFullscreenButtonClicked: function(event)
651     {
652         if (this.isFullScreen())
653             document.webkitExitFullscreen();
654         else
655             this.video.webkitRequestFullscreen();
656     },
657
658     handleControlsChange: function()
659     {
660         try {
661             this.updateBase();
662
663             if (this.shouldHaveControls())
664                 this.addControls();
665             else
666                 this.removeControls();
667         } catch(e) {
668             if (window.console)
669                 console.error(e);
670         }
671     },
672
673     nextRate: function()
674     {
675         return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
676     },
677
678     handleSeekBackMouseDown: function(event)
679     {
680         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
681         this.video.play();
682         this.video.playbackRate = this.nextRate() * -1;
683         this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
684     },
685
686     seekBackFaster: function()
687     {
688         this.video.playbackRate = this.nextRate() * -1;
689     },
690
691     handleSeekBackMouseUp: function(event)
692     {
693         this.video.playbackRate = this.video.defaultPlaybackRate;
694         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
695             this.video.pause();
696         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
697             this.video.play();
698         if (this.seekInterval)
699             clearInterval(this.seekInterval);
700     },
701
702     handleSeekForwardMouseDown: function(event)
703     {
704         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
705         this.video.play();
706         this.video.playbackRate = this.nextRate();
707         this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
708     },
709
710     seekForwardFaster: function()
711     {
712         this.video.playbackRate = this.nextRate();
713     },
714
715     handleSeekForwardMouseUp: function(event)
716     {
717         this.video.playbackRate = this.video.defaultPlaybackRate;
718         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
719             this.video.pause();
720         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
721             this.video.play();
722         if (this.seekInterval)
723             clearInterval(this.seekInterval);
724     },
725
726     updateDuration: function()
727     {
728         this.controls.timeline.min = this.video.startTime;
729         this.controls.timeline.max = this.video.duration;
730     },
731
732     formatTime: function(time)
733     {
734         if (isNaN(time))
735             time = 0;
736         var absTime = Math.abs(time);
737         var intSeconds = Math.floor(absTime % 60).toFixed(0);
738         var intMinutes = Math.floor(absTime / 60).toFixed(0);
739         return (time < 0 ? '-' : '' ) + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
740     },
741
742     updatePlaying: function()
743     {
744         if (this.canPlay()) {
745             this.controls.panel.classList.add(this.ClassNames.paused);
746             this.controls.playButton.classList.add(this.ClassNames.paused);
747         } else {
748             this.controls.panel.classList.remove(this.ClassNames.paused);
749             this.controls.playButton.classList.remove(this.ClassNames.paused);
750
751             this.controls.panel.classList.remove(this.ClassNames.show);
752             if (this.hideTimer)
753                 clearTimeout(this.hideTimer);
754             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideContrtolsDelay);
755         }
756     },
757
758     showControls: function()
759     {
760         this.controls.panel.classList.add(this.ClassNames.show);
761         this.controls.panel.classList.remove(this.ClassNames.hidden);
762     },
763
764     hideControls: function()
765     {
766         this.controls.panel.classList.remove(this.ClassNames.show);
767     },
768
769     removeControls: function()
770     {
771         if (this.controls.panel.parentNode)
772             this.controls.panel.parentNode.removeChild(this.controls.panel);
773         this.destroyCaptionMenu();
774     },
775
776     addControls: function()
777     {
778         this.base.appendChild(this.controls.panel);
779     },
780
781     updateTime: function()
782     {
783         var currentTime = this.video.currentTime;
784         var timeRemaining = (currentTime - this.video.startTime) - this.video.duration;
785         this.controls.currentTime.innerText = this.formatTime(currentTime);
786         this.controls.timeline.value = this.video.currentTime;
787         this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
788     },
789
790     updateReadyState: function()
791     {
792         this.setStatusHidden(this.video.readyState > HTMLMediaElement.HAVE_NOTHING);
793     },
794
795     setStatusHidden: function(hidden)
796     {
797         if (hidden) {
798             this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
799             this.controls.currentTime.classList.remove(this.ClassNames.hidden);
800             this.controls.timeline.classList.remove(this.ClassNames.hidden);
801             this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
802         } else {
803             this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
804             this.controls.currentTime.classList.add(this.ClassNames.hidden);
805             this.controls.timeline.classList.add(this.ClassNames.hidden);
806             this.controls.remainingTime.classList.add(this.ClassNames.hidden);
807         }
808     },
809
810     trackHasThumbnails: function(track)
811     {
812         return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
813     },
814
815     updateThumbnail: function()
816     {
817         for (var i = 0; i < this.video.textTracks.length; ++i) {
818             var track = this.video.textTracks[i];
819             if (this.trackHasThumbnails(track)) {
820                 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
821                 return;
822             }
823         }
824
825         this.controls.thumbnail.classList.add(this.ClassNames.hidden);
826     },
827
828     updateCaptionButton: function()
829     {
830         if (this.video.webkitHasClosedCaptions)
831             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
832         else
833             this.controls.captionButton.classList.add(this.ClassNames.hidden);
834     },
835
836     updateCaptionContainer: function()
837     {
838         if (!this.host.textTrackContainer)
839             return;
840
841         if (this.video.webkitHasClosedCaptions)
842             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
843         else
844             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
845
846         this.updateBase();
847         this.host.updateTextTrackContainer();
848     },
849
850     buildCaptionMenu: function()
851     {
852         var tracks = this.host.sortedTrackListForMenu(this.video.textTracks);
853         if (!tracks || !tracks.length)
854             return;
855
856         this.captionMenu = document.createElement('div');
857         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
858         this.base.appendChild(this.captionMenu);
859         this.captionMenuItems = [];
860
861         var offItem = this.host.captionMenuOffItem;
862         var automaticItem = this.host.captionMenuAutomaticItem;
863         var displayMode = this.host.captionDisplayMode;
864
865         var list = document.createElement('div');
866         this.captionMenu.appendChild(list);
867         list.classList.add(this.ClassNames.list);
868
869         var heading = document.createElement('h3');
870         list.appendChild(heading);
871         heading.innerText = 'Subtitles';
872
873         var ul = document.createElement('ul');
874         list.appendChild(ul);
875
876         for (var i = 0; i < tracks.length; ++i) {
877             var menuItem = document.createElement('li');
878             this.captionMenuItems.push(menuItem);
879             this.listenFor(menuItem, 'click', this.captionItemSelected);
880             ul.appendChild(menuItem);
881
882             var track = tracks[i];
883             menuItem.innerText = this.host.displayNameForTrack(track);
884             menuItem.track = track;
885
886             if (track === offItem) {
887                 var offMenu = menuItem;
888                 continue;
889             }
890
891             if (track === automaticItem) {
892                 if (displayMode === 'automatic')
893                     menuItem.classList.add(this.ClassNames.selected);
894                 continue;
895             }
896
897             if (displayMode != 'automatic' && track.mode === 'showing') {
898                 var trackMenuItemSelected = true;
899                 menuItem.classList.add(this.ClassNames.selected);
900             }
901         }
902
903         if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected)
904             offMenu.classList.add(this.ClassNames.selected);
905     },
906
907     captionItemSelected: function(event)
908     {
909         this.host.setSelectedTextTrack(event.target.track);
910         this.destroyCaptionMenu();
911     },
912
913     destroyCaptionMenu: function()
914     {
915         if (!this.captionMenu)
916             return;
917
918         this.captionMenuItems.forEach(function(item){
919             this.stopListeningFor(item, 'click', this.captionItemSelected);
920         }, this);
921
922         if (this.captionMenu.parentNode)
923             this.captionMenu.parentNode.removeChild(this.captionMenu);
924         delete this.captionMenu;
925         delete this.captionMenuItems;
926     },
927
928     updateHasAudio: function()
929     {
930         if (this.video.audioTracks.length)
931             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
932         else
933             this.controls.muteBox.classList.add(this.ClassNames.hidden);
934     },
935
936     updateVolume: function()
937     {
938         if (this.video.muted || !this.video.volume) {
939             this.controls.muteButton.classList.add(this.ClassNames.muted);
940             this.controls.volume.value = 0;
941         } else {
942             this.controls.muteButton.classList.remove(this.ClassNames.muted);
943             this.controls.volume.value = this.video.volume;
944         }
945     },
946
947     isAudio: function()
948     {
949         return this.video instanceof HTMLAudioElement;
950     },
951
952 };