From 2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 12 Jan 2022 15:07:21 +0100 Subject: [PATCH] Add fast forward/rewind on mobile --- .../player/mobile/peertube-mobile-buttons.ts | 72 +++++++++-- .../player/mobile/peertube-mobile-plugin.ts | 115 +++++++++++++++++- .../assets/player/peertube-player-manager.ts | 1 + client/src/sass/player/mobile.scss | 113 +++++++++++++++-- scripts/i18n/create-custom-files.ts | 2 + 5 files changed, 279 insertions(+), 24 deletions(-) diff --git a/client/src/assets/player/mobile/peertube-mobile-buttons.ts b/client/src/assets/player/mobile/peertube-mobile-buttons.ts index d6f8f35e3..94eeec023 100644 --- a/client/src/assets/player/mobile/peertube-mobile-buttons.ts +++ b/client/src/assets/player/mobile/peertube-mobile-buttons.ts @@ -1,23 +1,18 @@ import videojs from 'video.js' -import debug from 'debug' - -const logger = debug('peertube:player:mobile') - const Component = videojs.getComponent('Component') class PeerTubeMobileButtons extends Component { + private rewind: Element + private forward: Element + private rewindText: Element + private forwardText: Element + createEl () { const container = super.createEl('div', { className: 'vjs-mobile-buttons-overlay' }) as HTMLDivElement - container.addEventListener('click', () => { - logger('Set user as inactive') - - this.player_.userActive(false) - }) - const mainButton = super.createEl('div', { className: 'main-button' }) as HTMLDivElement @@ -33,10 +28,67 @@ class PeerTubeMobileButtons extends Component { this.player_.pause() }) + this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' }) + this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' }) + + for (let i = 0; i < 3; i++) { + this.rewind.appendChild(super.createEl('span', { className: 'icon' })) + this.forward.appendChild(super.createEl('span', { className: 'icon' })) + } + + this.rewindText = this.rewind.appendChild(super.createEl('div', { className: 'text' })) + this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' })) + + container.appendChild(this.rewind) container.appendChild(mainButton) + container.appendChild(this.forward) return container } + + displayFastSeek (amount: number) { + if (amount === 0) { + this.hideRewind() + this.hideForward() + return + } + + if (amount > 0) { + this.hideRewind() + this.displayForward(amount) + return + } + + if (amount < 0) { + this.hideForward() + this.displayRewind(amount) + return + } + } + + private hideRewind () { + this.rewind.classList.add('vjs-hidden') + this.rewindText.textContent = '' + } + + private displayRewind (amount: number) { + this.rewind.classList.remove('vjs-hidden') + this.rewindText.textContent = this.player().localize('{1} seconds', [ amount + '' ]) + } + + private hideForward () { + this.forward.classList.add('vjs-hidden') + this.forwardText.textContent = '' + } + + private displayForward (amount: number) { + this.forward.classList.remove('vjs-hidden') + this.forwardText.textContent = this.player().localize('{1} seconds', [ amount + '' ]) + } } videojs.registerComponent('PeerTubeMobileButtons', PeerTubeMobileButtons) + +export { + PeerTubeMobileButtons +} diff --git a/client/src/assets/player/mobile/peertube-mobile-plugin.ts b/client/src/assets/player/mobile/peertube-mobile-plugin.ts index 2ce6b4b33..3c0365e5b 100644 --- a/client/src/assets/player/mobile/peertube-mobile-plugin.ts +++ b/client/src/assets/player/mobile/peertube-mobile-plugin.ts @@ -1,18 +1,43 @@ -import './peertube-mobile-buttons' +import { PeerTubeMobileButtons } from './peertube-mobile-buttons' import videojs from 'video.js' +import debug from 'debug' + +const logger = debug('peertube:player:mobile') const Plugin = videojs.getPlugin('plugin') class PeerTubeMobilePlugin extends Plugin { + private static readonly DOUBLE_TAP_DELAY_MS = 250 + private static readonly SET_CURRENT_TIME_DELAY = 1000 + + private peerTubeMobileButtons: PeerTubeMobileButtons + + private seekAmount = 0 + + private lastTapEvent: TouchEvent + private tapTimeout: NodeJS.Timeout + private newActiveState: boolean + + private setCurrentTimeTimeout: NodeJS.Timeout constructor (player: videojs.Player, options: videojs.PlayerOptions) { super(player, options) - player.addChild('PeerTubeMobileButtons') + this.peerTubeMobileButtons = player.addChild('PeerTubeMobileButtons') as PeerTubeMobileButtons if (videojs.browser.IS_ANDROID && screen.orientation) { this.handleFullscreenRotation() } + + if (!this.player.options_.userActions) this.player.options_.userActions = {}; + + // FIXME: typings + (this.player.options_.userActions as any).click = false + this.player.options_.userActions.doubleClick = false + + this.player.one('play', () => { + this.initTouchStartEvents() + }) } private handleFullscreenRotation () { @@ -27,6 +52,92 @@ class PeerTubeMobilePlugin extends Plugin { private isPortraitVideo () { return this.player.videoWidth() < this.player.videoHeight() } + + private initTouchStartEvents () { + this.player.on('touchstart', (event: TouchEvent) => { + event.stopPropagation() + + if (this.tapTimeout) { + clearTimeout(this.tapTimeout) + this.tapTimeout = undefined + } + + if (this.lastTapEvent && event.timeStamp - this.lastTapEvent.timeStamp < PeerTubeMobilePlugin.DOUBLE_TAP_DELAY_MS) { + logger('Detected double tap') + + this.lastTapEvent = undefined + this.onDoubleTap(event) + return + } + + this.newActiveState = !this.player.userActive() + + this.tapTimeout = setTimeout(() => { + logger('No double tap detected, set user active state to %s.', this.newActiveState) + + this.player.userActive(this.newActiveState) + }, PeerTubeMobilePlugin.DOUBLE_TAP_DELAY_MS) + + this.lastTapEvent = event + }) + } + + private onDoubleTap (event: TouchEvent) { + const playerWidth = this.player.currentWidth() + + const rect = this.findPlayerTarget((event.target as HTMLElement)).getBoundingClientRect() + const offsetX = event.targetTouches[0].pageX - rect.left + + logger('Calculating double tap zone (player width: %d, offset X: %d)', playerWidth, offsetX) + + if (offsetX > 0.66 * playerWidth) { + if (this.seekAmount < 0) this.seekAmount = 0 + + this.seekAmount += 10 + + logger('Will forward %d seconds', this.seekAmount) + } else if (offsetX < 0.33 * playerWidth) { + if (this.seekAmount > 0) this.seekAmount = 0 + + this.seekAmount -= 10 + logger('Will rewind %d seconds', this.seekAmount) + } + + this.peerTubeMobileButtons.displayFastSeek(this.seekAmount) + + this.scheduleSetCurrentTime() + + } + + private findPlayerTarget (target: HTMLElement): HTMLElement { + if (target.classList.contains('video-js')) return target + + return this.findPlayerTarget(target.parentElement) + } + + private scheduleSetCurrentTime () { + this.player.pause() + this.player.addClass('vjs-fast-seeking') + + if (this.setCurrentTimeTimeout) clearTimeout(this.setCurrentTimeTimeout) + + this.setCurrentTimeTimeout = setTimeout(() => { + let newTime = this.player.currentTime() + this.seekAmount + this.seekAmount = 0 + + newTime = Math.max(0, newTime) + newTime = Math.min(this.player.duration(), newTime) + + this.player.currentTime(newTime) + this.seekAmount = 0 + this.peerTubeMobileButtons.displayFastSeek(0) + + this.player.removeClass('vjs-fast-seeking') + this.player.userActive(false) + + this.player.play() + }, PeerTubeMobilePlugin.SET_CURRENT_TIME_DELAY) + } } videojs.registerPlugin('peertubeMobile', PeerTubeMobilePlugin) diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 6b6c1e581..b5317f45b 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -22,6 +22,7 @@ import './videojs-components/settings-panel-child' import './videojs-components/theater-button' import './playlist/playlist-plugin' import './mobile/peertube-mobile-plugin' +import './mobile/peertube-mobile-buttons' import videojs from 'video.js' import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' import { PluginsManager } from '@root-helpers/plugins-manager' diff --git a/client/src/sass/player/mobile.scss b/client/src/sass/player/mobile.scss index d72dc41df..2688860a6 100644 --- a/client/src/sass/player/mobile.scss +++ b/client/src/sass/player/mobile.scss @@ -31,22 +31,89 @@ display: block; } - .main-button { - font-family: VideoJS; - font-weight: normal; - font-style: normal; - font-size: 5em; + .main-button, + .rewind-button, + .forward-button { width: fit-content; - margin: auto; + height: fit-content; position: relative; top: calc(50% - 10px); transform: translateY(-50%); } + + .main-button, + .rewind-button .icon, + .forward-button .icon { + font-family: VideoJS; + font-weight: normal; + font-style: normal; + } + + .main-button { + font-size: 5em; + margin: auto; + } + + .rewind-button, + .forward-button { + margin: 0 10px; + position: absolute; + text-align: center; + + .icon { + opacity: 0; + animation: fadeInAndOut 1s linear infinite; + + &::before { + font-size: 20px; + content: '\f101'; + display: inline-block; + width: 16px; + } + } + } + + .forward-button { + right: 5px; + + .icon { + &::before { + margin-left: -2px; + } + + &:nth-child(2) { + animation-delay: 0.25s; + } + + &:nth-child(3) { + animation-delay: 0.5s; + } + } + } + + .rewind-button { + left: 5px; + + .icon { + &::before { + margin-right: -2px; + transform: scaleX(-1); + } + + &:nth-child(1) { + animation-delay: 0.5s; + } + + &:nth-child(2) { + animation-delay: 0.25s; + } + } + } } .vjs-paused { .main-button { - &:before { + &::before { content: '\f101'; } } @@ -54,7 +121,7 @@ .vjs-playing { .main-button { - &:before { + &::before { content: '\f103'; } } @@ -62,7 +129,7 @@ .vjs-ended { .main-button { - &:before { + &::before { content: '\f116'; } } @@ -77,11 +144,33 @@ } } - &.vjs-seeking, - &.vjs-scrubbing, - &.vjs-waiting { + &.vjs-scrubbing { .vjs-mobile-buttons-overlay { display: none; } } + + &.vjs-seeking, + &.vjs-waiting, + &.vjs-fast-seeking { + .main-button { + display: none; + } + } +} + +@keyframes fadeInAndOut { + 0%, + 20% { + opacity: 0; + } + + 60%, + 70% { + opacity: 1; + } + + 100% { + opacity: 0; + } } diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index 7556866e6..248a5b038 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts @@ -50,7 +50,9 @@ const playerKeys = { 'Buffer State': 'Buffer State', 'Live Latency': 'Live Latency', 'P2P': 'P2P', + '{1} seconds': '{1} seconds', 'enabled': 'enabled', + 'Playlist: {1}': 'Playlist: {1}', 'disabled': 'disabled', ' off': ' off', 'Player mode': 'Player mode'