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