[iOS Media] Audio should hide timeline scrubber until playback starts
[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.hasWirelessPlaybackTargets = false;
10     this._pageScaleFactor = 1;
11     this.isListeningForPlaybackTargetAvailabilityEvent = false;
12
13     this.timelineContextName = "_webkit-media-controls-timeline-" + ControllerIOS.gLastTimelineId++;
14
15     Controller.call(this, root, video, host);
16
17     this.updateWirelessTargetAvailable();
18     this.updateWirelessPlaybackStatus();
19     this.setNeedsTimelineMetricsUpdate();
20
21     host.controlsDependOnPageScaleFactor = true;
22     this.doingSetup = false;
23 };
24
25 /* Enums */
26 ControllerIOS.StartPlaybackControls = 2;
27
28 /* Globals */
29 ControllerIOS.gWirelessImage = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 245"><g fill="#1060FE"><path d="M193.6,6.3v121.6H6.4V6.3H193.6 M199.1,0.7H0.9v132.7h198.2V0.7L199.1,0.7z"/><path d="M43.5,139.3c15.8,8,35.3,12.7,56.5,12.7s40.7-4.7,56.5-12.7H43.5z"/></g><g text-anchor="middle" font-family="Helvetica Neue"><text x="100" y="204" fill="white" font-size="24">##DEVICE_TYPE##</text><text x="100" y="234" fill="#5C5C5C" font-size="21">##DEVICE_NAME##</text></g></svg>';
30 ControllerIOS.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
31 ControllerIOS.gSimulateOptimizedFullscreenAvailable = false; // Used for testing when optimized fullscreen is not available.
32 ControllerIOS.gLastTimelineId = 0;
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, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
41         this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
42     },
43
44     removeVideoListeners: function() {
45         Controller.prototype.removeVideoListeners.call(this);
46
47         this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
48         this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
49         this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
50         this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
51
52         this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
53     },
54
55     createBase: function() {
56         Controller.prototype.createBase.call(this);
57
58         var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('button');
59         startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
60         startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
61
62         this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart);
63         this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange);
64         this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd);
65         this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart);
66         this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove);
67         this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut);
68
69         this.listenFor(document, 'visibilitychange', this.handleVisibilityChange);
70     },
71
72     shouldHaveStartPlaybackButton: function() {
73         var allowsInline = this.host.mediaPlaybackAllowsInline;
74
75         if (this.isPlaying)
76             return false;
77
78         if (this.isAudio() && allowsInline)
79             return false;
80
81         if (this.isFullScreen())
82             return false;
83
84         if (!this.video.currentSrc && this.video.error)
85             return false;
86
87         if (!this.video.controls && allowsInline)
88             return false;
89
90         if (this.video.currentSrc && this.video.error)
91             return true;
92
93         if (!this.doingSetup && !this.host.userGestureRequired && allowsInline)
94             return false;
95
96         return true;
97     },
98
99     shouldHaveControls: function() {
100         if (this.shouldHaveStartPlaybackButton())
101             return false;
102
103         return Controller.prototype.shouldHaveControls.call(this);
104     },
105
106     shouldHaveAnyUI: function() {
107         return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
108     },
109
110     currentPlaybackTargetIsWireless: function() {
111         return ControllerIOS.gSimulateWirelessPlaybackTarget || (('webkitCurrentPlaybackTargetIsWireless' in this.video) && this.video.webkitCurrentPlaybackTargetIsWireless);
112     },
113
114     updateWirelessPlaybackStatus: function() {
115         if (this.currentPlaybackTargetIsWireless()) {
116             var backgroundImageSVG = "url('" + ControllerIOS.gWirelessImage + "')";
117
118             var deviceName = "";
119             var deviceType = "";
120             var type = this.host.externalDeviceType;
121             if (type == "airplay") {
122                 deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
123                 deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
124             } else if (type == "tvout") {
125                 deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
126                 deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
127             }
128
129             backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_TYPE##', deviceType);
130             backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_NAME##', deviceName);
131
132             this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImageSVG;
133             this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
134
135             this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
136         } else {
137             this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
138         }
139     },
140
141     updateWirelessTargetAvailable: function() {
142         if (ControllerIOS.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets)
143             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
144         else
145             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
146     },
147
148     createControls: function() {
149         Controller.prototype.createControls.call(this);
150
151         var panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div');
152         panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent');
153
154         var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div');
155         inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-inline-playback-placeholder');
156         if (!ControllerIOS.gSimulateOptimizedFullscreenAvailable)
157             inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
158         inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Display Optimized Full Screen'));
159
160         var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
161         wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
162         wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
163         this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
164         this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
165         this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
166
167         if (!ControllerIOS.gSimulateWirelessPlaybackTarget)
168             wirelessTargetPicker.classList.add(this.ClassNames.hidden);
169
170         this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
171         this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
172         this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
173
174         this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
175         this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
176         this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
177         this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
178         this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
179         this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
180         this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
181         this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
182         this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
183         this.listenFor(this.controls.optimizedFullscreenButton, 'touchstart', this.handleOptimizedFullscreenTouchStart);
184         this.listenFor(this.controls.optimizedFullscreenButton, 'touchend', this.handleOptimizedFullscreenTouchEnd);
185         this.listenFor(this.controls.optimizedFullscreenButton, 'touchcancel', this.handleOptimizedFullscreenTouchCancel);
186         this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
187
188         this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
189     },
190
191     setControlsType: function(type) {
192         if (type === this.controlsType)
193             return;
194         Controller.prototype.setControlsType.call(this, type);
195
196         if (type === ControllerIOS.StartPlaybackControls)
197             this.addStartPlaybackControls();
198         else
199             this.removeStartPlaybackControls();
200
201         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
202     },
203
204     addStartPlaybackControls: function() {
205         this.base.appendChild(this.controls.startPlaybackButton);
206     },
207
208     removeStartPlaybackControls: function() {
209         if (this.controls.startPlaybackButton.parentNode)
210             this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
211     },
212
213     reconnectControls: function()
214     {
215         Controller.prototype.reconnectControls.call(this);
216
217         if (this.controlsType === ControllerIOS.StartPlaybackControls)
218             this.addStartPlaybackControls();
219     },
220
221     configureInlineControls: function() {
222         this.controls.panel.appendChild(this.controls.playButton);
223         this.controls.panel.appendChild(this.controls.statusDisplay);
224         this.controls.panel.appendChild(this.controls.timelineBox);
225         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
226         if (!this.isLive) {
227             this.controls.timelineBox.appendChild(this.controls.currentTime);
228             this.controls.timelineBox.appendChild(this.controls.timeline);
229             this.controls.timelineBox.appendChild(this.controls.remainingTime);
230         }
231         if (this.isAudio()) {
232             // Hide the scrubber on audio until the user starts playing.
233             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
234         } else {
235             if (ControllerIOS.gSimulateOptimizedFullscreenAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('optimized')))
236                 this.controls.panel.appendChild(this.controls.optimizedFullscreenButton);
237             this.controls.panel.appendChild(this.controls.fullscreenButton);
238         }
239     },
240
241     configureFullScreenControls: function() {
242         // Explicitly do nothing to override base-class behavior.
243     },
244
245     hideControls: function() {
246         Controller.prototype.hideControls.call(this);
247         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
248     },
249
250     showControls: function() {
251         Controller.prototype.showControls.call(this);
252         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
253     },
254
255     addControls: function() {
256         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
257         this.base.appendChild(this.controls.panelCompositedParent);
258         this.controls.panelCompositedParent.appendChild(this.controls.panel);
259         this.setNeedsTimelineMetricsUpdate();
260     },
261
262     updateControls: function() {
263         if (this.shouldHaveStartPlaybackButton())
264             this.setControlsType(ControllerIOS.StartPlaybackControls);
265         else if (this.presentationMode() === "fullscreen")
266             this.setControlsType(Controller.FullScreenControls);
267         else
268             this.setControlsType(Controller.InlineControls);
269
270         this.setNeedsTimelineMetricsUpdate();
271     },
272
273     updateTime: function() {
274         Controller.prototype.updateTime.call(this);
275         this.updateProgress();
276     },
277
278     progressFillStyle: function() {
279         return 'rgba(0, 0, 0, 0.5)';
280     },
281
282     addRoundedRect: function(ctx, x, y, width, height, radius) {
283         ctx.moveTo(x + radius, y);
284         ctx.arcTo(x + width, y, x + width, y + radius, radius);
285         ctx.lineTo(x + width, y + height - radius);
286         ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
287         ctx.lineTo(x + radius, y + height);
288         ctx.arcTo(x, y + height, x, y + height - radius, radius);
289         ctx.lineTo(x, y + radius);
290         ctx.arcTo(x, y, x + radius, y, radius);
291     },
292
293     drawTimelineBackground: function() {
294         var width = this.timelineWidth * window.devicePixelRatio;
295         var height = this.timelineHeight * window.devicePixelRatio;
296
297         if (!width || !height)
298             return;
299
300         var played = this.video.currentTime / this.video.duration;
301         var buffered = 0;
302         for (var i = 0, end = this.video.buffered.length; i < end; ++i)
303             buffered = Math.max(this.video.buffered.end(i), buffered);
304
305         buffered /= this.video.duration;
306
307         var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
308
309         ctx.clearRect(0, 0, width, height);
310
311         var midY = height / 2;
312
313         // 1. Draw the outline with a clip path that subtracts the
314         // middle of a lozenge. This produces a better result than
315         // stroking when we come to filling the parts below.
316         ctx.save();
317         ctx.beginPath();
318         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
319         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
320         ctx.closePath();
321         ctx.clip("evenodd");
322         ctx.fillStyle = "black";
323         ctx.fillRect(0, 0, width, height);
324         ctx.restore();
325
326         // 2. Draw the buffered part and played parts, using
327         // solid rectangles that are clipped to the outside of
328         // the lozenge.
329         ctx.save();
330         ctx.beginPath();
331         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
332         ctx.closePath();
333         ctx.clip();
334         ctx.fillStyle = "black";
335         ctx.fillRect(0, 0, Math.round(width * buffered) + 2, height);
336         ctx.fillStyle = "white";
337         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
338         ctx.restore();
339     },
340
341     formatTime: function(time) {
342         if (isNaN(time))
343             time = 0;
344         var absTime = Math.abs(time);
345         var intSeconds = Math.floor(absTime % 60).toFixed(0);
346         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
347         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
348         var sign = time < 0 ? '-' : String();
349
350         if (intHours > 0)
351             return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
352
353         return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
354     },
355
356     handleTimelineChange: function(event) {
357         Controller.prototype.handleTimelineChange.call(this);
358         this.updateProgress();
359     },
360
361     handlePlayButtonTouchStart: function() {
362         this.controls.playButton.classList.add('active');
363     },
364
365     handlePlayButtonTouchEnd: function(event) {
366         this.controls.playButton.classList.remove('active');
367
368         if (this.canPlay())
369             this.video.play();
370         else
371             this.video.pause();
372
373         return true;
374     },
375
376     handlePlayButtonTouchCancel: function(event) {
377         this.controls.playButton.classList.remove('active');
378         return true;
379     },
380
381     handleBaseGestureStart: function(event) {
382         this.gestureStartTime = new Date();
383         // If this gesture started with two fingers inside the video, then
384         // don't treat it as a potential zoom, unless we're still waiting
385         // to play.
386         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
387             event.preventDefault();
388     },
389
390     handleBaseGestureChange: function(event) {
391         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
392             return;
393
394         var scaleDetectionThreshold = 0.2;
395         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
396             delete this.lastDoubleTouchTime;
397
398         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
399             event.preventDefault();
400
401         var currentGestureTime = new Date();
402         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
403         if (!duration)
404             return;
405
406         var velocity = Math.abs(event.scale - 1) / duration;
407
408         var pinchOutVelocityThreshold = 2;
409         var pinchOutGestureScaleThreshold = 1.25;
410         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
411             return;
412
413         delete this.gestureStartTime;
414         this.video.webkitEnterFullscreen();
415     },
416
417     handleBaseGestureEnd: function(event) {
418         delete this.gestureStartTime;
419     },
420
421     handleWrapperTouchStart: function(event) {
422         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
423             return;
424
425         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
426
427         if (this.controlsAreHidden()) {
428             this.showControls();
429             if (this.hideTimer)
430                 clearTimeout(this.hideTimer);
431             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
432         } else if (!this.canPlay())
433             this.hideControls();
434     },
435
436     handlePanelTouchStart: function(event) {
437         this.video.style.webkitUserSelect = 'none';
438     },
439
440     handlePanelTouchEnd: function(event) {
441         this.video.style.removeProperty('-webkit-user-select');
442     },
443
444     handlePanelTouchCancel: function(event) {
445         this.video.style.removeProperty('-webkit-user-select');
446     },
447
448     handleVisibilityChange: function(event) {
449         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
450     },
451
452     presentationMode: function() {
453         if ('webkitPresentationMode' in this.video)
454             return this.video.webkitPresentationMode;
455
456         if (this.isFullScreen())
457             return 'fullscreen';
458
459         return 'inline';
460     },
461
462     isFullScreen: function()
463     {
464         return this.video.webkitDisplayingFullscreen && this.presentationMode() != 'optimized';
465     },
466
467     handleFullscreenButtonClicked: function(event) {
468         if ('webkitSetPresentationMode' in this.video) {
469             if (this.presentationMode() === 'fullscreen')
470                 this.video.webkitSetPresentationMode('inline');
471             else
472                 this.video.webkitSetPresentationMode('fullscreen');
473
474             return;
475         }
476
477         if (this.isFullScreen())
478             this.video.webkitExitFullscreen();
479         else
480             this.video.webkitEnterFullscreen();
481     },
482
483     handleFullscreenTouchStart: function() {
484         this.controls.fullscreenButton.classList.add('active');
485     },
486
487     handleFullscreenTouchEnd: function(event) {
488         this.controls.fullscreenButton.classList.remove('active');
489
490         this.handleFullscreenButtonClicked();
491
492         return true;
493     },
494
495     handleFullscreenTouchCancel: function(event) {
496         this.controls.fullscreenButton.classList.remove('active');
497         return true;
498     },
499
500     handleOptimizedFullscreenButtonClicked: function(event) {
501         if (!('webkitSetPresentationMode' in this.video))
502             return;
503
504         if (this.presentationMode() === 'optimized')
505             this.video.webkitSetPresentationMode('inline');
506         else
507             this.video.webkitSetPresentationMode('optimized');
508     },
509
510     handleOptimizedFullscreenTouchStart: function() {
511         this.controls.optimizedFullscreenButton.classList.add('active');
512     },
513
514     handleOptimizedFullscreenTouchEnd: function(event) {
515         this.controls.optimizedFullscreenButton.classList.remove('active');
516
517         this.handleOptimizedFullscreenButtonClicked();
518
519         return true;
520     },
521
522     handleOptimizedFullscreenTouchCancel: function(event) {
523         this.controls.optimizedFullscreenButton.classList.remove('active');
524         return true;
525     },
526
527     handleStartPlaybackButtonTouchStart: function(event) {
528         this.controls.startPlaybackButton.classList.add('active');
529     },
530
531     handleStartPlaybackButtonTouchEnd: function(event) {
532         this.controls.startPlaybackButton.classList.remove('active');
533         if (this.video.error)
534             return true;
535
536         this.video.play();
537         this.updateControls();
538
539         return true;
540     },
541
542     handleStartPlaybackButtonTouchCancel: function(event) {
543         this.controls.startPlaybackButton.classList.remove('active');
544         return true;
545     },
546
547     handleReadyStateChange: function(event) {
548         Controller.prototype.handleReadyStateChange.call(this, event);
549         this.updateControls();
550     },
551
552     handleWirelessPlaybackChange: function(event) {
553         this.updateWirelessPlaybackStatus();
554         this.setNeedsTimelineMetricsUpdate();
555     },
556
557     handleWirelessTargetAvailableChange: function(event) {
558         var wirelessPlaybackTargetsAvailable = event.availability == "available";
559         if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
560             return;
561
562         this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
563         this.updateWirelessTargetAvailable();
564         this.setNeedsTimelineMetricsUpdate();
565     },
566
567     handleWirelessPickerButtonTouchStart: function() {
568         if (!this.video.error)
569             this.controls.wirelessTargetPicker.classList.add('active');
570     },
571
572     handleWirelessPickerButtonTouchEnd: function(event) {
573         this.controls.wirelessTargetPicker.classList.remove('active');
574         this.video.webkitShowPlaybackTargetPicker();
575         return true;
576     },
577
578     handleWirelessPickerButtonTouchCancel: function(event) {
579         this.controls.wirelessTargetPicker.classList.remove('active');
580         return true;
581     },
582
583     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
584         var shouldListen = true;
585         if (this.video.error)
586             shouldListen = false;
587         if (this.controlsType === ControllerIOS.StartPlaybackControls)
588             shouldListen = false;
589         if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
590             shouldListen = false;
591         if (document.hidden)
592             shouldListen = false;
593
594         this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
595     },
596
597     updateStatusDisplay: function(event)
598     {
599         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
600         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
601         Controller.prototype.updateStatusDisplay.call(this, event);
602     },
603
604     setPlaying: function(isPlaying)
605     {
606         Controller.prototype.setPlaying.call(this, isPlaying);
607         if (isPlaying && this.isAudio())
608             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
609         this.updateControls();
610     },
611
612     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
613     {
614         if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
615             return;
616
617         if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
618             return;
619
620         this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
621         if (shouldListen)
622             this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
623         else
624             this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
625     },
626
627     get pageScaleFactor()
628     {
629         return this._pageScaleFactor;
630     },
631
632     set pageScaleFactor(newScaleFactor)
633     {
634         if (this._pageScaleFactor === newScaleFactor)
635             return;
636
637         this._pageScaleFactor = newScaleFactor;
638
639         if (newScaleFactor) {
640             var scaleValue = 1 / newScaleFactor;
641             var scaleTransform = "scale(" + scaleValue + ")";
642             if (this.controls.startPlaybackButton)
643                 this.controls.startPlaybackButton.style.webkitTransform = scaleTransform;
644             if (this.controls.panel) {
645                 var bottomAligment = -2 * scaleValue;
646                 this.controls.panel.style.bottom = bottomAligment + "px";
647                 this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px";
648                 this.controls.panel.style.width = Math.ceil(newScaleFactor * 100) + "%";
649                 this.controls.panel.style.webkitTransform = scaleTransform;
650                 this.setNeedsTimelineMetricsUpdate();
651                 this.updateProgress();
652             }
653         }
654     },
655
656     handlePresentationModeChange: function(event)
657     {
658         var presentationMode = this.presentationMode();
659
660         switch (presentationMode) {
661             case 'inline':
662                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
663                 break;
664             case 'optimized':
665                 var backgroundImage = "url('" + this.host.mediaUIImageData("optimized-fullscreen-placeholder") + "')";
666                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImage;
667                 this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', "video playback placeholder");
668                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
669                 break;
670         }
671
672         this.updateControls();
673         this.updateCaptionContainer();
674         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
675             this.showControls();
676     },
677
678     handleFullscreenChange: function(event)
679     {
680         Controller.prototype.handleFullscreenChange.call(this, event);
681         this.handlePresentationModeChange(event);
682     },
683
684 };
685
686 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
687 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });