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