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 | |
parent | e98ef69d1c8d4e39e45a52b04b3abf5fd1af3b2c (diff) | |
download | PeerTube-2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3.tar.gz PeerTube-2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3.tar.zst PeerTube-2dd0a8a8fd2fc85180fa3b45c5a6a56d07320ed3.zip |
Add fast forward/rewind on mobile
-rw-r--r-- | client/src/assets/player/mobile/peertube-mobile-buttons.ts | 72 | ||||
-rw-r--r-- | client/src/assets/player/mobile/peertube-mobile-plugin.ts | 115 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-manager.ts | 1 | ||||
-rw-r--r-- | client/src/sass/player/mobile.scss | 111 | ||||
-rwxr-xr-x | scripts/i18n/create-custom-files.ts | 2 |
5 files changed, 278 insertions, 23 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' |
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 @@ | |||
31 | display: block; | 31 | display: block; |
32 | } | 32 | } |
33 | 33 | ||
34 | .main-button { | 34 | .main-button, |
35 | .rewind-button, | ||
36 | .forward-button { | ||
37 | width: fit-content; | ||
38 | height: fit-content; | ||
39 | position: relative; | ||
40 | top: calc(50% - 10px); | ||
41 | transform: translateY(-50%); | ||
42 | } | ||
43 | |||
44 | .main-button, | ||
45 | .rewind-button .icon, | ||
46 | .forward-button .icon { | ||
35 | font-family: VideoJS; | 47 | font-family: VideoJS; |
36 | font-weight: normal; | 48 | font-weight: normal; |
37 | font-style: normal; | 49 | font-style: normal; |
50 | } | ||
51 | |||
52 | .main-button { | ||
38 | font-size: 5em; | 53 | font-size: 5em; |
39 | width: fit-content; | ||
40 | margin: auto; | 54 | margin: auto; |
41 | position: relative; | 55 | } |
42 | top: calc(50% - 10px); | 56 | |
43 | transform: translateY(-50%); | 57 | .rewind-button, |
58 | .forward-button { | ||
59 | margin: 0 10px; | ||
60 | position: absolute; | ||
61 | text-align: center; | ||
62 | |||
63 | .icon { | ||
64 | opacity: 0; | ||
65 | animation: fadeInAndOut 1s linear infinite; | ||
66 | |||
67 | &::before { | ||
68 | font-size: 20px; | ||
69 | content: '\f101'; | ||
70 | display: inline-block; | ||
71 | width: 16px; | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | |||
76 | .forward-button { | ||
77 | right: 5px; | ||
78 | |||
79 | .icon { | ||
80 | &::before { | ||
81 | margin-left: -2px; | ||
82 | } | ||
83 | |||
84 | &:nth-child(2) { | ||
85 | animation-delay: 0.25s; | ||
86 | } | ||
87 | |||
88 | &:nth-child(3) { | ||
89 | animation-delay: 0.5s; | ||
90 | } | ||
91 | } | ||
92 | } | ||
93 | |||
94 | .rewind-button { | ||
95 | left: 5px; | ||
96 | |||
97 | .icon { | ||
98 | &::before { | ||
99 | margin-right: -2px; | ||
100 | transform: scaleX(-1); | ||
101 | } | ||
102 | |||
103 | &:nth-child(1) { | ||
104 | animation-delay: 0.5s; | ||
105 | } | ||
106 | |||
107 | &:nth-child(2) { | ||
108 | animation-delay: 0.25s; | ||
109 | } | ||
110 | } | ||
44 | } | 111 | } |
45 | } | 112 | } |
46 | 113 | ||
47 | .vjs-paused { | 114 | .vjs-paused { |
48 | .main-button { | 115 | .main-button { |
49 | &:before { | 116 | &::before { |
50 | content: '\f101'; | 117 | content: '\f101'; |
51 | } | 118 | } |
52 | } | 119 | } |
@@ -54,7 +121,7 @@ | |||
54 | 121 | ||
55 | .vjs-playing { | 122 | .vjs-playing { |
56 | .main-button { | 123 | .main-button { |
57 | &:before { | 124 | &::before { |
58 | content: '\f103'; | 125 | content: '\f103'; |
59 | } | 126 | } |
60 | } | 127 | } |
@@ -62,7 +129,7 @@ | |||
62 | 129 | ||
63 | .vjs-ended { | 130 | .vjs-ended { |
64 | .main-button { | 131 | .main-button { |
65 | &:before { | 132 | &::before { |
66 | content: '\f116'; | 133 | content: '\f116'; |
67 | } | 134 | } |
68 | } | 135 | } |
@@ -77,11 +144,33 @@ | |||
77 | } | 144 | } |
78 | } | 145 | } |
79 | 146 | ||
80 | &.vjs-seeking, | 147 | &.vjs-scrubbing { |
81 | &.vjs-scrubbing, | ||
82 | &.vjs-waiting { | ||
83 | .vjs-mobile-buttons-overlay { | 148 | .vjs-mobile-buttons-overlay { |
84 | display: none; | 149 | display: none; |
85 | } | 150 | } |
86 | } | 151 | } |
152 | |||
153 | &.vjs-seeking, | ||
154 | &.vjs-waiting, | ||
155 | &.vjs-fast-seeking { | ||
156 | .main-button { | ||
157 | display: none; | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | |||
162 | @keyframes fadeInAndOut { | ||
163 | 0%, | ||
164 | 20% { | ||
165 | opacity: 0; | ||
166 | } | ||
167 | |||
168 | 60%, | ||
169 | 70% { | ||
170 | opacity: 1; | ||
171 | } | ||
172 | |||
173 | 100% { | ||
174 | opacity: 0; | ||
175 | } | ||
87 | } | 176 | } |
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 = { | |||
50 | 'Buffer State': 'Buffer State', | 50 | 'Buffer State': 'Buffer State', |
51 | 'Live Latency': 'Live Latency', | 51 | 'Live Latency': 'Live Latency', |
52 | 'P2P': 'P2P', | 52 | 'P2P': 'P2P', |
53 | '{1} seconds': '{1} seconds', | ||
53 | 'enabled': 'enabled', | 54 | 'enabled': 'enabled', |
55 | 'Playlist: {1}': 'Playlist: {1}', | ||
54 | 'disabled': 'disabled', | 56 | 'disabled': 'disabled', |
55 | ' off': ' off', | 57 | ' off': ' off', |
56 | 'Player mode': 'Player mode' | 58 | 'Player mode': 'Player mode' |