[iOS Media] Airplay button should be blue when active
[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     this._timelineIsHidden = false;
22     this._currentDisplayWidth = 0;
23     this.scheduleUpdateLayoutForDisplayedWidth();
24
25     host.controlsDependOnPageScaleFactor = true;
26     this.doingSetup = false;
27 };
28
29 /* Constants */
30 ControllerIOS.MinimumTimelineWidth = 200;
31 ControllerIOS.ButtonWidth = 42;
32
33 /* Enums */
34 ControllerIOS.StartPlaybackControls = 2;
35
36 /* Globals */
37 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>';
38 ControllerIOS.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
39 ControllerIOS.gSimulateOptimizedFullscreenAvailable = false; // Used for testing when optimized fullscreen is not available.
40 ControllerIOS.gLastTimelineId = 0;
41
42 ControllerIOS.prototype = {
43     addVideoListeners: function() {
44         Controller.prototype.addVideoListeners.call(this);
45
46         this.listenFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
47         this.listenFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
48         this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
49         this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
50     },
51
52     removeVideoListeners: function() {
53         Controller.prototype.removeVideoListeners.call(this);
54
55         this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
56         this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
57         this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
58         this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
59
60         this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
61     },
62
63     createBase: function() {
64         Controller.prototype.createBase.call(this);
65
66         var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('button');
67         startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
68         startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
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.mediaPlaybackAllowsInline;
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     currentPlaybackTargetIsWireless: function() {
119         return ControllerIOS.gSimulateWirelessPlaybackTarget || (('webkitCurrentPlaybackTargetIsWireless' in this.video) && this.video.webkitCurrentPlaybackTargetIsWireless);
120     },
121
122     updateWirelessPlaybackStatus: function() {
123         if (this.currentPlaybackTargetIsWireless()) {
124             var backgroundImageSVG = "url('" + ControllerIOS.gWirelessImage + "')";
125
126             var deviceName = "";
127             var deviceType = "";
128             var type = this.host.externalDeviceType;
129             if (type == "airplay") {
130                 deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
131                 deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
132             } else if (type == "tvout") {
133                 deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
134                 deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
135             }
136
137             backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_TYPE##', deviceType);
138             backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_NAME##', deviceName);
139
140             this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImageSVG;
141             this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
142
143             this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
144             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing);
145         } else {
146             this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
147             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing);
148         }
149     },
150
151     updateWirelessTargetAvailable: function() {
152         if (ControllerIOS.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets)
153             this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
154         else
155             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
156     },
157
158     createControls: function() {
159         Controller.prototype.createControls.call(this);
160
161         var panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div');
162         panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent');
163
164         var spacer = this.controls.spacer = document.createElement('div');
165         spacer.setAttribute('pseudo', '-webkit-media-controls-spacer');
166         spacer.classList.add(this.ClassNames.hidden);
167
168         var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div');
169         inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-inline-playback-placeholder');
170         if (!ControllerIOS.gSimulateOptimizedFullscreenAvailable)
171             inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
172         inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Display Optimized Full Screen'));
173
174         var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
175         wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
176         wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
177         this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
178         this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
179         this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
180
181         if (!ControllerIOS.gSimulateWirelessPlaybackTarget)
182             wirelessTargetPicker.classList.add(this.ClassNames.hidden);
183
184         this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
185         this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
186         this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
187
188         this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
189         this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
190         this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
191         this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
192         this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
193         this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
194         this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
195         this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
196         this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
197         this.listenFor(this.controls.optimizedFullscreenButton, 'touchstart', this.handleOptimizedFullscreenTouchStart);
198         this.listenFor(this.controls.optimizedFullscreenButton, 'touchend', this.handleOptimizedFullscreenTouchEnd);
199         this.listenFor(this.controls.optimizedFullscreenButton, 'touchcancel', this.handleOptimizedFullscreenTouchCancel);
200         this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
201
202         this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
203     },
204
205     setControlsType: function(type) {
206         if (type === this.controlsType)
207             return;
208         Controller.prototype.setControlsType.call(this, type);
209
210         if (type === ControllerIOS.StartPlaybackControls)
211             this.addStartPlaybackControls();
212         else
213             this.removeStartPlaybackControls();
214
215         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
216     },
217
218     addStartPlaybackControls: function() {
219         this.base.appendChild(this.controls.startPlaybackButton);
220     },
221
222     removeStartPlaybackControls: function() {
223         if (this.controls.startPlaybackButton.parentNode)
224             this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
225     },
226
227     reconnectControls: function()
228     {
229         Controller.prototype.reconnectControls.call(this);
230
231         if (this.controlsType === ControllerIOS.StartPlaybackControls)
232             this.addStartPlaybackControls();
233     },
234
235     configureInlineControls: function() {
236         this.controls.panel.appendChild(this.controls.playButton);
237         this.controls.panel.appendChild(this.controls.statusDisplay);
238         this.controls.panel.appendChild(this.controls.spacer);
239         this.controls.panel.appendChild(this.controls.timelineBox);
240         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
241         if (!this.isLive) {
242             this.controls.timelineBox.appendChild(this.controls.currentTime);
243             this.controls.timelineBox.appendChild(this.controls.timeline);
244             this.controls.timelineBox.appendChild(this.controls.remainingTime);
245         }
246         if (this.isAudio()) {
247             // Hide the scrubber on audio until the user starts playing.
248             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
249         } else {
250             if (ControllerIOS.gSimulateOptimizedFullscreenAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('optimized')))
251                 this.controls.panel.appendChild(this.controls.optimizedFullscreenButton);
252             this.controls.panel.appendChild(this.controls.fullscreenButton);
253         }
254     },
255
256     configureFullScreenControls: function() {
257         // Explicitly do nothing to override base-class behavior.
258     },
259
260     hideControls: function() {
261         Controller.prototype.hideControls.call(this);
262         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
263     },
264
265     showControls: function() {
266         this.updateLayoutForDisplayedWidth();
267         this.updateTime(true);
268         this.updateProgress(true);
269         Controller.prototype.showControls.call(this);
270         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
271     },
272
273     addControls: function() {
274         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
275         this.base.appendChild(this.controls.panelCompositedParent);
276         this.controls.panelCompositedParent.appendChild(this.controls.panel);
277         this.setNeedsTimelineMetricsUpdate();
278     },
279
280     updateControls: function() {
281         if (this.shouldHaveStartPlaybackButton())
282             this.setControlsType(ControllerIOS.StartPlaybackControls);
283         else if (this.presentationMode() === "fullscreen")
284             this.setControlsType(Controller.FullScreenControls);
285         else
286             this.setControlsType(Controller.InlineControls);
287
288         this.updateLayoutForDisplayedWidth();
289         this.setNeedsTimelineMetricsUpdate();
290     },
291
292     updateTime: function(forceUpdate) {
293         Controller.prototype.updateTime.call(this, forceUpdate);
294         this.updateProgress();
295     },
296
297     drawTimelineBackground: function() {
298         var width = this.timelineWidth * window.devicePixelRatio;
299         var height = this.timelineHeight * window.devicePixelRatio;
300
301         if (!width || !height)
302             return;
303
304         var played = this.video.currentTime / this.video.duration;
305         var buffered = 0;
306         for (var i = 0, end = this.video.buffered.length; i < end; ++i)
307             buffered = Math.max(this.video.buffered.end(i), buffered);
308
309         buffered /= this.video.duration;
310
311         var ctx = this.video.ownerDocument.getCSSCanvasContext('2d', this.timelineContextName, width, height);
312
313         ctx.clearRect(0, 0, width, height);
314
315         var midY = height / 2;
316
317         // 1. Draw the outline with a clip path that subtracts the
318         // middle of a lozenge. This produces a better result than
319         // stroking when we come to filling the parts below.
320         ctx.save();
321         ctx.beginPath();
322         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
323         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
324         ctx.closePath();
325         ctx.clip("evenodd");
326         ctx.fillStyle = "black";
327         ctx.fillRect(0, 0, width, height);
328         ctx.restore();
329
330         // 2. Draw the buffered part and played parts, using
331         // solid rectangles that are clipped to the outside of
332         // the lozenge.
333         ctx.save();
334         ctx.beginPath();
335         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
336         ctx.closePath();
337         ctx.clip();
338         ctx.fillStyle = "black";
339         ctx.fillRect(0, 0, Math.round(width * buffered) + 2, height);
340         ctx.fillStyle = "white";
341         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
342         ctx.restore();
343     },
344
345     formatTime: function(time) {
346         if (isNaN(time))
347             time = 0;
348         var absTime = Math.abs(time);
349         var intSeconds = Math.floor(absTime % 60).toFixed(0);
350         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
351         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
352         var sign = time < 0 ? '-' : String();
353
354         if (intHours > 0)
355             return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
356
357         return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
358     },
359
360     handleTimelineChange: function(event) {
361         Controller.prototype.handleTimelineChange.call(this);
362         this.updateProgress();
363     },
364
365     handlePlayButtonTouchStart: function() {
366         this.controls.playButton.classList.add('active');
367     },
368
369     handlePlayButtonTouchEnd: function(event) {
370         this.controls.playButton.classList.remove('active');
371
372         if (this.canPlay())
373             this.video.play();
374         else
375             this.video.pause();
376
377         return true;
378     },
379
380     handlePlayButtonTouchCancel: function(event) {
381         this.controls.playButton.classList.remove('active');
382         return true;
383     },
384
385     handleBaseGestureStart: function(event) {
386         this.gestureStartTime = new Date();
387         // If this gesture started with two fingers inside the video, then
388         // don't treat it as a potential zoom, unless we're still waiting
389         // to play.
390         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
391             event.preventDefault();
392     },
393
394     handleBaseGestureChange: function(event) {
395         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
396             return;
397
398         var scaleDetectionThreshold = 0.2;
399         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
400             delete this.lastDoubleTouchTime;
401
402         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
403             event.preventDefault();
404
405         var currentGestureTime = new Date();
406         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
407         if (!duration)
408             return;
409
410         var velocity = Math.abs(event.scale - 1) / duration;
411
412         var pinchOutVelocityThreshold = 2;
413         var pinchOutGestureScaleThreshold = 1.25;
414         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
415             return;
416
417         delete this.gestureStartTime;
418         this.video.webkitEnterFullscreen();
419     },
420
421     handleBaseGestureEnd: function(event) {
422         delete this.gestureStartTime;
423     },
424
425     handleWrapperTouchStart: function(event) {
426         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
427             return;
428
429         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
430
431         if (this.controlsAreHidden()) {
432             this.showControls();
433             if (this.hideTimer)
434                 clearTimeout(this.hideTimer);
435             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
436         } else if (!this.canPlay())
437             this.hideControls();
438     },
439
440     handlePanelTouchStart: function(event) {
441         this.video.style.webkitUserSelect = 'none';
442     },
443
444     handlePanelTouchEnd: function(event) {
445         this.video.style.removeProperty('-webkit-user-select');
446     },
447
448     handlePanelTouchCancel: function(event) {
449         this.video.style.removeProperty('-webkit-user-select');
450     },
451
452     handleVisibilityChange: function(event) {
453         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
454     },
455
456     presentationMode: function() {
457         if ('webkitPresentationMode' in this.video)
458             return this.video.webkitPresentationMode;
459
460         if (this.isFullScreen())
461             return 'fullscreen';
462
463         return 'inline';
464     },
465
466     isFullScreen: function()
467     {
468         return this.video.webkitDisplayingFullscreen && this.presentationMode() != 'optimized';
469     },
470
471     handleFullscreenButtonClicked: function(event) {
472         if ('webkitSetPresentationMode' in this.video) {
473             if (this.presentationMode() === 'fullscreen')
474                 this.video.webkitSetPresentationMode('inline');
475             else
476                 this.video.webkitSetPresentationMode('fullscreen');
477
478             return;
479         }
480
481         if (this.isFullScreen())
482             this.video.webkitExitFullscreen();
483         else
484             this.video.webkitEnterFullscreen();
485     },
486
487     handleFullscreenTouchStart: function() {
488         this.controls.fullscreenButton.classList.add('active');
489     },
490
491     handleFullscreenTouchEnd: function(event) {
492         this.controls.fullscreenButton.classList.remove('active');
493
494         this.handleFullscreenButtonClicked();
495
496         return true;
497     },
498
499     handleFullscreenTouchCancel: function(event) {
500         this.controls.fullscreenButton.classList.remove('active');
501         return true;
502     },
503
504     handleOptimizedFullscreenButtonClicked: function(event) {
505         if (!('webkitSetPresentationMode' in this.video))
506             return;
507
508         if (this.presentationMode() === 'optimized')
509             this.video.webkitSetPresentationMode('inline');
510         else
511             this.video.webkitSetPresentationMode('optimized');
512     },
513
514     handleOptimizedFullscreenTouchStart: function() {
515         this.controls.optimizedFullscreenButton.classList.add('active');
516     },
517
518     handleOptimizedFullscreenTouchEnd: function(event) {
519         this.controls.optimizedFullscreenButton.classList.remove('active');
520
521         this.handleOptimizedFullscreenButtonClicked();
522
523         return true;
524     },
525
526     handleOptimizedFullscreenTouchCancel: function(event) {
527         this.controls.optimizedFullscreenButton.classList.remove('active');
528         return true;
529     },
530
531     handleStartPlaybackButtonTouchStart: function(event) {
532         this.controls.startPlaybackButton.classList.add('active');
533     },
534
535     handleStartPlaybackButtonTouchEnd: function(event) {
536         this.controls.startPlaybackButton.classList.remove('active');
537         if (this.video.error)
538             return true;
539
540         this.video.play();
541         this.updateControls();
542
543         return true;
544     },
545
546     handleStartPlaybackButtonTouchCancel: function(event) {
547         this.controls.startPlaybackButton.classList.remove('active');
548         return true;
549     },
550
551     handleReadyStateChange: function(event) {
552         Controller.prototype.handleReadyStateChange.call(this, event);
553         this.updateControls();
554     },
555
556     handleWirelessPlaybackChange: function(event) {
557         this.updateWirelessPlaybackStatus();
558         this.setNeedsTimelineMetricsUpdate();
559     },
560
561     handleWirelessTargetAvailableChange: function(event) {
562         var wirelessPlaybackTargetsAvailable = event.availability == "available";
563         if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
564             return;
565
566         this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
567         this.updateWirelessTargetAvailable();
568         this.setNeedsTimelineMetricsUpdate();
569     },
570
571     handleWirelessPickerButtonTouchStart: function() {
572         if (!this.video.error)
573             this.controls.wirelessTargetPicker.classList.add('active');
574     },
575
576     handleWirelessPickerButtonTouchEnd: function(event) {
577         this.controls.wirelessTargetPicker.classList.remove('active');
578         this.video.webkitShowPlaybackTargetPicker();
579         return true;
580     },
581
582     handleWirelessPickerButtonTouchCancel: function(event) {
583         this.controls.wirelessTargetPicker.classList.remove('active');
584         return true;
585     },
586
587     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
588         var shouldListen = true;
589         if (this.video.error)
590             shouldListen = false;
591         if (this.controlsType === ControllerIOS.StartPlaybackControls)
592             shouldListen = false;
593         if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
594             shouldListen = false;
595         if (document.hidden)
596             shouldListen = false;
597
598         this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
599     },
600
601     updateStatusDisplay: function(event)
602     {
603         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
604         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
605         Controller.prototype.updateStatusDisplay.call(this, event);
606     },
607
608     setPlaying: function(isPlaying)
609     {
610         Controller.prototype.setPlaying.call(this, isPlaying);
611
612         this.updateControls();
613
614         if (isPlaying && this.isAudio() && !this._timelineIsHidden) {
615             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
616             this.controls.spacer.classList.add(this.ClassNames.hidden);
617         }
618
619         if (isPlaying)
620             this.hasPlayed = true;
621         else
622             this.showControls();
623     },
624
625     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
626     {
627         if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
628             return;
629
630         if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
631             return;
632
633         this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
634         if (shouldListen)
635             this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
636         else
637             this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
638     },
639
640     get pageScaleFactor()
641     {
642         return this._pageScaleFactor;
643     },
644
645     set pageScaleFactor(newScaleFactor)
646     {
647         if (this._pageScaleFactor === newScaleFactor)
648             return;
649
650         this._pageScaleFactor = newScaleFactor;
651
652         if (newScaleFactor) {
653             var scaleValue = 1 / newScaleFactor;
654             var scaleTransform = "scale(" + scaleValue + ")";
655             if (this.controls.startPlaybackButton)
656                 this.controls.startPlaybackButton.style.webkitTransform = scaleTransform;
657             if (this.controls.panel) {
658                 var bottomAligment = -2 * scaleValue;
659                 this.controls.panel.style.bottom = bottomAligment + "px";
660                 this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px";
661                 this.controls.panel.style.width = Math.ceil(newScaleFactor * 100) + "%";
662                 this.controls.panel.style.webkitTransform = scaleTransform;
663                 this.setNeedsTimelineMetricsUpdate();
664                 this.updateProgress();
665                 this.scheduleUpdateLayoutForDisplayedWidth();
666             }
667         }
668     },
669
670     handlePresentationModeChange: function(event)
671     {
672         var presentationMode = this.presentationMode();
673
674         switch (presentationMode) {
675             case 'inline':
676                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
677                 break;
678             case 'optimized':
679                 var backgroundImage = "url('" + this.host.mediaUIImageData("optimized-fullscreen-placeholder") + "')";
680                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImage;
681                 this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', "video playback placeholder");
682                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
683                 break;
684         }
685
686         this.updateControls();
687         this.updateCaptionContainer();
688         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
689             this.showControls();
690     },
691
692     handleFullscreenChange: function(event)
693     {
694         Controller.prototype.handleFullscreenChange.call(this, event);
695         this.handlePresentationModeChange(event);
696     },
697
698     scheduleUpdateLayoutForDisplayedWidth: function ()
699     {
700         setTimeout(function () {
701             this.updateLayoutForDisplayedWidth();
702         }.bind(this), 0);
703     },
704
705     updateLayoutForDisplayedWidth: function()
706     {
707         if (!this.controls || !this.controls.panel)
708             return;
709
710         var visibleWidth = this.controls.panel.getBoundingClientRect().width * this._pageScaleFactor;
711         if (visibleWidth <= 0 || visibleWidth == this._currentDisplayWidth)
712             return;
713
714         this._currentDisplayWidth = visibleWidth;
715
716         // We need to work out how many right-hand side buttons are available.
717         this.updateWirelessTargetAvailable();
718         this.updateFullscreenButtons();
719
720         var visibleButtonWidth = ControllerIOS.ButtonWidth; // We always try to show the fullscreen button.
721
722         if (!this.controls.wirelessTargetPicker.classList.contains(this.ClassNames.hidden))
723             visibleButtonWidth += ControllerIOS.ButtonWidth;
724         if (!this.controls.optimizedFullscreenButton.classList.contains(this.ClassNames.hidden))
725             visibleButtonWidth += ControllerIOS.ButtonWidth;
726
727         // Check if there is enough room for the scrubber.
728         if ((visibleWidth - visibleButtonWidth) < ControllerIOS.MinimumTimelineWidth) {
729             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
730             this.controls.spacer.classList.remove(this.ClassNames.hidden);
731             this._timelineIsHidden = true;
732         } else {
733             if (!this.isAudio() || this.hasPlayed) {
734                 this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
735                 this.controls.spacer.classList.add(this.ClassNames.hidden);
736                 this._timelineIsHidden = false;
737             } else
738                 this.controls.spacer.classList.remove(this.ClassNames.hidden);
739         }
740
741         // Drop the airplay button if there isn't enough space.
742         if (visibleWidth < visibleButtonWidth) {
743             this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
744             visibleButtonWidth -= ControllerIOS.ButtonWidth;
745         }
746
747         // Drop the optimized fullscreen button if there still isn't enough space.
748         if (visibleWidth < visibleButtonWidth) {
749             this.controls.optimizedFullscreenButton.classList.add(this.ClassNames.hidden);
750             visibleButtonWidth -= ControllerIOS.ButtonWidth;
751         }
752
753         // And finally, drop the fullscreen button as a last resort.
754         if (visibleWidth < visibleButtonWidth) {
755             this.controls.fullscreenButton.classList.add(this.ClassNames.hidden);
756             visibleButtonWidth -= ControllerIOS.ButtonWidth;
757         } else
758             this.controls.fullscreenButton.classList.remove(this.ClassNames.hidden);
759     },
760
761 };
762
763 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
764 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });