Unreviewed, rolling out r184667 and r184682.
[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 wirelessTargetPicker = this.controls.wirelessTargetPicker;
112         this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
113         this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
114         this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);
115
116         this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
117         this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
118         this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);
119
120         this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
121         this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
122         this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
123         this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
124         this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
125         this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
126         this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
127         this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
128         this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
129         this.listenFor(this.controls.optimizedFullscreenButton, 'touchstart', this.handleOptimizedFullscreenTouchStart);
130         this.listenFor(this.controls.optimizedFullscreenButton, 'touchend', this.handleOptimizedFullscreenTouchEnd);
131         this.listenFor(this.controls.optimizedFullscreenButton, 'touchcancel', this.handleOptimizedFullscreenTouchCancel);
132         this.listenFor(this.controls.timeline, 'touchstart', this.handleTimelineTouchStart);
133         this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);
134
135         this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
136     },
137
138     setControlsType: function(type) {
139         if (type === this.controlsType)
140             return;
141         Controller.prototype.setControlsType.call(this, type);
142
143         if (type === ControllerIOS.StartPlaybackControls)
144             this.addStartPlaybackControls();
145         else
146             this.removeStartPlaybackControls();
147     },
148
149     addStartPlaybackControls: function() {
150         this.base.appendChild(this.controls.startPlaybackButton);
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             if (Controller.gSimulateOptimizedFullscreenAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('optimized')))
184                 this.controls.panel.appendChild(this.controls.optimizedFullscreenButton);
185             this.controls.panel.appendChild(this.controls.fullscreenButton);
186         }
187     },
188
189     configureFullScreenControls: function() {
190         // Explicitly do nothing to override base-class behavior.
191     },
192
193     addControls: function() {
194         this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
195         this.base.appendChild(this.controls.panelContainer);
196         this.controls.panelContainer.appendChild(this.controls.panelBackground);
197         this.controls.panelContainer.appendChild(this.controls.panel);
198         this.setNeedsTimelineMetricsUpdate();
199     },
200
201     updateControls: function() {
202         if (this.shouldHaveStartPlaybackButton())
203             this.setControlsType(ControllerIOS.StartPlaybackControls);
204         else if (this.presentationMode() === "fullscreen")
205             this.setControlsType(Controller.FullScreenControls);
206         else
207             this.setControlsType(Controller.InlineControls);
208
209         this.updateLayoutForDisplayedWidth();
210         this.setNeedsTimelineMetricsUpdate();
211     },
212
213     drawTimelineBackground: function() {
214         var width = this.timelineWidth * window.devicePixelRatio;
215         var height = this.timelineHeight * window.devicePixelRatio;
216
217         if (!width || !height)
218             return;
219
220         var played = this.video.currentTime / this.video.duration;
221         var buffered = 0;
222         var bufferedRanges = this.video.buffered;
223         if (bufferedRanges && bufferedRanges.length)
224             buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered);
225
226         buffered /= this.video.duration;
227         buffered = Math.max(buffered, played);
228
229         var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
230
231         ctx.clearRect(0, 0, width, height);
232
233         var midY = height / 2;
234
235         // 1. Draw the buffered part and played parts, using
236         // solid rectangles that are clipped to the outside of
237         // the lozenge.
238         ctx.save();
239         ctx.beginPath();
240         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
241         ctx.closePath();
242         ctx.clip();
243         ctx.fillStyle = "white";
244         ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
245         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
246         ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height);
247         ctx.restore();
248
249         // 2. Draw the outline with a clip path that subtracts the
250         // middle of a lozenge. This produces a better result than
251         // stroking.
252         ctx.save();
253         ctx.beginPath();
254         this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
255         this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
256         ctx.closePath();
257         ctx.clip("evenodd");
258         ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
259         ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
260         ctx.restore();
261     },
262
263     formatTime: function(time) {
264         if (isNaN(time))
265             time = 0;
266         var absTime = Math.abs(time);
267         var intSeconds = Math.floor(absTime % 60).toFixed(0);
268         var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
269         var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
270         var sign = time < 0 ? '-' : String();
271
272         if (intHours > 0)
273             return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
274
275         return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
276     },
277
278     handlePlayButtonTouchStart: function() {
279         this.controls.playButton.classList.add('active');
280     },
281
282     handlePlayButtonTouchEnd: function(event) {
283         this.controls.playButton.classList.remove('active');
284
285         if (this.canPlay())
286             this.video.play();
287         else
288             this.video.pause();
289
290         return true;
291     },
292
293     handlePlayButtonTouchCancel: function(event) {
294         this.controls.playButton.classList.remove('active');
295         return true;
296     },
297
298     handleBaseGestureStart: function(event) {
299         this.gestureStartTime = new Date();
300         // If this gesture started with two fingers inside the video, then
301         // don't treat it as a potential zoom, unless we're still waiting
302         // to play.
303         if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
304             event.preventDefault();
305     },
306
307     handleBaseGestureChange: function(event) {
308         if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
309             return;
310
311         var scaleDetectionThreshold = 0.2;
312         if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
313             delete this.lastDoubleTouchTime;
314
315         if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
316             event.preventDefault();
317
318         var currentGestureTime = new Date();
319         var duration = (currentGestureTime - this.gestureStartTime) / 1000;
320         if (!duration)
321             return;
322
323         var velocity = Math.abs(event.scale - 1) / duration;
324
325         var pinchOutVelocityThreshold = 2;
326         var pinchOutGestureScaleThreshold = 1.25;
327         if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
328             return;
329
330         delete this.gestureStartTime;
331         this.video.webkitEnterFullscreen();
332     },
333
334     handleBaseGestureEnd: function(event) {
335         delete this.gestureStartTime;
336     },
337
338     handleWrapperTouchStart: function(event) {
339         if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
340             return;
341
342         this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
343
344         if (this.controlsAreHidden()) {
345             this.showControls();
346             if (this.hideTimer)
347                 clearTimeout(this.hideTimer);
348             this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
349         } else if (!this.canPlay())
350             this.hideControls();
351
352         return true;
353     },
354
355     handlePanelTouchStart: function(event) {
356         this.video.style.webkitUserSelect = 'none';
357     },
358
359     handlePanelTouchEnd: function(event) {
360         this.video.style.removeProperty('-webkit-user-select');
361     },
362
363     handlePanelTouchCancel: function(event) {
364         this.video.style.removeProperty('-webkit-user-select');
365     },
366
367     handleVisibilityChange: function(event) {
368         this.updateShouldListenForPlaybackTargetAvailabilityEvent();
369     },
370
371     presentationMode: function() {
372         if ('webkitPresentationMode' in this.video)
373             return this.video.webkitPresentationMode;
374
375         if (this.isFullScreen())
376             return 'fullscreen';
377
378         return 'inline';
379     },
380
381     isFullScreen: function()
382     {
383         return this.video.webkitDisplayingFullscreen && this.presentationMode() != 'optimized';
384     },
385
386     handleFullscreenButtonClicked: function(event) {
387         if ('webkitSetPresentationMode' in this.video) {
388             if (this.presentationMode() === 'fullscreen')
389                 this.video.webkitSetPresentationMode('inline');
390             else
391                 this.video.webkitSetPresentationMode('fullscreen');
392
393             return;
394         }
395
396         if (this.isFullScreen())
397             this.video.webkitExitFullscreen();
398         else
399             this.video.webkitEnterFullscreen();
400     },
401
402     handleFullscreenTouchStart: function() {
403         this.controls.fullscreenButton.classList.add('active');
404     },
405
406     handleFullscreenTouchEnd: function(event) {
407         this.controls.fullscreenButton.classList.remove('active');
408
409         this.handleFullscreenButtonClicked();
410
411         return true;
412     },
413
414     handleFullscreenTouchCancel: function(event) {
415         this.controls.fullscreenButton.classList.remove('active');
416         return true;
417     },
418
419     handleOptimizedFullscreenButtonClicked: function(event) {
420         if (!('webkitSetPresentationMode' in this.video))
421             return;
422
423         if (this.presentationMode() === 'optimized')
424             this.video.webkitSetPresentationMode('inline');
425         else
426             this.video.webkitSetPresentationMode('optimized');
427     },
428
429     handleOptimizedFullscreenTouchStart: function() {
430         this.controls.optimizedFullscreenButton.classList.add('active');
431     },
432
433     handleOptimizedFullscreenTouchEnd: function(event) {
434         this.controls.optimizedFullscreenButton.classList.remove('active');
435
436         this.handleOptimizedFullscreenButtonClicked();
437
438         return true;
439     },
440
441     handleOptimizedFullscreenTouchCancel: function(event) {
442         this.controls.optimizedFullscreenButton.classList.remove('active');
443         return true;
444     },
445
446     handleStartPlaybackButtonTouchStart: function(event) {
447         this.controls.startPlaybackButton.classList.add('active');
448     },
449
450     handleStartPlaybackButtonTouchEnd: function(event) {
451         this.controls.startPlaybackButton.classList.remove('active');
452         if (this.video.error)
453             return true;
454
455         this.video.play();
456         this.updateControls();
457
458         return true;
459     },
460
461     handleStartPlaybackButtonTouchCancel: function(event) {
462         this.controls.startPlaybackButton.classList.remove('active');
463         return true;
464     },
465
466     handleTimelineTouchStart: function(event) {
467         this.scrubbing = true;
468         this.listenFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
469         this.listenFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
470     },
471
472     handleTimelineTouchEnd: function(event) {
473         this.stopListeningFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
474         this.stopListeningFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
475         this.scrubbing = false;
476     },
477
478     handleWirelessPickerButtonTouchStart: function() {
479         if (!this.video.error)
480             this.controls.wirelessTargetPicker.classList.add('active');
481     },
482
483     handleWirelessPickerButtonTouchEnd: function(event) {
484         this.controls.wirelessTargetPicker.classList.remove('active');
485         return this.handleWirelessPickerButtonClicked();
486     },
487
488     handleWirelessPickerButtonTouchCancel: function(event) {
489         this.controls.wirelessTargetPicker.classList.remove('active');
490         return true;
491     },
492
493     updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
494         if (this.controlsType === ControllerIOS.StartPlaybackControls) {
495             this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
496             return;
497         }
498
499         Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
500     },
501
502     updateWirelessTargetPickerButton: function() {
503     },
504
505     updateStatusDisplay: function(event)
506     {
507         this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
508         Controller.prototype.updateStatusDisplay.call(this, event);
509     },
510
511     setPlaying: function(isPlaying)
512     {
513         Controller.prototype.setPlaying.call(this, isPlaying);
514
515         this.updateControls();
516
517         if (isPlaying && this.isAudio())
518             this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
519
520         if (isPlaying)
521             this.hasPlayed = true;
522         else
523             this.showControls();
524     },
525
526     setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
527     {
528         if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
529             return;
530
531         Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen);
532     },
533
534     handlePresentationModeChange: function(event)
535     {
536         var presentationMode = this.presentationMode();
537
538         switch (presentationMode) {
539             case 'inline':
540                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = "";
541                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
542                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.optimized);
543                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.optimized);
544                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.optimized);
545
546                 this.controls.optimizedFullscreenButton.classList.remove(this.ClassNames.returnFromOptimized);
547                 break;
548             case 'optimized':
549                 var backgroundImage = "url('" + this.host.mediaUIImageData("optimized-fullscreen-placeholder") + "')";
550                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = backgroundImage;
551                 this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', "video playback placeholder");
552                 this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.optimized);
553                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
554
555                 this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.host.mediaUIImageData("optimized-fullscreen-placeholder-text");
556                 this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.optimized);
557                 this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
558                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.optimized);
559
560                 this.controls.optimizedFullscreenButton.classList.add(this.ClassNames.returnFromOptimized);
561                 break;
562             default:
563                 this.controls.inlinePlaybackPlaceholder.style.backgroundImage = "";
564                 this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.optimized);
565                 this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.optimized);
566                 this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.optimized);
567
568                 this.controls.optimizedFullscreenButton.classList.remove(this.ClassNames.returnFromOptimized);
569                 break;
570         }
571
572         this.updateControls();
573         this.updateCaptionContainer();
574         if (presentationMode != 'fullscreen' && this.video.paused && this.controlsAreHidden())
575             this.showControls();
576     },
577
578     handleFullscreenChange: function(event)
579     {
580         Controller.prototype.handleFullscreenChange.call(this, event);
581         this.handlePresentationModeChange(event);
582     },
583
584     controlsAlwaysVisible: function()
585     {
586         if (this.presentationMode() === 'optimized')
587             return true;
588
589         return Controller.prototype.controlsAlwaysVisible.call(this);
590     },
591
592
593 };
594
595 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
596 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });