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