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