[mediacontrols] Add a test for the show controls button
[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 /* Enums */
26 ControllerIOS.StartPlaybackControls = 2;
27
28 ControllerIOS.prototype = {
29     /* Constants */
30     MinimumTimelineWidth: 200,
31     ButtonWidth: 42,
32
33     get idiom()
34     {
35         return "ios";
36     },
37
38     addVideoListeners: function() {
39         Controller.prototype.addVideoListeners.call(this);
40
41         this.listenFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
42         this.listenFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
43         this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
44     },
45
46     removeVideoListeners: function() {
47         Controller.prototype.removeVideoListeners.call(this);
48
49         this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
50         this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
51         this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
52     },
53
54     createBase: function() {
55         Controller.prototype.createBase.call(this);
56
57         var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('div');
58         startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
59         startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
60         startPlaybackButton.setAttribute('role', 'button');
61
62         var startPlaybackBackground = document.createElement('div');
63         startPlaybackBackground.setAttribute('pseudo', '-webkit-media-controls-start-playback-background');
64         startPlaybackBackground.classList.add('webkit-media-controls-start-playback-background');
65         startPlaybackButton.appendChild(startPlaybackBackground);
66
67         var startPlaybackTint = document.createElement('div');
68         startPlaybackTint.setAttribute('pseudo', '-webkit-media-controls-start-playback-tint');
69         startPlaybackButton.appendChild(startPlaybackTint);
70
71         var startPlaybackGlyph = document.createElement('div');
72         startPlaybackGlyph.setAttribute('pseudo', '-webkit-media-controls-start-playback-glyph');
73         startPlaybackGlyph.classList.add('webkit-media-controls-start-playback-glyph');
74         startPlaybackButton.appendChild(startPlaybackGlyph);
75
76         this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart);
77         this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange);
78         this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd);
79         this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart);
80         this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove);
81         this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut);
82
83         this.listenFor(document, 'visibilitychange', this.handleVisibilityChange);
84     },
85
86     shouldHaveStartPlaybackButton: function() {
87         var allowsInline = this.host.allowsInlineMediaPlayback;
88
89         if (this.isPlaying || (this.hasPlayed && allowsInline))
90             return false;
91
92         if (this.isAudio() && allowsInline)
93             return false;
94
95         if (this.doingSetup)
96             return true;
97
98         if (this.isFullScreen())
99             return false;
100
101         if (!this.video.currentSrc && this.video.error)
102             return false;
103
104         if (!this.video.controls && allowsInline)
105             return false;
106
107         if (this.video.currentSrc && this.video.error)
108             return true;
109
110         return true;
111     },
112
113     shouldHaveControls: function() {
114         if (this.shouldHaveStartPlaybackButton())
115             return false;
116
117         return Controller.prototype.shouldHaveControls.call(this);
118     },
119
120     shouldHaveAnyUI: function() {
121         return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
122     },
123
124     createControls: function() {
125         Controller.prototype.createControls.call(this);
126
127         var panelContainer = this.controls.panelContainer = document.createElement('div');
128         panelContainer.setAttribute('pseudo', '-webkit-media-controls-panel-container');
129
130         var wirelessTargetPicker = this.controls.wirelessTargetPicker;
131         this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
132         this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
133         this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
134
135         this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
136         this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
137         this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
138
139         this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
140         this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
141         this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
142         this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
143         this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
144         this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
145         this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
146         this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
147         this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
148         this.listenFor(this.controls.pictureInPictureButton, 'touchstart', this.handlePictureInPictureTouchStart);
149         this.listenFor(this.controls.pictureInPictureButton, 'touchend', this.handlePictureInPictureTouchEnd);
150         this.listenFor(this.controls.pictureInPictureButton, 'touchcancel', this.handlePictureInPictureTouchCancel);
151         this.listenFor(this.controls.timeline, 'touchstart', this.handleTimelineTouchStart);
152         this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
153
154         this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
155     },
156
157     setControlsType: function(type) {
158         if (type === this.controlsType)
159             return;
160         Controller.prototype.setControlsType.call(this, type);
161
162         if (type === ControllerIOS.StartPlaybackControls)
163             this.addStartPlaybackControls();
164         else
165             this.removeStartPlaybackControls();
166     },
167
168     addStartPlaybackControls: function() {
169         this.base.appendChild(this.controls.startPlaybackButton);
170     },
171
172     removeStartPlaybackControls: function() {
173         if (this.controls.startPlaybackButton.parentNode)
174             this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
175     },
176
177     reconnectControls: function()
178     {
179         Controller.prototype.reconnectControls.call(this);
180
181         if (this.controlsType === ControllerIOS.StartPlaybackControls)
182             this.addStartPlaybackControls();
183     },
184
185     configureInlineControls: function() {
186         this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
187         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
188         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
189         this.controls.panel.appendChild(this.controls.playButton);
190         this.controls.panel.appendChild(this.controls.statusDisplay);
191         this.controls.panel.appendChild(this.controls.timelineBox);
192         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
193         if (!this.isLive) {
194             this.controls.timelineBox.appendChild(this.controls.currentTime);
195             this.controls.timelineBox.appendChild(this.controls.timeline);
196             this.controls.timelineBox.appendChild(this.controls.remainingTime);
197         }
198         if (this.isAudio()) {
199             // Hide the scrubber on audio until the user starts playing.
200             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
201         } else {
202             this.updatePictureInPictureButton();
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     controlsAreHidden: function()
212     {
213         // Controls are only ever actually hidden when they are removed from the tree
214         return !this.controls.panelContainer.parentElement;
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     drawTimelineBackground: function() {
238         var width = this.timelineWidth * window.devicePixelRatio;
239         var height = this.timelineHeight * window.devicePixelRatio;
240
241         if (!width || !height)
242             return;
243
244         var played = this.video.currentTime / this.video.duration;
245         var buffered = 0;
246         var bufferedRanges = this.video.buffered;
247         if (bufferedRanges && bufferedRanges.length)
248             buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered);
249
250         buffered /= this.video.duration;
251         buffered = Math.max(buffered, played);
252
253         var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
254
255         ctx.clearRect(0, 0, width, height);
256
257         var midY = height / 2;
258
259         // 1. Draw the buffered part and played parts, using
260         // solid rectangles that are clipped to the outside of
261         // the lozenge.
262         ctx.save();
263         ctx.beginPath();
264         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
265         ctx.closePath();
266         ctx.clip();
267         ctx.fillStyle = "white";
268         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
269         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
270         ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height);
271         ctx.restore();
272
273         // 2. Draw the outline with a clip path that subtracts the
274         // middle of a lozenge. This produces a better result than
275         // stroking.
276         ctx.save();
277         ctx.beginPath();
278         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
279         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
280         ctx.closePath();
281         ctx.clip("evenodd");
282         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
283         ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
284         ctx.restore();
285     },
286
287     formatTime: function(time) {
288         if (isNaN(time))
289             time = 0;
290         var absTime = Math.abs(time);
291         var intSeconds = Math.floor(absTime % 60).toFixed(0);
292         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
293         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
294         var sign = time < 0 ? '-' : String();
295
296         if (intHours > 0)
297             return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
298
299         return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
300     },
301
302     handlePlayButtonTouchStart: function() {
303         this.controls.playButton.classList.add('active');
304     },
305
306     handlePlayButtonTouchEnd: function(event) {
307         this.controls.playButton.classList.remove('active');
308
309         if (this.canPlay()) {
310             this.video.play();
311             this.showControls();
312         } else
313             this.video.pause();
314
315         return true;
316     },
317
318     handlePlayButtonTouchCancel: function(event) {
319         this.controls.playButton.classList.remove('active');
320         return true;
321     },
322
323     handleBaseGestureStart: function(event) {
324         this.gestureStartTime = new Date();
325         // If this gesture started with two fingers inside the video, then
326         // don't treat it as a potential zoom, unless we're still waiting
327         // to play.
328         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
329             event.preventDefault();
330     },
331
332     handleBaseGestureChange: function(event) {
333         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
334             return;
335
336         var scaleDetectionThreshold = 0.2;
337         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
338             delete this.lastDoubleTouchTime;
339
340         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
341             event.preventDefault();
342
343         var currentGestureTime = new Date();
344         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
345         if (!duration)
346             return;
347
348         var velocity = Math.abs(event.scale - 1) / duration;
349
350         var pinchOutVelocityThreshold = 2;
351         var pinchOutGestureScaleThreshold = 1.25;
352         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
353             return;
354
355         delete this.gestureStartTime;
356         this.video.webkitEnterFullscreen();
357     },
358
359     handleBaseGestureEnd: function(event) {
360         delete this.gestureStartTime;
361     },
362
363     handleWrapperTouchStart: function(event) {
364         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
365             return;
366
367         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
368
369         if (this.controlsAreHidden() || !this.controls.panel.classList.contains(this.ClassNames.show)) {
370             this.showControls();
371             this.resetHideControlsTimer();
372         } else if (!this.canPlay())
373             this.hideControls();
374     },
375
376     handlePanelTouchStart: function(event) {
377         this.video.style.webkitUserSelect = 'none';
378     },
379
380     handlePanelTouchEnd: function(event) {
381         this.video.style.removeProperty('-webkit-user-select');
382     },
383
384     handlePanelTouchCancel: function(event) {
385         this.video.style.removeProperty('-webkit-user-select');
386     },
387
388     handleVisibilityChange: function(event) {
389         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
390     },
391
392     handlePanelTransitionEnd: function(event)
393     {
394         var opacity = window.getComputedStyle(this.controls.panel).opacity;
395         if (!parseInt(opacity) && !this.controlsAlwaysVisible()) {
396             this.base.removeChild(this.controls.inlinePlaybackPlaceholder);
397             this.base.removeChild(this.controls.panelContainer);
398         }
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() != 'picture-in-picture';
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     handlePictureInPictureButtonClicked: function(event) {
450         if (!('webkitSetPresentationMode' in this.video))
451             return;
452
453         if (this.presentationMode() === 'picture-in-picture')
454             this.video.webkitSetPresentationMode('inline');
455         else
456             this.video.webkitSetPresentationMode('picture-in-picture');
457     },
458
459     handlePictureInPictureTouchStart: function() {
460         this.controls.pictureInPictureButton.classList.add('active');
461     },
462
463     handlePictureInPictureTouchEnd: function(event) {
464         this.controls.pictureInPictureButton.classList.remove('active');
465
466         this.handlePictureInPictureButtonClicked();
467
468         return true;
469     },
470
471     handlePictureInPictureTouchCancel: function(event) {
472         this.controls.pictureInPictureButton.classList.remove('active');
473         return true;
474     },
475
476     handleStartPlaybackButtonTouchStart: function(event) {
477         this.controls.startPlaybackButton.classList.add('active');
478         this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-background').classList.add('active');
479     },
480
481     handleStartPlaybackButtonTouchEnd: function(event) {
482         this.controls.startPlaybackButton.classList.remove('active');
483         this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-background').classList.remove('active');
484
485         if (this.video.error)
486             return true;
487
488         this.video.play();
489         this.updateControls();
490
491         return true;
492     },
493
494     handleStartPlaybackButtonTouchCancel: function(event) {
495         this.controls.startPlaybackButton.classList.remove('active');
496         return true;
497     },
498
499     handleTimelineTouchStart: function(event) {
500         this.scrubbing = true;
501         this.listenFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
502         this.listenFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
503     },
504
505     handleTimelineTouchEnd: function(event) {
506         this.stopListeningFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
507         this.stopListeningFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
508         this.scrubbing = false;
509     },
510
511     handleWirelessPickerButtonTouchStart: function() {
512         if (!this.video.error)
513             this.controls.wirelessTargetPicker.classList.add('active');
514     },
515
516     handleWirelessPickerButtonTouchEnd: function(event) {
517         this.controls.wirelessTargetPicker.classList.remove('active');
518         return this.handleWirelessPickerButtonClicked();
519     },
520
521     handleWirelessPickerButtonTouchCancel: function(event) {
522         this.controls.wirelessTargetPicker.classList.remove('active');
523         return true;
524     },
525
526     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
527         if (this.controlsType === ControllerIOS.StartPlaybackControls) {
528             this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
529             return;
530         }
531
532         Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
533     },
534
535     updateWirelessTargetPickerButton: function() {
536     },
537
538     updateStatusDisplay: function(event)
539     {
540         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
541         this.controls.startPlaybackButton.querySelector(".webkit-media-controls-start-playback-glyph").classList.toggle(this.ClassNames.failed, this.video.error !== null);
542         Controller.prototype.updateStatusDisplay.call(this, event);
543     },
544
545     setPlaying: function(isPlaying)
546     {
547         Controller.prototype.setPlaying.call(this, isPlaying);
548
549         this.updateControls();
550
551         if (isPlaying && this.isAudio())
552             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
553
554         if (isPlaying)
555             this.hasPlayed = true;
556         else
557             this.showControls();
558     },
559
560     showControls: function()
561     {
562         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
563         if (!this.video.controls)
564             return;
565
566         this.updateForShowingControls();
567         if (this.shouldHaveControls() && !this.controls.panelContainer.parentElement) {
568             this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
569             this.base.appendChild(this.controls.panelContainer);
570         }
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     updatePictureInPictureButton: function()
582     {
583         var shouldShowPictureInPictureButton = Controller.gSimulatePictureInPictureAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('picture-in-picture'));
584         if (shouldShowPictureInPictureButton) {
585             this.controls.panel.appendChild(this.controls.pictureInPictureButton);
586             this.controls.pictureInPictureButton.classList.remove(this.ClassNames.hidden);
587         } else
588             this.controls.pictureInPictureButton.classList.add(this.ClassNames.hidden);
589     },
590
591     handlePresentationModeChange: function(event)
592     {
593         var presentationMode = this.presentationMode();
594
595         switch (presentationMode) {
596             case 'inline':
597                 this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
598                 this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture);
599                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
600                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
601                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
602                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
603
604                 this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
605                 break;
606             case 'picture-in-picture':
607                 this.controls.panel.classList.add(this.ClassNames.pictureInPicture);
608                 this.controls.panelContainer.classList.add(this.ClassNames.pictureInPicture);
609                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture);
610                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
611
612                 this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in Picture in Picture');
613                 this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture);
614                 this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
615                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture);
616
617                 this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture);
618                 break;
619             default:
620                 this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
621                 this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture);
622                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
623                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
624                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
625
626                 this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
627                 break;
628         }
629
630         this.updateControls();
631         this.updateCaptionContainer();
632         this.resetHideControlsTimer();
633         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
634             this.showControls();
635     },
636
637     handleFullscreenChange: function(event)
638     {
639         Controller.prototype.handleFullscreenChange.call(this, event);
640         this.handlePresentationModeChange(event);
641     },
642
643     controlsAlwaysVisible: function()
644     {
645         if (this.presentationMode() === 'picture-in-picture')
646             return true;
647
648         return Controller.prototype.controlsAlwaysVisible.call(this);
649     },
650
651     // Due to the bad way we are faking inheritance here, in particular the extends method
652     // on Controller.prototype, we don't copy getters and setters from the prototype. This
653     // means we have to implement them again, here in the subclass.
654     // FIXME: Use ES6 classes!
655
656     get scrubbing()
657     {
658         return Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").get.call(this);
659     },
660
661     set scrubbing(flag)
662     {
663         Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").set.call(this, flag);
664     },
665
666     get pageScaleFactor()
667     {
668         return this._pageScaleFactor;
669     },
670
671     set pageScaleFactor(newScaleFactor)
672     {
673         if (!newScaleFactor || this._pageScaleFactor === newScaleFactor)
674             return;
675
676         this._pageScaleFactor = newScaleFactor;
677
678         var scaleValue = 1 / newScaleFactor;
679         var scaleTransform = "scale(" + scaleValue + ")";
680         if (this.controls.startPlaybackButton)
681             this.controls.startPlaybackButton.style.webkitTransform = scaleTransform;
682         if (this.controls.panel) {
683             if (scaleValue > 1) {
684                 this.controls.panel.style.width = "100%";
685                 this.controls.panel.style.zoom = scaleValue;
686                 this.controls.panel.style.webkitTransform = "scale(1)";
687                 this.controls.timelineBox.style.webkitTextSizeAdjust = (100 * scaleValue) + "%";
688             } else {
689                 var bottomAligment = -2 * scaleValue;
690                 this.controls.panel.style.bottom = bottomAligment + "px";
691                 this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px";
692                 this.controls.panel.style.width = Math.round(newScaleFactor * 100) + "%";
693                 this.controls.panel.style.webkitTransform = scaleTransform;
694                 this.controls.timelineBox.style.webkitTextSizeAdjust = "auto";
695                 this.controls.panel.style.zoom = 1;
696             }
697             this.controls.panelBackground.style.height = (50 * scaleValue) + "px";
698
699             this.setNeedsTimelineMetricsUpdate();
700             this.updateProgress();
701             this.scheduleUpdateLayoutForDisplayedWidth();
702         }
703     },
704
705 };
706
707 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
708 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });