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