[MediaControls][iOS] Enable JavaScript Media Controls on iOS.
[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     HideControlsDelay: 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     KeyCodes: {
80         enter: 13,
81         escape: 27,
82         space: 32,
83         pageUp: 33,
84         pageDown: 34,
85         end: 35,
86         home: 36,
87         left: 37,
88         up: 38,
89         right: 39,
90         down: 40
91     },
92
93     // Localized string accessor
94     UIString: function(s){
95         if (this.localizedStrings[s])
96             return this.localizedStrings[s];
97         else
98             return s; // FIXME: log something if string not localized.
99     },
100     localizedStrings: {
101         // FIXME: Move localization to ext strings file <http://webkit.org/b/120956>
102         'Aborted': 'Aborted',
103         'Audio Playback': 'Audio Playback',
104         'Captions': 'Captions',
105         'Display Full Screen': 'Display Full Screen',
106         'Duration': 'Duration',
107         'Elapsed': 'Elapsed',
108         'Error': 'Error',
109         'Exit Full Screen': 'Exit Full Screen',
110         'Fast Forward': 'Fast Forward',
111         'Loading': 'Loading',
112         'Maximum Volume': 'Maximum Volume',
113         'Minimum Volume': 'Minimum Volume',
114         'Mute': 'Mute',
115         'Pause': 'Pause',
116         'Play': 'Play',
117         'Remaining': 'Remaining',
118         'Rewind': 'Rewind',
119         'Rewind %%sec%% Seconds': 'Rewind %%sec%% Seconds',
120         'Stalled': 'Stalled',
121         'Subtitles': 'Subtitles',
122         'Suspended': 'Suspended',
123         'Unmute': 'Unmute',
124         'Video Playback': 'Video Playback',
125         'Volume': 'Volume',
126         'Waiting': 'Waiting'
127     },
128
129     listenFor: function(element, eventName, handler, useCapture)
130     {
131         if (typeof useCapture === 'undefined')
132             useCapture = false;
133
134         if (!(this.listeners[eventName] instanceof Array))
135             this.listeners[eventName] = [];
136         this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
137         element.addEventListener(eventName, this, useCapture);
138     },
139
140     stopListeningFor: function(element, eventName, handler, useCapture)
141     {
142         if (typeof useCapture === 'undefined')
143             useCapture = false;
144
145         if (!(this.listeners[eventName] instanceof Array))
146             return;
147
148         this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
149             return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
150         });
151         element.removeEventListener(eventName, this, useCapture);
152     },
153
154     addVideoListeners: function()
155     {
156         for (name in this.HandledVideoEvents) {
157             this.listenFor(this.video, name, this.HandledVideoEvents[name]);
158         };
159
160         /* text tracks */
161         this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
162         this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
163         this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
164
165         /* audio tracks */
166         this.listenFor(this.video.audioTracks, 'change', this.updateHasAudio);
167         this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
168         this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
169
170         /* controls attribute */
171         this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
172         this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
173     },
174
175     removeVideoListeners: function()
176     {
177         for (name in this.HandledVideoEvents) {
178             this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
179         };
180
181         /* text tracks */
182         this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
183         this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
184         this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
185
186         /* audio tracks */
187         this.stopListeningFor(this.video.audioTracks, 'change', this.updateHasAudio);
188         this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
189         this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
190
191         /* controls attribute */
192         this.controlsObserver.disconnect();
193         delete(this.controlsObserver);
194     },
195
196     handleEvent: function(event)
197     {
198         try {
199             if (event.target === this.video) {
200                 var handlerName = this.HandledVideoEvents[event.type];
201                 var handler = this[handlerName];
202                 if (handler && handler instanceof Function)
203                     handler.call(this, event);
204             } else {
205                 if (!(this.listeners[event.type] instanceof Array))
206                     return;
207
208                 this.listeners[event.type].forEach(function(entry) {
209                     if (entry.element === event.currentTarget && entry.handler instanceof Function)
210                         entry.handler.call(this, event);
211                 }, this);
212             }
213         } catch(e) {
214             if (window.console)
215                 console.error(e);
216         }
217     },
218
219     createBase: function()
220     {
221         var base = this.base = document.createElement('div');
222         base.setAttribute('pseudo', '-webkit-media-controls');
223         this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
224         this.listenFor(base, 'mouseout', this.handleWrapperMouseOut);
225         if (this.host.textTrackContainer)
226             base.appendChild(this.host.textTrackContainer);
227     },
228
229     shouldHaveControls: function()
230     {
231         return this.video.controls || this.isFullScreen();
232     },
233
234     updateBase: function()
235     {
236         if (this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length)) {
237             if (!this.base.parentNode)
238                 this.root.appendChild(this.base);
239         } else {
240             if (this.base.parentNode)
241                 this.base.parentNode.removeChild(this.base);
242         }
243     },
244
245     createControls: function()
246     {
247         var panel = this.controls.panel = document.createElement('div');
248         panel.setAttribute('pseudo', '-webkit-media-controls-panel');
249         panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
250         panel.setAttribute('role', 'toolbar');
251         this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
252         this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
253         this.listenFor(panel, 'click', this.handlePanelClick);
254         this.listenFor(panel, 'dblclick', this.handlePanelClick);
255
256         var rewindButton = this.controls.rewindButton = document.createElement('button');
257         rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
258         rewindButton.setAttribute('aria-label', this.UIString('Rewind %%sec%% Seconds').replace('%%sec%%', this.RewindAmount));
259         this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
260
261         var seekBackButton = this.controls.seekBackButton = document.createElement('button');
262         seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
263         seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
264         this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
265         this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
266
267         var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
268         seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
269         seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
270         this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
271         this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
272
273         var playButton = this.controls.playButton = document.createElement('button');
274         playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
275         playButton.setAttribute('aria-label', this.UIString('Play'));
276         this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
277
278         var statusDisplay = this.controls.statusDisplay = document.createElement('div');
279         statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
280         statusDisplay.classList.add(this.ClassNames.hidden);
281
282         var timelineBox = this.controls.timelineBox = document.createElement('div');
283         timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
284
285         var currentTime = this.controls.currentTime = document.createElement('div');
286         currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
287         currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
288         currentTime.setAttribute('role', 'timer');
289
290         var timeline = this.controls.timeline = document.createElement('input');
291         timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
292         timeline.setAttribute('aria-label', this.UIString('Duration'));
293         timeline.type = 'range';
294         this.listenFor(timeline, 'change', this.handleTimelineChange);
295         this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
296         this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
297         this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
298         this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
299         this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
300         timeline.step = .01;
301
302         var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
303         thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
304
305         var thumbnail = this.controls.thumbnail = document.createElement('div');
306         thumbnail.classList.add(this.ClassNames.thumbnail);
307
308         var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
309         thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
310
311         var remainingTime = this.controls.remainingTime = document.createElement('div');
312         remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
313         remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
314         remainingTime.setAttribute('role', 'timer');
315
316         var muteBox = this.controls.muteBox = document.createElement('div');
317         muteBox.classList.add(this.ClassNames.muteBox);
318
319         var muteButton = this.controls.muteButton = document.createElement('button');
320         muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
321         muteButton.setAttribute('aria-label', this.UIString('Mute'));
322         this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
323
324         var minButton = this.controls.minButton = document.createElement('button');
325         minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
326         minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
327         this.listenFor(minButton, 'click', this.handleMinButtonClicked);
328
329         var maxButton = this.controls.maxButton = document.createElement('button');
330         maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
331         maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
332         this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
333
334         var volumeBox = this.controls.volumeBox = document.createElement('div');
335         volumeBox.classList.add(this.ClassNames.volumeBox);
336
337         var volume = this.controls.volume = document.createElement('input');
338         volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
339         volume.setAttribute('aria-label', this.UIString('Volume'));
340         volume.type = 'range';
341         volume.min = 0;
342         volume.max = 1;
343         volume.step = .01;
344         this.listenFor(volume, 'change', this.handleVolumeSliderChange);
345
346         var captionButton = this.controls.captionButton = document.createElement('button');
347         captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
348         captionButton.setAttribute('aria-label', this.UIString('Captions'));
349         captionButton.setAttribute('aria-haspopup', 'true');
350         this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
351
352         var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
353         fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
354         fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
355         this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
356     },
357
358     setControlsType: function(type)
359     {
360         if (type === this.controlsType)
361             return;
362
363         this.disconnectControls();
364
365         if (type === Controller.InlineControls)
366             this.configureInlineControls();
367         else
368             this.configureFullScreenControls();
369
370         if (this.shouldHaveControls())
371             this.addControls();
372     },
373
374     disconnectControls: function(event)
375     {
376         for (item in this.controls) {
377             var control = this.controls[item];
378             if (control && control.parentNode)
379                 control.parentNode.removeChild(control);
380        }
381     },
382
383     configureInlineControls: function()
384     {
385         this.controls.panel.appendChild(this.controls.rewindButton);
386         this.controls.panel.appendChild(this.controls.playButton);
387         this.controls.panel.appendChild(this.controls.statusDisplay);
388         this.controls.panel.appendChild(this.controls.timelineBox);
389         this.controls.timelineBox.appendChild(this.controls.currentTime);
390         this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
391         this.controls.thumbnailTrack.appendChild(this.controls.timeline);
392         this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
393         this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
394         this.controls.timelineBox.appendChild(this.controls.remainingTime);
395         this.controls.panel.appendChild(this.controls.muteBox);
396         this.controls.muteBox.appendChild(this.controls.volumeBox);
397         this.controls.volumeBox.appendChild(this.controls.volume);
398         this.controls.muteBox.appendChild(this.controls.muteButton);
399         this.controls.panel.appendChild(this.controls.captionButton);
400         if (!this.isAudio())
401             this.controls.panel.appendChild(this.controls.fullscreenButton);
402
403         this.controls.panel.style.removeProperty('left');
404         this.controls.panel.style.removeProperty('top');
405         this.controls.panel.style.removeProperty('bottom');
406     },
407
408     configureFullScreenControls: function()
409     {
410         this.controls.panel.appendChild(this.controls.volumeBox);
411         this.controls.volumeBox.appendChild(this.controls.minButton);
412         this.controls.volumeBox.appendChild(this.controls.volume);
413         this.controls.volumeBox.appendChild(this.controls.maxButton);
414         this.controls.panel.appendChild(this.controls.seekBackButton);
415         this.controls.panel.appendChild(this.controls.playButton);
416         this.controls.panel.appendChild(this.controls.seekForwardButton);
417         this.controls.panel.appendChild(this.controls.captionButton);
418         if (!this.isAudio())
419             this.controls.panel.appendChild(this.controls.fullscreenButton);
420         this.controls.panel.appendChild(this.controls.timelineBox);
421         this.controls.timelineBox.appendChild(this.controls.currentTime);
422         this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
423         this.controls.thumbnailTrack.appendChild(this.controls.timeline);
424         this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
425         this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
426         this.controls.timelineBox.appendChild(this.controls.remainingTime);
427     },
428
429     handleLoadStart: function(event)
430     {
431         this.controls.statusDisplay.innerText = this.UIString('Loading');
432     },
433
434     handleError: function(event)
435     {
436         this.controls.statusDisplay.innerText = this.UIString('Error');
437     },
438
439     handleAbort: function(event)
440     {
441         this.controls.statusDisplay.innerText = this.UIString('Aborted');
442     },
443
444     handleSuspend: function(event)
445     {
446         this.controls.statusDisplay.innerText = this.UIString('Suspended');
447     },
448
449     handleStalled: function(event)
450     {
451         this.controls.statusDisplay.innerText = this.UIString('Stalled');
452     },
453
454     handleWaiting: function(event)
455     {
456         this.controls.statusDisplay.innerText = this.UIString('Waiting');
457     },
458
459     handleReadyStateChange: function(event)
460     {
461         this.updateReadyState();
462         this.updateCaptionButton();
463         this.updateCaptionContainer();
464     },
465
466     handleTimeUpdate: function(event)
467     {
468         if (!this.scrubbing)
469             this.updateTime();
470     },
471
472     handleDurationChange: function(event)
473     {
474         this.updateDuration();
475         this.updateTime();
476     },
477
478     handlePlay: function(event)
479     {
480         this.updatePlaying();
481     },
482
483     handlePause: function(event)
484     {
485         this.updatePlaying();
486     },
487
488     handleVolumeChange: function(event)
489     {
490         this.updateVolume();
491     },
492
493     handleTextTrackChange: function(event)
494     {
495         this.updateCaptionContainer();
496     },
497
498     handleTextTrackAdd: function(event)
499     {
500         var track = event.track;
501         this.listenFor(track, 'cuechange', this.handleTextTrackCueChange);
502
503         if (this.trackHasThumbnails(track) && track.mode === 'disabled')
504             track.mode = 'hidden';
505
506         this.updateThumbnail();
507         this.updateCaptionButton();
508         this.updateCaptionContainer();
509     },
510
511     handleTextTrackRemove: function(event)
512     {
513         var track = event.track;
514         this.stopListeningFor(track, 'cuechange', this.handleTextTrackCueChange);
515         this.updateThumbnail();
516         this.updateCaptionButton();
517         this.updateCaptionContainer();
518     },
519
520     handleTextTrackCueChange: function(event)
521     {
522         this.updateCaptionContainer();
523     },
524
525     isFullScreen: function()
526     {
527         return document.webkitCurrentFullScreenElement === this.video;
528     },
529
530     handleFullscreenChange: function(event)
531     {
532         this.updateBase();
533
534         if (this.isFullScreen()) {
535             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
536             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
537             this.setControlsType(Controller.FullScreenControls);
538         } else {
539             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
540             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
541             this.setControlsType(Controller.InlineControls);
542         }
543     },
544
545     handleWrapperMouseMove: function(event)
546     {
547         this.showControls();
548         if (this.hideTimer)
549             clearTimeout(this.hideTimer);
550         this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
551
552         if (!this.isDragging)
553             return;
554         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
555         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
556         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
557         event.stopPropagation()
558     },
559
560     handleWrapperMouseOut: function(event)
561     {
562         this.hideControls();
563         if (this.hideTimer)
564             clearTimeout(this.hideTimer);
565     },
566
567     handleWrapperMouseUp: function(event)
568     {
569         this.isDragging = false;
570         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
571     },
572
573     handlePanelMouseDown: function(event)
574     {
575         if (event.target != this.controls.panel)
576             return;
577
578         if (!this.isFullScreen())
579             return;
580
581         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
582         this.isDragging = true;
583         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
584         this.initialOffset = new WebKitPoint(
585             parseInt(this.controls.panel.style.left) | 0,
586             parseInt(this.controls.panel.style.top) | 0
587         );
588     },
589
590     handlePanelTransitionEnd: function(event)
591     {
592         var opacity = window.getComputedStyle(this.controls.panel).opacity;
593         if (parseInt(opacity) > 0)
594             this.controls.panel.classList.remove(this.ClassNames.hidden);
595         else
596             this.controls.panel.classList.add(this.ClassNames.hidden);
597     },
598
599     handlePanelClick: function(event)
600     {
601         // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
602         event.preventDefault();
603     },
604
605     handleRewindButtonClicked: function(event)
606     {
607         var newTime = Math.max(
608                                this.video.currentTime - this.RewindAmount,
609                                this.video.seekable.start(0));
610         this.video.currentTime = newTime;
611     },
612
613     canPlay: function()
614     {
615         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
616     },
617
618     handlePlayButtonClicked: function(event)
619     {
620         if (this.canPlay())
621             this.video.play();
622         else
623             this.video.pause();
624     },
625
626     handleTimelineChange: function(event)
627     {
628         this.video.fastSeek(this.controls.timeline.value);
629     },
630
631     handleTimelineDown: function(event)
632     {
633         this.controls.thumbnail.classList.add(this.ClassNames.show);
634     },
635
636     handleTimelineUp: function(event)
637     {
638         this.controls.thumbnail.classList.remove(this.ClassNames.show);
639     },
640
641     handleTimelineMouseOver: function(event)
642     {
643         this.controls.thumbnail.classList.add(this.ClassNames.show);
644     },
645
646     handleTimelineMouseOut: function(event)
647     {
648         this.controls.thumbnail.classList.remove(this.ClassNames.show);
649     },
650
651     handleTimelineMouseMove: function(event)
652     {
653         if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
654             return;
655
656         this.controls.thumbnail.classList.add(this.ClassNames.show);
657         var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
658         var percent = (localPoint.x - this.controls.timeline.offsetLeft) / this.controls.timeline.offsetWidth;
659         percent = Math.max(Math.min(1, percent), 0);
660         this.controls.thumbnail.style.left = percent * 100 + '%';
661
662         var thumbnailTime = percent * this.video.duration;
663         for (var i = 0; i < this.video.textTracks.length; ++i) {
664             var track = this.video.textTracks[i];
665             if (!this.trackHasThumbnails(track))
666                 continue;
667
668             if (!track.cues)
669                 continue;
670
671             for (var j = 0; j < track.cues.length; ++j) {
672                 var cue = track.cues[j];
673                 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
674                     this.controls.thumbnailImage.src = cue.text;
675                     return;
676                 }
677             }
678         }
679     },
680
681     handleTimelineMouseDown: function(event)
682     {
683         this.scrubbing = true;
684     },
685
686     handleTimelineMouseUp: function(event)
687     {
688         this.scrubbing = false;
689
690         // Do a precise seek when we lift the mouse:
691         this.video.currentTime = this.controls.timeline.value;
692     },
693
694     handleMuteButtonClicked: function(event)
695     {
696         this.video.muted = !this.video.muted;
697         if (this.video.muted)
698             this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute'));
699     },
700
701     handleMinButtonClicked: function(event)
702     {
703         if (this.video.muted) {
704             this.video.muted = false;
705             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
706         }
707         this.video.volume = 0;
708     },
709
710     handleMaxButtonClicked: function(event)
711     {
712         if (this.video.muted) {
713             this.video.muted = false;
714             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
715         }
716         this.video.volume = 1;
717     },
718
719     handleVolumeSliderChange: function(event)
720     {
721         if (this.video.muted) {
722             this.video.muted = false;
723             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
724         }
725         this.video.volume = this.controls.volume.value;
726     },
727
728     handleCaptionButtonClicked: function(event)
729     {
730         if (this.captionMenu)
731             this.destroyCaptionMenu();
732         else
733             this.buildCaptionMenu();
734     },
735
736     handleFullscreenButtonClicked: function(event)
737     {
738         if (this.isFullScreen())
739             document.webkitExitFullscreen();
740         else
741             this.video.webkitRequestFullscreen();
742     },
743
744     handleControlsChange: function()
745     {
746         try {
747             this.updateBase();
748
749             if (this.shouldHaveControls())
750                 this.addControls();
751             else
752                 this.removeControls();
753         } catch(e) {
754             if (window.console)
755                 console.error(e);
756         }
757     },
758
759     nextRate: function()
760     {
761         return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
762     },
763
764     handleSeekBackMouseDown: function(event)
765     {
766         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
767         this.video.play();
768         this.video.playbackRate = this.nextRate() * -1;
769         this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
770     },
771
772     seekBackFaster: function()
773     {
774         this.video.playbackRate = this.nextRate() * -1;
775     },
776
777     handleSeekBackMouseUp: function(event)
778     {
779         this.video.playbackRate = this.video.defaultPlaybackRate;
780         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
781             this.video.pause();
782         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
783             this.video.play();
784         if (this.seekInterval)
785             clearInterval(this.seekInterval);
786     },
787
788     handleSeekForwardMouseDown: function(event)
789     {
790         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
791         this.video.play();
792         this.video.playbackRate = this.nextRate();
793         this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
794     },
795
796     seekForwardFaster: function()
797     {
798         this.video.playbackRate = this.nextRate();
799     },
800
801     handleSeekForwardMouseUp: function(event)
802     {
803         this.video.playbackRate = this.video.defaultPlaybackRate;
804         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
805             this.video.pause();
806         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
807             this.video.play();
808         if (this.seekInterval)
809             clearInterval(this.seekInterval);
810     },
811
812     updateDuration: function()
813     {
814         this.controls.timeline.min = 0;
815         this.controls.timeline.max = this.video.duration;
816     },
817
818     formatTime: function(time)
819     {
820         if (isNaN(time))
821             time = 0;
822         var absTime = Math.abs(time);
823         var intSeconds = Math.floor(absTime % 60).toFixed(0);
824         var intMinutes = Math.floor(absTime / 60).toFixed(0);
825         return (time < 0 ? '-' : '' ) + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
826     },
827
828     updatePlaying: function()
829     {
830         if (this.canPlay()) {
831             this.controls.panel.classList.add(this.ClassNames.paused);
832             this.controls.playButton.classList.add(this.ClassNames.paused);
833             this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
834         } else {
835             this.controls.panel.classList.remove(this.ClassNames.paused);
836             this.controls.playButton.classList.remove(this.ClassNames.paused);
837             this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
838
839             this.hideControls();
840             if (this.hideTimer)
841                 clearTimeout(this.hideTimer);
842             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
843         }
844     },
845
846     showControls: function()
847     {
848         this.controls.panel.classList.add(this.ClassNames.show);
849         this.controls.panel.classList.remove(this.ClassNames.hidden);
850     },
851
852     hideControls: function()
853     {
854         this.controls.panel.classList.remove(this.ClassNames.show);
855     },
856
857     controlsAreHidden: function()
858     {
859         return !this.controls.panel.classList.contains(this.ClassNames.show) || this.controls.panel.classList.contains(this.ClassNames.hidden);
860     },
861
862     removeControls: function()
863     {
864         if (this.controls.panel.parentNode)
865             this.controls.panel.parentNode.removeChild(this.controls.panel);
866         this.destroyCaptionMenu();
867     },
868
869     addControls: function()
870     {
871         this.base.appendChild(this.controls.panel);
872     },
873
874     updateTime: function()
875     {
876         var currentTime = this.video.currentTime;
877         var timeRemaining = currentTime - this.video.duration;
878         this.controls.currentTime.innerText = this.formatTime(currentTime);
879         this.controls.timeline.value = this.video.currentTime;
880         this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
881     },
882
883     updateReadyState: function()
884     {
885         this.setStatusHidden(this.video.readyState > HTMLMediaElement.HAVE_NOTHING);
886     },
887
888     setStatusHidden: function(hidden)
889     {
890         if (hidden) {
891             this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
892             this.controls.currentTime.classList.remove(this.ClassNames.hidden);
893             this.controls.timeline.classList.remove(this.ClassNames.hidden);
894             this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
895         } else {
896             this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
897             this.controls.currentTime.classList.add(this.ClassNames.hidden);
898             this.controls.timeline.classList.add(this.ClassNames.hidden);
899             this.controls.remainingTime.classList.add(this.ClassNames.hidden);
900         }
901     },
902
903     trackHasThumbnails: function(track)
904     {
905         return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
906     },
907
908     updateThumbnail: function()
909     {
910         for (var i = 0; i < this.video.textTracks.length; ++i) {
911             var track = this.video.textTracks[i];
912             if (this.trackHasThumbnails(track)) {
913                 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
914                 return;
915             }
916         }
917
918         this.controls.thumbnail.classList.add(this.ClassNames.hidden);
919     },
920
921     updateCaptionButton: function()
922     {
923         if (this.video.webkitHasClosedCaptions)
924             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
925         else
926             this.controls.captionButton.classList.add(this.ClassNames.hidden);
927     },
928
929     updateCaptionContainer: function()
930     {
931         if (!this.host.textTrackContainer)
932             return;
933
934         if (this.video.webkitHasClosedCaptions)
935             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
936         else
937             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
938
939         this.updateBase();
940         this.host.updateTextTrackContainer();
941     },
942
943     buildCaptionMenu: function()
944     {
945         var tracks = this.host.sortedTrackListForMenu(this.video.textTracks);
946         if (!tracks || !tracks.length)
947             return;
948
949         this.captionMenu = document.createElement('div');
950         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
951         this.base.appendChild(this.captionMenu);
952         this.captionMenuItems = [];
953
954         var offItem = this.host.captionMenuOffItem;
955         var automaticItem = this.host.captionMenuAutomaticItem;
956         var displayMode = this.host.captionDisplayMode;
957
958         var list = document.createElement('div');
959         this.captionMenu.appendChild(list);
960         list.classList.add(this.ClassNames.list);
961
962         var heading = document.createElement('h3');
963         heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
964         list.appendChild(heading);
965         heading.innerText = this.UIString('Subtitles');
966
967         var ul = document.createElement('ul');
968         ul.setAttribute('role', 'menu');
969         ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
970         list.appendChild(ul);
971
972         for (var i = 0; i < tracks.length; ++i) {
973             var menuItem = document.createElement('li');
974             menuItem.setAttribute('role', 'menuitemradio');
975             menuItem.setAttribute('tabindex', '-1');
976             this.captionMenuItems.push(menuItem);
977             this.listenFor(menuItem, 'click', this.captionItemSelected);
978             this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
979             ul.appendChild(menuItem);
980
981             var track = tracks[i];
982             menuItem.innerText = this.host.displayNameForTrack(track);
983             menuItem.track = track;
984
985             if (track === offItem) {
986                 var offMenu = menuItem;
987                 continue;
988             }
989
990             if (track === automaticItem) {
991                 if (displayMode === 'automatic') {
992                     menuItem.classList.add(this.ClassNames.selected);
993                     menuItem.setAttribute('tabindex', '0');
994                     menuItem.setAttribute('aria-checked', 'true');
995                 }
996                 continue;
997             }
998
999             if (displayMode != 'automatic' && track.mode === 'showing') {
1000                 var trackMenuItemSelected = true;
1001                 menuItem.classList.add(this.ClassNames.selected);
1002                 menuItem.setAttribute('tabindex', '0');
1003                 menuItem.setAttribute('aria-checked', 'true');
1004             }
1005
1006         }
1007
1008         if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
1009             offMenu.classList.add(this.ClassNames.selected);
1010             menuItem.setAttribute('tabindex', '0');
1011             menuItem.setAttribute('aria-checked', 'true');
1012         }
1013         
1014         // focus first selected menuitem
1015         for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
1016             var item = this.captionMenuItems[i];
1017             if (item.classList.contains(this.ClassNames.selected)) {
1018                 item.focus();
1019                 break;
1020             }
1021         }
1022         
1023     },
1024
1025     captionItemSelected: function(event)
1026     {
1027         this.host.setSelectedTextTrack(event.target.track);
1028         this.destroyCaptionMenu();
1029     },
1030
1031     focusSiblingCaptionItem: function(event)
1032     {
1033         var currentItem = event.target;
1034         var pendingItem = false;
1035         switch(event.keyCode) {
1036         case this.KeyCodes.left:
1037         case this.KeyCodes.up:
1038             pendingItem = currentItem.previousSibling;
1039             break;
1040         case this.KeyCodes.right:
1041         case this.KeyCodes.down:
1042             pendingItem = currentItem.nextSibling;
1043             break;
1044         }
1045         if (pendingItem) {
1046             currentItem.setAttribute('tabindex', '-1');
1047             pendingItem.setAttribute('tabindex', '0');
1048             pendingItem.focus();
1049         }
1050     },
1051
1052     handleCaptionItemKeyUp: function(event)
1053     {
1054         switch (event.keyCode) {
1055         case this.KeyCodes.enter:
1056         case this.KeyCodes.space:
1057             this.captionItemSelected(event);
1058             break;
1059         case this.KeyCodes.escape:
1060             this.destroyCaptionMenu();
1061             break;
1062         case this.KeyCodes.left:
1063         case this.KeyCodes.up:
1064         case this.KeyCodes.right:
1065         case this.KeyCodes.down:
1066             this.focusSiblingCaptionItem(event);
1067             break;
1068         default:
1069             return;
1070         }
1071         // handled
1072         event.stopPropagation();
1073         event.preventDefault();
1074     },
1075
1076     destroyCaptionMenu: function()
1077     {
1078         if (!this.captionMenu)
1079             return;
1080
1081         this.captionMenuItems.forEach(function(item){
1082             this.stopListeningFor(item, 'click', this.captionItemSelected);
1083             this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
1084         }, this);
1085
1086         // FKA and AX: focus the trigger before destroying the element with focus
1087         if (this.controls.captionButton)
1088             this.controls.captionButton.focus();
1089
1090         if (this.captionMenu.parentNode)
1091             this.captionMenu.parentNode.removeChild(this.captionMenu);
1092         delete this.captionMenu;
1093         delete this.captionMenuItems;
1094     },
1095
1096     updateHasAudio: function()
1097     {
1098         if (this.video.audioTracks.length)
1099             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
1100         else
1101             this.controls.muteBox.classList.add(this.ClassNames.hidden);
1102     },
1103
1104     updateVolume: function()
1105     {
1106         if (this.video.muted || !this.video.volume) {
1107             this.controls.muteButton.classList.add(this.ClassNames.muted);
1108             this.controls.volume.value = 0;
1109         } else {
1110             this.controls.muteButton.classList.remove(this.ClassNames.muted);
1111             this.controls.volume.value = this.video.volume;
1112         }
1113     },
1114
1115     isAudio: function()
1116     {
1117         return this.video instanceof HTMLAudioElement;
1118     },
1119
1120 };