[GStreamer][GL] crash within triggerRepaint
[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     this.hasWirelessPlaybackTargets = false;
16     this.canToggleShowControlsButton = false;
17     this.isListeningForPlaybackTargetAvailabilityEvent = false;
18     this.currentTargetIsWireless = false;
19     this.wirelessPlaybackDisabled = false;
20     this.isVolumeSliderActive = false;
21     this.currentDisplayWidth = 0;
22     this._scrubbing = false;
23     this._pageScaleFactor = 1;
24
25     this.addVideoListeners();
26     this.createBase();
27     this.createControls();
28     this.createTimeClones();
29     this.updateBase();
30     this.updateControls();
31     this.updateDuration();
32     this.updateProgress();
33     this.updateTime();
34     this.updateReadyState();
35     this.updatePlaying();
36     this.updateThumbnail();
37     this.updateCaptionButton();
38     this.updateCaptionContainer();
39     this.updateFullscreenButtons();
40     this.updateVolume();
41     this.updateHasAudio();
42     this.updateHasVideo();
43     this.updateWirelessTargetAvailable();
44     this.updateWirelessPlaybackStatus();
45     this.scheduleUpdateLayoutForDisplayedWidth();
46
47     this.listenFor(this.root, 'resize', this.handleRootResize);
48 };
49
50 /* Enums */
51 Controller.InlineControls = 0;
52 Controller.FullScreenControls = 1;
53
54 Controller.PlayAfterSeeking = 0;
55 Controller.PauseAfterSeeking = 1;
56
57 /* Globals */
58 Controller.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
59 Controller.gSimulatePictureInPictureAvailable = false; // Used for testing when picture-in-picture is not available.
60
61 Controller.prototype = {
62
63     /* Constants */
64     HandledVideoEvents: {
65         loadstart: 'handleLoadStart',
66         error: 'handleError',
67         abort: 'handleAbort',
68         suspend: 'handleSuspend',
69         stalled: 'handleStalled',
70         waiting: 'handleWaiting',
71         emptied: 'handleReadyStateChange',
72         loadedmetadata: 'handleReadyStateChange',
73         loadeddata: 'handleReadyStateChange',
74         canplay: 'handleReadyStateChange',
75         canplaythrough: 'handleReadyStateChange',
76         timeupdate: 'handleTimeUpdate',
77         durationchange: 'handleDurationChange',
78         playing: 'handlePlay',
79         pause: 'handlePause',
80         progress: 'handleProgress',
81         volumechange: 'handleVolumeChange',
82         webkitfullscreenchange: 'handleFullscreenChange',
83         webkitbeginfullscreen: 'handleFullscreenChange',
84         webkitendfullscreen: 'handleFullscreenChange',
85     },
86     PlaceholderPollingDelay: 33,
87     HideControlsDelay: 4 * 1000,
88     RewindAmount: 30,
89     MaximumSeekRate: 8,
90     SeekDelay: 1500,
91     ClassNames: {
92         active: 'active',
93         dropped: 'dropped',
94         exit: 'exit',
95         failed: 'failed',
96         hidden: 'hidden',
97         hiding: 'hiding',
98         threeDigitTime: 'three-digit-time',
99         fourDigitTime: 'four-digit-time',
100         fiveDigitTime: 'five-digit-time',
101         sixDigitTime: 'six-digit-time',
102         list: 'list',
103         muteBox: 'mute-box',
104         muted: 'muted',
105         paused: 'paused',
106         pictureInPicture: 'picture-in-picture',
107         playing: 'playing',
108         returnFromPictureInPicture: 'return-from-picture-in-picture',
109         selected: 'selected',
110         show: 'show',
111         small: 'small',
112         thumbnail: 'thumbnail',
113         thumbnailImage: 'thumbnail-image',
114         thumbnailTrack: 'thumbnail-track',
115         volumeBox: 'volume-box',
116         noVideo: 'no-video',
117         down: 'down',
118         out: 'out',
119         pictureInPictureButton: 'picture-in-picture-button',
120         placeholderShowing: 'placeholder-showing',
121         usesLTRUserInterfaceLayoutDirection: 'uses-ltr-user-interface-layout-direction'
122     },
123     KeyCodes: {
124         enter: 13,
125         escape: 27,
126         space: 32,
127         pageUp: 33,
128         pageDown: 34,
129         end: 35,
130         home: 36,
131         left: 37,
132         up: 38,
133         right: 39,
134         down: 40
135     },
136     MinimumTimelineWidth: 100,
137     ButtonWidth: 32,
138
139     extend: function(child)
140     {
141         // This function doesn't actually do what we want it to. In particular it
142         // is not copying the getters and setters to the child class, since they are
143         // not enumerable. What we should do is use ES6 classes, or assign the __proto__
144         // directly.
145         // FIXME: Use ES6 classes.
146
147         for (var property in this) {
148             if (!child.hasOwnProperty(property))
149                 child[property] = this[property];
150         }
151     },
152
153     get idiom()
154     {
155         return "apple";
156     },
157
158     UIString: function(developmentString, replaceString, replacementString)
159     {
160         var localized = UIStringTable[developmentString];
161         if (replaceString && replacementString)
162             return localized.replace(replaceString, replacementString);
163
164         if (localized)
165             return localized;
166
167         console.error("Localization for string \"" + developmentString + "\" not found.");
168         return "LOCALIZED STRING NOT FOUND";
169     },
170
171     listenFor: function(element, eventName, handler, useCapture)
172     {
173         if (typeof useCapture === 'undefined')
174             useCapture = false;
175
176         if (!(this.listeners[eventName] instanceof Array))
177             this.listeners[eventName] = [];
178         this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
179         element.addEventListener(eventName, this, useCapture);
180     },
181
182     stopListeningFor: function(element, eventName, handler, useCapture)
183     {
184         if (typeof useCapture === 'undefined')
185             useCapture = false;
186
187         if (!(this.listeners[eventName] instanceof Array))
188             return;
189
190         this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
191             return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
192         });
193         element.removeEventListener(eventName, this, useCapture);
194     },
195
196     addVideoListeners: function()
197     {
198         for (var name in this.HandledVideoEvents) {
199             this.listenFor(this.video, name, this.HandledVideoEvents[name]);
200         };
201
202         /* text tracks */
203         this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
204         this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
205         this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
206
207         /* audio tracks */
208         this.listenFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
209         this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
210         this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
211
212         /* video tracks */
213         this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
214         this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
215         this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
216
217         /* controls attribute */
218         this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
219         this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
220
221         this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
222
223         if ('webkitPresentationMode' in this.video)
224             this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
225     },
226
227     removeVideoListeners: function()
228     {
229         for (var name in this.HandledVideoEvents) {
230             this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
231         };
232
233         /* text tracks */
234         this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
235         this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
236         this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
237
238         /* audio tracks */
239         this.stopListeningFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
240         this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
241         this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
242
243         /* video tracks */
244         this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
245         this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
246         this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
247
248         /* controls attribute */
249         this.controlsObserver.disconnect();
250         delete(this.controlsObserver);
251
252         this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
253         this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
254
255         if ('webkitPresentationMode' in this.video)
256             this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
257     },
258
259     handleEvent: function(event)
260     {
261         var preventDefault = false;
262
263         try {
264             if (event.target === this.video) {
265                 var handlerName = this.HandledVideoEvents[event.type];
266                 var handler = this[handlerName];
267                 if (handler && handler instanceof Function)
268                     handler.call(this, event);
269             }
270
271             if (!(this.listeners[event.type] instanceof Array))
272                 return;
273
274             this.listeners[event.type].forEach(function(entry) {
275                 if (entry.element === event.currentTarget && entry.handler instanceof Function)
276                     preventDefault |= entry.handler.call(this, event);
277             }, this);
278         } catch(e) {
279             if (window.console)
280                 console.error(e);
281         }
282
283         if (preventDefault) {
284             event.stopPropagation();
285             event.preventDefault();
286         }
287     },
288
289     createBase: function()
290     {
291         var base = this.base = document.createElement('div');
292         base.setAttribute('pseudo', '-webkit-media-controls');
293         this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
294         this.listenFor(this.video, 'mouseout', this.handleWrapperMouseOut);
295         if (this.host.textTrackContainer)
296             base.appendChild(this.host.textTrackContainer);
297     },
298
299     shouldHaveAnyUI: function()
300     {
301         return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length) || this.currentPlaybackTargetIsWireless();
302     },
303
304     shouldHaveControls: function()
305     {
306         if (!this.isAudio() && !this.host.allowsInlineMediaPlayback)
307             return true;
308
309         if (this.isFullScreen() || this.presentationMode() === 'picture-in-picture' || this.currentPlaybackTargetIsWireless())
310             return true;
311
312         return this.video.controls;
313     },
314
315     setNeedsTimelineMetricsUpdate: function()
316     {
317         this.timelineMetricsNeedsUpdate = true;
318     },
319
320     scheduleUpdateLayoutForDisplayedWidth: function()
321     {
322         setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
323     },
324
325     updateTimelineMetricsIfNeeded: function()
326     {
327         if (this.timelineMetricsNeedsUpdate && !this.controlsAreHidden()) {
328             this.timelineLeft = this.controls.timeline.offsetLeft;
329             this.timelineWidth = this.controls.timeline.offsetWidth;
330             this.timelineHeight = this.controls.timeline.offsetHeight;
331             this.timelineMetricsNeedsUpdate = false;
332         }
333     },
334
335     updateBase: function()
336     {
337         if (this.shouldHaveAnyUI()) {
338             if (!this.base.parentNode) {
339                 this.root.appendChild(this.base);
340             }
341         } else {
342             if (this.base.parentNode) {
343                 this.base.parentNode.removeChild(this.base);
344             }
345         }
346     },
347
348     createControls: function()
349     {
350         var panel = this.controls.panel = document.createElement('div');
351         panel.setAttribute('pseudo', '-webkit-media-controls-panel');
352         panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
353         panel.setAttribute('role', 'toolbar');
354         this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
355         this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
356         this.listenFor(panel, 'click', this.handlePanelClick);
357         this.listenFor(panel, 'dblclick', this.handlePanelClick);
358         this.listenFor(panel, 'dragstart', this.handlePanelDragStart);
359
360         var panelBackgroundContainer = this.controls.panelBackgroundContainer = document.createElement('div');
361         panelBackgroundContainer.setAttribute('pseudo', '-webkit-media-controls-panel-background-container');
362
363         var panelTint = this.controls.panelTint = document.createElement('div');
364         panelTint.setAttribute('pseudo', '-webkit-media-controls-panel-tint');
365         this.listenFor(panelTint, 'mousedown', this.handlePanelMouseDown);
366         this.listenFor(panelTint, 'transitionend', this.handlePanelTransitionEnd);
367         this.listenFor(panelTint, 'click', this.handlePanelClick);
368         this.listenFor(panelTint, 'dblclick', this.handlePanelClick);
369         this.listenFor(panelTint, 'dragstart', this.handlePanelDragStart);
370
371         var panelBackground = this.controls.panelBackground = document.createElement('div');
372         panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background');
373
374         var rewindButton = this.controls.rewindButton = document.createElement('button');
375         rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
376         rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
377         this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
378
379         var seekBackButton = this.controls.seekBackButton = document.createElement('button');
380         seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
381         seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
382         this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
383         this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
384
385         var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
386         seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
387         seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
388         this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
389         this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
390
391         var playButton = this.controls.playButton = document.createElement('button');
392         playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
393         playButton.setAttribute('aria-label', this.UIString('Play'));
394         this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
395
396         var statusDisplay = this.controls.statusDisplay = document.createElement('div');
397         statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
398         statusDisplay.classList.add(this.ClassNames.hidden);
399
400         var timelineBox = this.controls.timelineBox = document.createElement('div');
401         timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
402
403         var currentTime = this.controls.currentTime = document.createElement('div');
404         currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
405         currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
406         currentTime.setAttribute('role', 'timer');
407
408         var timeline = this.controls.timeline = document.createElement('input');
409         timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
410         timeline.setAttribute('aria-label', this.UIString('Duration'));
411         timeline.type = 'range';
412         timeline.value = 0;
413         this.listenFor(timeline, 'input', this.handleTimelineInput);
414         this.listenFor(timeline, 'change', this.handleTimelineChange);
415         this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
416         this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
417         this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
418         this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
419         this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
420         timeline.step = .01;
421
422         this.timelineContextName = "_webkit-media-controls-timeline-" + this.host.generateUUID();
423         timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
424
425         var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
426         thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
427
428         var thumbnail = this.controls.thumbnail = document.createElement('div');
429         thumbnail.classList.add(this.ClassNames.thumbnail);
430
431         var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
432         thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
433
434         var remainingTime = this.controls.remainingTime = document.createElement('div');
435         remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
436         remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
437         remainingTime.setAttribute('role', 'timer');
438
439         var muteBox = this.controls.muteBox = document.createElement('div');
440         muteBox.classList.add(this.ClassNames.muteBox);
441         this.listenFor(muteBox, 'mouseover', this.handleMuteBoxOver);
442
443         var muteButton = this.controls.muteButton = document.createElement('button');
444         muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
445         muteButton.setAttribute('aria-label', this.UIString('Mute'));
446         this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
447
448         var minButton = this.controls.minButton = document.createElement('button');
449         minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
450         minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
451         this.listenFor(minButton, 'click', this.handleMinButtonClicked);
452
453         var maxButton = this.controls.maxButton = document.createElement('button');
454         maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
455         maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
456         this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
457
458         var volumeBox = this.controls.volumeBox = document.createElement('div');
459         volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
460         volumeBox.classList.add(this.ClassNames.volumeBox);
461
462         var volumeBoxBackground = this.controls.volumeBoxBackground = document.createElement('div');
463         volumeBoxBackground.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-background');
464
465         var volumeBoxTint = this.controls.volumeBoxTint = document.createElement('div');
466         volumeBoxTint.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-tint');
467
468         var volume = this.controls.volume = document.createElement('input');
469         volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
470         volume.setAttribute('aria-label', this.UIString('Volume'));
471         volume.type = 'range';
472         volume.min = 0;
473         volume.max = 1;
474         volume.step = .01;
475         this.listenFor(volume, 'input', this.handleVolumeSliderInput);
476         this.listenFor(volume, 'mousedown', this.handleVolumeSliderMouseDown);
477         this.listenFor(volume, 'mouseup', this.handleVolumeSliderMouseUp);
478
479         this.volumeContextName = "_webkit-media-controls-volume-" + this.host.generateUUID();
480         volume.style.backgroundImage = '-webkit-canvas(' + this.volumeContextName + ')';
481
482         var captionButton = this.controls.captionButton = document.createElement('button');
483         captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
484         captionButton.setAttribute('aria-label', this.UIString('Captions'));
485         captionButton.setAttribute('aria-haspopup', 'true');
486         captionButton.setAttribute('aria-owns', 'audioAndTextTrackMenu');
487         this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
488
489         var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
490         fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
491         fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
492         this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
493
494         var pictureInPictureButton = this.controls.pictureInPictureButton = document.createElement('button');
495         pictureInPictureButton.setAttribute('pseudo', '-webkit-media-controls-picture-in-picture-button');
496         pictureInPictureButton.setAttribute('aria-label', this.UIString('Display Picture in Picture'));
497         pictureInPictureButton.classList.add(this.ClassNames.pictureInPictureButton);
498         this.listenFor(pictureInPictureButton, 'click', this.handlePictureInPictureButtonClicked);
499
500         var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div');
501         inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status');
502         inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Video Playback Placeholder'));
503         this.listenFor(inlinePlaybackPlaceholder, 'click', this.handlePlaceholderClick);
504         this.listenFor(inlinePlaybackPlaceholder, 'dblclick', this.handlePlaceholderClick);
505         if (!Controller.gSimulatePictureInPictureAvailable)
506             inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
507
508         var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div');
509         inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text');
510
511         var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p');
512         inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top');
513
514         var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p');
515         inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom');
516
517         var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
518         wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
519         wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
520         this.listenFor(wirelessTargetPicker, 'click', this.handleWirelessPickerButtonClicked);
521
522         // Show controls button is an accessibility workaround since the controls are now removed from the DOM. http://webkit.org/b/145684
523         var showControlsButton = this.showControlsButton = document.createElement('button');
524         showControlsButton.setAttribute('pseudo', '-webkit-media-show-controls');
525         this.showShowControlsButton(false);
526         showControlsButton.setAttribute('aria-label', this.UIString('Show Controls'));
527         this.listenFor(showControlsButton, 'click', this.handleShowControlsClick);
528         this.base.appendChild(showControlsButton);
529
530         if (!Controller.gSimulateWirelessPlaybackTarget)
531             wirelessTargetPicker.classList.add(this.ClassNames.hidden);
532     },
533
534     createTimeClones: function()
535     {
536         var currentTimeClone = this.currentTimeClone = document.createElement('div');
537         currentTimeClone.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
538         currentTimeClone.setAttribute('aria-hidden', 'true');
539         currentTimeClone.classList.add('clone');
540         this.base.appendChild(currentTimeClone);
541
542         var remainingTimeClone = this.remainingTimeClone = document.createElement('div');
543         remainingTimeClone.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
544         remainingTimeClone.setAttribute('aria-hidden', 'true');
545         remainingTimeClone.classList.add('clone');
546         this.base.appendChild(remainingTimeClone);
547     },
548
549     setControlsType: function(type)
550     {
551         if (type === this.controlsType)
552             return;
553         this.controlsType = type;
554
555         this.reconnectControls();
556         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
557     },
558
559     setIsLive: function(live)
560     {
561         if (live === this.isLive)
562             return;
563         this.isLive = live;
564
565         this.updateStatusDisplay();
566
567         this.reconnectControls();
568     },
569
570     reconnectControls: function()
571     {
572         this.disconnectControls();
573
574         if (this.controlsType === Controller.InlineControls)
575             this.configureInlineControls();
576         else if (this.controlsType == Controller.FullScreenControls)
577             this.configureFullScreenControls();
578         if (this.shouldHaveControls() || this.currentPlaybackTargetIsWireless())
579             this.addControls();
580     },
581
582     disconnectControls: function(event)
583     {
584         for (var item in this.controls) {
585             var control = this.controls[item];
586             if (control && control.parentNode)
587                 control.parentNode.removeChild(control);
588        }
589     },
590
591     configureInlineControls: function()
592     {
593         this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
594         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
595         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
596         this.controls.panel.appendChild(this.controls.panelBackgroundContainer);
597         this.controls.panelBackgroundContainer.appendChild(this.controls.panelBackground);
598         this.controls.panelBackgroundContainer.appendChild(this.controls.panelTint);
599         this.controls.panel.appendChild(this.controls.playButton);
600         if (!this.isLive)
601             this.controls.panel.appendChild(this.controls.rewindButton);
602         this.controls.panel.appendChild(this.controls.statusDisplay);
603         if (!this.isLive) {
604             this.controls.panel.appendChild(this.controls.timelineBox);
605             this.controls.timelineBox.appendChild(this.controls.currentTime);
606             this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
607             this.controls.thumbnailTrack.appendChild(this.controls.timeline);
608             this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
609             this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
610             this.controls.timelineBox.appendChild(this.controls.remainingTime);
611         }
612         this.controls.panel.appendChild(this.controls.muteBox);
613         this.controls.muteBox.appendChild(this.controls.volumeBox);
614         this.controls.volumeBox.appendChild(this.controls.volumeBoxBackground);
615         this.controls.volumeBox.appendChild(this.controls.volumeBoxTint);
616         this.controls.volumeBox.appendChild(this.controls.volume);
617         this.controls.muteBox.appendChild(this.controls.muteButton);
618         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
619         this.controls.panel.appendChild(this.controls.captionButton);
620         if (!this.isAudio()) {
621             this.updatePictureInPictureButton();
622             this.controls.panel.appendChild(this.controls.fullscreenButton);
623         }
624
625         this.controls.panel.style.removeProperty('left');
626         this.controls.panel.style.removeProperty('top');
627         this.controls.panel.style.removeProperty('bottom');
628     },
629
630     configureFullScreenControls: function()
631     {
632         this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
633         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
634         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
635         this.controls.panel.appendChild(this.controls.panelBackground);
636         this.controls.panel.appendChild(this.controls.panelTint);
637         this.controls.panel.appendChild(this.controls.volumeBox);
638         this.controls.volumeBox.appendChild(this.controls.minButton);
639         this.controls.volumeBox.appendChild(this.controls.volume);
640         this.controls.volumeBox.appendChild(this.controls.maxButton);
641         this.controls.panel.appendChild(this.controls.seekBackButton);
642         this.controls.panel.appendChild(this.controls.playButton);
643         this.controls.panel.appendChild(this.controls.seekForwardButton);
644         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
645         this.controls.panel.appendChild(this.controls.captionButton);
646         if (!this.isAudio()) {
647             this.updatePictureInPictureButton();
648             this.controls.panel.appendChild(this.controls.fullscreenButton);
649         }
650         if (!this.isLive) {
651             this.controls.panel.appendChild(this.controls.timelineBox);
652             this.controls.timelineBox.appendChild(this.controls.currentTime);
653             this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
654             this.controls.thumbnailTrack.appendChild(this.controls.timeline);
655             this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
656             this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
657             this.controls.timelineBox.appendChild(this.controls.remainingTime);
658         } else
659             this.controls.panel.appendChild(this.controls.statusDisplay);
660     },
661
662     updateControls: function()
663     {
664         if (this.isFullScreen())
665             this.setControlsType(Controller.FullScreenControls);
666         else
667             this.setControlsType(Controller.InlineControls);
668
669         this.setNeedsUpdateForDisplayedWidth();
670         this.updateLayoutForDisplayedWidth();
671         this.setNeedsTimelineMetricsUpdate();
672
673         if (this.shouldHaveControls()) {
674             this.controls.panel.classList.add(this.ClassNames.show);
675             this.controls.panel.classList.remove(this.ClassNames.hidden);
676             this.resetHideControlsTimer();
677             this.showShowControlsButton(false);
678         } else {
679             this.controls.panel.classList.remove(this.ClassNames.show);
680             this.controls.panel.classList.add(this.ClassNames.hidden);
681             this.showShowControlsButton(true);
682         }
683     },
684
685     isPlayable: function()
686     {
687         return this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error;
688     },
689
690     updateStatusDisplay: function(event)
691     {
692         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
693         if (this.video.error !== null)
694             this.controls.statusDisplay.innerText = this.UIString('Error');
695         else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
696             this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
697         else if (!this.isPlayable() && this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
698             this.controls.statusDisplay.innerText = this.UIString('Loading');
699         else
700             this.controls.statusDisplay.innerText = '';
701
702         this.setStatusHidden(!this.isLive && this.isPlayable());
703     },
704
705     handleLoadStart: function(event)
706     {
707         this.updateStatusDisplay();
708         this.updateProgress();
709     },
710
711     handleError: function(event)
712     {
713         this.updateStatusDisplay();
714     },
715
716     handleAbort: function(event)
717     {
718         this.updateStatusDisplay();
719     },
720
721     handleSuspend: function(event)
722     {
723         this.updateStatusDisplay();
724     },
725
726     handleStalled: function(event)
727     {
728         this.updateStatusDisplay();
729         this.updateProgress();
730     },
731
732     handleWaiting: function(event)
733     {
734         this.updateStatusDisplay();
735     },
736
737     handleReadyStateChange: function(event)
738     {
739         this.updateReadyState();
740         this.updateDuration();
741         this.updateCaptionButton();
742         this.updateCaptionContainer();
743         this.updateFullscreenButtons();
744         this.updateWirelessTargetAvailable();
745         this.updateWirelessTargetPickerButton();
746         this.updateProgress();
747         this.updateControls();
748     },
749
750     handleTimeUpdate: function(event)
751     {
752         if (!this.scrubbing) {
753             this.updateTime();
754             this.updateProgress();
755         }
756         this.drawTimelineBackground();
757     },
758
759     handleDurationChange: function(event)
760     {
761         this.updateDuration();
762         this.updateTime();
763         this.updateProgress();
764     },
765
766     handlePlay: function(event)
767     {
768         this.setPlaying(true);
769     },
770
771     handlePause: function(event)
772     {
773         this.setPlaying(false);
774     },
775
776     handleProgress: function(event)
777     {
778         this.updateProgress();
779     },
780
781     handleVolumeChange: function(event)
782     {
783         this.updateVolume();
784     },
785
786     handleTextTrackChange: function(event)
787     {
788         this.updateCaptionContainer();
789     },
790
791     handleTextTrackAdd: function(event)
792     {
793         var track = event.track;
794
795         if (this.trackHasThumbnails(track) && track.mode === 'disabled')
796             track.mode = 'hidden';
797
798         this.updateThumbnail();
799         this.updateCaptionButton();
800         this.updateCaptionContainer();
801     },
802
803     handleTextTrackRemove: function(event)
804     {
805         this.updateThumbnail();
806         this.updateCaptionButton();
807         this.updateCaptionContainer();
808     },
809
810     handleAudioTrackChange: function(event)
811     {
812         this.updateHasAudio();
813     },
814
815     handleAudioTrackAdd: function(event)
816     {
817         this.updateHasAudio();
818         this.updateCaptionButton();
819     },
820
821     handleAudioTrackRemove: function(event)
822     {
823         this.updateHasAudio();
824         this.updateCaptionButton();
825     },
826
827     presentationMode: function() {
828         if ('webkitPresentationMode' in this.video)
829             return this.video.webkitPresentationMode;
830
831         if (this.isFullScreen())
832             return 'fullscreen';
833
834         return 'inline';
835     },
836
837     isFullScreen: function()
838     {
839         if (!this.video.webkitDisplayingFullscreen)
840             return false;
841
842         if ('webkitPresentationMode' in this.video && this.video.webkitPresentationMode === 'picture-in-picture')
843             return false;
844
845         return true;
846     },
847
848     updatePictureInPictureButton: function()
849     {
850         var shouldShowPictureInPictureButton = (Controller.gSimulatePictureInPictureAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('picture-in-picture'))) && this.hasVideo();
851         if (shouldShowPictureInPictureButton) {
852             if (!this.controls.pictureInPictureButton.parentElement) {
853                 if (this.controls.fullscreenButton.parentElement == this.controls.panel)
854                     this.controls.panel.insertBefore(this.controls.pictureInPictureButton, this.controls.fullscreenButton);
855                 else
856                     this.controls.panel.appendChild(this.controls.pictureInPictureButton);
857             }
858             this.controls.pictureInPictureButton.classList.remove(this.ClassNames.hidden);
859         } else
860             this.controls.pictureInPictureButton.classList.add(this.ClassNames.hidden);
861     },
862
863     showInlinePlaybackPlaceholderWhenSafe: function() {
864         if (this.presentationMode() != 'picture-in-picture')
865             return;
866
867         if (!this.host.isVideoLayerInline) {
868             this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
869             this.base.classList.add(this.ClassNames.placeholderShowing);
870         } else
871             setTimeout(this.showInlinePlaybackPlaceholderWhenSafe.bind(this), this.PlaceholderPollingDelay);
872     },
873
874     handlePresentationModeChange: function(event)
875     {
876         var presentationMode = this.presentationMode();
877
878         switch (presentationMode) {
879             case 'inline':
880                 this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
881                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
882                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
883                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
884                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
885                 this.base.classList.remove(this.ClassNames.placeholderShowing);
886
887                 this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
888                 break;
889             case 'picture-in-picture':
890                 this.controls.panel.classList.add(this.ClassNames.pictureInPicture);
891                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture);
892                 this.showInlinePlaybackPlaceholderWhenSafe();
893
894                 this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in Picture in Picture');
895                 this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture);
896                 this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
897                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture);
898
899                 this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture);
900                 break;
901             default:
902                 this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
903                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
904                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
905                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
906
907                 this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
908                 break;
909         }
910
911         this.updateControls();
912         this.updateCaptionContainer();
913         this.resetHideControlsTimer();
914         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
915             this.showControls();
916         this.host.setPreparedForInline(presentationMode === 'inline')
917     },
918
919     handleFullscreenChange: function(event)
920     {
921         this.updateBase();
922         this.updateControls();
923         this.updateFullscreenButtons();
924         this.updateWirelessPlaybackStatus();
925
926         if (this.isFullScreen()) {
927             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
928             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
929             this.host.enteredFullscreen();
930         } else {
931             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
932             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
933             this.host.exitedFullscreen();
934         }
935
936         if ('webkitPresentationMode' in this.video)
937             this.handlePresentationModeChange(event);
938     },
939
940     handleShowControlsClick: function(event)
941     {
942         if (!this.video.controls && !this.isFullScreen())
943             return;
944
945         if (this.controlsAreHidden())
946             this.showControls(true);
947     },
948
949     handleWrapperMouseMove: function(event)
950     {
951         if (!this.video.controls && !this.isFullScreen())
952             return;
953
954         if (this.controlsAreHidden())
955             this.showControls();
956         this.resetHideControlsTimer();
957
958         if (!this.isDragging)
959             return;
960         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
961         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
962         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
963         event.stopPropagation()
964     },
965
966     handleWrapperMouseOut: function(event)
967     {
968         this.hideControls();
969         this.clearHideControlsTimer();
970     },
971
972     handleWrapperMouseUp: function(event)
973     {
974         this.isDragging = false;
975         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
976     },
977
978     handlePanelMouseDown: function(event)
979     {
980         if (event.target != this.controls.panelTint && event.target != this.controls.inlinePlaybackPlaceholder)
981             return;
982
983         if (!this.isFullScreen())
984             return;
985
986         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
987         this.isDragging = true;
988         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
989         this.initialOffset = new WebKitPoint(
990             parseInt(this.controls.panel.style.left) | 0,
991             parseInt(this.controls.panel.style.top) | 0
992         );
993     },
994
995     handlePanelTransitionEnd: function(event)
996     {
997         var opacity = window.getComputedStyle(this.controls.panel).opacity;
998         if (!parseInt(opacity) && !this.controlsAlwaysVisible() && (this.video.controls || this.isFullScreen())) {
999             this.base.removeChild(this.controls.inlinePlaybackPlaceholder);
1000             this.base.removeChild(this.controls.panel);
1001         }
1002     },
1003
1004     handlePanelClick: function(event)
1005     {
1006         // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
1007         event.preventDefault();
1008     },
1009
1010     handlePanelDragStart: function(event)
1011     {
1012         // Prevent drags in the panel from triggering a drag event on the <video> element.
1013         event.preventDefault();
1014     },
1015
1016     handlePlaceholderClick: function(event)
1017     {
1018         // Prevent clicks in the placeholder from playing or pausing the video in a MediaDocument.
1019         event.preventDefault();
1020     },
1021
1022     handleRewindButtonClicked: function(event)
1023     {
1024         var newTime = Math.max(
1025                                this.video.currentTime - this.RewindAmount,
1026                                this.video.seekable.start(0));
1027         this.video.currentTime = newTime;
1028         return true;
1029     },
1030
1031     canPlay: function()
1032     {
1033         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
1034     },
1035
1036     handlePlayButtonClicked: function(event)
1037     {
1038         if (this.canPlay()) {
1039             this.canToggleShowControlsButton = true;
1040             this.video.play();
1041         } else
1042             this.video.pause();
1043         return true;
1044     },
1045
1046     handleTimelineInput: function(event)
1047     {
1048         if (this.scrubbing)
1049             this.video.pause();
1050
1051         this.video.fastSeek(this.controls.timeline.value);
1052         this.updateControlsWhileScrubbing();
1053     },
1054
1055     handleTimelineChange: function(event)
1056     {
1057         this.video.currentTime = this.controls.timeline.value;
1058         this.updateProgress();
1059     },
1060
1061     handleTimelineDown: function(event)
1062     {
1063         this.controls.thumbnail.classList.add(this.ClassNames.show);
1064     },
1065
1066     handleTimelineUp: function(event)
1067     {
1068         this.controls.thumbnail.classList.remove(this.ClassNames.show);
1069     },
1070
1071     handleTimelineMouseOver: function(event)
1072     {
1073         this.controls.thumbnail.classList.add(this.ClassNames.show);
1074     },
1075
1076     handleTimelineMouseOut: function(event)
1077     {
1078         this.controls.thumbnail.classList.remove(this.ClassNames.show);
1079     },
1080
1081     handleTimelineMouseMove: function(event)
1082     {
1083         if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
1084             return;
1085
1086         this.updateTimelineMetricsIfNeeded();
1087         this.controls.thumbnail.classList.add(this.ClassNames.show);
1088         var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
1089         var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
1090         percent = Math.max(Math.min(1, percent), 0);
1091         this.controls.thumbnail.style.left = percent * 100 + '%';
1092
1093         var thumbnailTime = percent * this.video.duration;
1094         for (var i = 0; i < this.video.textTracks.length; ++i) {
1095             var track = this.video.textTracks[i];
1096             if (!this.trackHasThumbnails(track))
1097                 continue;
1098
1099             if (!track.cues)
1100                 continue;
1101
1102             for (var j = 0; j < track.cues.length; ++j) {
1103                 var cue = track.cues[j];
1104                 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
1105                     this.controls.thumbnailImage.src = cue.text;
1106                     return;
1107                 }
1108             }
1109         }
1110     },
1111
1112     handleTimelineMouseDown: function(event)
1113     {
1114         this.scrubbing = true;
1115     },
1116
1117     handleTimelineMouseUp: function(event)
1118     {
1119         this.scrubbing = false;
1120     },
1121
1122     handleMuteButtonClicked: function(event)
1123     {
1124         this.video.muted = !this.video.muted;
1125         if (this.video.muted)
1126             this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute'));
1127         this.drawVolumeBackground();
1128         return true;
1129     },
1130
1131     handleMuteBoxOver: function(event)
1132     {
1133         this.drawVolumeBackground();
1134     },
1135
1136     handleMinButtonClicked: function(event)
1137     {
1138         if (this.video.muted) {
1139             this.video.muted = false;
1140             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
1141         }
1142         this.video.volume = 0;
1143         return true;
1144     },
1145
1146     handleMaxButtonClicked: function(event)
1147     {
1148         if (this.video.muted) {
1149             this.video.muted = false;
1150             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
1151         }
1152         this.video.volume = 1;
1153     },
1154
1155     handleVolumeSliderInput: function(event)
1156     {
1157         if (this.video.muted) {
1158             this.video.muted = false;
1159             this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
1160         }
1161         this.video.volume = this.controls.volume.value;
1162         this.drawVolumeBackground();
1163     },
1164
1165     handleVolumeSliderMouseDown: function(event)
1166     {
1167         this.isVolumeSliderActive = true;
1168         this.drawVolumeBackground();
1169     },
1170
1171     handleVolumeSliderMouseUp: function(event)
1172     {
1173         this.isVolumeSliderActive = false;
1174         this.drawVolumeBackground();
1175     },
1176
1177     handleCaptionButtonClicked: function(event)
1178     {
1179         if (this.captionMenu)
1180             this.destroyCaptionMenu();
1181         else
1182             this.buildCaptionMenu();
1183         return true;
1184     },
1185
1186     hasVideo: function()
1187     {
1188         return this.video.videoTracks && this.video.videoTracks.length;
1189     },
1190
1191     updateFullscreenButtons: function()
1192     {
1193         var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVideo();
1194         this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden && !this.isFullScreen());
1195         this.updatePictureInPictureButton();
1196         this.setNeedsUpdateForDisplayedWidth();
1197         this.updateLayoutForDisplayedWidth();
1198     },
1199
1200     handleFullscreenButtonClicked: function(event)
1201     {
1202         if (this.isFullScreen())
1203             this.video.webkitExitFullscreen();
1204         else
1205             this.video.webkitEnterFullscreen();
1206         return true;
1207     },
1208     
1209     updateWirelessTargetPickerButton: function() {
1210         var wirelessTargetPickerColor;
1211         if (this.controls.wirelessTargetPicker.classList.contains('playing'))
1212             wirelessTargetPickerColor = "-apple-wireless-playback-target-active";
1213         else
1214             wirelessTargetPickerColor = "rgba(255,255,255,0.45)";
1215         if (window.devicePixelRatio == 2)
1216             this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,0.5 L 16,0.5 L 16,15.5 L 0,15.5 z M 0,14.5 L 16,14.5 L 8,5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 3.5,13.25 L 12.5,13.25 L 8,8 z'/></svg>\")";
1217         else
1218             this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,1 L 16,1 L 16,16 L 0,16 z M 0,15 L 16,15 L 8,5.5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2.5' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 2.75,14 L 13.25,14 L 8,8.75 z'/></svg>\")";
1219     },
1220
1221     handleControlsChange: function()
1222     {
1223         try {
1224             this.updateBase();
1225
1226             if (this.shouldHaveControls() && !this.hasControls())
1227                 this.addControls();
1228             else if (!this.shouldHaveControls() && this.hasControls())
1229                 this.removeControls();
1230         } catch(e) {
1231             if (window.console)
1232                 console.error(e);
1233         }
1234     },
1235
1236     nextRate: function()
1237     {
1238         return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
1239     },
1240
1241     handleSeekBackMouseDown: function(event)
1242     {
1243         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
1244         this.video.play();
1245         this.video.playbackRate = this.nextRate() * -1;
1246         this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
1247     },
1248
1249     seekBackFaster: function()
1250     {
1251         this.video.playbackRate = this.nextRate() * -1;
1252     },
1253
1254     handleSeekBackMouseUp: function(event)
1255     {
1256         this.video.playbackRate = this.video.defaultPlaybackRate;
1257         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
1258             this.video.pause();
1259         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
1260             this.video.play();
1261         if (this.seekInterval)
1262             clearInterval(this.seekInterval);
1263     },
1264
1265     handleSeekForwardMouseDown: function(event)
1266     {
1267         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
1268         this.video.play();
1269         this.video.playbackRate = this.nextRate();
1270         this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
1271     },
1272
1273     seekForwardFaster: function()
1274     {
1275         this.video.playbackRate = this.nextRate();
1276     },
1277
1278     handleSeekForwardMouseUp: function(event)
1279     {
1280         this.video.playbackRate = this.video.defaultPlaybackRate;
1281         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
1282             this.video.pause();
1283         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
1284             this.video.play();
1285         if (this.seekInterval)
1286             clearInterval(this.seekInterval);
1287     },
1288
1289     updateDuration: function()
1290     {
1291         var duration = this.video.duration;
1292         this.controls.timeline.min = 0;
1293         this.controls.timeline.max = duration;
1294
1295         this.setIsLive(duration === Number.POSITIVE_INFINITY);
1296
1297         var timeControls = [this.controls.currentTime, this.controls.remainingTime, this.currentTimeClone, this.remainingTimeClone];
1298
1299         function removeTimeClass(className) {
1300             for (let element of timeControls)
1301                 element.classList.remove(className);
1302         }
1303
1304         function addTimeClass(className) {
1305             for (let element of timeControls)
1306                 element.classList.add(className);
1307         }
1308
1309         // Reset existing style.
1310         removeTimeClass(this.ClassNames.threeDigitTime);
1311         removeTimeClass(this.ClassNames.fourDigitTime);
1312         removeTimeClass(this.ClassNames.fiveDigitTime);
1313         removeTimeClass(this.ClassNames.sixDigitTime);
1314
1315         if (duration >= 60*60*10)
1316             addTimeClass(this.ClassNames.sixDigitTime);
1317         else if (duration >= 60*60)
1318             addTimeClass(this.ClassNames.fiveDigitTime);
1319         else if (duration >= 60*10)
1320             addTimeClass(this.ClassNames.fourDigitTime);
1321         else
1322             addTimeClass(this.ClassNames.threeDigitTime);
1323     },
1324
1325     progressFillStyle: function(context)
1326     {
1327         var height = this.timelineHeight;
1328         var gradient = context.createLinearGradient(0, 0, 0, height);
1329         gradient.addColorStop(0, 'rgb(2, 2, 2)');
1330         gradient.addColorStop(1, 'rgb(23, 23, 23)');
1331         return gradient;
1332     },
1333
1334     updateProgress: function()
1335     {
1336         this.updateTimelineMetricsIfNeeded();
1337         this.drawTimelineBackground();
1338     },
1339
1340     addRoundedRect: function(ctx, x, y, width, height, radius) {
1341         ctx.moveTo(x + radius, y);
1342         ctx.arcTo(x + width, y, x + width, y + radius, radius);
1343         ctx.lineTo(x + width, y + height - radius);
1344         ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
1345         ctx.lineTo(x + radius, y + height);
1346         ctx.arcTo(x, y + height, x, y + height - radius, radius);
1347         ctx.lineTo(x, y + radius);
1348         ctx.arcTo(x, y, x + radius, y, radius);
1349     },
1350
1351     drawTimelineBackground: function() {
1352         var dpr = window.devicePixelRatio;
1353         var width = this.timelineWidth * dpr;
1354         var height = this.timelineHeight * dpr;
1355
1356         if (!width || !height)
1357             return;
1358
1359         var played = this.controls.timeline.value / this.controls.timeline.max;
1360         var buffered = 0;
1361         for (var i = 0, end = this.video.buffered.length; i < end; ++i)
1362             buffered = Math.max(this.video.buffered.end(i), buffered);
1363
1364         buffered /= this.video.duration;
1365
1366         var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
1367
1368         width /= dpr;
1369         height /= dpr;
1370
1371         ctx.save();
1372         ctx.scale(dpr, dpr);
1373         ctx.clearRect(0, 0, width, height);
1374
1375         var timelineHeight = 3;
1376         var trackHeight = 1;
1377         var scrubberWidth = 3;
1378         var scrubberHeight = 15;
1379         var borderSize = 2;
1380         var scrubberPosition = Math.max(0, Math.min(width - scrubberWidth, Math.round(width * played)));
1381
1382         // Draw buffered section.
1383         ctx.save();
1384         if (this.isAudio())
1385             ctx.fillStyle = "rgb(71, 71, 71)";
1386         else
1387             ctx.fillStyle = "rgb(30, 30, 30)";
1388         ctx.fillRect(1, 8, Math.round(width * buffered) - borderSize, trackHeight);
1389         ctx.restore();
1390
1391         // Draw timeline border.
1392         ctx.save();
1393         ctx.beginPath();
1394         this.addRoundedRect(ctx, scrubberPosition, 7, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
1395         this.addRoundedRect(ctx, scrubberPosition + 1, 8, width - scrubberPosition - borderSize , trackHeight, trackHeight / 2.0);
1396         ctx.closePath();
1397         ctx.clip("evenodd");
1398         if (this.isAudio())
1399             ctx.fillStyle = "rgb(71, 71, 71)";
1400         else
1401             ctx.fillStyle = "rgb(30, 30, 30)";
1402         ctx.fillRect(0, 0, width, height);
1403         ctx.restore();
1404
1405         // Draw played section.
1406         ctx.save();
1407         ctx.beginPath();
1408         this.addRoundedRect(ctx, 0, 7, width, timelineHeight, timelineHeight / 2.0);
1409         ctx.closePath();
1410         ctx.clip();
1411         if (this.isAudio())
1412             ctx.fillStyle = "rgb(116, 116, 116)";
1413         else
1414             ctx.fillStyle = "rgb(75, 75, 75)";
1415         ctx.fillRect(0, 0, width * played, height);
1416         ctx.restore();
1417
1418         // Draw the scrubber.
1419         ctx.save();
1420         ctx.clearRect(scrubberPosition - 1, 0, scrubberWidth + borderSize, height, 0);
1421         ctx.beginPath();
1422         this.addRoundedRect(ctx, scrubberPosition, 1, scrubberWidth, scrubberHeight, 1);
1423         ctx.closePath();
1424         ctx.clip();
1425         if (this.isAudio())
1426             ctx.fillStyle = "rgb(181, 181, 181)";
1427         else
1428             ctx.fillStyle = "rgb(140, 140, 140)";
1429         ctx.fillRect(0, 0, width, height);
1430         ctx.restore();
1431
1432         ctx.restore();
1433     },
1434
1435     drawVolumeBackground: function() {
1436         var dpr = window.devicePixelRatio;
1437         var width = this.controls.volume.offsetWidth * dpr;
1438         var height = this.controls.volume.offsetHeight * dpr;
1439
1440         if (!width || !height)
1441             return;
1442
1443         var ctx = document.getCSSCanvasContext('2d', this.volumeContextName, width, height);
1444
1445         width /= dpr;
1446         height /= dpr;
1447
1448         ctx.save();
1449         ctx.scale(dpr, dpr);
1450         ctx.clearRect(0, 0, width, height);
1451
1452         var seekerPosition = this.controls.volume.value;
1453         var trackHeight = 1;
1454         var timelineHeight = 3;
1455         var scrubberRadius = 3.5;
1456         var scrubberDiameter = 2 * scrubberRadius;
1457         var borderSize = 2;
1458
1459         var scrubberPosition = Math.round(seekerPosition * (width - scrubberDiameter - borderSize));
1460
1461
1462         // Draw portion of volume under slider thumb.
1463         ctx.save();
1464         ctx.beginPath();
1465         this.addRoundedRect(ctx, 0, 3, scrubberPosition + 2, timelineHeight, timelineHeight / 2.0);
1466         ctx.closePath();
1467         ctx.clip();
1468         ctx.fillStyle = "rgb(75, 75, 75)";
1469         ctx.fillRect(0, 0, width, height);
1470         ctx.restore();
1471
1472         // Draw portion of volume above slider thumb.
1473         ctx.save();
1474         ctx.beginPath();
1475         this.addRoundedRect(ctx, scrubberPosition, 3, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
1476         ctx.closePath();
1477         ctx.clip();
1478         ctx.fillStyle = "rgb(30, 30, 30)";
1479         ctx.fillRect(0, 0, width, height);
1480         ctx.restore();
1481
1482         // Clear a hole in the slider for the scrubber.
1483         ctx.save();
1484         ctx.beginPath();
1485         this.addRoundedRect(ctx, scrubberPosition, 0, scrubberDiameter + borderSize, height, (scrubberDiameter + borderSize) / 2.0);
1486         ctx.closePath();
1487         ctx.clip();
1488         ctx.clearRect(0, 0, width, height);
1489         ctx.restore();
1490
1491         // Draw scrubber.
1492         ctx.save();
1493         ctx.beginPath();
1494         this.addRoundedRect(ctx, scrubberPosition + 1, 1, scrubberDiameter, scrubberDiameter, scrubberRadius);
1495         ctx.closePath();
1496         ctx.clip();
1497         if (this.isVolumeSliderActive)
1498             ctx.fillStyle = "white";
1499         else
1500             ctx.fillStyle = "rgb(140, 140, 140)";
1501         ctx.fillRect(0, 0, width, height);
1502         ctx.restore();
1503
1504         ctx.restore();
1505     },
1506
1507     formatTime: function(time)
1508     {
1509         if (isNaN(time))
1510             time = 0;
1511         var absTime = Math.abs(time);
1512         var intSeconds = Math.floor(absTime % 60).toFixed(0);
1513         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
1514         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
1515         var sign = time < 0 ? '-' : String();
1516
1517         if (intHours > 0)
1518             return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
1519
1520         return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
1521     },
1522
1523     updatePlaying: function()
1524     {
1525         this.setPlaying(!this.canPlay());
1526     },
1527
1528     setPlaying: function(isPlaying)
1529     {
1530         if (!this.video.controls && !this.isFullScreen())
1531             return;
1532
1533         if (this.isPlaying === isPlaying)
1534             return;
1535         this.isPlaying = isPlaying;
1536
1537         if (!isPlaying) {
1538             this.controls.panel.classList.add(this.ClassNames.paused);
1539             if (this.controls.panelBackground)
1540                 this.controls.panelBackground.classList.add(this.ClassNames.paused);
1541             this.controls.playButton.classList.add(this.ClassNames.paused);
1542             this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
1543             this.showControls();
1544         } else {
1545             this.controls.panel.classList.remove(this.ClassNames.paused);
1546             if (this.controls.panelBackground)
1547                this.controls.panelBackground.classList.remove(this.ClassNames.paused);
1548             this.controls.playButton.classList.remove(this.ClassNames.paused);
1549             this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
1550             this.resetHideControlsTimer();
1551             this.canToggleShowControlsButton = true;
1552         }
1553     },
1554
1555     updateForShowingControls: function()
1556     {
1557         this.updateLayoutForDisplayedWidth();
1558         this.setNeedsTimelineMetricsUpdate();
1559         this.updateTime();
1560         this.updateProgress();
1561         this.drawVolumeBackground();
1562         this.drawTimelineBackground();
1563         this.controls.panel.classList.add(this.ClassNames.show);
1564         this.controls.panel.classList.remove(this.ClassNames.hidden);
1565         if (this.controls.panelBackground) {
1566             this.controls.panelBackground.classList.add(this.ClassNames.show);
1567             this.controls.panelBackground.classList.remove(this.ClassNames.hidden);
1568         }
1569     },
1570
1571     showShowControlsButton: function (shouldShow) {
1572         this.showControlsButton.hidden = !shouldShow;
1573         if (shouldShow && this.shouldHaveControls())
1574             this.showControlsButton.focus();
1575     },
1576
1577     showControls: function(focusControls)
1578     {
1579         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
1580         if (!this.video.controls && !this.isFullScreen())
1581             return;
1582
1583         this.updateForShowingControls();
1584         if (this.shouldHaveControls() && !this.controls.panel.parentElement) {
1585             this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
1586             this.base.appendChild(this.controls.panel);
1587             if (focusControls)
1588                 this.controls.playButton.focus();
1589         }
1590         this.showShowControlsButton(false);
1591     },
1592
1593     hideControls: function()
1594     {
1595         if (this.controlsAlwaysVisible())
1596             return;
1597
1598         this.clearHideControlsTimer();
1599         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
1600         this.controls.panel.classList.remove(this.ClassNames.show);
1601         if (this.controls.panelBackground)
1602             this.controls.panelBackground.classList.remove(this.ClassNames.show);
1603         this.showShowControlsButton(this.isPlayable() && this.isPlaying && this.canToggleShowControlsButton);
1604     },
1605
1606     setNeedsUpdateForDisplayedWidth: function()
1607     {
1608         this.currentDisplayWidth = 0;
1609     },
1610
1611     scheduleUpdateLayoutForDisplayedWidth: function()
1612     {
1613         setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
1614     },
1615
1616     isControlVisible: function(control)
1617     {
1618         if (!control)
1619             return false;
1620         if (!this.root.contains(control))
1621             return false;
1622         return !control.classList.contains(this.ClassNames.hidden)
1623     },
1624
1625     updateLayoutForDisplayedWidth: function()
1626     {
1627         if (!this.controls || !this.controls.panel)
1628             return;
1629
1630         var visibleWidth = this.controls.panel.getBoundingClientRect().width;
1631         if (this._pageScaleFactor > 1)
1632             visibleWidth *= this._pageScaleFactor;
1633
1634         if (visibleWidth <= 0 || visibleWidth == this.currentDisplayWidth)
1635             return;
1636
1637         this.currentDisplayWidth = visibleWidth;
1638
1639         // Filter all the buttons which are not explicitly hidden.
1640         var buttons = [this.controls.playButton, this.controls.rewindButton, this.controls.captionButton,
1641                        this.controls.fullscreenButton, this.controls.pictureInPictureButton,
1642                        this.controls.wirelessTargetPicker, this.controls.muteBox];
1643         var visibleButtons = buttons.filter(this.isControlVisible, this);
1644
1645         // This tells us how much room we need in order to display every visible button.
1646         var visibleButtonWidth = this.ButtonWidth * visibleButtons.length;
1647
1648         var currentTimeWidth = this.currentTimeClone.getBoundingClientRect().width;
1649         var remainingTimeWidth = this.remainingTimeClone.getBoundingClientRect().width;
1650
1651         // Check if there is enough room for the scrubber.
1652         var shouldDropTimeline = (visibleWidth - visibleButtonWidth - currentTimeWidth - remainingTimeWidth) < this.MinimumTimelineWidth;
1653         this.controls.timeline.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1654         this.controls.currentTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1655         this.controls.thumbnailTrack.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1656         this.controls.remainingTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1657
1658         // Then controls in the following order:
1659         var removeOrder = [this.controls.wirelessTargetPicker, this.controls.pictureInPictureButton,
1660                            this.controls.captionButton, this.controls.muteBox, this.controls.rewindButton,
1661                            this.controls.fullscreenButton];
1662         removeOrder.forEach(function(control) {
1663             var shouldDropControl = visibleWidth < visibleButtonWidth && this.isControlVisible(control);
1664             control.classList.toggle(this.ClassNames.dropped, shouldDropControl);
1665             if (shouldDropControl)
1666                 visibleButtonWidth -= this.ButtonWidth;
1667         }, this);
1668     },
1669
1670     controlsAlwaysVisible: function()
1671     {
1672         if (this.presentationMode() === 'picture-in-picture')
1673             return true;
1674
1675         return this.isAudio() || this.currentPlaybackTargetIsWireless() || this.scrubbing;
1676     },
1677
1678     controlsAreHidden: function()
1679     {
1680         return !this.controlsAlwaysVisible() && !this.controls.panel.classList.contains(this.ClassNames.show) && !this.controls.panel.parentElement;
1681     },
1682
1683     removeControls: function()
1684     {
1685         if (this.controls.panel.parentNode)
1686             this.controls.panel.parentNode.removeChild(this.controls.panel);
1687         this.destroyCaptionMenu();
1688     },
1689
1690     addControls: function()
1691     {
1692         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
1693         this.base.appendChild(this.controls.panel);
1694         this.updateControls();
1695     },
1696
1697     hasControls: function()
1698     {
1699         return this.controls.panel.parentElement;
1700     },
1701
1702     updateTime: function()
1703     {
1704         var currentTime = this.video.currentTime;
1705         var timeRemaining = currentTime - this.video.duration;
1706         this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
1707         this.controls.timeline.value = this.video.currentTime;
1708         this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1709     },
1710     
1711     updateControlsWhileScrubbing: function()
1712     {
1713         if (!this.scrubbing)
1714             return;
1715
1716         var currentTime = (this.controls.timeline.value / this.controls.timeline.max) * this.video.duration;
1717         var timeRemaining = currentTime - this.video.duration;
1718         this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
1719         this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1720         this.drawTimelineBackground();
1721     },
1722
1723     updateReadyState: function()
1724     {
1725         this.updateStatusDisplay();
1726     },
1727
1728     setStatusHidden: function(hidden)
1729     {
1730         if (this.statusHidden === hidden)
1731             return;
1732
1733         this.statusHidden = hidden;
1734
1735         if (hidden) {
1736             this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
1737             this.controls.currentTime.classList.remove(this.ClassNames.hidden);
1738             this.controls.timeline.classList.remove(this.ClassNames.hidden);
1739             this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
1740             this.setNeedsTimelineMetricsUpdate();
1741             this.showControls();
1742         } else {
1743             this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
1744             this.controls.currentTime.classList.add(this.ClassNames.hidden);
1745             this.controls.timeline.classList.add(this.ClassNames.hidden);
1746             this.controls.remainingTime.classList.add(this.ClassNames.hidden);
1747             this.hideControls();
1748         }
1749         this.updateWirelessTargetAvailable();
1750     },
1751
1752     trackHasThumbnails: function(track)
1753     {
1754         return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
1755     },
1756
1757     updateThumbnail: function()
1758     {
1759         for (var i = 0; i < this.video.textTracks.length; ++i) {
1760             var track = this.video.textTracks[i];
1761             if (this.trackHasThumbnails(track)) {
1762                 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
1763                 return;
1764             }
1765         }
1766
1767         this.controls.thumbnail.classList.add(this.ClassNames.hidden);
1768     },
1769
1770     updateCaptionButton: function()
1771     {
1772         var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
1773         var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1774
1775         if ((textTracks && textTracks.length) || (audioTracks && audioTracks.length > 1))
1776             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
1777         else
1778             this.controls.captionButton.classList.add(this.ClassNames.hidden);
1779         this.setNeedsUpdateForDisplayedWidth();
1780         this.updateLayoutForDisplayedWidth();
1781     },
1782
1783     updateCaptionContainer: function()
1784     {
1785         if (!this.host.textTrackContainer)
1786             return;
1787
1788         var hasClosedCaptions = this.video.webkitHasClosedCaptions;
1789         var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
1790
1791         if (hasClosedCaptions && hasHiddenClass)
1792             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
1793         else if (!hasClosedCaptions && !hasHiddenClass)
1794             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
1795
1796         this.updateBase();
1797         this.host.updateTextTrackContainer();
1798     },
1799
1800     buildCaptionMenu: function()
1801     {
1802         var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
1803         var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1804
1805         if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
1806             return;
1807
1808         this.captionMenu = document.createElement('div');
1809         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
1810         this.captionMenu.setAttribute('id', 'audioAndTextTrackMenu');
1811         this.base.appendChild(this.captionMenu);
1812         this.captionMenuItems = [];
1813
1814         var offItem = this.host.captionMenuOffItem;
1815         var automaticItem = this.host.captionMenuAutomaticItem;
1816         var displayMode = this.host.captionDisplayMode;
1817
1818         var list = document.createElement('div');
1819         this.captionMenu.appendChild(list);
1820         list.classList.add(this.ClassNames.list);
1821
1822         if (audioTracks && audioTracks.length > 1) {
1823             var heading = document.createElement('h3');
1824             heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
1825             list.appendChild(heading);
1826             heading.innerText = this.UIString('Audio');
1827
1828             var ul = document.createElement('ul');
1829             ul.setAttribute('role', 'menu');
1830             ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
1831             list.appendChild(ul);
1832
1833             for (var i = 0; i < audioTracks.length; ++i) {
1834                 var menuItem = document.createElement('li');
1835                 menuItem.setAttribute('role', 'menuitemradio');
1836                 menuItem.setAttribute('tabindex', '-1');
1837                 this.captionMenuItems.push(menuItem);
1838                 this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
1839                 this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
1840                 ul.appendChild(menuItem);
1841
1842                 var track = audioTracks[i];
1843                 menuItem.innerText = this.host.displayNameForTrack(track);
1844                 menuItem.track = track;
1845
1846                 var itemCheckmark = document.createElement("img");
1847                 itemCheckmark.classList.add("checkmark-container");
1848                 menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
1849
1850                 if (track.enabled) {
1851                     menuItem.classList.add(this.ClassNames.selected);
1852                     menuItem.setAttribute('tabindex', '0');
1853                     menuItem.setAttribute('aria-checked', 'true');
1854                 }
1855             }
1856         }
1857
1858         if (textTracks && textTracks.length > 2) {
1859             var heading = document.createElement('h3');
1860             heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
1861             list.appendChild(heading);
1862             heading.innerText = this.UIString('Subtitles');
1863
1864             var ul = document.createElement('ul');
1865             ul.setAttribute('role', 'menu');
1866             ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
1867             list.appendChild(ul);
1868
1869             for (var i = 0; i < textTracks.length; ++i) {
1870                 var menuItem = document.createElement('li');
1871                 menuItem.setAttribute('role', 'menuitemradio');
1872                 menuItem.setAttribute('tabindex', '-1');
1873                 this.captionMenuItems.push(menuItem);
1874                 this.listenFor(menuItem, 'click', this.captionItemSelected);
1875                 this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
1876                 ul.appendChild(menuItem);
1877
1878                 var track = textTracks[i];
1879                 menuItem.innerText = this.host.displayNameForTrack(track);
1880                 menuItem.track = track;
1881
1882                 var itemCheckmark = document.createElement("img");
1883                 itemCheckmark.classList.add("checkmark-container");
1884                 menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
1885
1886                 if (track === offItem) {
1887                     var offMenu = menuItem;
1888                     continue;
1889                 }
1890
1891                 if (track === automaticItem) {
1892                     if (displayMode === 'automatic') {
1893                         menuItem.classList.add(this.ClassNames.selected);
1894                         menuItem.setAttribute('tabindex', '0');
1895                         menuItem.setAttribute('aria-checked', 'true');
1896                     }
1897                     continue;
1898                 }
1899
1900                 if (displayMode != 'automatic' && track.mode === 'showing') {
1901                     var trackMenuItemSelected = true;
1902                     menuItem.classList.add(this.ClassNames.selected);
1903                     menuItem.setAttribute('tabindex', '0');
1904                     menuItem.setAttribute('aria-checked', 'true');
1905                 }
1906
1907             }
1908
1909             if (offMenu && (displayMode === 'forced-only' || displayMode === 'manual') && !trackMenuItemSelected) {
1910                 offMenu.classList.add(this.ClassNames.selected);
1911                 offMenu.setAttribute('tabindex', '0');
1912                 offMenu.setAttribute('aria-checked', 'true');
1913             }
1914         }
1915         
1916         // focus first selected menuitem
1917         for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
1918             var item = this.captionMenuItems[i];
1919             if (item.classList.contains(this.ClassNames.selected)) {
1920                 item.focus();
1921                 break;
1922             }
1923         }
1924         
1925     },
1926
1927     captionItemSelected: function(event)
1928     {
1929         this.host.setSelectedTextTrack(event.target.track);
1930         this.destroyCaptionMenu();
1931     },
1932
1933     focusSiblingCaptionItem: function(event)
1934     {
1935         var currentItem = event.target;
1936         var pendingItem = false;
1937         switch(event.keyCode) {
1938         case this.KeyCodes.left:
1939         case this.KeyCodes.up:
1940             pendingItem = currentItem.previousSibling;
1941             break;
1942         case this.KeyCodes.right:
1943         case this.KeyCodes.down:
1944             pendingItem = currentItem.nextSibling;
1945             break;
1946         }
1947         if (pendingItem) {
1948             currentItem.setAttribute('tabindex', '-1');
1949             pendingItem.setAttribute('tabindex', '0');
1950             pendingItem.focus();
1951         }
1952     },
1953
1954     handleCaptionItemKeyUp: function(event)
1955     {
1956         switch (event.keyCode) {
1957         case this.KeyCodes.enter:
1958         case this.KeyCodes.space:
1959             this.captionItemSelected(event);
1960             break;
1961         case this.KeyCodes.escape:
1962             this.destroyCaptionMenu();
1963             break;
1964         case this.KeyCodes.left:
1965         case this.KeyCodes.up:
1966         case this.KeyCodes.right:
1967         case this.KeyCodes.down:
1968             this.focusSiblingCaptionItem(event);
1969             break;
1970         default:
1971             return;
1972         }
1973         // handled
1974         event.stopPropagation();
1975         event.preventDefault();
1976     },
1977
1978     audioTrackItemSelected: function(event)
1979     {
1980         for (var i = 0; i < this.video.audioTracks.length; ++i) {
1981             var track = this.video.audioTracks[i];
1982             track.enabled = (track == event.target.track);
1983         }
1984
1985         this.destroyCaptionMenu();
1986     },
1987
1988     focusSiblingAudioTrackItem: function(event)
1989     {
1990         var currentItem = event.target;
1991         var pendingItem = false;
1992         switch(event.keyCode) {
1993             case this.KeyCodes.left:
1994             case this.KeyCodes.up:
1995                 pendingItem = currentItem.previousSibling;
1996                 break;
1997             case this.KeyCodes.right:
1998             case this.KeyCodes.down:
1999                 pendingItem = currentItem.nextSibling;
2000                 break;
2001         }
2002         if (pendingItem) {
2003             currentItem.setAttribute('tabindex', '-1');
2004             pendingItem.setAttribute('tabindex', '0');
2005             pendingItem.focus();
2006         }
2007     },
2008
2009     handleAudioTrackItemKeyUp: function(event)
2010     {
2011         switch (event.keyCode) {
2012             case this.KeyCodes.enter:
2013             case this.KeyCodes.space:
2014                 this.audioTrackItemSelected(event);
2015                 break;
2016             case this.KeyCodes.escape:
2017                 this.destroyCaptionMenu();
2018                 break;
2019             case this.KeyCodes.left:
2020             case this.KeyCodes.up:
2021             case this.KeyCodes.right:
2022             case this.KeyCodes.down:
2023                 this.focusSiblingAudioTrackItem(event);
2024                 break;
2025             default:
2026                 return;
2027         }
2028         // handled
2029         event.stopPropagation();
2030         event.preventDefault();
2031     },
2032
2033     destroyCaptionMenu: function()
2034     {
2035         if (!this.captionMenu)
2036             return;
2037
2038         this.captionMenuItems.forEach(function(item){
2039             this.stopListeningFor(item, 'click', this.captionItemSelected);
2040             this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
2041         }, this);
2042
2043         // FKA and AX: focus the trigger before destroying the element with focus
2044         if (this.controls.captionButton)
2045             this.controls.captionButton.focus();
2046
2047         if (this.captionMenu.parentNode)
2048             this.captionMenu.parentNode.removeChild(this.captionMenu);
2049         delete this.captionMenu;
2050         delete this.captionMenuItems;
2051     },
2052
2053     updateHasAudio: function()
2054     {
2055         if (this.video.audioTracks.length && !this.currentPlaybackTargetIsWireless())
2056             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
2057         else
2058             this.controls.muteBox.classList.add(this.ClassNames.hidden);
2059
2060         this.setNeedsUpdateForDisplayedWidth();
2061         this.updateLayoutForDisplayedWidth();
2062     },
2063
2064     updateHasVideo: function()
2065     {
2066         this.controls.panel.classList.toggle(this.ClassNames.noVideo, !this.hasVideo());
2067         // The availability of the picture-in-picture button as well as the full-screen
2068         // button depends no the value returned by hasVideo(), so make sure we invalidate
2069         // the availability of both controls.
2070         this.updateFullscreenButtons();
2071     },
2072
2073     updateVolume: function()
2074     {
2075         if (this.video.muted || !this.video.volume) {
2076             this.controls.muteButton.classList.add(this.ClassNames.muted);
2077             this.controls.volume.value = 0;
2078         } else {
2079             this.controls.muteButton.classList.remove(this.ClassNames.muted);
2080             this.controls.volume.value = this.video.volume;
2081         }
2082         this.drawVolumeBackground();
2083     },
2084
2085     isAudio: function()
2086     {
2087         return this.video instanceof HTMLAudioElement;
2088     },
2089
2090     clearHideControlsTimer: function()
2091     {
2092         if (this.hideTimer)
2093             clearTimeout(this.hideTimer);
2094         this.hideTimer = null;
2095     },
2096
2097     resetHideControlsTimer: function()
2098     {
2099         if (this.hideTimer) {
2100             clearTimeout(this.hideTimer);
2101             this.hideTimer = null;
2102         }
2103
2104         if (this.isPlaying)
2105             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
2106     },
2107
2108     handlePictureInPictureButtonClicked: function(event) {
2109         if (!('webkitSetPresentationMode' in this.video))
2110             return;
2111
2112         if (this.presentationMode() === 'picture-in-picture')
2113             this.video.webkitSetPresentationMode('inline');
2114         else
2115             this.video.webkitSetPresentationMode('picture-in-picture');
2116     },
2117
2118     currentPlaybackTargetIsWireless: function() {
2119         if (Controller.gSimulateWirelessPlaybackTarget)
2120             return true;
2121
2122         if (!this.currentTargetIsWireless || this.wirelessPlaybackDisabled)
2123             return false;
2124
2125         return true;
2126     },
2127
2128     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
2129         var shouldListen = true;
2130         if (this.video.error)
2131             shouldListen = false;
2132         if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
2133             shouldListen = false;
2134         if (document.hidden)
2135             shouldListen = false;
2136
2137         this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
2138     },
2139
2140     updateWirelessPlaybackStatus: function() {
2141         if (this.currentPlaybackTargetIsWireless()) {
2142             var deviceName = "";
2143             var deviceType = "";
2144             var type = this.host.externalDeviceType;
2145             if (type == "airplay") {
2146                 deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
2147                 deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
2148             } else if (type == "tvout") {
2149                 deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
2150                 deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
2151             }
2152
2153             this.controls.inlinePlaybackPlaceholderTextTop.innerText = deviceType;
2154             this.controls.inlinePlaybackPlaceholderTextBottom.innerText = deviceName;
2155             this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
2156             this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
2157             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing);
2158             if (!this.isFullScreen() && (this.video.offsetWidth <= 250 || this.video.offsetHeight <= 200)) {
2159                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.small);
2160                 this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.small);
2161                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.small);
2162             } else {
2163                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.small);
2164                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.small);
2165                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.small);
2166             }
2167             this.controls.volumeBox.classList.add(this.ClassNames.hidden);
2168             this.controls.muteBox.classList.add(this.ClassNames.hidden);
2169             this.updateBase();
2170             this.showControls();
2171         } else {
2172             this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
2173             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing);
2174             this.controls.volumeBox.classList.remove(this.ClassNames.hidden);
2175             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
2176         }
2177         this.setNeedsUpdateForDisplayedWidth();
2178         this.updateLayoutForDisplayedWidth();
2179         this.reconnectControls();
2180         this.updateWirelessTargetPickerButton();
2181     },
2182
2183     updateWirelessTargetAvailable: function() {
2184         this.currentTargetIsWireless = this.video.webkitCurrentPlaybackTargetIsWireless;
2185         this.wirelessPlaybackDisabled = this.video.webkitWirelessVideoPlaybackDisabled;
2186
2187         var wirelessPlaybackTargetsAvailable = Controller.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets;
2188         if (this.wirelessPlaybackDisabled)
2189             wirelessPlaybackTargetsAvailable = false;
2190
2191         if (wirelessPlaybackTargetsAvailable && this.isPlayable())
2192             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
2193         else
2194             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
2195         this.setNeedsUpdateForDisplayedWidth();
2196         this.updateLayoutForDisplayedWidth();
2197     },
2198
2199     handleWirelessPickerButtonClicked: function(event)
2200     {
2201         this.video.webkitShowPlaybackTargetPicker();
2202         return true;
2203     },
2204
2205     handleWirelessPlaybackChange: function(event) {
2206         this.updateWirelessTargetAvailable();
2207         this.updateWirelessPlaybackStatus();
2208         this.setNeedsTimelineMetricsUpdate();
2209     },
2210
2211     handleWirelessTargetAvailableChange: function(event) {
2212         var wirelessPlaybackTargetsAvailable = event.availability == "available";
2213         if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
2214             return;
2215
2216         this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
2217         this.updateWirelessTargetAvailable();
2218         this.setNeedsTimelineMetricsUpdate();
2219     },
2220
2221     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) {
2222         if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
2223             return;
2224
2225         if (shouldListen && this.video.error)
2226             return;
2227
2228         this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
2229         if (shouldListen)
2230             this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
2231         else
2232             this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
2233     },
2234
2235     get scrubbing()
2236     {
2237         return this._scrubbing;
2238     },
2239
2240     set scrubbing(flag)
2241     {
2242         if (this._scrubbing == flag)
2243             return;
2244         this._scrubbing = flag;
2245
2246         if (this._scrubbing)
2247             this.wasPlayingWhenScrubbingStarted = !this.video.paused;
2248         else if (this.wasPlayingWhenScrubbingStarted && this.video.paused) {
2249             this.video.play();
2250             this.resetHideControlsTimer();
2251         }
2252     },
2253
2254     get pageScaleFactor()
2255     {
2256         return this._pageScaleFactor;
2257     },
2258
2259     set pageScaleFactor(newScaleFactor)
2260     {
2261         if (this._pageScaleFactor === newScaleFactor)
2262             return;
2263
2264         this._pageScaleFactor = newScaleFactor;
2265     },
2266
2267     set usesLTRUserInterfaceLayoutDirection(usesLTRUserInterfaceLayoutDirection)
2268     {
2269         this.controls.volumeBox.classList.toggle(this.ClassNames.usesLTRUserInterfaceLayoutDirection, usesLTRUserInterfaceLayoutDirection);
2270     },
2271
2272     handleRootResize: function(event)
2273     {
2274         this.updateLayoutForDisplayedWidth();
2275         this.setNeedsTimelineMetricsUpdate();
2276         this.updateTimelineMetricsIfNeeded();
2277         this.drawTimelineBackground();
2278     },
2279
2280     getCurrentControlsStatus: function ()
2281     {
2282         var result = {
2283             idiom: this.idiom,
2284             status: "ok"
2285         };
2286
2287         var elements = [
2288             {
2289                 name: "Show Controls",
2290                 object: this.showControlsButton,
2291                 extraProperties: ["hidden"]
2292             },
2293             {
2294                 name: "Status Display",
2295                 object: this.controls.statusDisplay,
2296                 styleValues: ["display"],
2297                 extraProperties: ["textContent"]
2298             },
2299             {
2300                 name: "Play Button",
2301                 object: this.controls.playButton
2302             },
2303             {
2304                 name: "Rewind Button",
2305                 object: this.controls.rewindButton
2306             },
2307             {
2308                 name: "Timeline Box",
2309                 object: this.controls.timelineBox
2310             },
2311             {
2312                 name: "Mute Box",
2313                 object: this.controls.muteBox
2314             },
2315             {
2316                 name: "Fullscreen Button",
2317                 object: this.controls.fullscreenButton
2318             },
2319             {
2320                 name: "AppleTV Device Picker",
2321                 object: this.controls.wirelessTargetPicker,
2322                 styleValues: ["display"],
2323                 extraProperties: ["hidden"],
2324             },
2325             {
2326                 name: "Picture-in-picture Button",
2327                 object: this.controls.pictureInPictureButton,
2328                 extraProperties: ["parentElement"],
2329             },
2330             {
2331                 name: "Track Menu",
2332                 object: this.captionMenu
2333             },
2334             {
2335                 name: "Inline playback placeholder",
2336                 object: this.controls.inlinePlaybackPlaceholder,
2337             },
2338         ];
2339
2340         elements.forEach(function (element) {
2341             var obj = element.object;
2342             delete element.object;
2343
2344             element.computedStyle = {};
2345             if (obj && element.styleValues) {
2346                 var computedStyle = window.getComputedStyle(obj);
2347                 element.styleValues.forEach(function (propertyName) {
2348                     element.computedStyle[propertyName] = computedStyle[propertyName];
2349                 });
2350                 delete element.styleValues;
2351             }
2352
2353             element.bounds = obj ? obj.getBoundingClientRect() : null;
2354             element.className = obj ? obj.className : null;
2355             element.ariaLabel = obj ? obj.getAttribute('aria-label') : null;
2356
2357             if (element.extraProperties) {
2358                 element.extraProperties.forEach(function (property) {
2359                     element[property] = obj ? obj[property] : null;
2360                 });
2361                 delete element.extraProperties;
2362             }
2363
2364              element.element = obj;
2365         });
2366
2367         result.elements = elements;
2368
2369         return JSON.stringify(result);
2370     }
2371
2372 };