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