function createControls(root, video, host) { return new ControllerIOS(root, video, host); }; function ControllerIOS(root, video, host) { this.doingSetup = true; this._pageScaleFactor = 1; this.timelineContextName = "_webkit-media-controls-timeline-" + host.generateUUID(); Controller.call(this, root, video, host); this.setNeedsTimelineMetricsUpdate(); this._timelineIsHidden = false; this._currentDisplayWidth = 0; this.scheduleUpdateLayoutForDisplayedWidth(); host.controlsDependOnPageScaleFactor = true; this.doingSetup = false; }; /* Enums */ ControllerIOS.StartPlaybackControls = 2; ControllerIOS.prototype = { /* Constants */ MinimumTimelineWidth: 150, ButtonWidth: 42, get idiom() { return "ios"; }, createBase: function() { Controller.prototype.createBase.call(this); var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('div'); startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button'); startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback')); startPlaybackButton.setAttribute('role', 'button'); var startPlaybackBackground = document.createElement('div'); startPlaybackBackground.setAttribute('pseudo', '-webkit-media-controls-start-playback-background'); startPlaybackBackground.classList.add('webkit-media-controls-start-playback-background'); startPlaybackButton.appendChild(startPlaybackBackground); var startPlaybackGlyph = document.createElement('div'); startPlaybackGlyph.setAttribute('pseudo', '-webkit-media-controls-start-playback-glyph'); startPlaybackGlyph.classList.add('webkit-media-controls-start-playback-glyph'); startPlaybackButton.appendChild(startPlaybackGlyph); this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart); this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange); this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd); this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart); this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove); this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut); this.listenFor(document, 'visibilitychange', this.handleVisibilityChange); }, shouldHaveStartPlaybackButton: function() { var allowsInline = this.host.allowsInlineMediaPlayback; if (this.isPlaying || (this.hasPlayed && allowsInline)) return false; if (this.isAudio() && allowsInline) return false; if (this.doingSetup) return true; if (this.isFullScreen()) return false; if (!this.video.currentSrc && this.video.error) return false; if (!this.video.controls && allowsInline) return false; if (this.video.currentSrc && this.video.error) return true; return true; }, shouldHaveControls: function() { if (this.shouldHaveStartPlaybackButton()) return false; return Controller.prototype.shouldHaveControls.call(this); }, shouldHaveAnyUI: function() { return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless(); }, createControls: function() { Controller.prototype.createControls.call(this); var panelContainer = this.controls.panelContainer = document.createElement('div'); panelContainer.setAttribute('pseudo', '-webkit-media-controls-panel-container'); var wirelessTargetPicker = this.controls.wirelessTargetPicker; this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart); this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd); this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel); this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart); this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd); this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel); this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart); this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd); this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel); this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart); this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd); this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel); this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart); this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd); this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel); this.listenFor(this.controls.pictureInPictureButton, 'touchstart', this.handlePictureInPictureTouchStart); this.listenFor(this.controls.pictureInPictureButton, 'touchend', this.handlePictureInPictureTouchEnd); this.listenFor(this.controls.pictureInPictureButton, 'touchcancel', this.handlePictureInPictureTouchCancel); this.listenFor(this.controls.timeline, 'touchstart', this.handleTimelineTouchStart); this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked); this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')'; }, setControlsType: function(type) { if (type === this.controlsType) return; Controller.prototype.setControlsType.call(this, type); if (type === ControllerIOS.StartPlaybackControls) this.addStartPlaybackControls(); else this.removeStartPlaybackControls(); }, addStartPlaybackControls: function() { this.base.appendChild(this.controls.startPlaybackButton); this.showShowControlsButton(false); }, removeStartPlaybackControls: function() { if (this.controls.startPlaybackButton.parentNode) this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton); }, reconnectControls: function() { Controller.prototype.reconnectControls.call(this); if (this.controlsType === ControllerIOS.StartPlaybackControls) this.addStartPlaybackControls(); }, configureInlineControls: function() { this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText); this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop); this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom); this.controls.panel.appendChild(this.controls.playButton); this.controls.panel.appendChild(this.controls.statusDisplay); this.controls.panel.appendChild(this.controls.timelineBox); this.controls.panel.appendChild(this.controls.wirelessTargetPicker); if (!this.isLive) { this.controls.timelineBox.appendChild(this.controls.currentTime); this.controls.timelineBox.appendChild(this.controls.timeline); this.controls.timelineBox.appendChild(this.controls.remainingTime); } if (this.isAudio()) { // Hide the scrubber on audio until the user starts playing. this.controls.timelineBox.classList.add(this.ClassNames.hidden); } else { this.updatePictureInPictureButton(); this.controls.panel.appendChild(this.controls.fullscreenButton); } }, configureFullScreenControls: function() { // Explicitly do nothing to override base-class behavior. }, controlsAreHidden: function() { // Controls are only ever actually hidden when they are removed from the tree return !this.controls.panelContainer.parentElement; }, addControls: function() { this.base.appendChild(this.controls.inlinePlaybackPlaceholder); this.base.appendChild(this.controls.panelContainer); this.controls.panelContainer.appendChild(this.controls.panelBackground); this.controls.panelContainer.appendChild(this.controls.panel); this.setNeedsTimelineMetricsUpdate(); }, updateControls: function() { if (this.shouldHaveStartPlaybackButton()) this.setControlsType(ControllerIOS.StartPlaybackControls); else if (this.presentationMode() === "fullscreen") this.setControlsType(Controller.FullScreenControls); else this.setControlsType(Controller.InlineControls); this.updateLayoutForDisplayedWidth(); this.setNeedsTimelineMetricsUpdate(); }, drawTimelineBackground: function() { var width = this.timelineWidth * window.devicePixelRatio; var height = this.timelineHeight * window.devicePixelRatio; if (!width || !height) return; var played = this.video.currentTime / this.video.duration; var buffered = 0; var bufferedRanges = this.video.buffered; if (bufferedRanges && bufferedRanges.length) buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered); buffered /= this.video.duration; buffered = Math.max(buffered, played); var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height); ctx.clearRect(0, 0, width, height); var midY = height / 2; // 1. Draw the buffered part and played parts, using // solid rectangles that are clipped to the outside of // the lozenge. ctx.save(); ctx.beginPath(); this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3); ctx.closePath(); ctx.clip(); ctx.fillStyle = "white"; ctx.fillRect(0, 0, Math.round(width * played) + 2, height); ctx.fillStyle = "rgba(0, 0, 0, 0.55)"; ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height); ctx.restore(); // 2. Draw the outline with a clip path that subtracts the // middle of a lozenge. This produces a better result than // stroking. ctx.save(); ctx.beginPath(); this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3); this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2); ctx.closePath(); ctx.clip("evenodd"); ctx.fillStyle = "rgba(0, 0, 0, 0.55)"; ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height); ctx.restore(); }, formatTime: function(time) { if (isNaN(time)) time = 0; var absTime = Math.abs(time); var intSeconds = Math.floor(absTime % 60).toFixed(0); var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); var sign = time < 0 ? '-' : String(); if (intHours > 0) return sign + intHours + ':' + String('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2); return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2); }, handlePlayButtonTouchStart: function() { this.controls.playButton.classList.add('active'); }, handlePlayButtonTouchEnd: function(event) { this.controls.playButton.classList.remove('active'); if (this.canPlay()) { this.video.play(); this.showControls(); } else this.video.pause(); return true; }, handlePlayButtonTouchCancel: function(event) { this.controls.playButton.classList.remove('active'); return true; }, handleBaseGestureStart: function(event) { this.gestureStartTime = new Date(); // If this gesture started with two fingers inside the video, then // don't treat it as a potential zoom, unless we're still waiting // to play. if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls) event.preventDefault(); }, handleBaseGestureChange: function(event) { if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls) return; var scaleDetectionThreshold = 0.2; if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold) delete this.lastDoubleTouchTime; if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0) event.preventDefault(); var currentGestureTime = new Date(); var duration = (currentGestureTime - this.gestureStartTime) / 1000; if (!duration) return; var velocity = Math.abs(event.scale - 1) / duration; var pinchOutVelocityThreshold = 2; var pinchOutGestureScaleThreshold = 1.25; if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold) return; delete this.gestureStartTime; this.video.webkitEnterFullscreen(); }, handleBaseGestureEnd: function(event) { delete this.gestureStartTime; }, handleWrapperTouchStart: function(event) { if (event.target != this.base && event.target != this.controls.inlinePlaybackPlaceholder) return; this.mostRecentNumberOfTargettedTouches = event.targetTouches.length; if (this.controlsAreHidden() || !this.controls.panel.classList.contains(this.ClassNames.show)) { this.showControls(); this.resetHideControlsTimer(); } else if (!this.canPlay()) this.hideControls(); }, handlePanelTouchStart: function(event) { this.video.style.webkitUserSelect = 'none'; }, handlePanelTouchEnd: function(event) { this.video.style.removeProperty('-webkit-user-select'); }, handlePanelTouchCancel: function(event) { this.video.style.removeProperty('-webkit-user-select'); }, handleVisibilityChange: function(event) { this.updateShouldListenForPlaybackTargetAvailabilityEvent(); }, handlePanelTransitionEnd: function(event) { var opacity = window.getComputedStyle(this.controls.panel).opacity; if (!parseInt(opacity) && !this.controlsAlwaysVisible()) { this.base.removeChild(this.controls.inlinePlaybackPlaceholder); this.base.removeChild(this.controls.panelContainer); } }, handleFullscreenButtonClicked: function(event) { if ('webkitSetPresentationMode' in this.video) { if (this.presentationMode() === 'fullscreen') this.video.webkitSetPresentationMode('inline'); else this.video.webkitSetPresentationMode('fullscreen'); return; } if (this.isFullScreen()) this.video.webkitExitFullscreen(); else this.video.webkitEnterFullscreen(); }, handleFullscreenTouchStart: function() { this.controls.fullscreenButton.classList.add('active'); }, handleFullscreenTouchEnd: function(event) { this.controls.fullscreenButton.classList.remove('active'); this.handleFullscreenButtonClicked(); return true; }, handleFullscreenTouchCancel: function(event) { this.controls.fullscreenButton.classList.remove('active'); return true; }, handlePictureInPictureTouchStart: function() { this.controls.pictureInPictureButton.classList.add('active'); }, handlePictureInPictureTouchEnd: function(event) { this.controls.pictureInPictureButton.classList.remove('active'); this.handlePictureInPictureButtonClicked(); return true; }, handlePictureInPictureTouchCancel: function(event) { this.controls.pictureInPictureButton.classList.remove('active'); return true; }, handleStartPlaybackButtonTouchStart: function(event) { this.controls.startPlaybackButton.classList.add('active'); this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-glyph').classList.add('active'); }, handleStartPlaybackButtonTouchEnd: function(event) { this.controls.startPlaybackButton.classList.remove('active'); this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-glyph').classList.remove('active'); if (this.video.error) return true; this.video.play(); this.canToggleShowControlsButton = true; this.updateControls(); return true; }, handleStartPlaybackButtonTouchCancel: function(event) { this.controls.startPlaybackButton.classList.remove('active'); return true; }, handleTimelineTouchStart: function(event) { this.scrubbing = true; this.listenFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd); this.listenFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd); }, handleTimelineTouchEnd: function(event) { this.stopListeningFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd); this.stopListeningFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd); this.scrubbing = false; }, handleWirelessPickerButtonTouchStart: function() { if (!this.video.error) this.controls.wirelessTargetPicker.classList.add('active'); }, handleWirelessPickerButtonTouchEnd: function(event) { this.controls.wirelessTargetPicker.classList.remove('active'); return this.handleWirelessPickerButtonClicked(); }, handleWirelessPickerButtonTouchCancel: function(event) { this.controls.wirelessTargetPicker.classList.remove('active'); return true; }, updateShouldListenForPlaybackTargetAvailabilityEvent: function() { if (this.controlsType === ControllerIOS.StartPlaybackControls) { this.setShouldListenForPlaybackTargetAvailabilityEvent(false); return; } Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this); }, updateWirelessTargetPickerButton: function() { }, updateStatusDisplay: function(event) { this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null); this.controls.startPlaybackButton.querySelector(".webkit-media-controls-start-playback-glyph").classList.toggle(this.ClassNames.failed, this.video.error !== null); Controller.prototype.updateStatusDisplay.call(this, event); }, setPlaying: function(isPlaying) { Controller.prototype.setPlaying.call(this, isPlaying); this.updateControls(); if (isPlaying && this.isAudio()) this.controls.timelineBox.classList.remove(this.ClassNames.hidden); if (isPlaying) this.hasPlayed = true; else this.showControls(); }, showControls: function() { this.updateShouldListenForPlaybackTargetAvailabilityEvent(); if (!this.video.controls) return; this.updateForShowingControls(); if (this.shouldHaveControls() && !this.controls.panelContainer.parentElement) { this.base.appendChild(this.controls.inlinePlaybackPlaceholder); this.base.appendChild(this.controls.panelContainer); this.showShowControlsButton(false); } }, setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) { if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error)) return; Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen); }, shouldReturnVideoLayerToInline: function() { return this.presentationMode() === 'inline'; }, updatePictureInPicturePlaceholder: function(event) { var presentationMode = this.presentationMode(); switch (presentationMode) { case 'inline': this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture); break; case 'picture-in-picture': this.controls.panelContainer.classList.add(this.ClassNames.pictureInPicture); break; default: this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture); break; } Controller.prototype.updatePictureInPicturePlaceholder.call(this, event); }, // Due to the bad way we are faking inheritance here, in particular the extends method // on Controller.prototype, we don't copy getters and setters from the prototype. This // means we have to implement them again, here in the subclass. // FIXME: Use ES6 classes! get scrubbing() { return Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").get.call(this); }, set scrubbing(flag) { Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").set.call(this, flag); }, get pageScaleFactor() { return this._pageScaleFactor; }, set pageScaleFactor(newScaleFactor) { if (!newScaleFactor || this._pageScaleFactor === newScaleFactor) return; this._pageScaleFactor = newScaleFactor; var scaleValue = 1 / newScaleFactor; var scaleTransform = "scale(" + scaleValue + ")"; function applyScaleFactorToElement(element) { if (scaleValue > 1) { element.style.zoom = scaleValue; element.style.webkitTransform = "scale(1)"; } else { element.style.zoom = 1; element.style.webkitTransform = scaleTransform; } } if (this.controls.startPlaybackButton) applyScaleFactorToElement(this.controls.startPlaybackButton); if (this.controls.panel) { applyScaleFactorToElement(this.controls.panel); if (scaleValue > 1) { this.controls.panel.style.width = "100%"; this.controls.timelineBox.style.webkitTextSizeAdjust = (100 * scaleValue) + "%"; } else { var bottomAligment = -2 * scaleValue; this.controls.panel.style.bottom = bottomAligment + "px"; this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px"; this.controls.panel.style.width = Math.round(newScaleFactor * 100) + "%"; this.controls.timelineBox.style.webkitTextSizeAdjust = "auto"; } this.controls.panelBackground.style.height = (50 * scaleValue) + "px"; this.setNeedsTimelineMetricsUpdate(); this.updateProgress(); this.scheduleUpdateLayoutForDisplayedWidth(); } }, }; Object.create(Controller.prototype).extend(ControllerIOS.prototype); Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });