1167cddd28af01fdfc036bdee15563a6f9d55d10
[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     handlePresentationModeChange: function(event)
913     {
914         var presentationMode = this.presentationMode();
915
916         switch (presentationMode) {
917             case 'inline':
918                 this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
919                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
920                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
921                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
922                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
923                 this.base.classList.remove(this.ClassNames.placeholderShowing);
924
925                 this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
926                 break;
927             case 'picture-in-picture':
928                 this.controls.panel.classList.add(this.ClassNames.pictureInPicture);
929                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture);
930                 this.showInlinePlaybackPlaceholderWhenSafe();
931
932                 this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in Picture in Picture');
933                 this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture);
934                 this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
935                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture);
936
937                 this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture);
938                 break;
939             default:
940                 this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
941                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
942                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
943                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
944
945                 this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
946                 break;
947         }
948
949         this.updateControls();
950         this.updateCaptionContainer();
951         this.resetHideControlsTimer();
952         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
953             this.showControls();
954         this.host.setPreparedForInline(presentationMode === 'inline')
955     },
956
957     handleFullscreenChange: function(event)
958     {
959         this.updateBase();
960         this.updateControls();
961         this.updateFullscreenButtons();
962         this.updateWirelessPlaybackStatus();
963
964         if (this.isFullScreen()) {
965             this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
966             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
967             this.host.enteredFullscreen();
968         } else {
969             this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
970             this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
971             this.host.exitedFullscreen();
972         }
973
974         if ('webkitPresentationMode' in this.video)
975             this.handlePresentationModeChange(event);
976     },
977
978     handleShowControlsClick: function(event)
979     {
980         if (!this.video.controls && !this.isFullScreen())
981             return;
982
983         if (this.controlsAreHidden())
984             this.showControls(true);
985     },
986
987     handleWrapperMouseMove: function(event)
988     {
989         if (!this.video.controls && !this.isFullScreen())
990             return;
991
992         if (this.controlsAreHidden())
993             this.showControls();
994         this.resetHideControlsTimer();
995
996         if (!this.isDragging)
997             return;
998         var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
999         this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
1000         this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
1001         event.stopPropagation()
1002     },
1003
1004     handleWrapperMouseOut: function(event)
1005     {
1006         this.hideControls();
1007         this.clearHideControlsTimer();
1008     },
1009
1010     handleWrapperMouseUp: function(event)
1011     {
1012         this.isDragging = false;
1013         this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
1014     },
1015
1016     handlePanelMouseDown: function(event)
1017     {
1018         if (event.target != this.controls.panelTint && event.target != this.controls.inlinePlaybackPlaceholder)
1019             return;
1020
1021         if (!this.isFullScreen())
1022             return;
1023
1024         this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
1025         this.isDragging = true;
1026         this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
1027         this.initialOffset = new WebKitPoint(
1028             parseInt(this.controls.panel.style.left) | 0,
1029             parseInt(this.controls.panel.style.top) | 0
1030         );
1031     },
1032
1033     handlePanelTransitionEnd: function(event)
1034     {
1035         var opacity = window.getComputedStyle(this.controls.panel).opacity;
1036         if (!parseInt(opacity) && !this.controlsAlwaysVisible() && (this.video.controls || this.isFullScreen())) {
1037             this.base.removeChild(this.controls.inlinePlaybackPlaceholder);
1038             this.base.removeChild(this.controls.panel);
1039         }
1040     },
1041
1042     handlePanelClick: function(event)
1043     {
1044         // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
1045         event.preventDefault();
1046     },
1047
1048     handlePanelDragStart: function(event)
1049     {
1050         // Prevent drags in the panel from triggering a drag event on the <video> element.
1051         event.preventDefault();
1052     },
1053
1054     handlePlaceholderClick: function(event)
1055     {
1056         // Prevent clicks in the placeholder from playing or pausing the video in a MediaDocument.
1057         event.preventDefault();
1058     },
1059
1060     handleRewindButtonClicked: function(event)
1061     {
1062         var newTime = Math.max(
1063                                this.video.currentTime - this.RewindAmount,
1064                                this.video.seekable.start(0));
1065         this.video.currentTime = newTime;
1066         return true;
1067     },
1068
1069     canPlay: function()
1070     {
1071         return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
1072     },
1073
1074     handlePlayButtonClicked: function(event)
1075     {
1076         if (this.canPlay()) {
1077             this.canToggleShowControlsButton = true;
1078             this.video.play();
1079         } else
1080             this.video.pause();
1081         return true;
1082     },
1083
1084     handleTimelineInput: function(event)
1085     {
1086         if (this.scrubbing)
1087             this.video.pause();
1088
1089         this.video.fastSeek(this.controls.timeline.value);
1090         this.updateControlsWhileScrubbing();
1091     },
1092
1093     handleTimelineChange: function(event)
1094     {
1095         this.video.currentTime = this.controls.timeline.value;
1096         this.updateProgress();
1097     },
1098
1099     handleTimelineDown: function(event)
1100     {
1101         this.controls.thumbnail.classList.add(this.ClassNames.show);
1102     },
1103
1104     handleTimelineUp: function(event)
1105     {
1106         this.controls.thumbnail.classList.remove(this.ClassNames.show);
1107     },
1108
1109     handleTimelineMouseOver: function(event)
1110     {
1111         this.controls.thumbnail.classList.add(this.ClassNames.show);
1112     },
1113
1114     handleTimelineMouseOut: function(event)
1115     {
1116         this.controls.thumbnail.classList.remove(this.ClassNames.show);
1117     },
1118
1119     handleTimelineMouseMove: function(event)
1120     {
1121         if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
1122             return;
1123
1124         this.updateTimelineMetricsIfNeeded();
1125         this.controls.thumbnail.classList.add(this.ClassNames.show);
1126         var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
1127         var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
1128         percent = Math.max(Math.min(1, percent), 0);
1129         this.controls.thumbnail.style.left = percent * 100 + '%';
1130
1131         var thumbnailTime = percent * this.video.duration;
1132         for (var i = 0; i < this.video.textTracks.length; ++i) {
1133             var track = this.video.textTracks[i];
1134             if (!this.trackHasThumbnails(track))
1135                 continue;
1136
1137             if (!track.cues)
1138                 continue;
1139
1140             for (var j = 0; j < track.cues.length; ++j) {
1141                 var cue = track.cues[j];
1142                 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
1143                     this.controls.thumbnailImage.src = cue.text;
1144                     return;
1145                 }
1146             }
1147         }
1148     },
1149
1150     handleTimelineMouseDown: function(event)
1151     {
1152         this.scrubbing = true;
1153     },
1154
1155     handleTimelineMouseUp: function(event)
1156     {
1157         this.scrubbing = false;
1158     },
1159     
1160     handleTimelineKeyDown: function(event)
1161     {
1162         if (event.keyCode == this.KeyCodes.left)
1163             this.controls.timeline.value = this.decrementTimelineValue();
1164         else if (event.keyCode == this.KeyCodes.right)
1165             this.controls.timeline.value = this.incrementTimelineValue();
1166     },
1167
1168     handleMuteButtonClicked: function(event)
1169     {
1170         this.video.muted = !this.video.muted;
1171         if (this.video.muted)
1172             this.controls.muteButton.setAttribute('aria-checked', 'true');
1173         else
1174             this.controls.muteButton.setAttribute('aria-checked', 'false');
1175         this.drawVolumeBackground();
1176         return true;
1177     },
1178
1179     handleMuteBoxOver: function(event)
1180     {
1181         this.drawVolumeBackground();
1182     },
1183
1184     handleMinButtonClicked: function(event)
1185     {
1186         if (this.video.muted) {
1187             this.video.muted = false;
1188             this.controls.muteButton.setAttribute('aria-checked', 'false');
1189         }
1190         this.video.volume = 0;
1191         return true;
1192     },
1193
1194     handleMaxButtonClicked: function(event)
1195     {
1196         if (this.video.muted) {
1197             this.video.muted = false;
1198             this.controls.muteButton.setAttribute('aria-checked', 'false');
1199         }
1200         this.video.volume = 1;
1201     },
1202
1203     updateVideoVolume: function()
1204     {
1205         if (this.video.muted) {
1206             this.video.muted = false;
1207             this.controls.muteButton.setAttribute('aria-checked', 'false');
1208         }
1209         this.video.volume = this.controls.volume.value;
1210         this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
1211     },
1212
1213     handleVolumeSliderInput: function(event)
1214     {
1215         this.updateVideoVolume();
1216         this.drawVolumeBackground();
1217     },
1218     
1219     handleVolumeSliderChange: function(event)
1220     {
1221         this.updateVideoVolume();
1222     },
1223
1224     handleVolumeSliderMouseDown: function(event)
1225     {
1226         this.isVolumeSliderActive = true;
1227         this.drawVolumeBackground();
1228     },
1229
1230     handleVolumeSliderMouseUp: function(event)
1231     {
1232         this.isVolumeSliderActive = false;
1233         this.drawVolumeBackground();
1234     },
1235
1236     handleCaptionButtonClicked: function(event)
1237     {
1238         if (this.captionMenu)
1239             this.destroyCaptionMenu();
1240         else
1241             this.buildCaptionMenu();
1242         return true;
1243     },
1244
1245     hasVideo: function()
1246     {
1247         return this.video.videoTracks && this.video.videoTracks.length;
1248     },
1249
1250     updateFullscreenButtons: function()
1251     {
1252         var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVideo();
1253         this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden && !this.isFullScreen());
1254         this.updatePictureInPictureButton();
1255         this.setNeedsUpdateForDisplayedWidth();
1256         this.updateLayoutForDisplayedWidth();
1257     },
1258
1259     handleFullscreenButtonClicked: function(event)
1260     {
1261         if (this.isFullScreen())
1262             this.video.webkitExitFullscreen();
1263         else
1264             this.video.webkitEnterFullscreen();
1265         return true;
1266     },
1267     
1268     updateWirelessTargetPickerButton: function() {
1269         var wirelessTargetPickerColor;
1270         if (this.controls.wirelessTargetPicker.classList.contains('playing'))
1271             wirelessTargetPickerColor = "-apple-wireless-playback-target-active";
1272         else
1273             wirelessTargetPickerColor = "rgba(255,255,255,0.45)";
1274         if (window.devicePixelRatio == 2)
1275             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>\")";
1276         else
1277             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>\")";
1278     },
1279
1280     handleControlsChange: function()
1281     {
1282         try {
1283             this.updateBase();
1284
1285             if (this.shouldHaveControls() && !this.hasControls())
1286                 this.addControls();
1287             else if (!this.shouldHaveControls() && this.hasControls())
1288                 this.removeControls();
1289         } catch(e) {
1290             if (window.console)
1291                 console.error(e);
1292         }
1293     },
1294
1295     nextRate: function()
1296     {
1297         return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
1298     },
1299
1300     handleSeekBackMouseDown: function(event)
1301     {
1302         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
1303         this.video.play();
1304         this.video.playbackRate = this.nextRate() * -1;
1305         this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
1306     },
1307
1308     seekBackFaster: function()
1309     {
1310         this.video.playbackRate = this.nextRate() * -1;
1311     },
1312
1313     handleSeekBackMouseUp: function(event)
1314     {
1315         this.video.playbackRate = this.video.defaultPlaybackRate;
1316         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
1317             this.video.pause();
1318         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
1319             this.video.play();
1320         if (this.seekInterval)
1321             clearInterval(this.seekInterval);
1322     },
1323
1324     handleSeekForwardMouseDown: function(event)
1325     {
1326         this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
1327         this.video.play();
1328         this.video.playbackRate = this.nextRate();
1329         this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
1330     },
1331
1332     seekForwardFaster: function()
1333     {
1334         this.video.playbackRate = this.nextRate();
1335     },
1336
1337     handleSeekForwardMouseUp: function(event)
1338     {
1339         this.video.playbackRate = this.video.defaultPlaybackRate;
1340         if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
1341             this.video.pause();
1342         else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
1343             this.video.play();
1344         if (this.seekInterval)
1345             clearInterval(this.seekInterval);
1346     },
1347
1348     updateDuration: function()
1349     {
1350         var duration = this.video.duration;
1351         this.controls.timeline.min = 0;
1352         this.controls.timeline.max = duration;
1353
1354         this.setIsLive(duration === Number.POSITIVE_INFINITY);
1355
1356         var timeControls = [this.controls.currentTime, this.controls.remainingTime, this.currentTimeClone, this.remainingTimeClone];
1357
1358         function removeTimeClass(className) {
1359             for (let element of timeControls)
1360                 element.classList.remove(className);
1361         }
1362
1363         function addTimeClass(className) {
1364             for (let element of timeControls)
1365                 element.classList.add(className);
1366         }
1367
1368         // Reset existing style.
1369         removeTimeClass(this.ClassNames.threeDigitTime);
1370         removeTimeClass(this.ClassNames.fourDigitTime);
1371         removeTimeClass(this.ClassNames.fiveDigitTime);
1372         removeTimeClass(this.ClassNames.sixDigitTime);
1373
1374         if (duration >= 60*60*10)
1375             addTimeClass(this.ClassNames.sixDigitTime);
1376         else if (duration >= 60*60)
1377             addTimeClass(this.ClassNames.fiveDigitTime);
1378         else if (duration >= 60*10)
1379             addTimeClass(this.ClassNames.fourDigitTime);
1380         else
1381             addTimeClass(this.ClassNames.threeDigitTime);
1382     },
1383
1384     progressFillStyle: function(context)
1385     {
1386         var height = this.timelineHeight;
1387         var gradient = context.createLinearGradient(0, 0, 0, height);
1388         gradient.addColorStop(0, 'rgb(2, 2, 2)');
1389         gradient.addColorStop(1, 'rgb(23, 23, 23)');
1390         return gradient;
1391     },
1392
1393     updateProgress: function()
1394     {
1395         this.updateTimelineMetricsIfNeeded();
1396         this.drawTimelineBackground();
1397     },
1398
1399     addRoundedRect: function(ctx, x, y, width, height, radius) {
1400         ctx.moveTo(x + radius, y);
1401         ctx.arcTo(x + width, y, x + width, y + radius, radius);
1402         ctx.lineTo(x + width, y + height - radius);
1403         ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
1404         ctx.lineTo(x + radius, y + height);
1405         ctx.arcTo(x, y + height, x, y + height - radius, radius);
1406         ctx.lineTo(x, y + radius);
1407         ctx.arcTo(x, y, x + radius, y, radius);
1408     },
1409
1410     drawTimelineBackground: function() {
1411         var dpr = window.devicePixelRatio;
1412         var width = this.timelineWidth * dpr;
1413         var height = this.timelineHeight * dpr;
1414
1415         if (!width || !height)
1416             return;
1417
1418         var played = this.controls.timeline.value / this.controls.timeline.max;
1419         var buffered = 0;
1420         for (var i = 0, end = this.video.buffered.length; i < end; ++i)
1421             buffered = Math.max(this.video.buffered.end(i), buffered);
1422
1423         buffered /= this.video.duration;
1424
1425         var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
1426
1427         width /= dpr;
1428         height /= dpr;
1429
1430         ctx.save();
1431         ctx.scale(dpr, dpr);
1432         ctx.clearRect(0, 0, width, height);
1433
1434         var timelineHeight = 3;
1435         var trackHeight = 1;
1436         var scrubberWidth = 3;
1437         var scrubberHeight = 15;
1438         var borderSize = 2;
1439         var scrubberPosition = Math.max(0, Math.min(width - scrubberWidth, Math.round(width * played)));
1440
1441         // Draw buffered section.
1442         ctx.save();
1443         if (this.isAudio())
1444             ctx.fillStyle = "rgb(71, 71, 71)";
1445         else
1446             ctx.fillStyle = "rgb(30, 30, 30)";
1447         ctx.fillRect(1, 8, Math.round(width * buffered) - borderSize, trackHeight);
1448         ctx.restore();
1449
1450         // Draw timeline border.
1451         ctx.save();
1452         ctx.beginPath();
1453         this.addRoundedRect(ctx, scrubberPosition, 7, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
1454         this.addRoundedRect(ctx, scrubberPosition + 1, 8, width - scrubberPosition - borderSize , trackHeight, trackHeight / 2.0);
1455         ctx.closePath();
1456         ctx.clip("evenodd");
1457         if (this.isAudio())
1458             ctx.fillStyle = "rgb(71, 71, 71)";
1459         else
1460             ctx.fillStyle = "rgb(30, 30, 30)";
1461         ctx.fillRect(0, 0, width, height);
1462         ctx.restore();
1463
1464         // Draw played section.
1465         ctx.save();
1466         ctx.beginPath();
1467         this.addRoundedRect(ctx, 0, 7, width, timelineHeight, timelineHeight / 2.0);
1468         ctx.closePath();
1469         ctx.clip();
1470         if (this.isAudio())
1471             ctx.fillStyle = "rgb(116, 116, 116)";
1472         else
1473             ctx.fillStyle = "rgb(75, 75, 75)";
1474         ctx.fillRect(0, 0, width * played, height);
1475         ctx.restore();
1476
1477         // Draw the scrubber.
1478         ctx.save();
1479         ctx.clearRect(scrubberPosition - 1, 0, scrubberWidth + borderSize, height, 0);
1480         ctx.beginPath();
1481         this.addRoundedRect(ctx, scrubberPosition, 1, scrubberWidth, scrubberHeight, 1);
1482         ctx.closePath();
1483         ctx.clip();
1484         if (this.isAudio())
1485             ctx.fillStyle = "rgb(181, 181, 181)";
1486         else
1487             ctx.fillStyle = "rgb(140, 140, 140)";
1488         ctx.fillRect(0, 0, width, height);
1489         ctx.restore();
1490
1491         ctx.restore();
1492     },
1493
1494     drawVolumeBackground: function() {
1495         var dpr = window.devicePixelRatio;
1496         var width = this.controls.volume.offsetWidth * dpr;
1497         var height = this.controls.volume.offsetHeight * dpr;
1498
1499         if (!width || !height)
1500             return;
1501
1502         var ctx = document.getCSSCanvasContext('2d', this.volumeContextName, width, height);
1503
1504         width /= dpr;
1505         height /= dpr;
1506
1507         ctx.save();
1508         ctx.scale(dpr, dpr);
1509         ctx.clearRect(0, 0, width, height);
1510
1511         var seekerPosition = this.controls.volume.value;
1512         var trackHeight = 1;
1513         var timelineHeight = 3;
1514         var scrubberRadius = 3.5;
1515         var scrubberDiameter = 2 * scrubberRadius;
1516         var borderSize = 2;
1517
1518         var scrubberPosition = Math.round(seekerPosition * (width - scrubberDiameter - borderSize));
1519
1520
1521         // Draw portion of volume under slider thumb.
1522         ctx.save();
1523         ctx.beginPath();
1524         this.addRoundedRect(ctx, 0, 3, scrubberPosition + 2, timelineHeight, timelineHeight / 2.0);
1525         ctx.closePath();
1526         ctx.clip();
1527         ctx.fillStyle = "rgb(75, 75, 75)";
1528         ctx.fillRect(0, 0, width, height);
1529         ctx.restore();
1530
1531         // Draw portion of volume above slider thumb.
1532         ctx.save();
1533         ctx.beginPath();
1534         this.addRoundedRect(ctx, scrubberPosition, 3, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
1535         ctx.closePath();
1536         ctx.clip();
1537         ctx.fillStyle = "rgb(30, 30, 30)";
1538         ctx.fillRect(0, 0, width, height);
1539         ctx.restore();
1540
1541         // Clear a hole in the slider for the scrubber.
1542         ctx.save();
1543         ctx.beginPath();
1544         this.addRoundedRect(ctx, scrubberPosition, 0, scrubberDiameter + borderSize, height, (scrubberDiameter + borderSize) / 2.0);
1545         ctx.closePath();
1546         ctx.clip();
1547         ctx.clearRect(0, 0, width, height);
1548         ctx.restore();
1549
1550         // Draw scrubber.
1551         ctx.save();
1552         ctx.beginPath();
1553         this.addRoundedRect(ctx, scrubberPosition + 1, 1, scrubberDiameter, scrubberDiameter, scrubberRadius);
1554         ctx.closePath();
1555         ctx.clip();
1556         if (this.isVolumeSliderActive)
1557             ctx.fillStyle = "white";
1558         else
1559             ctx.fillStyle = "rgb(140, 140, 140)";
1560         ctx.fillRect(0, 0, width, height);
1561         ctx.restore();
1562
1563         ctx.restore();
1564     },
1565
1566     formatTime: function(time)
1567     {
1568         if (isNaN(time))
1569             time = 0;
1570         var absTime = Math.abs(time);
1571         var intSeconds = Math.floor(absTime % 60).toFixed(0);
1572         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
1573         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
1574         var sign = time < 0 ? '-' : String();
1575
1576         if (intHours > 0)
1577             return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
1578
1579         return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
1580     },
1581
1582     updatePlaying: function()
1583     {
1584         this.setPlaying(!this.canPlay());
1585     },
1586
1587     setPlaying: function(isPlaying)
1588     {
1589         if (!this.video.controls && !this.isFullScreen())
1590             return;
1591
1592         if (this.isPlaying === isPlaying)
1593             return;
1594         this.isPlaying = isPlaying;
1595
1596         if (!isPlaying) {
1597             this.controls.panel.classList.add(this.ClassNames.paused);
1598             if (this.controls.panelBackground)
1599                 this.controls.panelBackground.classList.add(this.ClassNames.paused);
1600             this.controls.playButton.classList.add(this.ClassNames.paused);
1601             this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
1602             this.showControls();
1603         } else {
1604             this.controls.panel.classList.remove(this.ClassNames.paused);
1605             if (this.controls.panelBackground)
1606                this.controls.panelBackground.classList.remove(this.ClassNames.paused);
1607             this.controls.playButton.classList.remove(this.ClassNames.paused);
1608             this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
1609             this.resetHideControlsTimer();
1610             this.canToggleShowControlsButton = true;
1611         }
1612     },
1613
1614     updateForShowingControls: function()
1615     {
1616         this.updateLayoutForDisplayedWidth();
1617         this.setNeedsTimelineMetricsUpdate();
1618         this.updateTime();
1619         this.updateProgress();
1620         this.drawVolumeBackground();
1621         this.drawTimelineBackground();
1622         this.controls.panel.classList.add(this.ClassNames.show);
1623         this.controls.panel.classList.remove(this.ClassNames.hidden);
1624         if (this.controls.panelBackground) {
1625             this.controls.panelBackground.classList.add(this.ClassNames.show);
1626             this.controls.panelBackground.classList.remove(this.ClassNames.hidden);
1627         }
1628     },
1629
1630     showShowControlsButton: function (shouldShow) {
1631         this.showControlsButton.hidden = !shouldShow;
1632         if (shouldShow && this.shouldHaveControls())
1633             this.showControlsButton.focus();
1634     },
1635
1636     showControls: function(focusControls)
1637     {
1638         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
1639         if (!this.video.controls && !this.isFullScreen())
1640             return;
1641
1642         this.updateForShowingControls();
1643         if (this.shouldHaveControls() && !this.controls.panel.parentElement) {
1644             this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
1645             this.base.appendChild(this.controls.panel);
1646             if (focusControls)
1647                 this.controls.playButton.focus();
1648         }
1649         this.showShowControlsButton(false);
1650     },
1651
1652     hideControls: function()
1653     {
1654         if (this.controlsAlwaysVisible())
1655             return;
1656
1657         this.clearHideControlsTimer();
1658         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
1659         this.controls.panel.classList.remove(this.ClassNames.show);
1660         if (this.controls.panelBackground)
1661             this.controls.panelBackground.classList.remove(this.ClassNames.show);
1662         this.showShowControlsButton(this.isPlayable() && this.isPlaying && this.canToggleShowControlsButton);
1663     },
1664
1665     setNeedsUpdateForDisplayedWidth: function()
1666     {
1667         this.currentDisplayWidth = 0;
1668     },
1669
1670     scheduleUpdateLayoutForDisplayedWidth: function()
1671     {
1672         setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
1673     },
1674
1675     isControlVisible: function(control)
1676     {
1677         if (!control)
1678             return false;
1679         if (!this.root.contains(control))
1680             return false;
1681         return !control.classList.contains(this.ClassNames.hidden)
1682     },
1683
1684     updateLayoutForDisplayedWidth: function()
1685     {
1686         if (!this.controls || !this.controls.panel)
1687             return;
1688
1689         var visibleWidth = this.controls.panel.getBoundingClientRect().width;
1690         if (this._pageScaleFactor > 1)
1691             visibleWidth *= this._pageScaleFactor;
1692
1693         if (visibleWidth <= 0 || visibleWidth == this.currentDisplayWidth)
1694             return;
1695
1696         this.currentDisplayWidth = visibleWidth;
1697
1698         // Filter all the buttons which are not explicitly hidden.
1699         var buttons = [this.controls.playButton, this.controls.rewindButton, this.controls.captionButton,
1700                        this.controls.fullscreenButton, this.controls.pictureInPictureButton,
1701                        this.controls.wirelessTargetPicker, this.controls.muteBox];
1702         var visibleButtons = buttons.filter(this.isControlVisible, this);
1703
1704         // This tells us how much room we need in order to display every visible button.
1705         var visibleButtonWidth = this.ButtonWidth * visibleButtons.length;
1706
1707         var currentTimeWidth = this.currentTimeClone.getBoundingClientRect().width;
1708         var remainingTimeWidth = this.remainingTimeClone.getBoundingClientRect().width;
1709
1710         // Check if there is enough room for the scrubber.
1711         var shouldDropTimeline = (visibleWidth - visibleButtonWidth - currentTimeWidth - remainingTimeWidth) < this.MinimumTimelineWidth;
1712         this.controls.timeline.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1713         this.controls.currentTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1714         this.controls.thumbnailTrack.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1715         this.controls.remainingTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
1716
1717         // Then controls in the following order:
1718         var removeOrder = [this.controls.wirelessTargetPicker, this.controls.pictureInPictureButton,
1719                            this.controls.captionButton, this.controls.muteBox, this.controls.rewindButton,
1720                            this.controls.fullscreenButton];
1721         removeOrder.forEach(function(control) {
1722             var shouldDropControl = visibleWidth < visibleButtonWidth && this.isControlVisible(control);
1723             control.classList.toggle(this.ClassNames.dropped, shouldDropControl);
1724             if (shouldDropControl)
1725                 visibleButtonWidth -= this.ButtonWidth;
1726         }, this);
1727     },
1728
1729     controlsAlwaysVisible: function()
1730     {
1731         if (this.presentationMode() === 'picture-in-picture')
1732             return true;
1733
1734         return this.isAudio() || this.currentPlaybackTargetIsWireless() || this.scrubbing;
1735     },
1736
1737     controlsAreHidden: function()
1738     {
1739         return !this.controlsAlwaysVisible() && !this.controls.panel.classList.contains(this.ClassNames.show) && !this.controls.panel.parentElement;
1740     },
1741
1742     removeControls: function()
1743     {
1744         if (this.controls.panel.parentNode)
1745             this.controls.panel.parentNode.removeChild(this.controls.panel);
1746         this.destroyCaptionMenu();
1747     },
1748
1749     addControls: function()
1750     {
1751         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
1752         this.base.appendChild(this.controls.panel);
1753         this.updateControls();
1754     },
1755
1756     hasControls: function()
1757     {
1758         return this.controls.panel.parentElement;
1759     },
1760
1761     updateTime: function()
1762     {
1763         var currentTime = this.video.currentTime;
1764         var timeRemaining = currentTime - this.video.duration;
1765         this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
1766         this.controls.currentTime.setAttribute('aria-label', `${this.UIString('Elapsed')} ${this.formatTime(currentTime)}`);
1767         this.controls.timeline.value = this.video.currentTime;
1768         this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1769         this.controls.remainingTime.setAttribute('aria-label', `${this.UIString('Remaining')} ${this.formatTime(timeRemaining)}`);
1770     },
1771     
1772     updateControlsWhileScrubbing: function()
1773     {
1774         if (!this.scrubbing)
1775             return;
1776
1777         var currentTime = (this.controls.timeline.value / this.controls.timeline.max) * this.video.duration;
1778         var timeRemaining = currentTime - this.video.duration;
1779         this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
1780         this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1781         this.drawTimelineBackground();
1782     },
1783
1784     updateReadyState: function()
1785     {
1786         this.updateStatusDisplay();
1787     },
1788
1789     setStatusHidden: function(hidden)
1790     {
1791         if (this.statusHidden === hidden)
1792             return;
1793
1794         this.statusHidden = hidden;
1795
1796         if (hidden) {
1797             this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
1798             this.controls.currentTime.classList.remove(this.ClassNames.hidden);
1799             this.controls.timeline.classList.remove(this.ClassNames.hidden);
1800             this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
1801             this.setNeedsTimelineMetricsUpdate();
1802             this.showControls();
1803         } else {
1804             this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
1805             this.controls.currentTime.classList.add(this.ClassNames.hidden);
1806             this.controls.timeline.classList.add(this.ClassNames.hidden);
1807             this.controls.remainingTime.classList.add(this.ClassNames.hidden);
1808             this.hideControls();
1809         }
1810         this.updateWirelessTargetAvailable();
1811     },
1812
1813     trackHasThumbnails: function(track)
1814     {
1815         return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
1816     },
1817
1818     updateThumbnail: function()
1819     {
1820         for (var i = 0; i < this.video.textTracks.length; ++i) {
1821             var track = this.video.textTracks[i];
1822             if (this.trackHasThumbnails(track)) {
1823                 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
1824                 return;
1825             }
1826         }
1827
1828         this.controls.thumbnail.classList.add(this.ClassNames.hidden);
1829     },
1830
1831     updateCaptionButton: function()
1832     {
1833         var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
1834         var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1835
1836         if ((textTracks && textTracks.length) || (audioTracks && audioTracks.length > 1))
1837             this.controls.captionButton.classList.remove(this.ClassNames.hidden);
1838         else
1839             this.controls.captionButton.classList.add(this.ClassNames.hidden);
1840         this.setNeedsUpdateForDisplayedWidth();
1841         this.updateLayoutForDisplayedWidth();
1842     },
1843
1844     updateCaptionContainer: function()
1845     {
1846         if (!this.host.textTrackContainer)
1847             return;
1848
1849         var hasClosedCaptions = this.video.webkitHasClosedCaptions;
1850         var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
1851
1852         if (hasClosedCaptions && hasHiddenClass)
1853             this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
1854         else if (!hasClosedCaptions && !hasHiddenClass)
1855             this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
1856
1857         this.updateBase();
1858         this.host.updateTextTrackContainer();
1859     },
1860
1861     buildCaptionMenu: function()
1862     {
1863         var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
1864         var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1865
1866         if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
1867             return;
1868
1869         this.captionMenu = document.createElement('div');
1870         this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
1871         this.captionMenu.setAttribute('id', 'audioAndTextTrackMenu');
1872         this.base.appendChild(this.captionMenu);
1873         this.captionMenuItems = [];
1874
1875         var offItem = this.host.captionMenuOffItem;
1876         var automaticItem = this.host.captionMenuAutomaticItem;
1877         var displayMode = this.host.captionDisplayMode;
1878
1879         var list = document.createElement('div');
1880         this.captionMenu.appendChild(list);
1881         list.classList.add(this.ClassNames.list);
1882
1883         if (audioTracks && audioTracks.length > 1) {
1884             var heading = document.createElement('h3');
1885             heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
1886             list.appendChild(heading);
1887             heading.innerText = this.UIString('Audio');
1888
1889             var ul = document.createElement('ul');
1890             ul.setAttribute('role', 'menu');
1891             ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
1892             list.appendChild(ul);
1893
1894             for (var i = 0; i < audioTracks.length; ++i) {
1895                 var menuItem = document.createElement('li');
1896                 menuItem.setAttribute('role', 'menuitemradio');
1897                 menuItem.setAttribute('tabindex', '-1');
1898                 this.captionMenuItems.push(menuItem);
1899                 this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
1900                 this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
1901                 ul.appendChild(menuItem);
1902
1903                 var track = audioTracks[i];
1904                 menuItem.innerText = this.host.displayNameForTrack(track);
1905                 menuItem.track = track;
1906
1907                 var itemCheckmark = document.createElement("img");
1908                 itemCheckmark.classList.add("checkmark-container");
1909                 menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
1910
1911                 if (track.enabled) {
1912                     menuItem.classList.add(this.ClassNames.selected);
1913                     menuItem.setAttribute('tabindex', '0');
1914                     menuItem.setAttribute('aria-checked', 'true');
1915                 }
1916             }
1917         }
1918
1919         if (textTracks && textTracks.length > 2) {
1920             var heading = document.createElement('h3');
1921             heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
1922             list.appendChild(heading);
1923             heading.innerText = this.UIString('Subtitles');
1924
1925             var ul = document.createElement('ul');
1926             ul.setAttribute('role', 'menu');
1927             ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
1928             list.appendChild(ul);
1929
1930             for (var i = 0; i < textTracks.length; ++i) {
1931                 var menuItem = document.createElement('li');
1932                 menuItem.setAttribute('role', 'menuitemradio');
1933                 menuItem.setAttribute('tabindex', '-1');
1934                 this.captionMenuItems.push(menuItem);
1935                 this.listenFor(menuItem, 'click', this.captionItemSelected);
1936                 this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
1937                 ul.appendChild(menuItem);
1938
1939                 var track = textTracks[i];
1940                 menuItem.innerText = this.host.displayNameForTrack(track);
1941                 menuItem.track = track;
1942
1943                 var itemCheckmark = document.createElement("img");
1944                 itemCheckmark.classList.add("checkmark-container");
1945                 menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
1946
1947                 if (track === offItem) {
1948                     var offMenu = menuItem;
1949                     continue;
1950                 }
1951
1952                 if (track === automaticItem) {
1953                     if (displayMode === 'automatic') {
1954                         menuItem.classList.add(this.ClassNames.selected);
1955                         menuItem.setAttribute('tabindex', '0');
1956                         menuItem.setAttribute('aria-checked', 'true');
1957                     }
1958                     continue;
1959                 }
1960
1961                 if (displayMode != 'automatic' && track.mode === 'showing') {
1962                     var trackMenuItemSelected = true;
1963                     menuItem.classList.add(this.ClassNames.selected);
1964                     menuItem.setAttribute('tabindex', '0');
1965                     menuItem.setAttribute('aria-checked', 'true');
1966                 }
1967
1968             }
1969
1970             if (offMenu && (displayMode === 'forced-only' || displayMode === 'manual') && !trackMenuItemSelected) {
1971                 offMenu.classList.add(this.ClassNames.selected);
1972                 offMenu.setAttribute('tabindex', '0');
1973                 offMenu.setAttribute('aria-checked', 'true');
1974             }
1975         }
1976         
1977         // focus first selected menuitem
1978         for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
1979             var item = this.captionMenuItems[i];
1980             if (item.classList.contains(this.ClassNames.selected)) {
1981                 item.focus();
1982                 break;
1983             }
1984         }
1985         
1986     },
1987
1988     captionItemSelected: function(event)
1989     {
1990         this.host.setSelectedTextTrack(event.target.track);
1991         this.destroyCaptionMenu();
1992     },
1993
1994     focusSiblingCaptionItem: function(event)
1995     {
1996         var currentItem = event.target;
1997         var pendingItem = false;
1998         switch(event.keyCode) {
1999         case this.KeyCodes.left:
2000         case this.KeyCodes.up:
2001             pendingItem = currentItem.previousSibling;
2002             break;
2003         case this.KeyCodes.right:
2004         case this.KeyCodes.down:
2005             pendingItem = currentItem.nextSibling;
2006             break;
2007         }
2008         if (pendingItem) {
2009             currentItem.setAttribute('tabindex', '-1');
2010             pendingItem.setAttribute('tabindex', '0');
2011             pendingItem.focus();
2012         }
2013     },
2014
2015     handleCaptionItemKeyUp: function(event)
2016     {
2017         switch (event.keyCode) {
2018         case this.KeyCodes.enter:
2019         case this.KeyCodes.space:
2020             this.captionItemSelected(event);
2021             break;
2022         case this.KeyCodes.escape:
2023             this.destroyCaptionMenu();
2024             break;
2025         case this.KeyCodes.left:
2026         case this.KeyCodes.up:
2027         case this.KeyCodes.right:
2028         case this.KeyCodes.down:
2029             this.focusSiblingCaptionItem(event);
2030             break;
2031         default:
2032             return;
2033         }
2034         // handled
2035         event.stopPropagation();
2036         event.preventDefault();
2037     },
2038
2039     audioTrackItemSelected: function(event)
2040     {
2041         for (var i = 0; i < this.video.audioTracks.length; ++i) {
2042             var track = this.video.audioTracks[i];
2043             track.enabled = (track == event.target.track);
2044         }
2045
2046         this.destroyCaptionMenu();
2047     },
2048
2049     focusSiblingAudioTrackItem: function(event)
2050     {
2051         var currentItem = event.target;
2052         var pendingItem = false;
2053         switch(event.keyCode) {
2054             case this.KeyCodes.left:
2055             case this.KeyCodes.up:
2056                 pendingItem = currentItem.previousSibling;
2057                 break;
2058             case this.KeyCodes.right:
2059             case this.KeyCodes.down:
2060                 pendingItem = currentItem.nextSibling;
2061                 break;
2062         }
2063         if (pendingItem) {
2064             currentItem.setAttribute('tabindex', '-1');
2065             pendingItem.setAttribute('tabindex', '0');
2066             pendingItem.focus();
2067         }
2068     },
2069
2070     handleAudioTrackItemKeyUp: function(event)
2071     {
2072         switch (event.keyCode) {
2073             case this.KeyCodes.enter:
2074             case this.KeyCodes.space:
2075                 this.audioTrackItemSelected(event);
2076                 break;
2077             case this.KeyCodes.escape:
2078                 this.destroyCaptionMenu();
2079                 break;
2080             case this.KeyCodes.left:
2081             case this.KeyCodes.up:
2082             case this.KeyCodes.right:
2083             case this.KeyCodes.down:
2084                 this.focusSiblingAudioTrackItem(event);
2085                 break;
2086             default:
2087                 return;
2088         }
2089         // handled
2090         event.stopPropagation();
2091         event.preventDefault();
2092     },
2093
2094     destroyCaptionMenu: function()
2095     {
2096         if (!this.captionMenu)
2097             return;
2098
2099         this.captionMenuItems.forEach(function(item){
2100             this.stopListeningFor(item, 'click', this.captionItemSelected);
2101             this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
2102         }, this);
2103
2104         // FKA and AX: focus the trigger before destroying the element with focus
2105         if (this.controls.captionButton)
2106             this.controls.captionButton.focus();
2107
2108         if (this.captionMenu.parentNode)
2109             this.captionMenu.parentNode.removeChild(this.captionMenu);
2110         delete this.captionMenu;
2111         delete this.captionMenuItems;
2112     },
2113
2114     updateHasAudio: function()
2115     {
2116         if (this.video.audioTracks.length && !this.currentPlaybackTargetIsWireless())
2117             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
2118         else
2119             this.controls.muteBox.classList.add(this.ClassNames.hidden);
2120
2121         this.setNeedsUpdateForDisplayedWidth();
2122         this.updateLayoutForDisplayedWidth();
2123     },
2124
2125     updateHasVideo: function()
2126     {
2127         this.controls.panel.classList.toggle(this.ClassNames.noVideo, !this.hasVideo());
2128         // The availability of the picture-in-picture button as well as the full-screen
2129         // button depends no the value returned by hasVideo(), so make sure we invalidate
2130         // the availability of both controls.
2131         this.updateFullscreenButtons();
2132     },
2133
2134     updateVolume: function()
2135     {
2136         if (this.video.muted || !this.video.volume) {
2137             this.controls.muteButton.classList.add(this.ClassNames.muted);
2138             this.controls.volume.value = 0;
2139         } else {
2140             this.controls.muteButton.classList.remove(this.ClassNames.muted);
2141             this.controls.volume.value = this.video.volume;
2142         }
2143         this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
2144         this.drawVolumeBackground();
2145     },
2146
2147     isAudio: function()
2148     {
2149         return this.video instanceof HTMLAudioElement;
2150     },
2151
2152     clearHideControlsTimer: function()
2153     {
2154         if (this.hideTimer)
2155             clearTimeout(this.hideTimer);
2156         this.hideTimer = null;
2157     },
2158
2159     resetHideControlsTimer: function()
2160     {
2161         if (this.hideTimer) {
2162             clearTimeout(this.hideTimer);
2163             this.hideTimer = null;
2164         }
2165
2166         if (this.isPlaying)
2167             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
2168     },
2169
2170     handlePictureInPictureButtonClicked: function(event) {
2171         if (!('webkitSetPresentationMode' in this.video))
2172             return;
2173
2174         if (this.presentationMode() === 'picture-in-picture')
2175             this.video.webkitSetPresentationMode('inline');
2176         else
2177             this.video.webkitSetPresentationMode('picture-in-picture');
2178     },
2179
2180     currentPlaybackTargetIsWireless: function() {
2181         if (Controller.gSimulateWirelessPlaybackTarget)
2182             return true;
2183
2184         if (!this.currentTargetIsWireless || this.wirelessPlaybackDisabled)
2185             return false;
2186
2187         return true;
2188     },
2189
2190     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
2191         var shouldListen = true;
2192         if (this.video.error)
2193             shouldListen = false;
2194         if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
2195             shouldListen = false;
2196         if (document.hidden)
2197             shouldListen = false;
2198
2199         this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
2200     },
2201
2202     updateWirelessPlaybackStatus: function() {
2203         if (this.currentPlaybackTargetIsWireless()) {
2204             var deviceName = "";
2205             var deviceType = "";
2206             var type = this.host.externalDeviceType;
2207             if (type == "airplay") {
2208                 deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
2209                 deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
2210             } else if (type == "tvout") {
2211                 deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
2212                 deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
2213             }
2214
2215             this.controls.inlinePlaybackPlaceholderTextTop.innerText = deviceType;
2216             this.controls.inlinePlaybackPlaceholderTextBottom.innerText = deviceName;
2217             this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
2218             this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.appleTV);
2219             this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
2220             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing);
2221             if (!this.isFullScreen() && (this.video.offsetWidth <= 250 || this.video.offsetHeight <= 200)) {
2222                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.small);
2223                 this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.small);
2224                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.small);
2225             } else {
2226                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.small);
2227                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.small);
2228                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.small);
2229             }
2230             this.controls.volumeBox.classList.add(this.ClassNames.hidden);
2231             this.controls.muteBox.classList.add(this.ClassNames.hidden);
2232             this.updateBase();
2233             this.showControls();
2234         } else {
2235             this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
2236             this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.appleTV);
2237             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing);
2238             this.controls.volumeBox.classList.remove(this.ClassNames.hidden);
2239             this.controls.muteBox.classList.remove(this.ClassNames.hidden);
2240         }
2241         this.setNeedsUpdateForDisplayedWidth();
2242         this.updateLayoutForDisplayedWidth();
2243         this.reconnectControls();
2244         this.updateWirelessTargetPickerButton();
2245     },
2246
2247     updateWirelessTargetAvailable: function() {
2248         this.currentTargetIsWireless = this.video.webkitCurrentPlaybackTargetIsWireless;
2249         this.wirelessPlaybackDisabled = this.video.webkitWirelessVideoPlaybackDisabled;
2250
2251         var wirelessPlaybackTargetsAvailable = Controller.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets;
2252         if (this.wirelessPlaybackDisabled)
2253             wirelessPlaybackTargetsAvailable = false;
2254
2255         if (wirelessPlaybackTargetsAvailable && this.isPlayable())
2256             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
2257         else
2258             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
2259         this.setNeedsUpdateForDisplayedWidth();
2260         this.updateLayoutForDisplayedWidth();
2261     },
2262
2263     handleWirelessPickerButtonClicked: function(event)
2264     {
2265         this.video.webkitShowPlaybackTargetPicker();
2266         return true;
2267     },
2268
2269     handleWirelessPlaybackChange: function(event) {
2270         this.updateWirelessTargetAvailable();
2271         this.updateWirelessPlaybackStatus();
2272         this.setNeedsTimelineMetricsUpdate();
2273     },
2274
2275     handleWirelessTargetAvailableChange: function(event) {
2276         var wirelessPlaybackTargetsAvailable = event.availability == "available";
2277         if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
2278             return;
2279
2280         this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
2281         this.updateWirelessTargetAvailable();
2282         this.setNeedsTimelineMetricsUpdate();
2283     },
2284
2285     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) {
2286         if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
2287             return;
2288
2289         if (shouldListen && this.video.error)
2290             return;
2291
2292         this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
2293         if (shouldListen)
2294             this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
2295         else
2296             this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
2297     },
2298
2299     get scrubbing()
2300     {
2301         return this._scrubbing;
2302     },
2303
2304     set scrubbing(flag)
2305     {
2306         if (this._scrubbing == flag)
2307             return;
2308         this._scrubbing = flag;
2309
2310         if (this._scrubbing)
2311             this.wasPlayingWhenScrubbingStarted = !this.video.paused;
2312         else if (this.wasPlayingWhenScrubbingStarted && this.video.paused) {
2313             this.video.play();
2314             this.resetHideControlsTimer();
2315         }
2316     },
2317
2318     get pageScaleFactor()
2319     {
2320         return this._pageScaleFactor;
2321     },
2322
2323     set pageScaleFactor(newScaleFactor)
2324     {
2325         if (this._pageScaleFactor === newScaleFactor)
2326             return;
2327
2328         this._pageScaleFactor = newScaleFactor;
2329     },
2330
2331     set usesLTRUserInterfaceLayoutDirection(usesLTRUserInterfaceLayoutDirection)
2332     {
2333         this.controls.volumeBox.classList.toggle(this.ClassNames.usesLTRUserInterfaceLayoutDirection, usesLTRUserInterfaceLayoutDirection);
2334     },
2335
2336     handleRootResize: function(event)
2337     {
2338         this.updateLayoutForDisplayedWidth();
2339         this.setNeedsTimelineMetricsUpdate();
2340         this.updateTimelineMetricsIfNeeded();
2341         this.drawTimelineBackground();
2342     },
2343
2344     getCurrentControlsStatus: function ()
2345     {
2346         var result = {
2347             idiom: this.idiom,
2348             status: "ok"
2349         };
2350
2351         var elements = [
2352             {
2353                 name: "Show Controls",
2354                 object: this.showControlsButton,
2355                 extraProperties: ["hidden"],
2356             },
2357             {
2358                 name: "Status Display",
2359                 object: this.controls.statusDisplay,
2360                 styleValues: ["display"],
2361                 extraProperties: ["textContent"],
2362             },
2363             {
2364                 name: "Play Button",
2365                 object: this.controls.playButton,
2366                 extraProperties: ["hidden"],
2367             },
2368             {
2369                 name: "Rewind Button",
2370                 object: this.controls.rewindButton,
2371                 extraProperties: ["hidden"],
2372             },
2373             {
2374                 name: "Timeline Box",
2375                 object: this.controls.timelineBox,
2376             },
2377             {
2378                 name: "Mute Box",
2379                 object: this.controls.muteBox,
2380                 extraProperties: ["hidden"],
2381             },
2382             {
2383                 name: "Fullscreen Button",
2384                 object: this.controls.fullscreenButton,
2385                 extraProperties: ["hidden"],
2386             },
2387             {
2388                 name: "AppleTV Device Picker",
2389                 object: this.controls.wirelessTargetPicker,
2390                 styleValues: ["display"],
2391                 extraProperties: ["hidden"],
2392             },
2393             {
2394                 name: "Picture-in-picture Button",
2395                 object: this.controls.pictureInPictureButton,
2396                 extraProperties: ["parentElement", "hidden"],
2397             },
2398             {
2399                 name: "Caption Button",
2400                 object: this.controls.captionButton,
2401                 extraProperties: ["hidden"],
2402             },
2403             {
2404                 name: "Timeline",
2405                 object: this.controls.timeline,
2406                 extraProperties: ["hidden"],
2407             },
2408             {
2409                 name: "Current Time",
2410                 object: this.controls.currentTime,
2411                 extraProperties: ["hidden"],
2412             },
2413             {
2414                 name: "Thumbnail Track",
2415                 object: this.controls.thumbnailTrack,
2416                 extraProperties: ["hidden"],
2417             },
2418             {
2419                 name: "Time Remaining",
2420                 object: this.controls.remainingTime,
2421                 extraProperties: ["hidden"],
2422             },
2423             {
2424                 name: "Track Menu",
2425                 object: this.captionMenu,
2426             },
2427             {
2428                 name: "Inline playback placeholder",
2429                 object: this.controls.inlinePlaybackPlaceholder,
2430             },
2431             {
2432                 name: "Media Controls Panel",
2433                 object: this.controls.panel,
2434                 extraProperties: ["hidden"],
2435             },
2436             {
2437                 name: "Control Base Element",
2438                 object: this.base || null,
2439             },
2440         ];
2441
2442         elements.forEach(function (element) {
2443             var obj = element.object;
2444             delete element.object;
2445
2446             element.computedStyle = {};
2447             if (obj && element.styleValues) {
2448                 var computedStyle = window.getComputedStyle(obj);
2449                 element.styleValues.forEach(function (propertyName) {
2450                     element.computedStyle[propertyName] = computedStyle[propertyName];
2451                 });
2452                 delete element.styleValues;
2453             }
2454
2455             element.bounds = obj ? obj.getBoundingClientRect() : null;
2456             element.className = obj ? obj.className : null;
2457             element.ariaLabel = obj ? obj.getAttribute('aria-label') : null;
2458
2459             if (element.extraProperties) {
2460                 element.extraProperties.forEach(function (property) {
2461                     element[property] = obj ? obj[property] : null;
2462                 });
2463                 delete element.extraProperties;
2464             }
2465
2466              element.element = obj;
2467         });
2468
2469         result.elements = elements;
2470
2471         return JSON.stringify(result);
2472     }
2473
2474 };