[MediaControls] Refactor media controls & bring improvements made to iOS 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.doingSetup = true;
9     this._pageScaleFactor = 1;
10
11     this.timelineContextName = "_webkit-media-controls-timeline-" + host.generateUUID();
12
13     Controller.call(this, root, video, host);
14
15     this.setNeedsTimelineMetricsUpdate();
16
17     this._timelineIsHidden = false;
18     this._currentDisplayWidth = 0;
19     this.scheduleUpdateLayoutForDisplayedWidth();
20
21     host.controlsDependOnPageScaleFactor = true;
22     this.doingSetup = false;
23 };
24
25 /* Enums */
26 ControllerIOS.StartPlaybackControls = 2;
27
28
29 ControllerIOS.prototype = {
30     /* Constants */
31     MinimumTimelineWidth: 200,
32     ButtonWidth: 42,
33
34     addVideoListeners: function() {
35         Controller.prototype.addVideoListeners.call(this);
36
37         this.listenFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
38         this.listenFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
39         this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
40     },
41
42     removeVideoListeners: function() {
43         Controller.prototype.removeVideoListeners.call(this);
44
45         this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange);
46         this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange);
47         this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
48     },
49
50     createBase: function() {
51         Controller.prototype.createBase.call(this);
52
53         var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('button');
54         startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
55         startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
56
57         this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart);
58         this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange);
59         this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd);
60         this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart);
61         this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove);
62         this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut);
63
64         this.listenFor(document, 'visibilitychange', this.handleVisibilityChange);
65     },
66
67     shouldHaveStartPlaybackButton: function() {
68         var allowsInline = this.host.mediaPlaybackAllowsInline;
69
70         if (this.isPlaying || (this.hasPlayed && allowsInline))
71             return false;
72
73         if (this.isAudio() && allowsInline)
74             return false;
75
76         if (this.doingSetup)
77             return true;
78
79         if (this.isFullScreen())
80             return false;
81
82         if (!this.video.currentSrc && this.video.error)
83             return false;
84
85         if (!this.video.controls && allowsInline)
86             return false;
87
88         if (this.video.currentSrc && this.video.error)
89             return true;
90
91         return true;
92     },
93
94     shouldHaveControls: function() {
95         if (this.shouldHaveStartPlaybackButton())
96             return false;
97
98         return Controller.prototype.shouldHaveControls.call(this);
99     },
100
101     shouldHaveAnyUI: function() {
102         return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
103     },
104
105     createControls: function() {
106         Controller.prototype.createControls.call(this);
107
108         var panelContainer = this.controls.panelContainer = document.createElement('div');
109         panelContainer.setAttribute('pseudo', '-webkit-media-controls-panel-container');
110
111         var panelBackground = this.controls.panelBackground = document.createElement('div');
112         panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background');
113
114         var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div');
115         inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text');
116
117         var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p');
118         inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top');
119
120         var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p');
121         inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom');
122
123         var wirelessTargetPicker = this.controls.wirelessTargetPicker
124         this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
125         this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
126         this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
127
128         this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
129         this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
130         this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
131
132         this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
133         this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
134         this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
135         this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
136         this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
137         this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
138         this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
139         this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
140         this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
141         this.listenFor(this.controls.optimizedFullscreenButton, 'touchstart', this.handleOptimizedFullscreenTouchStart);
142         this.listenFor(this.controls.optimizedFullscreenButton, 'touchend', this.handleOptimizedFullscreenTouchEnd);
143         this.listenFor(this.controls.optimizedFullscreenButton, 'touchcancel', this.handleOptimizedFullscreenTouchCancel);
144         this.listenFor(this.controls.timeline, 'touchstart', this.handleTimelineTouchStart);
145         this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
146
147         this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
148     },
149
150     setControlsType: function(type) {
151         if (type === this.controlsType)
152             return;
153         Controller.prototype.setControlsType.call(this, type);
154
155         if (type === ControllerIOS.StartPlaybackControls)
156             this.addStartPlaybackControls();
157         else
158             this.removeStartPlaybackControls();
159     },
160
161     addStartPlaybackControls: function() {
162         this.base.appendChild(this.controls.startPlaybackButton);
163     },
164
165     removeStartPlaybackControls: function() {
166         if (this.controls.startPlaybackButton.parentNode)
167             this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
168     },
169
170     reconnectControls: function()
171     {
172         Controller.prototype.reconnectControls.call(this);
173
174         if (this.controlsType === ControllerIOS.StartPlaybackControls)
175             this.addStartPlaybackControls();
176     },
177
178     configureInlineControls: function() {
179         this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
180         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
181         this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
182         this.controls.panel.appendChild(this.controls.playButton);
183         this.controls.panel.appendChild(this.controls.statusDisplay);
184         this.controls.panel.appendChild(this.controls.timelineBox);
185         this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
186         if (!this.isLive) {
187             this.controls.timelineBox.appendChild(this.controls.currentTime);
188             this.controls.timelineBox.appendChild(this.controls.timeline);
189             this.controls.timelineBox.appendChild(this.controls.remainingTime);
190         }
191         if (this.isAudio()) {
192             // Hide the scrubber on audio until the user starts playing.
193             this.controls.timelineBox.classList.add(this.ClassNames.hidden);
194         } else {
195             if (Controller.gSimulateOptimizedFullscreenAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('optimized')))
196                 this.controls.panel.appendChild(this.controls.optimizedFullscreenButton);
197             this.controls.panel.appendChild(this.controls.fullscreenButton);
198         }
199     },
200
201     configureFullScreenControls: function() {
202         // Explicitly do nothing to override base-class behavior.
203     },
204
205     addControls: function() {
206         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
207         this.base.appendChild(this.controls.panelContainer);
208         this.controls.panelContainer.appendChild(this.controls.panelBackground);
209         this.controls.panelContainer.appendChild(this.controls.panel);
210         this.setNeedsTimelineMetricsUpdate();
211     },
212
213     updateControls: function() {
214         if (this.shouldHaveStartPlaybackButton())
215             this.setControlsType(ControllerIOS.StartPlaybackControls);
216         else if (this.presentationMode() === "fullscreen")
217             this.setControlsType(Controller.FullScreenControls);
218         else
219             this.setControlsType(Controller.InlineControls);
220
221         this.updateLayoutForDisplayedWidth();
222         this.setNeedsTimelineMetricsUpdate();
223     },
224
225     drawTimelineBackground: function() {
226         var width = this.timelineWidth * window.devicePixelRatio;
227         var height = this.timelineHeight * window.devicePixelRatio;
228
229         if (!width || !height)
230             return;
231
232         var played = this.video.currentTime / this.video.duration;
233         var buffered = 0;
234         var bufferedRanges = this.video.buffered;
235         if (bufferedRanges && bufferedRanges.length)
236             buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered);
237
238         buffered /= this.video.duration;
239         buffered = Math.max(buffered, played);
240
241         var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
242
243         ctx.clearRect(0, 0, width, height);
244
245         var midY = height / 2;
246
247         // 1. Draw the buffered part and played parts, using
248         // solid rectangles that are clipped to the outside of
249         // the lozenge.
250         ctx.save();
251         ctx.beginPath();
252         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
253         ctx.closePath();
254         ctx.clip();
255         ctx.fillStyle = "white";
256         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
257         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
258         ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height);
259         ctx.restore();
260
261         // 2. Draw the outline with a clip path that subtracts the
262         // middle of a lozenge. This produces a better result than
263         // stroking.
264         ctx.save();
265         ctx.beginPath();
266         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
267         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
268         ctx.closePath();
269         ctx.clip("evenodd");
270         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
271         ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
272         ctx.restore();
273     },
274
275     formatTime: function(time) {
276         if (isNaN(time))
277             time = 0;
278         var absTime = Math.abs(time);
279         var intSeconds = Math.floor(absTime % 60).toFixed(0);
280         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
281         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
282         var sign = time < 0 ? '-' : String();
283
284         if (intHours > 0)
285             return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
286
287         return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
288     },
289
290     handlePlayButtonTouchStart: function() {
291         this.controls.playButton.classList.add('active');
292     },
293
294     handlePlayButtonTouchEnd: function(event) {
295         this.controls.playButton.classList.remove('active');
296
297         if (this.canPlay())
298             this.video.play();
299         else
300             this.video.pause();
301
302         return true;
303     },
304
305     handlePlayButtonTouchCancel: function(event) {
306         this.controls.playButton.classList.remove('active');
307         return true;
308     },
309
310     handleBaseGestureStart: function(event) {
311         this.gestureStartTime = new Date();
312         // If this gesture started with two fingers inside the video, then
313         // don't treat it as a potential zoom, unless we're still waiting
314         // to play.
315         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
316             event.preventDefault();
317     },
318
319     handleBaseGestureChange: function(event) {
320         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
321             return;
322
323         var scaleDetectionThreshold = 0.2;
324         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
325             delete this.lastDoubleTouchTime;
326
327         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
328             event.preventDefault();
329
330         var currentGestureTime = new Date();
331         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
332         if (!duration)
333             return;
334
335         var velocity = Math.abs(event.scale - 1) / duration;
336
337         var pinchOutVelocityThreshold = 2;
338         var pinchOutGestureScaleThreshold = 1.25;
339         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
340             return;
341
342         delete this.gestureStartTime;
343         this.video.webkitEnterFullscreen();
344     },
345
346     handleBaseGestureEnd: function(event) {
347         delete this.gestureStartTime;
348     },
349
350     handleWrapperTouchStart: function(event) {
351         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
352             return;
353
354         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
355
356         if (this.controlsAreHidden()) {
357             this.showControls();
358             if (this.hideTimer)
359                 clearTimeout(this.hideTimer);
360             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
361         } else if (!this.canPlay())
362             this.hideControls();
363
364         return true;
365     },
366
367     handlePanelTouchStart: function(event) {
368         this.video.style.webkitUserSelect = 'none';
369     },
370
371     handlePanelTouchEnd: function(event) {
372         this.video.style.removeProperty('-webkit-user-select');
373     },
374
375     handlePanelTouchCancel: function(event) {
376         this.video.style.removeProperty('-webkit-user-select');
377     },
378
379     handleVisibilityChange: function(event) {
380         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
381     },
382
383     presentationMode: function() {
384         if ('webkitPresentationMode' in this.video)
385             return this.video.webkitPresentationMode;
386
387         if (this.isFullScreen())
388             return 'fullscreen';
389
390         return 'inline';
391     },
392
393     isFullScreen: function()
394     {
395         return this.video.webkitDisplayingFullscreen && this.presentationMode() != 'optimized';
396     },
397
398     handleFullscreenButtonClicked: function(event) {
399         if ('webkitSetPresentationMode' in this.video) {
400             if (this.presentationMode() === 'fullscreen')
401                 this.video.webkitSetPresentationMode('inline');
402             else
403                 this.video.webkitSetPresentationMode('fullscreen');
404
405             return;
406         }
407
408         if (this.isFullScreen())
409             this.video.webkitExitFullscreen();
410         else
411             this.video.webkitEnterFullscreen();
412     },
413
414     handleFullscreenTouchStart: function() {
415         this.controls.fullscreenButton.classList.add('active');
416     },
417
418     handleFullscreenTouchEnd: function(event) {
419         this.controls.fullscreenButton.classList.remove('active');
420
421         this.handleFullscreenButtonClicked();
422
423         return true;
424     },
425
426     handleFullscreenTouchCancel: function(event) {
427         this.controls.fullscreenButton.classList.remove('active');
428         return true;
429     },
430
431     handleOptimizedFullscreenButtonClicked: function(event) {
432         if (!('webkitSetPresentationMode' in this.video))
433             return;
434
435         if (this.presentationMode() === 'optimized')
436             this.video.webkitSetPresentationMode('inline');
437         else
438             this.video.webkitSetPresentationMode('optimized');
439     },
440
441     handleOptimizedFullscreenTouchStart: function() {
442         this.controls.optimizedFullscreenButton.classList.add('active');
443     },
444
445     handleOptimizedFullscreenTouchEnd: function(event) {
446         this.controls.optimizedFullscreenButton.classList.remove('active');
447
448         this.handleOptimizedFullscreenButtonClicked();
449
450         return true;
451     },
452
453     handleOptimizedFullscreenTouchCancel: function(event) {
454         this.controls.optimizedFullscreenButton.classList.remove('active');
455         return true;
456     },
457
458     handleStartPlaybackButtonTouchStart: function(event) {
459         this.controls.startPlaybackButton.classList.add('active');
460     },
461
462     handleStartPlaybackButtonTouchEnd: function(event) {
463         this.controls.startPlaybackButton.classList.remove('active');
464         if (this.video.error)
465             return true;
466
467         this.video.play();
468         this.updateControls();
469
470         return true;
471     },
472
473     handleStartPlaybackButtonTouchCancel: function(event) {
474         this.controls.startPlaybackButton.classList.remove('active');
475         return true;
476     },
477
478     handleTimelineTouchStart: function(event) {
479         this.scrubbing = true;
480         this.listenFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
481         this.listenFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
482     },
483
484     handleTimelineTouchEnd: function(event) {
485         this.stopListeningFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
486         this.stopListeningFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
487         this.scrubbing = false;
488     },
489
490     handleWirelessPickerButtonTouchStart: function() {
491         if (!this.video.error)
492             this.controls.wirelessTargetPicker.classList.add('active');
493     },
494
495     handleWirelessPickerButtonTouchEnd: function(event) {
496         this.controls.wirelessTargetPicker.classList.remove('active');
497         return this.handleWirelessPickerButtonClicked();
498     },
499
500     handleWirelessPickerButtonTouchCancel: function(event) {
501         this.controls.wirelessTargetPicker.classList.remove('active');
502         return true;
503     },
504
505     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
506         if (this.controlsType === ControllerIOS.StartPlaybackControls) {
507             this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
508             return;
509         }
510
511         Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
512     },
513
514     updateStatusDisplay: function(event)
515     {
516         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
517         Controller.prototype.updateStatusDisplay.call(this, event);
518     },
519
520     setPlaying: function(isPlaying)
521     {
522         Controller.prototype.setPlaying.call(this, isPlaying);
523
524         this.updateControls();
525
526         if (isPlaying && this.isAudio())
527             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
528
529         if (isPlaying)
530             this.hasPlayed = true;
531         else
532             this.showControls();
533     },
534
535     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
536     {
537         if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
538             return;
539
540         Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen);
541     },
542
543     handlePresentationModeChange: function(event)
544     {
545         var presentationMode = this.presentationMode();
546
547         switch (presentationMode) {
548             case 'inline':
549                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
550                 break;
551             case 'optimized':
552                 var backgroundImage = "url('" + this.host.mediaUIImageData("optimized-fullscreen-placeholder") + "')";
553                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImage;
554                 this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', "video playback placeholder");
555                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
556                 break;
557         }
558
559         this.updateControls();
560         this.updateCaptionContainer();
561         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
562             this.showControls();
563     },
564
565     handleFullscreenChange: function(event)
566     {
567         Controller.prototype.handleFullscreenChange.call(this, event);
568         this.handlePresentationModeChange(event);
569     },
570
571     controlsAlwaysVisible: function()
572     {
573         if (this.presentationMode() === 'optimized')
574             return true;
575
576         return Controller.prototype.controlsAlwaysVisible.call(this);
577     },
578
579
580 };
581
582 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
583 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });