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