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