1 function createControls(root, video, host)
3 return new ControllerIOS(root, video, host);
6 function ControllerIOS(root, video, host)
8 this.doingSetup = true;
9 this._pageScaleFactor = 1;
11 this.timelineContextName = "_webkit-media-controls-timeline-" + host.generateUUID();
13 Controller.call(this, root, video, host);
15 this.setNeedsTimelineMetricsUpdate();
17 this._timelineIsHidden = false;
18 this._currentDisplayWidth = 0;
19 this.scheduleUpdateLayoutForDisplayedWidth();
21 host.controlsDependOnPageScaleFactor = true;
22 this.doingSetup = false;
26 ControllerIOS.StartPlaybackControls = 2;
28 ControllerIOS.prototype = {
30 MinimumTimelineWidth: 150,
38 createBase: function() {
39 Controller.prototype.createBase.call(this);
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');
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);
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);
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);
63 this.listenFor(document, 'visibilitychange', this.handleVisibilityChange);
66 shouldHaveStartPlaybackButton: function() {
67 var allowsInline = this.host.allowsInlineMediaPlayback;
69 if (this.isPlaying || (this.hasPlayed && allowsInline))
72 if (this.isAudio() && allowsInline)
78 if (this.isFullScreen())
81 if (!this.video.currentSrc && this.video.error)
84 if (!this.video.controls && allowsInline)
87 if (this.video.currentSrc && this.video.error)
93 shouldHaveControls: function() {
94 if (this.shouldHaveStartPlaybackButton())
97 return Controller.prototype.shouldHaveControls.call(this);
100 shouldHaveAnyUI: function() {
101 return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
104 createControls: function() {
105 Controller.prototype.createControls.call(this);
107 var panelContainer = this.controls.panelContainer = document.createElement('div');
108 panelContainer.setAttribute('pseudo', '-webkit-media-controls-panel-container');
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);
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);
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);
134 this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
137 setControlsType: function(type) {
138 if (type === this.controlsType)
140 Controller.prototype.setControlsType.call(this, type);
142 if (type === ControllerIOS.StartPlaybackControls)
143 this.addStartPlaybackControls();
145 this.removeStartPlaybackControls();
148 addStartPlaybackControls: function() {
149 this.base.appendChild(this.controls.startPlaybackButton);
150 this.showShowControlsButton(false);
153 removeStartPlaybackControls: function() {
154 if (this.controls.startPlaybackButton.parentNode)
155 this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
158 reconnectControls: function()
160 Controller.prototype.reconnectControls.call(this);
162 if (this.controlsType === ControllerIOS.StartPlaybackControls)
163 this.addStartPlaybackControls();
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);
175 this.controls.timelineBox.appendChild(this.controls.currentTime);
176 this.controls.timelineBox.appendChild(this.controls.timeline);
177 this.controls.timelineBox.appendChild(this.controls.remainingTime);
179 if (this.isAudio()) {
180 // Hide the scrubber on audio until the user starts playing.
181 this.controls.timelineBox.classList.add(this.ClassNames.hidden);
183 this.updatePictureInPictureButton();
184 this.controls.panel.appendChild(this.controls.fullscreenButton);
188 configureFullScreenControls: function() {
189 // Explicitly do nothing to override base-class behavior.
192 controlsAreHidden: function()
194 // Controls are only ever actually hidden when they are removed from the tree
195 return !this.controls.panelContainer.parentElement;
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();
206 updateControls: function() {
207 if (this.shouldHaveStartPlaybackButton())
208 this.setControlsType(ControllerIOS.StartPlaybackControls);
209 else if (this.presentationMode() === "fullscreen")
210 this.setControlsType(Controller.FullScreenControls);
212 this.setControlsType(Controller.InlineControls);
214 this.updateLayoutForDisplayedWidth();
215 this.setNeedsTimelineMetricsUpdate();
218 drawTimelineBackground: function() {
219 var width = this.timelineWidth * window.devicePixelRatio;
220 var height = this.timelineHeight * window.devicePixelRatio;
222 if (!width || !height)
225 var played = this.video.currentTime / this.video.duration;
227 var bufferedRanges = this.video.buffered;
228 if (bufferedRanges && bufferedRanges.length)
229 buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered);
231 buffered /= this.video.duration;
232 buffered = Math.max(buffered, played);
234 var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
236 ctx.clearRect(0, 0, width, height);
238 var midY = height / 2;
240 // 1. Draw the buffered part and played parts, using
241 // solid rectangles that are clipped to the outside of
245 this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
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);
254 // 2. Draw the outline with a clip path that subtracts the
255 // middle of a lozenge. This produces a better result than
259 this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
260 this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
263 ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
264 ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
268 formatTime: function(time) {
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();
278 return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);
280 return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
283 handlePlayButtonTouchStart: function() {
284 this.controls.playButton.classList.add('active');
287 handlePlayButtonTouchEnd: function(event) {
288 this.controls.playButton.classList.remove('active');
290 if (this.canPlay()) {
299 handlePlayButtonTouchCancel: function(event) {
300 this.controls.playButton.classList.remove('active');
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
309 if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
310 event.preventDefault();
313 handleBaseGestureChange: function(event) {
314 if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
317 var scaleDetectionThreshold = 0.2;
318 if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
319 delete this.lastDoubleTouchTime;
321 if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
322 event.preventDefault();
324 var currentGestureTime = new Date();
325 var duration = (currentGestureTime - this.gestureStartTime) / 1000;
329 var velocity = Math.abs(event.scale - 1) / duration;
331 var pinchOutVelocityThreshold = 2;
332 var pinchOutGestureScaleThreshold = 1.25;
333 if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
336 delete this.gestureStartTime;
337 this.video.webkitEnterFullscreen();
340 handleBaseGestureEnd: function(event) {
341 delete this.gestureStartTime;
344 handleWrapperTouchStart: function(event) {
345 if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder)
348 this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;
350 if (this.controlsAreHidden() || !this.controls.panel.classList.contains(this.ClassNames.show)) {
352 this.resetHideControlsTimer();
353 } else if (!this.canPlay())
357 handlePanelTouchStart: function(event) {
358 this.video.style.webkitUserSelect = 'none';
361 handlePanelTouchEnd: function(event) {
362 this.video.style.removeProperty('-webkit-user-select');
365 handlePanelTouchCancel: function(event) {
366 this.video.style.removeProperty('-webkit-user-select');
369 handleVisibilityChange: function(event) {
370 this.updateShouldListenForPlaybackTargetAvailabilityEvent();
373 handlePanelTransitionEnd: function(event)
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);
382 handleFullscreenButtonClicked: function(event) {
383 if ('webkitSetPresentationMode' in this.video) {
384 if (this.presentationMode() === 'fullscreen')
385 this.video.webkitSetPresentationMode('inline');
387 this.video.webkitSetPresentationMode('fullscreen');
392 if (this.isFullScreen())
393 this.video.webkitExitFullscreen();
395 this.video.webkitEnterFullscreen();
398 handleFullscreenTouchStart: function() {
399 this.controls.fullscreenButton.classList.add('active');
402 handleFullscreenTouchEnd: function(event) {
403 this.controls.fullscreenButton.classList.remove('active');
405 this.handleFullscreenButtonClicked();
410 handleFullscreenTouchCancel: function(event) {
411 this.controls.fullscreenButton.classList.remove('active');
415 handlePictureInPictureTouchStart: function() {
416 this.controls.pictureInPictureButton.classList.add('active');
419 handlePictureInPictureTouchEnd: function(event) {
420 this.controls.pictureInPictureButton.classList.remove('active');
422 this.handlePictureInPictureButtonClicked();
427 handlePictureInPictureTouchCancel: function(event) {
428 this.controls.pictureInPictureButton.classList.remove('active');
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');
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');
441 if (this.video.error)
445 this.canToggleShowControlsButton = true;
446 this.updateControls();
451 handleStartPlaybackButtonTouchCancel: function(event) {
452 this.controls.startPlaybackButton.classList.remove('active');
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);
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;
468 handleWirelessPickerButtonTouchStart: function() {
469 if (!this.video.error)
470 this.controls.wirelessTargetPicker.classList.add('active');
473 handleWirelessPickerButtonTouchEnd: function(event) {
474 this.controls.wirelessTargetPicker.classList.remove('active');
475 return this.handleWirelessPickerButtonClicked();
478 handleWirelessPickerButtonTouchCancel: function(event) {
479 this.controls.wirelessTargetPicker.classList.remove('active');
483 updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
484 if (this.controlsType === ControllerIOS.StartPlaybackControls) {
485 this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
489 Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
492 updateWirelessTargetPickerButton: function() {
495 updateStatusDisplay: function(event)
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);
502 setPlaying: function(isPlaying)
504 Controller.prototype.setPlaying.call(this, isPlaying);
506 this.updateControls();
508 if (isPlaying && this.isAudio())
509 this.controls.timelineBox.classList.remove(this.ClassNames.hidden);
512 this.hasPlayed = true;
517 showControls: function()
519 this.updateShouldListenForPlaybackTargetAvailabilityEvent();
520 if (!this.video.controls)
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);
531 setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
533 if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
536 Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen);
539 shouldReturnVideoLayerToInline: function()
541 return this.presentationMode() === 'inline';
544 handlePresentationModeChange: function(event)
546 var presentationMode = this.presentationMode();
548 switch (presentationMode) {
550 this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture);
552 case 'picture-in-picture':
553 this.controls.panelContainer.classList.add(this.ClassNames.pictureInPicture);
556 this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture);
560 Controller.prototype.handlePresentationModeChange.call(this, event);
563 // Due to the bad way we are faking inheritance here, in particular the extends method
564 // on Controller.prototype, we don't copy getters and setters from the prototype. This
565 // means we have to implement them again, here in the subclass.
566 // FIXME: Use ES6 classes!
570 return Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").get.call(this);
575 Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").set.call(this, flag);
578 get pageScaleFactor()
580 return this._pageScaleFactor;
583 set pageScaleFactor(newScaleFactor)
585 if (!newScaleFactor || this._pageScaleFactor === newScaleFactor)
588 this._pageScaleFactor = newScaleFactor;
590 var scaleValue = 1 / newScaleFactor;
591 var scaleTransform = "scale(" + scaleValue + ")";
593 function applyScaleFactorToElement(element) {
594 if (scaleValue > 1) {
595 element.style.zoom = scaleValue;
596 element.style.webkitTransform = "scale(1)";
598 element.style.zoom = 1;
599 element.style.webkitTransform = scaleTransform;
603 if (this.controls.startPlaybackButton)
604 applyScaleFactorToElement(this.controls.startPlaybackButton);
605 if (this.controls.panel) {
606 applyScaleFactorToElement(this.controls.panel);
607 if (scaleValue > 1) {
608 this.controls.panel.style.width = "100%";
609 this.controls.timelineBox.style.webkitTextSizeAdjust = (100 * scaleValue) + "%";
611 var bottomAligment = -2 * scaleValue;
612 this.controls.panel.style.bottom = bottomAligment + "px";
613 this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px";
614 this.controls.panel.style.width = Math.round(newScaleFactor * 100) + "%";
615 this.controls.timelineBox.style.webkitTextSizeAdjust = "auto";
617 this.controls.panelBackground.style.height = (50 * scaleValue) + "px";
619 this.setNeedsTimelineMetricsUpdate();
620 this.updateProgress();
621 this.scheduleUpdateLayoutForDisplayedWidth();
627 Object.create(Controller.prototype).extend(ControllerIOS.prototype);
628 Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });