e47c8d6b055060e99f53cb7b1d796bde02c6bbac
[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.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 */
20 ControllerIOS.StartPlaybackControls = 2;
21
22 /* Globals */
23 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>';
24 ControllerIOS.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
25
26 ControllerIOS.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
476 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
477 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });