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