diff options
author | Chocobozzz <me@florianbigard.com> | 2022-01-12 15:07:21 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-01-12 15:07:21 +0100 |
commit | 2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3 (patch) | |
tree | abb783daa18aab55fcda34e29a829792e7bcbc02 /client/src/assets/player | |
parent | e98ef69d1c8d4e39e45a52b04b3abf5fd1af3b2c (diff) | |
download | PeerTube-2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3.tar.gz PeerTube-2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3.tar.zst PeerTube-2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3.zip |
Add fast forward/rewind on mobile
Diffstat (limited to 'client/src/assets/player')
3 files changed, 176 insertions, 12 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 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | 2 | ||
3 | import debug from 'debug' | ||
4 | |||
5 | const logger = debug('peertube:player:mobile') | ||
6 | |||
7 | const Component = videojs.getComponent('Component') | 3 | const Component = videojs.getComponent('Component') |
8 | class PeerTubeMobileButtons extends Component { | 4 | class PeerTubeMobileButtons extends Component { |
9 | 5 | ||
6 | private rewind: Element | ||
7 | private forward: Element | ||
8 | private rewindText: Element | ||
9 | private forwardText: Element | ||
10 | |||
10 | createEl () { | 11 | createEl () { |
11 | const container = super.createEl('div', { | 12 | const container = super.createEl('div', { |
12 | className: 'vjs-mobile-buttons-overlay' | 13 | className: 'vjs-mobile-buttons-overlay' |
13 | }) as HTMLDivElement | 14 | }) as HTMLDivElement |
14 | 15 | ||
15 | container.addEventListener('click', () => { | ||
16 | logger('Set user as inactive') | ||
17 | |||
18 | this.player_.userActive(false) | ||
19 | }) | ||
20 | |||
21 | const mainButton = super.createEl('div', { | 16 | const mainButton = super.createEl('div', { |
22 | className: 'main-button' | 17 | className: 'main-button' |
23 | }) as HTMLDivElement | 18 | }) as HTMLDivElement |
@@ -33,10 +28,67 @@ class PeerTubeMobileButtons extends Component { | |||
33 | this.player_.pause() | 28 | this.player_.pause() |
34 | }) | 29 | }) |
35 | 30 | ||
31 | this.rewind = super.createEl('div', { className: 'rewind-button vjs-hidden' }) | ||
32 | this.forward = super.createEl('div', { className: 'forward-button vjs-hidden' }) | ||
33 | |||
34 | for (let i = 0; i < 3; i++) { | ||
35 | this.rewind.appendChild(super.createEl('span', { className: 'icon' })) | ||
36 | this.forward.appendChild(super.createEl('span', { className: 'icon' })) | ||
37 | } | ||
38 | |||
39 | this.rewindText = this.rewind.appendChild(super.createEl('div', { className: 'text' })) | ||
40 | this.forwardText = this.forward.appendChild(super.createEl('div', { className: 'text' })) | ||
41 | |||
42 | container.appendChild(this.rewind) | ||
36 | container.appendChild(mainButton) | 43 | container.appendChild(mainButton) |
44 | container.appendChild(this.forward) | ||
37 | 45 | ||
38 | return container | 46 | return container |
39 | } | 47 | } |
48 | |||
49 | displayFastSeek (amount: number) { | ||
50 | if (amount === 0) { | ||
51 | this.hideRewind() | ||
52 | this.hideForward() | ||
53 | return | ||
54 | } | ||
55 | |||
56 | if (amount > 0) { | ||
57 | this.hideRewind() | ||
58 | this.displayForward(amount) | ||
59 | return | ||
60 | } | ||
61 | |||
62 | if (amount < 0) { | ||
63 | this.hideForward() | ||
64 | this.displayRewind(amount) | ||
65 | return | ||
66 | } | ||
67 | } | ||
68 | |||
69 | private hideRewind () { | ||
70 | this.rewind.classList.add('vjs-hidden') | ||
71 | this.rewindText.textContent = '' | ||
72 | } | ||
73 | |||
74 | private displayRewind (amount: number) { | ||
75 | this.rewind.classList.remove('vjs-hidden') | ||
76 | this.rewindText.textContent = this.player().localize('{1} seconds', [ amount + '' ]) | ||
77 | } | ||
78 | |||
79 | private hideForward () { | ||
80 | this.forward.classList.add('vjs-hidden') | ||
81 | this.forwardText.textContent = '' | ||
82 | } | ||
83 | |||
84 | private displayForward (amount: number) { | ||
85 | this.forward.classList.remove('vjs-hidden') | ||
86 | this.forwardText.textContent = this.player().localize('{1} seconds', [ amount + '' ]) | ||
87 | } | ||
40 | } | 88 | } |
41 | 89 | ||
42 | videojs.registerComponent('PeerTubeMobileButtons', PeerTubeMobileButtons) | 90 | videojs.registerComponent('PeerTubeMobileButtons', PeerTubeMobileButtons) |
91 | |||
92 | export { | ||
93 | PeerTubeMobileButtons | ||
94 | } | ||
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 @@ | |||
1 | import './peertube-mobile-buttons' | 1 | import { PeerTubeMobileButtons } from './peertube-mobile-buttons' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import debug from 'debug' | ||
4 | |||
5 | const logger = debug('peertube:player:mobile') | ||
3 | 6 | ||
4 | const Plugin = videojs.getPlugin('plugin') | 7 | const Plugin = videojs.getPlugin('plugin') |
5 | 8 | ||
6 | class PeerTubeMobilePlugin extends Plugin { | 9 | class PeerTubeMobilePlugin extends Plugin { |
10 | private static readonly DOUBLE_TAP_DELAY_MS = 250 | ||
11 | private static readonly SET_CURRENT_TIME_DELAY = 1000 | ||
12 | |||
13 | private peerTubeMobileButtons: PeerTubeMobileButtons | ||
14 | |||
15 | private seekAmount = 0 | ||
16 | |||
17 | private lastTapEvent: TouchEvent | ||
18 | private tapTimeout: NodeJS.Timeout | ||
19 | private newActiveState: boolean | ||
20 | |||
21 | private setCurrentTimeTimeout: NodeJS.Timeout | ||
7 | 22 | ||
8 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 23 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { |
9 | super(player, options) | 24 | super(player, options) |
10 | 25 | ||
11 | player.addChild('PeerTubeMobileButtons') | 26 | this.peerTubeMobileButtons = player.addChild('PeerTubeMobileButtons') as PeerTubeMobileButtons |
12 | 27 | ||
13 | if (videojs.browser.IS_ANDROID && screen.orientation) { | 28 | if (videojs.browser.IS_ANDROID && screen.orientation) { |
14 | this.handleFullscreenRotation() | 29 | this.handleFullscreenRotation() |
15 | } | 30 | } |
31 | |||
32 | if (!this.player.options_.userActions) this.player.options_.userActions = {}; | ||
33 | |||
34 | // FIXME: typings | ||
35 | (this.player.options_.userActions as any).click = false | ||
36 | this.player.options_.userActions.doubleClick = false | ||
37 | |||
38 | this.player.one('play', () => { | ||
39 | this.initTouchStartEvents() | ||
40 | }) | ||
16 | } | 41 | } |
17 | 42 | ||
18 | private handleFullscreenRotation () { | 43 | private handleFullscreenRotation () { |
@@ -27,6 +52,92 @@ class PeerTubeMobilePlugin extends Plugin { | |||
27 | private isPortraitVideo () { | 52 | private isPortraitVideo () { |
28 | return this.player.videoWidth() < this.player.videoHeight() | 53 | return this.player.videoWidth() < this.player.videoHeight() |
29 | } | 54 | } |
55 | |||
56 | private initTouchStartEvents () { | ||
57 | this.player.on('touchstart', (event: TouchEvent) => { | ||
58 | event.stopPropagation() | ||
59 | |||
60 | if (this.tapTimeout) { | ||
61 | clearTimeout(this.tapTimeout) | ||
62 | this.tapTimeout = undefined | ||
63 | } | ||
64 | |||
65 | if (this.lastTapEvent && event.timeStamp - this.lastTapEvent.timeStamp < PeerTubeMobilePlugin.DOUBLE_TAP_DELAY_MS) { | ||
66 | logger('Detected double tap') | ||
67 | |||
68 | this.lastTapEvent = undefined | ||
69 | this.onDoubleTap(event) | ||
70 | return | ||
71 | } | ||
72 | |||
73 | this.newActiveState = !this.player.userActive() | ||
74 | |||
75 | this.tapTimeout = setTimeout(() => { | ||
76 | logger('No double tap detected, set user active state to %s.', this.newActiveState) | ||
77 | |||
78 | this.player.userActive(this.newActiveState) | ||
79 | }, PeerTubeMobilePlugin.DOUBLE_TAP_DELAY_MS) | ||
80 | |||
81 | this.lastTapEvent = event | ||
82 | }) | ||
83 | } | ||
84 | |||
85 | private onDoubleTap (event: TouchEvent) { | ||
86 | const playerWidth = this.player.currentWidth() | ||
87 | |||
88 | const rect = this.findPlayerTarget((event.target as HTMLElement)).getBoundingClientRect() | ||
89 | const offsetX = event.targetTouches[0].pageX - rect.left | ||
90 | |||
91 | logger('Calculating double tap zone (player width: %d, offset X: %d)', playerWidth, offsetX) | ||
92 | |||
93 | if (offsetX > 0.66 * playerWidth) { | ||
94 | if (this.seekAmount < 0) this.seekAmount = 0 | ||
95 | |||
96 | this.seekAmount += 10 | ||
97 | |||
98 | logger('Will forward %d seconds', this.seekAmount) | ||
99 | } else if (offsetX < 0.33 * playerWidth) { | ||
100 | if (this.seekAmount > 0) this.seekAmount = 0 | ||
101 | |||
102 | this.seekAmount -= 10 | ||
103 | logger('Will rewind %d seconds', this.seekAmount) | ||
104 | } | ||
105 | |||
106 | this.peerTubeMobileButtons.displayFastSeek(this.seekAmount) | ||
107 | |||
108 | this.scheduleSetCurrentTime() | ||
109 | |||
110 | } | ||
111 | |||
112 | private findPlayerTarget (target: HTMLElement): HTMLElement { | ||
113 | if (target.classList.contains('video-js')) return target | ||
114 | |||
115 | return this.findPlayerTarget(target.parentElement) | ||
116 | } | ||
117 | |||
118 | private scheduleSetCurrentTime () { | ||
119 | this.player.pause() | ||
120 | this.player.addClass('vjs-fast-seeking') | ||
121 | |||
122 | if (this.setCurrentTimeTimeout) clearTimeout(this.setCurrentTimeTimeout) | ||
123 | |||
124 | this.setCurrentTimeTimeout = setTimeout(() => { | ||
125 | let newTime = this.player.currentTime() + this.seekAmount | ||
126 | this.seekAmount = 0 | ||
127 | |||
128 | newTime = Math.max(0, newTime) | ||
129 | newTime = Math.min(this.player.duration(), newTime) | ||
130 | |||
131 | this.player.currentTime(newTime) | ||
132 | this.seekAmount = 0 | ||
133 | this.peerTubeMobileButtons.displayFastSeek(0) | ||
134 | |||
135 | this.player.removeClass('vjs-fast-seeking') | ||
136 | this.player.userActive(false) | ||
137 | |||
138 | this.player.play() | ||
139 | }, PeerTubeMobilePlugin.SET_CURRENT_TIME_DELAY) | ||
140 | } | ||
30 | } | 141 | } |
31 | 142 | ||
32 | videojs.registerPlugin('peertubeMobile', PeerTubeMobilePlugin) | 143 | 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' | |||
22 | import './videojs-components/theater-button' | 22 | import './videojs-components/theater-button' |
23 | import './playlist/playlist-plugin' | 23 | import './playlist/playlist-plugin' |
24 | import './mobile/peertube-mobile-plugin' | 24 | import './mobile/peertube-mobile-plugin' |
25 | import './mobile/peertube-mobile-buttons' | ||
25 | import videojs from 'video.js' | 26 | import videojs from 'video.js' |
26 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | 27 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' |
27 | import { PluginsManager } from '@root-helpers/plugins-manager' | 28 | import { PluginsManager } from '@root-helpers/plugins-manager' |