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