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