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