[iOS] Scrubber display is broken if the buffered range is empty
[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         buffered = Math.max(buffered, played);
256
257         var ctx = this.video.ownerDocument.getCSSCanvasContext('2d', this.timelineContextName, width, height);
258
259         ctx.clearRect(0, 0, width, height);
260
261         var midY = height / 2;
262
263         // 1. Draw the buffered part and played parts, using
264         // solid rectangles that are clipped to the outside of
265         // the lozenge.
266         ctx.save();
267         ctx.beginPath();
268         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
269         ctx.closePath();
270         ctx.clip();
271         ctx.fillStyle = "white";
272         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
273         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
274         ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height);
275         ctx.restore();
276
277         // 2. Draw the outline with a clip path that subtracts the
278         // middle of a lozenge. This produces a better result than
279         // stroking.
280         ctx.save();
281         ctx.beginPath();
282         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
283         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
284         ctx.closePath();
285         ctx.clip("evenodd");
286         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
287         ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
288         ctx.restore();
289     },
290
291     formatTime: function(time) {
292         if (isNaN(time))
293             time = 0;
294         var absTime = Math.abs(time);
295         var intSeconds = Math.floor(absTime % 60).toFixed(0);
296         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
297         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
298         var sign = time < 0 ? '-' : String();
299
300         if (intHours > 0)
301             return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
302
303         return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
304     },
305
306     handleTimelineChange: function(event) {
307         Controller.prototype.handleTimelineChange.call(this);
308         this.updateProgress();
309     },
310
311     handlePlayButtonTouchStart: function() {
312         this.controls.playButton.classList.add('active');
313     },
314
315     handlePlayButtonTouchEnd: function(event) {
316         this.controls.playButton.classList.remove('active');
317
318         if (this.canPlay())
319             this.video.play();
320         else
321             this.video.pause();
322
323         return true;
324     },
325
326     handlePlayButtonTouchCancel: function(event) {
327         this.controls.playButton.classList.remove('active');
328         return true;
329     },
330
331     handleBaseGestureStart: function(event) {
332         this.gestureStartTime = new Date();
333         // If this gesture started with two fingers inside the video, then
334         // don't treat it as a potential zoom, unless we're still waiting
335         // to play.
336         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
337             event.preventDefault();
338     },
339
340     handleBaseGestureChange: function(event) {
341         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
342             return;
343
344         var scaleDetectionThreshold = 0.2;
345         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
346             delete this.lastDoubleTouchTime;
347
348         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
349             event.preventDefault();
350
351         var currentGestureTime = new Date();
352         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
353         if (!duration)
354             return;
355
356         var velocity = Math.abs(event.scale - 1) / duration;
357
358         var pinchOutVelocityThreshold = 2;
359         var pinchOutGestureScaleThreshold = 1.25;
360         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
361             return;
362
363         delete this.gestureStartTime;
364         this.video.webkitEnterFullscreen();
365     },
366
367     handleBaseGestureEnd: function(event) {
368         delete this.gestureStartTime;
369     },
370
371     handleWrapperTouchStart: function(event) {
372         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
373             return;
374
375         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
376
377         if (this.controlsAreHidden()) {
378             this.showControls();
379             if (this.hideTimer)
380                 clearTimeout(this.hideTimer);
381             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
382         } else if (!this.canPlay())
383             this.hideControls();
384     },
385
386     handlePanelTouchStart: function(event) {
387         this.video.style.webkitUserSelect = 'none';
388     },
389
390     handlePanelTouchEnd: function(event) {
391         this.video.style.removeProperty('-webkit-user-select');
392     },
393
394     handlePanelTouchCancel: function(event) {
395         this.video.style.removeProperty('-webkit-user-select');
396     },
397
398     handleVisibilityChange: function(event) {
399         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
400     },
401
402     presentationMode: function() {
403         if ('webkitPresentationMode' in this.video)
404             return this.video.webkitPresentationMode;
405
406         if (this.isFullScreen())
407             return 'fullscreen';
408
409         return 'inline';
410     },
411
412     isFullScreen: function()
413     {
414         return this.video.webkitDisplayingFullscreen && this.presentationMode() != 'optimized';
415     },
416
417     handleFullscreenButtonClicked: function(event) {
418         if ('webkitSetPresentationMode' in this.video) {
419             if (this.presentationMode() === 'fullscreen')
420                 this.video.webkitSetPresentationMode('inline');
421             else
422                 this.video.webkitSetPresentationMode('fullscreen');
423
424             return;
425         }
426
427         if (this.isFullScreen())
428             this.video.webkitExitFullscreen();
429         else
430             this.video.webkitEnterFullscreen();
431     },
432
433     handleFullscreenTouchStart: function() {
434         this.controls.fullscreenButton.classList.add('active');
435     },
436
437     handleFullscreenTouchEnd: function(event) {
438         this.controls.fullscreenButton.classList.remove('active');
439
440         this.handleFullscreenButtonClicked();
441
442         return true;
443     },
444
445     handleFullscreenTouchCancel: function(event) {
446         this.controls.fullscreenButton.classList.remove('active');
447         return true;
448     },
449
450     handleOptimizedFullscreenButtonClicked: function(event) {
451         if (!('webkitSetPresentationMode' in this.video))
452             return;
453
454         if (this.presentationMode() === 'optimized')
455             this.video.webkitSetPresentationMode('inline');
456         else
457             this.video.webkitSetPresentationMode('optimized');
458     },
459
460     handleOptimizedFullscreenTouchStart: function() {
461         this.controls.optimizedFullscreenButton.classList.add('active');
462     },
463
464     handleOptimizedFullscreenTouchEnd: function(event) {
465         this.controls.optimizedFullscreenButton.classList.remove('active');
466
467         this.handleOptimizedFullscreenButtonClicked();
468
469         return true;
470     },
471
472     handleOptimizedFullscreenTouchCancel: function(event) {
473         this.controls.optimizedFullscreenButton.classList.remove('active');
474         return true;
475     },
476
477     handleStartPlaybackButtonTouchStart: function(event) {
478         this.controls.startPlaybackButton.classList.add('active');
479     },
480
481     handleStartPlaybackButtonTouchEnd: function(event) {
482         this.controls.startPlaybackButton.classList.remove('active');
483         if (this.video.error)
484             return true;
485
486         this.video.play();
487         this.updateControls();
488
489         return true;
490     },
491
492     handleStartPlaybackButtonTouchCancel: function(event) {
493         this.controls.startPlaybackButton.classList.remove('active');
494         return true;
495     },
496
497     handleReadyStateChange: function(event) {
498         Controller.prototype.handleReadyStateChange.call(this, event);
499         this.updateControls();
500     },
501
502     handleWirelessPickerButtonTouchStart: function() {
503         if (!this.video.error)
504             this.controls.wirelessTargetPicker.classList.add('active');
505     },
506
507     handleWirelessPickerButtonTouchEnd: function(event) {
508         this.controls.wirelessTargetPicker.classList.remove('active');
509         return this.handleWirelessPickerButtonClicked();
510     },
511
512     handleWirelessPickerButtonTouchCancel: function(event) {
513         this.controls.wirelessTargetPicker.classList.remove('active');
514         return true;
515     },
516
517     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
518         if (this.controlsType === ControllerIOS.StartPlaybackControls) {
519             this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
520             return;
521         }
522
523         Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
524     },
525
526     updateStatusDisplay: function(event)
527     {
528         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
529         Controller.prototype.updateStatusDisplay.call(this, event);
530     },
531
532     setPlaying: function(isPlaying)
533     {
534         Controller.prototype.setPlaying.call(this, isPlaying);
535
536         this.updateControls();
537
538         if (isPlaying && this.isAudio() && !this._timelineIsHidden) {
539             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
540             this.controls.spacer.classList.add(this.ClassNames.hidden);
541         }
542
543         if (isPlaying)
544             this.hasPlayed = true;
545         else
546             this.showControls();
547     },
548
549     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
550     {
551         if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
552             return;
553
554         Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen);
555     },
556
557     get pageScaleFactor()
558     {
559         return this._pageScaleFactor;
560     },
561
562     set pageScaleFactor(newScaleFactor)
563     {
564         if (this._pageScaleFactor === newScaleFactor)
565             return;
566
567         this._pageScaleFactor = newScaleFactor;
568
569         // FIXME: this should react to the scale change by
570         // unscaling the controls panel. However, this
571         // hits a bug with the backdrop blur layer getting
572         // too big and moving to a tiled layer.
573         // https://bugs.webkit.org/show_bug.cgi?id=142317
574     },
575
576     handlePresentationModeChange: function(event)
577     {
578         var presentationMode = this.presentationMode();
579
580         switch (presentationMode) {
581             case 'inline':
582                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
583                 break;
584             case 'optimized':
585                 var backgroundImage = "url('" + this.host.mediaUIImageData("optimized-fullscreen-placeholder") + "')";
586                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImage;
587                 this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', "video playback placeholder");
588                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
589                 break;
590         }
591
592         this.updateControls();
593         this.updateCaptionContainer();
594         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
595             this.showControls();
596     },
597
598     handleFullscreenChange: function(event)
599     {
600         Controller.prototype.handleFullscreenChange.call(this, event);
601         this.handlePresentationModeChange(event);
602     },
603
604     scheduleUpdateLayoutForDisplayedWidth: function ()
605     {
606         setTimeout(function () {
607             this.updateLayoutForDisplayedWidth();
608         }.bind(this), 0);
609     },
610
611     updateLayoutForDisplayedWidth: function()
612     {
613         if (!this.controls || !this.controls.panel)
614             return;
615
616         var visibleWidth = this.controls.panel.getBoundingClientRect().width * this._pageScaleFactor;
617         if (visibleWidth <= 0 || visibleWidth == this._currentDisplayWidth)
618             return;
619
620         this._currentDisplayWidth = visibleWidth;
621
622         // We need to work out how many right-hand side buttons are available.
623         this.updateWirelessTargetAvailable();
624         this.updateFullscreenButtons();
625
626         var visibleButtonWidth = ControllerIOS.ButtonWidth; // We always try to show the fullscreen button.
627
628         if (!this.controls.wirelessTargetPicker.classList.contains(this.ClassNames.hidden))
629             visibleButtonWidth += ControllerIOS.ButtonWidth;
630         if (!this.controls.optimizedFullscreenButton.classList.contains(this.ClassNames.hidden))
631             visibleButtonWidth += ControllerIOS.ButtonWidth;
632
633         // Check if there is enough room for the scrubber.
634         if ((visibleWidth - visibleButtonWidth) < ControllerIOS.MinimumTimelineWidth) {
635             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
636             this.controls.spacer.classList.remove(this.ClassNames.hidden);
637             this._timelineIsHidden = true;
638         } else {
639             if (!this.isAudio() || this.hasPlayed) {
640                 this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
641                 this.controls.spacer.classList.add(this.ClassNames.hidden);
642                 this._timelineIsHidden = false;
643             } else
644                 this.controls.spacer.classList.remove(this.ClassNames.hidden);
645         }
646
647         // Drop the airplay button if there isn't enough space.
648         if (visibleWidth < visibleButtonWidth) {
649             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
650             visibleButtonWidth -= ControllerIOS.ButtonWidth;
651         }
652
653         // Drop the optimized fullscreen button if there still isn't enough space.
654         if (visibleWidth < visibleButtonWidth) {
655             this.controls.optimizedFullscreenButton.classList.add(this.ClassNames.hidden);
656             visibleButtonWidth -= ControllerIOS.ButtonWidth;
657         }
658
659         // And finally, drop the fullscreen button as a last resort.
660         if (visibleWidth < visibleButtonWidth) {
661             this.controls.fullscreenButton.classList.add(this.ClassNames.hidden);
662             visibleButtonWidth -= ControllerIOS.ButtonWidth;
663         } else
664             this.controls.fullscreenButton.classList.remove(this.ClassNames.hidden);
665     },
666
667     controlsAlwaysVisible: function()
668     {
669         if (this.presentationMode() === 'optimized')
670             return true;
671
672         return Controller.prototype.controlsAlwaysVisible.call(this);
673     },
674
675
676 };
677
678 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
679 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });