[iOS] update control type when playback state changes
[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.updateVolume();
30     this.updateHasAudio();
31     this.updateHasVideo();
32 };
33
34 /* Enums */
35 Controller.InlineControls = 0;
36 Controller.FullScreenControls = 1;
37
38 Controller.PlayAfterSeeking = 0;
39 Controller.PauseAfterSeeking = 1;
40
41 /* Globals */
42 Controller.gLastTimelineId = 0;
43
44 Controller.prototype = {
45
46     /* Constants */
47     HandledVideoEvents: {
48         loadstart: 'handleLoadStart',
49         error: 'handleError',
50         abort: 'handleAbort',
51         suspend: 'handleSuspend',
52         stalled: 'handleStalled',
53         waiting: 'handleWaiting',
54         emptied: 'handleReadyStateChange',
55         loadedmetadata: 'handleReadyStateChange',
56         loadeddata: 'handleReadyStateChange',
57         canplay: 'handleReadyStateChange',
58         canplaythrough: 'handleReadyStateChange',
59         timeupdate: 'handleTimeUpdate',
60         durationchange: 'handleDurationChange',
61         playing: 'handlePlay',
62         pause: 'handlePause',
63         progress: 'handleProgress',
64         volumechange: 'handleVolumeChange',
65         webkitfullscreenchange: '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.updateHasAudio);
166         this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
167         this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
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.updateHasAudio);
192         this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
193         this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
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 panel = this.controls.panel = document.createElement('div');
286         panel.setAttribute('pseudo', '-webkit-media-controls-panel');
287         panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
288         panel.setAttribute('role', 'toolbar');
289         this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
290         this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
291         this.listenFor(panel, 'click', this.handlePanelClick);
292         this.listenFor(panel, 'dblclick', this.handlePanelClick);
293
294         var rewindButton = this.controls.rewindButton = document.createElement('button');
295         rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
296         rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
297         this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
298
299         var seekBackButton = this.controls.seekBackButton = document.createElement('button');
300         seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
301         seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
302         this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
303         this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
304
305         var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
306         seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
307         seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
308         this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
309         this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
310
311         var playButton = this.controls.playButton = document.createElement('button');
312         playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
313         playButton.setAttribute('aria-label', this.UIString('Play'));
314         this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
315
316         var statusDisplay = this.controls.statusDisplay = document.createElement('div');
317         statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
318         statusDisplay.classList.add(this.ClassNames.hidden);
319
320         var timelineBox = this.controls.timelineBox = document.createElement('div');
321         timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
322
323         var currentTime = this.controls.currentTime = document.createElement('div');
324         currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
325         currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
326         currentTime.setAttribute('role', 'timer');
327
328         var timeline = this.controls.timeline = document.createElement('input');
329         this.timelineID = ++Controller.gLastTimelineId;
330         timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
331         timeline.setAttribute('aria-label', this.UIString('Duration'));
332         timeline.style.backgroundImage = '-webkit-canvas(timeline-' + this.timelineID + ')';
333         timeline.type = 'range';
334         this.listenFor(timeline, 'input', this.handleTimelineChange);
335         this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
336         this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
337         this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
338         this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
339         this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
340         timeline.step = .01;
341
342         var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
343         thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
344
345         var thumbnail = this.controls.thumbnail = document.createElement('div');
346         thumbnail.classList.add(this.ClassNames.thumbnail);
347
348         var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
349         thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
350
351         var remainingTime = this.controls.remainingTime = document.createElement('div');
352         remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
353         remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
354         remainingTime.setAttribute('role', 'timer');
355
356         var muteBox = this.controls.muteBox = document.createElement('div');
357         muteBox.classList.add(this.ClassNames.muteBox);
358
359         var muteButton = this.controls.muteButton = document.createElement('button');
360         muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
361         muteButton.setAttribute('aria-label', this.UIString('Mute'));
362         this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
363
364         var minButton = this.controls.minButton = document.createElement('button');
365         minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
366         minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
367         this.listenFor(minButton, 'click', this.handleMinButtonClicked);
368
369         var maxButton = this.controls.maxButton = document.createElement('button');
370         maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
371         maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
372         this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
373
374         var volumeBox = this.controls.volumeBox = document.createElement('div');
375         volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
376         volumeBox.classList.add(this.ClassNames.volumeBox);
377
378         var volume = this.controls.volume = document.createElement('input');
379         volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
380         volume.setAttribute('aria-label', this.UIString('Volume'));
381         volume.type = 'range';
382         volume.min = 0;
383         volume.max = 1;
384         volume.step = .01;
385         this.listenFor(volume, 'change', this.handleVolumeSliderChange);
386
387         var captionButton = this.controls.captionButton = document.createElement('button');
388         captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
389         captionButton.setAttribute('aria-label', this.UIString('Captions'));
390         captionButton.setAttribute('aria-haspopup', 'true');
391         this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
392
393         var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
394         fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
395         fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
396         this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
397     },
398
399     setControlsType: function(type)
400     {
401         if (type === this.controlsType)
402             return;
403         this.controlsType = type;
404
405         this.reconnectControls();
406     },
407
408     setIsLive: function(live)
409     {
410         if (live === this.isLive)
411             return;
412         this.isLive = live;
413
414         this.updateStatusDisplay();
415
416         this.reconnectControls();
417     },
418
419     reconnectControls: function()
420     {
421         this.disconnectControls();
422
423         if (this.controlsType === Controller.InlineControls)
424             this.configureInlineControls();
425         else if (this.controlsType == Controller.FullScreenControls)
426             this.configureFullScreenControls();
427
428         if (this.shouldHaveControls())
429             this.addControls();
430     },
431
432     disconnectControls: function(event)
433     {
434         for (item in this.controls) {
435             var control = this.controls[item];
436             if (control && control.parentNode)
437                 control.parentNode.removeChild(control);
438        }
439     },
440
441     configureInlineControls: function()
442     {
443         if (!this.isLive)
444             this.controls.panel.appendChild(this.controls.rewindButton);
445         this.controls.panel.appendChild(this.controls.playButton);
446         this.controls.panel.appendChild(this.controls.statusDisplay);
447         if (!this.isLive) {
448             this.controls.panel.appendChild(this.controls.timelineBox);
449             this.controls.timelineBox.appendChild(this.controls.currentTime);
450             this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
451             this.controls.thumbnailTrack.appendChild(this.controls.timeline);
452             this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
453             this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
454             this.controls.timelineBox.appendChild(this.controls.remainingTime);
455         }
456         this.controls.panel.appendChild(this.controls.muteBox);
457         this.controls.muteBox.appendChild(this.controls.volumeBox);
458         this.controls.volumeBox.appendChild(this.controls.volume);
459         this.controls.muteBox.appendChild(this.controls.muteButton);
460         this.controls.panel.appendChild(this.controls.captionButton);
461         if (!this.isAudio())
462             this.controls.panel.appendChild(this.controls.fullscreenButton);
463
464         this.controls.panel.style.removeProperty('left');
465         this.controls.panel.style.removeProperty('top');
466         this.controls.panel.style.removeProperty('bottom');
467     },
468
469     configureFullScreenControls: function()
470     {
471         this.controls.panel.appendChild(this.controls.volumeBox);
472         this.controls.volumeBox.appendChild(this.controls.minButton);
473         this.controls.volumeBox.appendChild(this.controls.volume);
474         this.controls.volumeBox.appendChild(this.controls.maxButton);
475         this.controls.panel.appendChild(this.controls.seekBackButton);
476         this.controls.panel.appendChild(this.controls.playButton);
477         this.controls.panel.appendChild(this.controls.seekForwardButton);
478         this.controls.panel.appendChild(this.controls.captionButton);
479         if (!this.isAudio())
480             this.controls.panel.appendChild(this.controls.fullscreenButton);
481         if (!this.isLive) {
482             this.controls.panel.appendChild(this.controls.timelineBox);
483             this.controls.timelineBox.appendChild(this.controls.currentTime);
484             this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
485             this.controls.thumbnailTrack.appendChild(this.controls.timeline);
486             this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
487             this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
488             this.controls.timelineBox.appendChild(this.controls.remainingTime);
489         } else
490             this.controls.panel.appendChild(this.controls.statusDisplay);
491     },
492
493     updateControls: function()
494     {
495         if (this.isFullScreen())
496             this.setControlsType(Controller.FullScreenControls);
497         else
498             this.setControlsType(Controller.InlineControls);
499
500         this.setNeedsTimelineMetricsUpdate();
501     },
502
503     updateStatusDisplay: function(event)
504     {
505         if (this.video.error !== null)
506             this.controls.statusDisplay.innerText = this.UIString('Error');
507         else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
508             this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
509         else if (this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
510             this.controls.statusDisplay.innerText = this.UIString('Loading');
511         else
512             this.controls.statusDisplay.innerText = '';
513
514         this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error);
515     },
516
517     handleLoadStart: function(event)
518     {
519         this.updateStatusDisplay();
520         this.updateProgress();
521     },
522
523     handleError: function(event)
524     {
525         this.updateStatusDisplay();
526     },
527
528     handleAbort: function(event)
529     {
530         this.updateStatusDisplay();
531     },
532
533     handleSuspend: function(event)
534     {
535         this.updateStatusDisplay();
536     },
537
538     handleStalled: function(event)
539     {
540         this.updateStatusDisplay();
541         this.updateProgress();
542     },
543
544     handleWaiting: function(event)
545     {
546         this.updateStatusDisplay();
547     },
548
549     handleReadyStateChange: function(event)
550     {
551         this.updateReadyState();
552         this.updateDuration();
553         this.updateCaptionButton();
554         this.updateCaptionContainer();
555         this.updateProgress();
556     },
557
558     handleTimeUpdate: function(event)
559     {
560         if (!this.scrubbing)
561             this.updateTime();
562     },
563
564     handleDurationChange: function(event)
565     {
566         this.updateDuration();
567         this.updateTime();
568         this.updateProgress();
569     },
570
571     handlePlay: function(event)
572     {
573         this.setPlaying(true);
574     },
575
576     handlePause: function(event)
577     {
578         this.setPlaying(false);
579     },
580
581     handleProgress: function(event)
582     {
583         this.updateProgress();
584     },
585
586     handleVolumeChange: function(event)
587     {
588         this.updateVolume();
589     },
590
591     handleTextTrackChange: function(event)
592     {
593         this.updateCaptionContainer();
594     },
595
596     handleTextTrackAdd: function(event)
597     {
598         var track = event.track;
599
600         if (this.trackHasThumbnails(track) && track.mode === 'disabled')
601             track.mode = 'hidden';
602
603         this.updateThumbnail();
604         this.updateCaptionButton();
605         this.updateCaptionContainer();
606     },
607
608     handleTextTrackRemove: function(event)
609     {
610         this.updateThumbnail();
611         this.updateCaptionButton();
612         this.updateCaptionContainer();
613     },
614
615     isFullScreen: function()
616     {
617         return document.webkitCurrentFullScreenElement === this.video;
618     },
619
620     handleFullscreenChange: function(event)
621     {
622         this.updateBase();
623         this.updateControls();
624
625         if (this.isFullScreen()) {
626             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
627             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
628             this.host.enteredFullscreen();
629         } else {
630             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
631             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
632             this.host.exitedFullscreen();
633         }
634     },
635
636     handleWrapperMouseMove: function(event)
637     {
638         this.showControls();
639         this.resetHideControlsTimer();
640
641         if (!this.isDragging)
642             return;
643         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
644         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
645         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
646         event.stopPropagation()
647     },
648
649     handleWrapperMouseOut: function(event)
650     {
651         this.hideControls();
652         this.clearHideControlsTimer();
653     },
654
655     handleWrapperMouseUp: function(event)
656     {
657         this.isDragging = false;
658         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
659     },
660
661     handlePanelMouseDown: function(event)
662     {
663         if (event.target != this.controls.panel)
664             return;
665
666         if (!this.isFullScreen())
667             return;
668
669         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
670         this.isDragging = true;
671         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
672         this.initialOffset = new WebKitPoint(
673             parseInt(this.controls.panel.style.left) | 0,
674             parseInt(this.controls.panel.style.top) | 0
675         );
676     },
677
678     handlePanelTransitionEnd: function(event)
679     {
680         var opacity = window.getComputedStyle(this.controls.panel).opacity;
681         if (parseInt(opacity) > 0)
682             this.controls.panel.classList.remove(this.ClassNames.hidden);
683         else
684             this.controls.panel.classList.add(this.ClassNames.hidden);
685     },
686
687     handlePanelClick: function(event)
688     {
689         // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
690         event.preventDefault();
691     },
692
693     handleRewindButtonClicked: function(event)
694     {
695         var newTime = Math.max(
696                                this.video.currentTime - this.RewindAmount,
697                                this.video.seekable.start(0));
698         this.video.currentTime = newTime;
699         return true;
700     },
701
702     canPlay: function()
703     {
704         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
705     },
706
707     handlePlayButtonClicked: function(event)
708     {
709         if (this.canPlay())
710             this.video.play();
711         else
712             this.video.pause();
713         return true;
714     },
715
716     handleTimelineChange: function(event)
717     {
718         this.video.fastSeek(this.controls.timeline.value);
719     },
720
721     handleTimelineDown: function(event)
722     {
723         this.controls.thumbnail.classList.add(this.ClassNames.show);
724     },
725
726     handleTimelineUp: function(event)
727     {
728         this.controls.thumbnail.classList.remove(this.ClassNames.show);
729     },
730
731     handleTimelineMouseOver: function(event)
732     {
733         this.controls.thumbnail.classList.add(this.ClassNames.show);
734     },
735
736     handleTimelineMouseOut: function(event)
737     {
738         this.controls.thumbnail.classList.remove(this.ClassNames.show);
739     },
740
741     handleTimelineMouseMove: function(event)
742     {
743         if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
744             return;
745
746         this.updateTimelineMetricsIfNeeded();
747         this.controls.thumbnail.classList.add(this.ClassNames.show);
748         var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
749         var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
750         percent = Math.max(Math.min(1, percent), 0);
751         this.controls.thumbnail.style.left = percent * 100 + '%';
752
753         var thumbnailTime = percent * this.video.duration;
754         for (var i = 0; i < this.video.textTracks.length; ++i) {
755             var track = this.video.textTracks[i];
756             if (!this.trackHasThumbnails(track))
757                 continue;
758
759             if (!track.cues)
760                 continue;
761
762             for (var j = 0; j < track.cues.length; ++j) {
763                 var cue = track.cues[j];
764                 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
765                     this.controls.thumbnailImage.src = cue.text;
766                     return;
767                 }
768             }
769         }
770     },
771
772     handleTimelineMouseDown: function(event)
773     {
774         this.scrubbing = true;
775     },
776
777     handleTimelineMouseUp: function(event)
778     {
779         this.scrubbing = false;
780
781         // Do a precise seek when we lift the mouse:
782         this.video.currentTime = this.controls.timeline.value;
783     },
784
785     handleMuteButtonClicked: function(event)
786     {
787         this.video.muted = !this.video.muted;
788         if (this.video.muted)
789             this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute'));
790         return true;
791     },
792
793     handleMinButtonClicked: function(event)
794     {
795         if (this.video.muted) {
796             this.video.muted = false;
797             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
798         }
799         this.video.volume = 0;
800         return true;
801     },
802
803     handleMaxButtonClicked: function(event)
804     {
805         if (this.video.muted) {
806             this.video.muted = false;
807             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
808         }
809         this.video.volume = 1;
810     },
811
812     handleVolumeSliderChange: function(event)
813     {
814         if (this.video.muted) {
815             this.video.muted = false;
816             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
817         }
818         this.video.volume = this.controls.volume.value;
819     },
820
821     handleCaptionButtonClicked: function(event)
822     {
823         if (this.captionMenu)
824             this.destroyCaptionMenu();
825         else
826             this.buildCaptionMenu();
827         return true;
828     },
829
830     handleFullscreenButtonClicked: function(event)
831     {
832         if (this.isFullScreen())
833             document.webkitExitFullscreen();
834         else
835             this.video.webkitRequestFullscreen();
836         return true;
837     },
838
839     handleControlsChange: function()
840     {
841         try {
842             this.updateBase();
843
844             if (this.shouldHaveControls())
845                 this.addControls();
846             else
847                 this.removeControls();
848         } catch(e) {
849             if (window.console)
850                 console.error(e);
851         }
852     },
853
854     nextRate: function()
855     {
856         return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
857     },
858
859     handleSeekBackMouseDown: function(event)
860     {
861         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
862         this.video.play();
863         this.video.playbackRate = this.nextRate() * -1;
864         this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
865     },
866
867     seekBackFaster: function()
868     {
869         this.video.playbackRate = this.nextRate() * -1;
870     },
871
872     handleSeekBackMouseUp: function(event)
873     {
874         this.video.playbackRate = this.video.defaultPlaybackRate;
875         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
876             this.video.pause();
877         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
878             this.video.play();
879         if (this.seekInterval)
880             clearInterval(this.seekInterval);
881     },
882
883     handleSeekForwardMouseDown: function(event)
884     {
885         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
886         this.video.play();
887         this.video.playbackRate = this.nextRate();
888         this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
889     },
890
891     seekForwardFaster: function()
892     {
893         this.video.playbackRate = this.nextRate();
894     },
895
896     handleSeekForwardMouseUp: function(event)
897     {
898         this.video.playbackRate = this.video.defaultPlaybackRate;
899         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
900             this.video.pause();
901         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
902             this.video.play();
903         if (this.seekInterval)
904             clearInterval(this.seekInterval);
905     },
906
907     updateDuration: function()
908     {
909         var duration = this.video.duration;
910         this.controls.timeline.min = 0;
911         this.controls.timeline.max = duration;
912
913         this.setIsLive(duration === Number.POSITIVE_INFINITY);
914
915         this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
916         this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
917     },
918
919     progressFillStyle: function(context)
920     {
921         var height = this.timelineHeight;
922         var gradient = context.createLinearGradient(0, 0, 0, height);
923         gradient.addColorStop(0, 'rgb(2, 2, 2)');
924         gradient.addColorStop(1, 'rgb(23, 23, 23)');
925         return gradient;
926     },
927
928     updateProgress: function()
929     {
930         this.updateTimelineMetricsIfNeeded();
931
932         var width = this.timelineWidth;
933         var height = this.timelineHeight;
934
935         var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height);
936         context.clearRect(0, 0, width, height);
937
938         context.fillStyle = this.progressFillStyle(context);
939
940         var duration = this.video.duration;
941         var buffered = this.video.buffered;
942         for (var i = 0, end = buffered.length; i < end; ++i) {
943             var startTime = buffered.start(i);
944             var endTime = buffered.end(i);
945
946             var startX = width * startTime / duration;
947             var endX = width * endTime / duration;
948             context.fillRect(startX, 0, endX - startX, height);
949         }
950     },
951
952     formatTime: function(time)
953     {
954         if (isNaN(time))
955             time = 0;
956         var absTime = Math.abs(time);
957         var intSeconds = Math.floor(absTime % 60).toFixed(0);
958         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
959         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
960         var sign = time < 0 ? '-' : String();
961
962         if (intHours > 0)
963             return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
964
965         return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
966     },
967
968     updatePlaying: function()
969     {
970         this.setPlaying(!this.canPlay());
971     },
972
973     setPlaying: function(isPlaying)
974     {
975         if (this.isPlaying === isPlaying)
976             return;
977         this.isPlaying = isPlaying;
978
979         if (!isPlaying) {
980             this.controls.panel.classList.add(this.ClassNames.paused);
981             this.controls.playButton.classList.add(this.ClassNames.paused);
982             this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
983         } else {
984             this.controls.panel.classList.remove(this.ClassNames.paused);
985             this.controls.playButton.classList.remove(this.ClassNames.paused);
986             this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
987
988             this.hideControls();
989             this.resetHideControlsTimer();
990         }
991     },
992
993     showControls: function()
994     {
995         this.controls.panel.classList.add(this.ClassNames.show);
996         this.controls.panel.classList.remove(this.ClassNames.hidden);
997
998         this.setNeedsTimelineMetricsUpdate();
999     },
1000
1001     hideControls: function()
1002     {
1003         this.controls.panel.classList.remove(this.ClassNames.show);
1004     },
1005
1006     controlsAreHidden: function()
1007     {
1008         return !this.controls.panel.classList.contains(this.ClassNames.show) || this.controls.panel.classList.contains(this.ClassNames.hidden);
1009     },
1010
1011     removeControls: function()
1012     {
1013         if (this.controls.panel.parentNode)
1014             this.controls.panel.parentNode.removeChild(this.controls.panel);
1015         this.destroyCaptionMenu();
1016     },
1017
1018     addControls: function()
1019     {
1020         this.base.appendChild(this.controls.panel);
1021         this.setNeedsTimelineMetricsUpdate();
1022     },
1023
1024     updateTime: function()
1025     {
1026         var currentTime = this.video.currentTime;
1027         var timeRemaining = currentTime - this.video.duration;
1028         this.controls.currentTime.innerText = this.formatTime(currentTime);
1029         this.controls.timeline.value = this.video.currentTime;
1030         this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1031     },
1032
1033     updateReadyState: function()
1034     {
1035         this.updateStatusDisplay();
1036     },
1037
1038     setStatusHidden: function(hidden)
1039     {
1040         if (this.statusHidden === hidden)
1041             return;
1042
1043         this.statusHidden = hidden;
1044
1045         if (hidden) {
1046             this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
1047             this.controls.currentTime.classList.remove(this.ClassNames.hidden);
1048             this.controls.timeline.classList.remove(this.ClassNames.hidden);
1049             this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
1050             this.setNeedsTimelineMetricsUpdate();
1051         } else {
1052             this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
1053             this.controls.currentTime.classList.add(this.ClassNames.hidden);
1054             this.controls.timeline.classList.add(this.ClassNames.hidden);
1055             this.controls.remainingTime.classList.add(this.ClassNames.hidden);
1056         }
1057     },
1058
1059     trackHasThumbnails: function(track)
1060     {
1061         return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
1062     },
1063
1064     updateThumbnail: function()
1065     {
1066         for (var i = 0; i < this.video.textTracks.length; ++i) {
1067             var track = this.video.textTracks[i];
1068             if (this.trackHasThumbnails(track)) {
1069                 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
1070                 return;
1071             }
1072         }
1073
1074         this.controls.thumbnail.classList.add(this.ClassNames.hidden);
1075     },
1076
1077     updateCaptionButton: function()
1078     {
1079         if (this.video.webkitHasClosedCaptions)
1080             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
1081         else
1082             this.controls.captionButton.classList.add(this.ClassNames.hidden);
1083     },
1084
1085     updateCaptionContainer: function()
1086     {
1087         if (!this.host.textTrackContainer)
1088             return;
1089
1090         var hasClosedCaptions = this.video.webkitHasClosedCaptions;
1091         var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
1092
1093         if (hasClosedCaptions && hasHiddenClass)
1094             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
1095         else if (!hasClosedCaptions && !hasHiddenClass)
1096             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
1097
1098         this.updateBase();
1099         this.host.updateTextTrackContainer();
1100     },
1101
1102     buildCaptionMenu: function()
1103     {
1104         var tracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1105         if (!tracks || !tracks.length)
1106             return;
1107
1108         this.captionMenu = document.createElement('div');
1109         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
1110         this.base.appendChild(this.captionMenu);
1111         this.captionMenuItems = [];
1112
1113         var offItem = this.host.captionMenuOffItem;
1114         var automaticItem = this.host.captionMenuAutomaticItem;
1115         var displayMode = this.host.captionDisplayMode;
1116
1117         var list = document.createElement('div');
1118         this.captionMenu.appendChild(list);
1119         list.classList.add(this.ClassNames.list);
1120
1121         var heading = document.createElement('h3');
1122         heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
1123         list.appendChild(heading);
1124         heading.innerText = this.UIString('Subtitles');
1125
1126         var ul = document.createElement('ul');
1127         ul.setAttribute('role', 'menu');
1128         ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
1129         list.appendChild(ul);
1130
1131         for (var i = 0; i < tracks.length; ++i) {
1132             var menuItem = document.createElement('li');
1133             menuItem.setAttribute('role', 'menuitemradio');
1134             menuItem.setAttribute('tabindex', '-1');
1135             this.captionMenuItems.push(menuItem);
1136             this.listenFor(menuItem, 'click', this.captionItemSelected);
1137             this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
1138             ul.appendChild(menuItem);
1139
1140             var track = tracks[i];
1141             menuItem.innerText = this.host.displayNameForTrack(track);
1142             menuItem.track = track;
1143
1144             if (track === offItem) {
1145                 var offMenu = menuItem;
1146                 continue;
1147             }
1148
1149             if (track === automaticItem) {
1150                 if (displayMode === 'automatic') {
1151                     menuItem.classList.add(this.ClassNames.selected);
1152                     menuItem.setAttribute('tabindex', '0');
1153                     menuItem.setAttribute('aria-checked', 'true');
1154                 }
1155                 continue;
1156             }
1157
1158             if (displayMode != 'automatic' && track.mode === 'showing') {
1159                 var trackMenuItemSelected = true;
1160                 menuItem.classList.add(this.ClassNames.selected);
1161                 menuItem.setAttribute('tabindex', '0');
1162                 menuItem.setAttribute('aria-checked', 'true');
1163             }
1164
1165         }
1166
1167         if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
1168             offMenu.classList.add(this.ClassNames.selected);
1169             menuItem.setAttribute('tabindex', '0');
1170             menuItem.setAttribute('aria-checked', 'true');
1171         }
1172         
1173         // focus first selected menuitem
1174         for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
1175             var item = this.captionMenuItems[i];
1176             if (item.classList.contains(this.ClassNames.selected)) {
1177                 item.focus();
1178                 break;
1179             }
1180         }
1181         
1182     },
1183
1184     captionItemSelected: function(event)
1185     {
1186         this.host.setSelectedTextTrack(event.target.track);
1187         this.destroyCaptionMenu();
1188     },
1189
1190     focusSiblingCaptionItem: function(event)
1191     {
1192         var currentItem = event.target;
1193         var pendingItem = false;
1194         switch(event.keyCode) {
1195         case this.KeyCodes.left:
1196         case this.KeyCodes.up:
1197             pendingItem = currentItem.previousSibling;
1198             break;
1199         case this.KeyCodes.right:
1200         case this.KeyCodes.down:
1201             pendingItem = currentItem.nextSibling;
1202             break;
1203         }
1204         if (pendingItem) {
1205             currentItem.setAttribute('tabindex', '-1');
1206             pendingItem.setAttribute('tabindex', '0');
1207             pendingItem.focus();
1208         }
1209     },
1210
1211     handleCaptionItemKeyUp: function(event)
1212     {
1213         switch (event.keyCode) {
1214         case this.KeyCodes.enter:
1215         case this.KeyCodes.space:
1216             this.captionItemSelected(event);
1217             break;
1218         case this.KeyCodes.escape:
1219             this.destroyCaptionMenu();
1220             break;
1221         case this.KeyCodes.left:
1222         case this.KeyCodes.up:
1223         case this.KeyCodes.right:
1224         case this.KeyCodes.down:
1225             this.focusSiblingCaptionItem(event);
1226             break;
1227         default:
1228             return;
1229         }
1230         // handled
1231         event.stopPropagation();
1232         event.preventDefault();
1233     },
1234
1235     destroyCaptionMenu: function()
1236     {
1237         if (!this.captionMenu)
1238             return;
1239
1240         this.captionMenuItems.forEach(function(item){
1241             this.stopListeningFor(item, 'click', this.captionItemSelected);
1242             this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
1243         }, this);
1244
1245         // FKA and AX: focus the trigger before destroying the element with focus
1246         if (this.controls.captionButton)
1247             this.controls.captionButton.focus();
1248
1249         if (this.captionMenu.parentNode)
1250             this.captionMenu.parentNode.removeChild(this.captionMenu);
1251         delete this.captionMenu;
1252         delete this.captionMenuItems;
1253     },
1254
1255     updateHasAudio: function()
1256     {
1257         if (this.video.audioTracks.length)
1258             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
1259         else
1260             this.controls.muteBox.classList.add(this.ClassNames.hidden);
1261     },
1262
1263     updateHasVideo: function()
1264     {
1265         if (this.video.videoTracks.length)
1266             this.controls.panel.classList.remove(this.ClassNames.noVideo);
1267         else
1268             this.controls.panel.classList.add(this.ClassNames.noVideo);
1269     },
1270
1271     updateVolume: function()
1272     {
1273         if (this.video.muted || !this.video.volume) {
1274             this.controls.muteButton.classList.add(this.ClassNames.muted);
1275             this.controls.volume.value = 0;
1276         } else {
1277             this.controls.muteButton.classList.remove(this.ClassNames.muted);
1278             this.controls.volume.value = this.video.volume;
1279         }
1280     },
1281
1282     isAudio: function()
1283     {
1284         return this.video instanceof HTMLAudioElement;
1285     },
1286
1287     clearHideControlsTimer: function()
1288     {
1289         if (this.hideTimer)
1290             clearTimeout(this.hideTimer);
1291         this.hideTimer = null;
1292     },
1293
1294     resetHideControlsTimer: function()
1295     {
1296         if (this.hideTimer)
1297             clearTimeout(this.hideTimer);
1298         this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
1299     },
1300 };