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