1function createControls(root, video, host)
2{
3    return new ControllerIOS(root, video, host);
4};
5
6function ControllerIOS(root, video, host)
7{
8    this.hasWirelessPlaybackTargets = false;
9    this._pageScaleFactor = 1;
10    Controller.call(this, root, video, host);
11
12    this.updateWirelessTargetAvailable();
13    this.updateWirelessPlaybackStatus();
14    this.setNeedsTimelineMetricsUpdate();
15
16    host.controlsDependOnPageScaleFactor = true;
17};
18
19/* Enums */
20ControllerIOS.StartPlaybackControls = 2;
21
22/* Globals */
23ControllerIOS.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>';
24ControllerIOS.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
25
26ControllerIOS.prototype = {
27    addVideoListeners: function() {
28        Controller.prototype.addVideoListeners.call(this);
29
30        this.listenFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
31        this.listenFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
32
33        if (window.WebKitPlaybackTargetAvailabilityEvent) {
34            this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
35            this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
36        }
37    },
38
39    removeVideoListeners: function() {
40        Controller.prototype.removeVideoListeners.call(this);
41
42        this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
43        this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
44
45        if (window.WebKitPlaybackTargetAvailabilityEvent) {
46            this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
47            this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
48        }
49    },
50
51    createBase: function() {
52        Controller.prototype.createBase.call(this);
53
54        var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('button');
55        startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
56        startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
57
58        this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart);
59        this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange);
60        this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd);
61        this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart);
62        this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove);
63        this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut);
64    },
65
66    shouldHaveStartPlaybackButton: function() {
67        var allowsInline = this.host.mediaPlaybackAllowsInline;
68
69        if (this.isAudio() && allowsInline)
70            return false;
71
72        if (this.isFullScreen())
73            return false;
74
75        if (!this.video.currentSrc && this.video.error)
76            return false;
77
78        if (!this.video.controls && allowsInline)
79            return false;
80
81        if (this.video.currentSrc && this.video.error)
82            return true;
83
84        if (!this.host.userGestureRequired && allowsInline)
85            return false;
86
87        return true;
88    },
89
90    shouldHaveControls: function() {
91        if (this.shouldHaveStartPlaybackButton())
92            return false;
93
94        return Controller.prototype.shouldHaveControls.call(this);
95    },
96
97    shouldHaveAnyUI: function() {
98        return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
99    },
100
101    currentPlaybackTargetIsWireless: function() {
102        return ControllerIOS.gSimulateWirelessPlaybackTarget || (('webkitCurrentPlaybackTargetIsWireless' in this.video) && this.video.webkitCurrentPlaybackTargetIsWireless);
103    },
104
105    updateWirelessPlaybackStatus: function() {
106        if (this.currentPlaybackTargetIsWireless()) {
107            var backgroundImageSVG = "url('" + ControllerIOS.gWirelessImage + "')";
108
109            var deviceName = "";
110            var deviceType = "";
111            var type = this.host.externalDeviceType;
112            if (type == "airplay") {
113                deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
114                deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
115            } else if (type == "tvout") {
116                deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
117                deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
118            }
119
120            backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_TYPE##', deviceType);
121            backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_NAME##', deviceName);
122
123            this.controls.wirelessPlaybackStatus.style.backgroundImage = backgroundImageSVG;
124            this.controls.wirelessPlaybackStatus.setAttribute('aria-label', deviceType + ", " + deviceName);
125
126            this.controls.wirelessPlaybackStatus.classList.remove(this.ClassNames.hidden);
127            this.controls.wirelessTargetPicker.classList.add(this.ClassNames.active);
128        } else {
129            this.controls.wirelessPlaybackStatus.classList.add(this.ClassNames.hidden);
130            this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.active);
131        }
132    },
133
134    updateWirelessTargetAvailable: function() {
135        if (ControllerIOS.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets)
136            this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
137        else
138            this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
139    },
140
141    createControls: function() {
142        Controller.prototype.createControls.call(this);
143
144        var wirelessPlaybackStatus = this.controls.wirelessPlaybackStatus = document.createElement('div');
145        wirelessPlaybackStatus.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status');
146        wirelessPlaybackStatus.classList.add(this.ClassNames.hidden);
147
148        var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
149        wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
150        wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
151        this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
152        this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
153        this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
154
155        if (!ControllerIOS.gSimulateWirelessPlaybackTarget)
156            wirelessTargetPicker.classList.add(this.ClassNames.hidden);
157
158        this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
159        this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
160        this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
161
162        this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
163        this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
164        this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
165        this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
166        this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
167        this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
168        this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
169        this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
170        this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
171        this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
172    },
173
174    setControlsType: function(type) {
175        if (type === this.controlsType)
176            return;
177        Controller.prototype.setControlsType.call(this, type);
178
179        if (type === ControllerIOS.StartPlaybackControls)
180            this.addStartPlaybackControls();
181        else
182            this.removeStartPlaybackControls();
183    },
184
185    addStartPlaybackControls: function() {
186        this.base.appendChild(this.controls.startPlaybackButton);
187    },
188
189    removeStartPlaybackControls: function() {
190        if (this.controls.startPlaybackButton.parentNode)
191            this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
192    },
193
194    configureInlineControls: function() {
195        this.base.appendChild(this.controls.wirelessPlaybackStatus);
196
197        this.controls.panel.appendChild(this.controls.playButton);
198        this.controls.panel.appendChild(this.controls.statusDisplay);
199        this.controls.panel.appendChild(this.controls.timelineBox);
200        this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
201        if (!this.isLive) {
202            this.controls.timelineBox.appendChild(this.controls.currentTime);
203            this.controls.timelineBox.appendChild(this.controls.timeline);
204            this.controls.timelineBox.appendChild(this.controls.remainingTime);
205        }
206        if (!this.isAudio())
207            this.controls.panel.appendChild(this.controls.fullscreenButton);
208    },
209
210    configureFullScreenControls: function() {
211        // Do nothing
212    },
213
214    updateControls: function() {
215        if (this.shouldHaveStartPlaybackButton())
216            this.setControlsType(ControllerIOS.StartPlaybackControls);
217        else if (this.isFullScreen())
218            this.setControlsType(Controller.FullScreenControls);
219        else
220            this.setControlsType(Controller.InlineControls);
221
222        this.setNeedsTimelineMetricsUpdate();
223    },
224
225    updateTime: function() {
226        Controller.prototype.updateTime.call(this);
227        this.updateProgress();
228    },
229
230    progressFillStyle: function() {
231        return 'rgba(0, 0, 0, 0.5)';
232    },
233
234    updateProgress: function() {
235        Controller.prototype.updateProgress.call(this);
236
237        var width = this.timelineWidth;
238        var height = this.timelineHeight;
239
240        // Magic number, matching the value for ::-webkit-media-controls-timeline::-webkit-slider-thumb
241        // in mediaControlsiOS.css. Since we cannot ask the thumb for its offsetWidth as it's in its own
242        // shadow dom, just hard-code the value.
243        var thumbWidth = 16;
244        var endX = thumbWidth / 2 + (width - thumbWidth) * this.video.currentTime / this.video.duration;
245
246        var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height);
247        context.fillStyle = 'white';
248        context.fillRect(0, 0, endX, height);
249    },
250
251    formatTime: function(time) {
252        if (isNaN(time))
253            time = 0;
254        var absTime = Math.abs(time);
255        var intSeconds = Math.floor(absTime % 60).toFixed(0);
256        var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
257        var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
258        var sign = time < 0 ? '-' : String();
259
260        if (intHours > 0)
261            return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
262
263        return sign + String('00' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('00' + intSeconds).slice(-2);
264    },
265
266    handleTimelineChange: function(event) {
267        Controller.prototype.handleTimelineChange.call(this);
268        this.updateProgress();
269    },
270
271    handlePlayButtonTouchStart: function() {
272        this.controls.playButton.classList.add('active');
273    },
274
275    handlePlayButtonTouchEnd: function(event) {
276        this.controls.playButton.classList.remove('active');
277
278        if (this.canPlay())
279            this.video.play();
280        else
281            this.video.pause();
282
283        return true;
284    },
285
286    handlePlayButtonTouchCancel: function(event) {
287        this.controls.playButton.classList.remove('active');
288        return true;
289    },
290
291    handleBaseGestureStart: function(event) {
292        this.gestureStartTime = new Date();
293        // If this gesture started with two fingers inside the video, then
294        // don't treat it as a potential zoom, unless we're still waiting
295        // to play.
296        if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
297            event.preventDefault();
298    },
299
300    handleBaseGestureChange: function(event) {
301        if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
302            return;
303
304        if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
305            event.preventDefault();
306
307        var currentGestureTime = new Date();
308        var duration = (currentGestureTime - this.gestureStartTime) / 1000;
309        if (!duration)
310            return;
311
312        var velocity = Math.abs(event.scale - 1) / duration;
313
314        if (event.scale < 1.25 || velocity < 2)
315            return;
316
317        delete this.gestureStartTime;
318        this.video.webkitEnterFullscreen();
319    },
320
321    handleBaseGestureEnd: function(event) {
322        delete this.gestureStartTime;
323    },
324
325    handleWrapperTouchStart: function(event) {
326        if (event.target != this.base && event.target != this.controls.wirelessPlaybackStatus)
327            return;
328
329        this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
330
331        if (this.controlsAreHidden()) {
332            this.showControls();
333            if (this.hideTimer)
334                clearTimeout(this.hideTimer);
335            this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
336        } else if (!this.canPlay())
337            this.hideControls();
338    },
339
340    handlePanelTouchStart: function(event) {
341        this.video.style.webkitUserSelect = 'none';
342    },
343
344    handlePanelTouchEnd: function(event) {
345        this.video.style.removeProperty('-webkit-user-select');
346    },
347
348    handlePanelTouchCancel: function(event) {
349        this.video.style.removeProperty('-webkit-user-select');
350    },
351
352    isFullScreen: function()
353    {
354        return this.video.webkitDisplayingFullscreen;
355    },
356
357    handleFullscreenButtonClicked: function(event) {
358        if (this.isFullScreen())
359            this.video.webkitExitFullscreen();
360        else
361            this.video.webkitEnterFullscreen();
362    },
363
364    handleFullscreenTouchStart: function() {
365        this.controls.fullscreenButton.classList.add('active');
366    },
367
368    handleFullscreenTouchEnd: function(event) {
369        this.controls.fullscreenButton.classList.remove('active');
370
371        this.handleFullscreenButtonClicked();
372
373        return true;
374    },
375
376    handleFullscreenTouchCancel: function(event) {
377        this.controls.fullscreenButton.classList.remove('active');
378        return true;
379    },
380
381    handleStartPlaybackButtonTouchStart: function(event) {
382        this.controls.fullscreenButton.classList.add('active');
383    },
384
385    handleStartPlaybackButtonTouchEnd: function(event) {
386        this.controls.fullscreenButton.classList.remove('active');
387        if (this.video.error)
388            return true;
389
390        this.video.play();
391
392        return true;
393    },
394
395    handleStartPlaybackButtonTouchCancel: function(event) {
396        this.controls.fullscreenButton.classList.remove('active');
397        return true;
398    },
399
400    handleReadyStateChange: function(event) {
401        Controller.prototype.handleReadyStateChange.call(this, event);
402        this.updateControls();
403    },
404
405    handleWirelessPlaybackChange: function(event) {
406        this.updateWirelessPlaybackStatus();
407        this.setNeedsTimelineMetricsUpdate();
408    },
409
410    handleWirelessTargetAvailableChange: function(event) {
411        var wirelessPlaybackTargetsAvailable = event.availability == "available";
412        if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
413            return;
414
415        this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
416        this.updateWirelessTargetAvailable();
417        this.setNeedsTimelineMetricsUpdate();
418    },
419
420    handleWirelessPickerButtonTouchStart: function() {
421        if (!this.video.error)
422            this.controls.wirelessTargetPicker.classList.add('active');
423    },
424
425    handleWirelessPickerButtonTouchEnd: function(event) {
426        this.controls.wirelessTargetPicker.classList.remove('active');
427        this.video.webkitShowPlaybackTargetPicker();
428        return true;
429    },
430
431    handleWirelessPickerButtonTouchCancel: function(event) {
432        this.controls.wirelessTargetPicker.classList.remove('active');
433        return true;
434    },
435
436    updateStatusDisplay: function(event)
437    {
438        this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
439        Controller.prototype.updateStatusDisplay.call(this, event);
440    },
441
442    setPlaying: function(isPlaying)
443    {
444        this.updateControls();
445        Controller.prototype.setPlaying.call(this, isPlaying);
446    },
447
448    get pageScaleFactor() {
449        return this._pageScaleFactor;
450    },
451
452    set pageScaleFactor(newScaleFactor) {
453        if (this._pageScaleFactor === newScaleFactor)
454            return;
455
456        this._pageScaleFactor = newScaleFactor;
457
458        if (newScaleFactor) {
459            var scaleValue = 1 / newScaleFactor;
460            var scaleTransform = "scale(" + scaleValue + ")";
461            if (this.controls.startPlaybackButton)
462                this.controls.startPlaybackButton.style.webkitTransform = scaleTransform;
463            if (this.controls.panel) {
464                var bottomAligment = -2 * scaleValue;
465                this.controls.panel.style.bottom = bottomAligment + "px";
466                this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px";
467                this.controls.panel.style.width = Math.ceil(newScaleFactor * 100) + "%";
468                this.controls.panel.style.webkitTransform = scaleTransform;
469                this.setNeedsTimelineMetricsUpdate();
470                this.updateProgress();
471            }
472        }
473    }
474};
475
476Object.create(Controller.prototype).extend(ControllerIOS.prototype);
477Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });
478