2d06e0acfea2ffb382d01f9a462a39238bb7fbdf
[WebKit-https.git] / Source / WebCore / Modules / mediacontrols / mediaControlsiOS.js
1 function createControls(root, video, host)
2 {
3     return new ControllerIOS(root, video, host);
4 };
5
6 function ControllerIOS(root, video, host)
7 {
8     this.doingSetup = true;
9     this._pageScaleFactor = 1;
10
11     this.timelineContextName = "_webkit-media-controls-timeline-" + host.generateUUID();
12
13     Controller.call(this, root, video, host);
14
15     this.setNeedsTimelineMetricsUpdate();
16
17     this._timelineIsHidden = false;
18     this._currentDisplayWidth = 0;
19     this.scheduleUpdateLayoutForDisplayedWidth();
20
21     host.controlsDependOnPageScaleFactor = true;
22     this.doingSetup = false;
23 };
24
25 /* Constants */
26 ControllerIOS.MinimumTimelineWidth = 200;
27 ControllerIOS.ButtonWidth = 42;
28
29 /* Enums */
30 ControllerIOS.StartPlaybackControls = 2;
31
32
33 ControllerIOS.prototype = {
34     addVideoListeners: function() {
35         Controller.prototype.addVideoListeners.call(this);
36
37         this.listenFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
38         this.listenFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
39         this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
40     },
41
42     removeVideoListeners: function() {
43         Controller.prototype.removeVideoListeners.call(this);
44
45         this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
46         this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
47         this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
48     },
49
50     createBase: function() {
51         Controller.prototype.createBase.call(this);
52
53         var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('button');
54         startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
55         startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
56
57         this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart);
58         this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange);
59         this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd);
60         this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart);
61         this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove);
62         this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut);
63
64         this.listenFor(document, 'visibilitychange', this.handleVisibilityChange);
65     },
66
67     shouldHaveStartPlaybackButton: function() {
68         var allowsInline = this.host.mediaPlaybackAllowsInline;
69
70         if (this.isPlaying || (this.hasPlayed && allowsInline))
71             return false;
72
73         if (this.isAudio() && allowsInline)
74             return false;
75
76         if (this.doingSetup)
77             return true;
78
79         if (this.isFullScreen())
80             return false;
81
82         if (!this.video.currentSrc && this.video.error)
83             return false;
84
85         if (!this.video.controls && allowsInline)
86             return false;
87
88         if (this.video.currentSrc && this.video.error)
89             return true;
90
91         return true;
92     },
93
94     shouldHaveControls: function() {
95         if (this.shouldHaveStartPlaybackButton())
96             return false;
97
98         return Controller.prototype.shouldHaveControls.call(this);
99     },
100
101     shouldHaveAnyUI: function() {
102         return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
103     },
104
105     createControls: function() {
106         Controller.prototype.createControls.call(this);
107
108         var panelContainer = this.controls.panelContainer = document.createElement('div');
109         panelContainer.setAttribute('pseudo', '-webkit-media-controls-panel-container');
110
111         var panelBackground = this.controls.panelBackground = document.createElement('div');
112         panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background');
113
114         var spacer = this.controls.spacer = document.createElement('div');
115         spacer.setAttribute('pseudo', '-webkit-media-controls-spacer');
116         spacer.classList.add(this.ClassNames.hidden);
117
118         var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div');
119         inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text');
120
121         var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p');
122         inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top');
123
124         var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p');
125         inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom');
126
127         var wirelessTargetPicker = this.controls.wirelessTargetPicker
128         this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
129         this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
130         this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
131
132         this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
133         this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
134         this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
135
136         this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
137         this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
138         this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
139         this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
140         this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
141         this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
142         this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
143         this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
144         this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
145         this.listenFor(this.controls.optimizedFullscreenButton, 'touchstart', this.handleOptimizedFullscreenTouchStart);
146         this.listenFor(this.controls.optimizedFullscreenButton, 'touchend', this.handleOptimizedFullscreenTouchEnd);
147         this.listenFor(this.controls.optimizedFullscreenButton, 'touchcancel', this.handleOptimizedFullscreenTouchCancel);
148         this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
149
150         this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
151     },
152
153     setControlsType: function(type) {
154         if (type === this.controlsType)
155             return;
156         Controller.prototype.setControlsType.call(this, type);
157
158         if (type === ControllerIOS.StartPlaybackControls)
159             this.addStartPlaybackControls();
160         else
161             this.removeStartPlaybackControls();
162     },
163
164     addStartPlaybackControls: function() {
165         this.base.appendChild(this.controls.startPlaybackButton);
166     },
167
168     removeStartPlaybackControls: function() {
169         if (this.controls.startPlaybackButton.parentNode)
170             this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
171     },
172
173     reconnectControls: function()
174     {
175         Controller.prototype.reconnectControls.call(this);
176
177         if (this.controlsType === ControllerIOS.StartPlaybackControls)
178             this.addStartPlaybackControls();
179     },
180
181     configureInlineControls: function() {
182         this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
183         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
184         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
185         this.controls.panel.appendChild(this.controls.playButton);
186         this.controls.panel.appendChild(this.controls.statusDisplay);
187         this.controls.panel.appendChild(this.controls.spacer);
188         this.controls.panel.appendChild(this.controls.timelineBox);
189         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
190         if (!this.isLive) {
191             this.controls.timelineBox.appendChild(this.controls.currentTime);
192             this.controls.timelineBox.appendChild(this.controls.timeline);
193             this.controls.timelineBox.appendChild(this.controls.remainingTime);
194         }
195         if (this.isAudio()) {
196             // Hide the scrubber on audio until the user starts playing.
197             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
198         } else {
199             if (Controller.gSimulateOptimizedFullscreenAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('optimized')))
200                 this.controls.panel.appendChild(this.controls.optimizedFullscreenButton);
201             this.controls.panel.appendChild(this.controls.fullscreenButton);
202         }
203     },
204
205     configureFullScreenControls: function() {
206         // Explicitly do nothing to override base-class behavior.
207     },
208
209     showControls: function() {
210         this.updateLayoutForDisplayedWidth();
211         this.updateTime(true);
212         this.updateProgress(true);
213         Controller.prototype.showControls.call(this);
214     },
215
216     addControls: function() {
217         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
218         this.base.appendChild(this.controls.panelContainer);
219         this.controls.panelContainer.appendChild(this.controls.panelBackground);
220         this.controls.panelContainer.appendChild(this.controls.panel);
221         this.setNeedsTimelineMetricsUpdate();
222     },
223
224     updateControls: function() {
225         if (this.shouldHaveStartPlaybackButton())
226             this.setControlsType(ControllerIOS.StartPlaybackControls);
227         else if (this.presentationMode() === "fullscreen")
228             this.setControlsType(Controller.FullScreenControls);
229         else
230             this.setControlsType(Controller.InlineControls);
231
232         this.updateLayoutForDisplayedWidth();
233         this.setNeedsTimelineMetricsUpdate();
234     },
235
236     updateTime: function(forceUpdate) {
237         Controller.prototype.updateTime.call(this, forceUpdate);
238         this.updateProgress();
239     },
240
241     drawTimelineBackground: function() {
242         var width = this.timelineWidth * window.devicePixelRatio;
243         var height = this.timelineHeight * window.devicePixelRatio;
244
245         if (!width || !height)
246             return;
247
248         var played = this.video.currentTime / this.video.duration;
249         var buffered = 0;
250         var bufferedRanges = this.video.buffered;
251         if (bufferedRanges && bufferedRanges.length)
252             buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered);
253
254         buffered /= this.video.duration;
255
256         var ctx = this.video.ownerDocument.getCSSCanvasContext('2d', this.timelineContextName, width, height);
257
258         ctx.clearRect(0, 0, width, height);
259
260         var midY = height / 2;
261
262         // 1. Draw the buffered part and played parts, using
263         // solid rectangles that are clipped to the outside of
264         // the lozenge.
265         ctx.save();
266         ctx.beginPath();
267         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
268         ctx.closePath();
269         ctx.clip();
270         ctx.fillStyle = "white";
271         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
272         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
273         ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height);
274         ctx.restore();
275
276         // 2. Draw the outline with a clip path that subtracts the
277         // middle of a lozenge. This produces a better result than
278         // stroking.
279         ctx.save();
280         ctx.beginPath();
281         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
282         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
283         ctx.closePath();
284         ctx.clip("evenodd");
285         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
286         ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
287         ctx.restore();
288     },
289
290     formatTime: function(time) {
291         if (isNaN(time))
292             time = 0;
293         var absTime = Math.abs(time);
294         var intSeconds = Math.floor(absTime % 60).toFixed(0);
295         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
296         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
297         var sign = time < 0 ? '-' : String();
298
299         if (intHours > 0)
300             return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
301
302         return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
303     },
304
305     handleTimelineChange: function(event) {
306         Controller.prototype.handleTimelineChange.call(this);
307         this.updateProgress();
308     },
309
310     handlePlayButtonTouchStart: function() {
311         this.controls.playButton.classList.add('active');
312     },
313
314     handlePlayButtonTouchEnd: function(event) {
315         this.controls.playButton.classList.remove('active');
316
317         if (this.canPlay())
318             this.video.play();
319         else
320             this.video.pause();
321
322         return true;
323     },
324
325     handlePlayButtonTouchCancel: function(event) {
326         this.controls.playButton.classList.remove('active');
327         return true;
328     },
329
330     handleBaseGestureStart: function(event) {
331         this.gestureStartTime = new Date();
332         // If this gesture started with two fingers inside the video, then
333         // don't treat it as a potential zoom, unless we're still waiting
334         // to play.
335         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
336             event.preventDefault();
337     },
338
339     handleBaseGestureChange: function(event) {
340         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
341             return;
342
343         var scaleDetectionThreshold = 0.2;
344         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
345             delete this.lastDoubleTouchTime;
346
347         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
348             event.preventDefault();
349
350         var currentGestureTime = new Date();
351         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
352         if (!duration)
353             return;
354
355         var velocity = Math.abs(event.scale - 1) / duration;
356
357         var pinchOutVelocityThreshold = 2;
358         var pinchOutGestureScaleThreshold = 1.25;
359         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
360             return;
361
362         delete this.gestureStartTime;
363         this.video.webkitEnterFullscreen();
364     },
365
366     handleBaseGestureEnd: function(event) {
367         delete this.gestureStartTime;
368     },
369
370     handleWrapperTouchStart: function(event) {
371         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
372             return;
373
374         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
375
376         if (this.controlsAreHidden()) {
377             this.showControls();
378             if (this.hideTimer)
379                 clearTimeout(this.hideTimer);
380             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
381         } else if (!this.canPlay())
382             this.hideControls();
383     },
384
385     handlePanelTouchStart: function(event) {
386         this.video.style.webkitUserSelect = 'none';
387     },
388
389     handlePanelTouchEnd: function(event) {
390         this.video.style.removeProperty('-webkit-user-select');
391     },
392
393     handlePanelTouchCancel: function(event) {
394         this.video.style.removeProperty('-webkit-user-select');
395     },
396
397     handleVisibilityChange: function(event) {
398         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
399     },
400
401     presentationMode: function() {
402         if ('webkitPresentationMode' in this.video)
403             return this.video.webkitPresentationMode;
404
405         if (this.isFullScreen())
406             return 'fullscreen';
407
408         return 'inline';
409     },
410
411     isFullScreen: function()
412     {
413         return this.video.webkitDisplayingFullscreen && this.presentationMode() != 'optimized';
414     },
415
416     handleFullscreenButtonClicked: function(event) {
417         if ('webkitSetPresentationMode' in this.video) {
418             if (this.presentationMode() === 'fullscreen')
419                 this.video.webkitSetPresentationMode('inline');
420             else
421                 this.video.webkitSetPresentationMode('fullscreen');
422
423             return;
424         }
425
426         if (this.isFullScreen())
427             this.video.webkitExitFullscreen();
428         else
429             this.video.webkitEnterFullscreen();
430     },
431
432     handleFullscreenTouchStart: function() {
433         this.controls.fullscreenButton.classList.add('active');
434     },
435
436     handleFullscreenTouchEnd: function(event) {
437         this.controls.fullscreenButton.classList.remove('active');
438
439         this.handleFullscreenButtonClicked();
440
441         return true;
442     },
443
444     handleFullscreenTouchCancel: function(event) {
445         this.controls.fullscreenButton.classList.remove('active');
446         return true;
447     },
448
449     handleOptimizedFullscreenButtonClicked: function(event) {
450         if (!('webkitSetPresentationMode' in this.video))
451             return;
452
453         if (this.presentationMode() === 'optimized')
454             this.video.webkitSetPresentationMode('inline');
455         else
456             this.video.webkitSetPresentationMode('optimized');
457     },
458
459     handleOptimizedFullscreenTouchStart: function() {
460         this.controls.optimizedFullscreenButton.classList.add('active');
461     },
462
463     handleOptimizedFullscreenTouchEnd: function(event) {
464         this.controls.optimizedFullscreenButton.classList.remove('active');
465
466         this.handleOptimizedFullscreenButtonClicked();
467
468         return true;
469     },
470
471     handleOptimizedFullscreenTouchCancel: function(event) {
472         this.controls.optimizedFullscreenButton.classList.remove('active');
473         return true;
474     },
475
476     handleStartPlaybackButtonTouchStart: function(event) {
477         this.controls.startPlaybackButton.classList.add('active');
478     },
479
480     handleStartPlaybackButtonTouchEnd: function(event) {
481         this.controls.startPlaybackButton.classList.remove('active');
482         if (this.video.error)
483             return true;
484
485         this.video.play();
486         this.updateControls();
487
488         return true;
489     },
490
491     handleStartPlaybackButtonTouchCancel: function(event) {
492         this.controls.startPlaybackButton.classList.remove('active');
493         return true;
494     },
495
496     handleReadyStateChange: function(event) {
497         Controller.prototype.handleReadyStateChange.call(this, event);
498         this.updateControls();
499     },
500
501     handleWirelessPickerButtonTouchStart: function() {
502         if (!this.video.error)
503             this.controls.wirelessTargetPicker.classList.add('active');
504     },
505
506     handleWirelessPickerButtonTouchEnd: function(event) {
507         this.controls.wirelessTargetPicker.classList.remove('active');
508         return this.handleWirelessPickerButtonClicked();
509     },
510
511     handleWirelessPickerButtonTouchCancel: function(event) {
512         this.controls.wirelessTargetPicker.classList.remove('active');
513         return true;
514     },
515
516     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
517         if (this.controlsType === ControllerIOS.StartPlaybackControls) {
518             this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
519             return;
520         }
521
522         Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
523     },
524
525     updateStatusDisplay: function(event)
526     {
527         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
528         Controller.prototype.updateStatusDisplay.call(this, event);
529     },
530
531     setPlaying: function(isPlaying)
532     {
533         Controller.prototype.setPlaying.call(this, isPlaying);
534
535         this.updateControls();
536
537         if (isPlaying && this.isAudio() && !this._timelineIsHidden) {
538             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
539             this.controls.spacer.classList.add(this.ClassNames.hidden);
540         }
541
542         if (isPlaying)
543             this.hasPlayed = true;
544         else
545             this.showControls();
546     },
547
548     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
549     {
550         if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
551             return;
552
553         Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen);
554     },
555
556     get pageScaleFactor()
557     {
558         return this._pageScaleFactor;
559     },
560
561     set pageScaleFactor(newScaleFactor)
562     {
563         if (this._pageScaleFactor === newScaleFactor)
564             return;
565
566         this._pageScaleFactor = newScaleFactor;
567
568         // FIXME: this should react to the scale change by
569         // unscaling the controls panel. However, this
570         // hits a bug with the backdrop blur layer getting
571         // too big and moving to a tiled layer.
572         // https://bugs.webkit.org/show_bug.cgi?id=142317
573     },
574
575     handlePresentationModeChange: function(event)
576     {
577         var presentationMode = this.presentationMode();
578
579         switch (presentationMode) {
580             case 'inline':
581                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
582                 break;
583             case 'optimized':
584                 var backgroundImage = "url('" + this.host.mediaUIImageData("optimized-fullscreen-placeholder") + "')";
585                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImage;
586                 this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', "video playback placeholder");
587                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
588                 break;
589         }
590
591         this.updateControls();
592         this.updateCaptionContainer();
593         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
594             this.showControls();
595     },
596
597     handleFullscreenChange: function(event)
598     {
599         Controller.prototype.handleFullscreenChange.call(this, event);
600         this.handlePresentationModeChange(event);
601     },
602
603     scheduleUpdateLayoutForDisplayedWidth: function ()
604     {
605         setTimeout(function () {
606             this.updateLayoutForDisplayedWidth();
607         }.bind(this), 0);
608     },
609
610     updateLayoutForDisplayedWidth: function()
611     {
612         if (!this.controls || !this.controls.panel)
613             return;
614
615         var visibleWidth = this.controls.panel.getBoundingClientRect().width * this._pageScaleFactor;
616         if (visibleWidth <= 0 || visibleWidth == this._currentDisplayWidth)
617             return;
618
619         this._currentDisplayWidth = visibleWidth;
620
621         // We need to work out how many right-hand side buttons are available.
622         this.updateWirelessTargetAvailable();
623         this.updateFullscreenButtons();
624
625         var visibleButtonWidth = ControllerIOS.ButtonWidth; // We always try to show the fullscreen button.
626
627         if (!this.controls.wirelessTargetPicker.classList.contains(this.ClassNames.hidden))
628             visibleButtonWidth += ControllerIOS.ButtonWidth;
629         if (!this.controls.optimizedFullscreenButton.classList.contains(this.ClassNames.hidden))
630             visibleButtonWidth += ControllerIOS.ButtonWidth;
631
632         // Check if there is enough room for the scrubber.
633         if ((visibleWidth - visibleButtonWidth) < ControllerIOS.MinimumTimelineWidth) {
634             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
635             this.controls.spacer.classList.remove(this.ClassNames.hidden);
636             this._timelineIsHidden = true;
637         } else {
638             if (!this.isAudio() || this.hasPlayed) {
639                 this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
640                 this.controls.spacer.classList.add(this.ClassNames.hidden);
641                 this._timelineIsHidden = false;
642             } else
643                 this.controls.spacer.classList.remove(this.ClassNames.hidden);
644         }
645
646         // Drop the airplay button if there isn't enough space.
647         if (visibleWidth < visibleButtonWidth) {
648             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
649             visibleButtonWidth -= ControllerIOS.ButtonWidth;
650         }
651
652         // Drop the optimized fullscreen button if there still isn't enough space.
653         if (visibleWidth < visibleButtonWidth) {
654             this.controls.optimizedFullscreenButton.classList.add(this.ClassNames.hidden);
655             visibleButtonWidth -= ControllerIOS.ButtonWidth;
656         }
657
658         // And finally, drop the fullscreen button as a last resort.
659         if (visibleWidth < visibleButtonWidth) {
660             this.controls.fullscreenButton.classList.add(this.ClassNames.hidden);
661             visibleButtonWidth -= ControllerIOS.ButtonWidth;
662         } else
663             this.controls.fullscreenButton.classList.remove(this.ClassNames.hidden);
664     },
665
666     controlsAlwaysVisible: function()
667     {
668         if (this.presentationMode() === 'optimized')
669             return true;
670
671         return Controller.prototype.controlsAlwaysVisible.call(this);
672     },
673
674
675 };
676
677 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
678 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });