diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js')
-rw-r--r-- | Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js b/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js new file mode 100644 index 000000000..9d6d28abb --- /dev/null +++ b/Source/WebCore/Modules/mediacontrols/mediaControlsiOS.js @@ -0,0 +1,628 @@ +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 }); |