diff options
Diffstat (limited to 'client/src/assets')
3 files changed, 199 insertions, 82 deletions
diff --git a/client/src/assets/player/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/hotkeys/peertube-hotkeys-plugin.ts new file mode 100644 index 000000000..5920450bd --- /dev/null +++ b/client/src/assets/player/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -0,0 +1,196 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void } | ||
4 | |||
5 | const Plugin = videojs.getPlugin('plugin') | ||
6 | |||
7 | class PeerTubeHotkeysPlugin extends Plugin { | ||
8 | private static readonly VOLUME_STEP = 0.1 | ||
9 | private static readonly SEEK_STEP = 5 | ||
10 | |||
11 | private readonly handleKeyFunction: (event: KeyboardEvent) => void | ||
12 | |||
13 | private readonly handlers: KeyHandler[] | ||
14 | |||
15 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | ||
16 | super(player, options) | ||
17 | |||
18 | this.handlers = this.buildHandlers() | ||
19 | |||
20 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) | ||
21 | document.addEventListener('keydown', this.handleKeyFunction) | ||
22 | } | ||
23 | |||
24 | dispose () { | ||
25 | document.removeEventListener('keydown', this.handleKeyFunction) | ||
26 | } | ||
27 | |||
28 | private onKeyDown (event: KeyboardEvent) { | ||
29 | if (!this.isValidKeyTarget(event.target as HTMLElement)) return | ||
30 | |||
31 | for (const handler of this.handlers) { | ||
32 | if (handler.accept(event)) { | ||
33 | handler.cb(event) | ||
34 | return | ||
35 | } | ||
36 | } | ||
37 | } | ||
38 | |||
39 | private buildHandlers () { | ||
40 | const handlers: KeyHandler[] = [ | ||
41 | // Play | ||
42 | { | ||
43 | accept: e => (e.key === ' ' || e.key === 'MediaPlayPause'), | ||
44 | cb: e => { | ||
45 | e.preventDefault() | ||
46 | e.stopPropagation() | ||
47 | |||
48 | if (this.player.paused()) this.player.play() | ||
49 | else this.player.pause() | ||
50 | } | ||
51 | }, | ||
52 | |||
53 | // Increase volume | ||
54 | { | ||
55 | accept: e => this.isNaked(e, 'ArrowUp'), | ||
56 | cb: e => { | ||
57 | e.preventDefault() | ||
58 | this.player.volume(this.player.volume() + PeerTubeHotkeysPlugin.VOLUME_STEP) | ||
59 | } | ||
60 | }, | ||
61 | |||
62 | // Decrease volume | ||
63 | { | ||
64 | accept: e => this.isNaked(e, 'ArrowDown'), | ||
65 | cb: e => { | ||
66 | e.preventDefault() | ||
67 | this.player.volume(this.player.volume() - PeerTubeHotkeysPlugin.VOLUME_STEP) | ||
68 | } | ||
69 | }, | ||
70 | |||
71 | // Rewind | ||
72 | { | ||
73 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
74 | cb: e => { | ||
75 | e.preventDefault() | ||
76 | |||
77 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
78 | this.player.currentTime(target) | ||
79 | } | ||
80 | }, | ||
81 | |||
82 | // Forward | ||
83 | { | ||
84 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
85 | cb: e => { | ||
86 | e.preventDefault() | ||
87 | |||
88 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
89 | this.player.currentTime(target) | ||
90 | } | ||
91 | }, | ||
92 | |||
93 | // Fullscreen | ||
94 | { | ||
95 | // f key or Ctrl + Enter | ||
96 | accept: e => this.isNaked(e, 'f') || (!e.altKey && e.ctrlKey && e.key === 'Enter'), | ||
97 | cb: e => { | ||
98 | e.preventDefault() | ||
99 | |||
100 | if (this.player.isFullscreen()) this.player.exitFullscreen() | ||
101 | else this.player.requestFullscreen() | ||
102 | } | ||
103 | }, | ||
104 | |||
105 | // Mute | ||
106 | { | ||
107 | accept: e => this.isNaked(e, 'm'), | ||
108 | cb: e => { | ||
109 | e.preventDefault() | ||
110 | |||
111 | this.player.muted(!this.player.muted()) | ||
112 | } | ||
113 | }, | ||
114 | |||
115 | // Increase playback rate | ||
116 | { | ||
117 | accept: e => e.key === '>', | ||
118 | cb: () => { | ||
119 | const target = Math.min(this.player.playbackRate() + 0.1, 5) | ||
120 | |||
121 | this.player.playbackRate(parseFloat(target.toFixed(2))) | ||
122 | } | ||
123 | }, | ||
124 | |||
125 | // Decrease playback rate | ||
126 | { | ||
127 | accept: e => e.key === '<', | ||
128 | cb: () => { | ||
129 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) | ||
130 | |||
131 | this.player.playbackRate(parseFloat(target.toFixed(2))) | ||
132 | } | ||
133 | }, | ||
134 | |||
135 | // Previous frame | ||
136 | { | ||
137 | accept: e => e.key === ',', | ||
138 | cb: () => { | ||
139 | this.player.pause() | ||
140 | |||
141 | // Calculate movement distance (assuming 30 fps) | ||
142 | const dist = 1 / 30 | ||
143 | this.player.currentTime(this.player.currentTime() - dist) | ||
144 | } | ||
145 | }, | ||
146 | |||
147 | // Next frame | ||
148 | { | ||
149 | accept: e => e.key === '.', | ||
150 | cb: () => { | ||
151 | this.player.pause() | ||
152 | |||
153 | // Calculate movement distance (assuming 30 fps) | ||
154 | const dist = 1 / 30 | ||
155 | this.player.currentTime(this.player.currentTime() + dist) | ||
156 | } | ||
157 | } | ||
158 | ] | ||
159 | |||
160 | // 0-9 key handlers | ||
161 | for (let i = 0; i < 10; i++) { | ||
162 | handlers.push({ | ||
163 | accept: e => e.key === i + '', | ||
164 | cb: e => { | ||
165 | e.preventDefault() | ||
166 | |||
167 | this.player.currentTime(this.player.duration() * i * 0.1) | ||
168 | } | ||
169 | }) | ||
170 | } | ||
171 | |||
172 | return handlers | ||
173 | } | ||
174 | |||
175 | private isValidKeyTarget (eventEl: HTMLElement) { | ||
176 | const playerEl = this.player.el() | ||
177 | const activeEl = document.activeElement | ||
178 | const currentElTagName = eventEl.tagName.toLowerCase() | ||
179 | |||
180 | return ( | ||
181 | activeEl === playerEl || | ||
182 | activeEl === playerEl.querySelector('.vjs-tech') || | ||
183 | activeEl === playerEl.querySelector('.vjs-control-bar') || | ||
184 | eventEl.id === 'content' || | ||
185 | currentElTagName === 'body' || | ||
186 | currentElTagName === 'video' | ||
187 | ) | ||
188 | } | ||
189 | |||
190 | private isNaked (event: KeyboardEvent, key: string) { | ||
191 | return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key) | ||
192 | } | ||
193 | } | ||
194 | |||
195 | videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin) | ||
196 | export { PeerTubeHotkeysPlugin } | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index d715adf56..b9a289aa0 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import 'videojs-hotkeys/videojs.hotkeys' | ||
2 | import 'videojs-dock' | 1 | import 'videojs-dock' |
3 | import '@peertube/videojs-contextmenu' | 2 | import '@peertube/videojs-contextmenu' |
4 | import './upnext/end-card' | 3 | import './upnext/end-card' |
@@ -23,6 +22,7 @@ import './videojs-components/theater-button' | |||
23 | import './playlist/playlist-plugin' | 22 | import './playlist/playlist-plugin' |
24 | import './mobile/peertube-mobile-plugin' | 23 | import './mobile/peertube-mobile-plugin' |
25 | import './mobile/peertube-mobile-buttons' | 24 | import './mobile/peertube-mobile-buttons' |
25 | import './hotkeys/peertube-hotkeys-plugin' | ||
26 | import videojs from 'video.js' | 26 | import videojs from 'video.js' |
27 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' | 27 | import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' |
28 | import { PluginsManager } from '@root-helpers/plugins-manager' | 28 | import { PluginsManager } from '@root-helpers/plugins-manager' |
@@ -192,6 +192,7 @@ export class PeertubePlayerManager { | |||
192 | }) | 192 | }) |
193 | 193 | ||
194 | if (isMobile()) player.peertubeMobile() | 194 | if (isMobile()) player.peertubeMobile() |
195 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() | ||
195 | 196 | ||
196 | player.bezels() | 197 | player.bezels() |
197 | 198 | ||
@@ -286,10 +287,6 @@ export class PeertubePlayerManager { | |||
286 | plugins.playlist = commonOptions.playlist | 287 | plugins.playlist = commonOptions.playlist |
287 | } | 288 | } |
288 | 289 | ||
289 | if (commonOptions.enableHotkeys === true) { | ||
290 | PeertubePlayerManager.addHotkeysOptions(plugins) | ||
291 | } | ||
292 | |||
293 | if (isHLS) { | 290 | if (isHLS) { |
294 | const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule) | 291 | const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule) |
295 | 292 | ||
@@ -638,82 +635,6 @@ export class PeertubePlayerManager { | |||
638 | player.contextmenuUI({ content }) | 635 | player.contextmenuUI({ content }) |
639 | } | 636 | } |
640 | 637 | ||
641 | private static addHotkeysOptions (plugins: VideoJSPluginOptions) { | ||
642 | const isNaked = (event: KeyboardEvent, key: string) => | ||
643 | (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key) | ||
644 | |||
645 | Object.assign(plugins, { | ||
646 | hotkeys: { | ||
647 | skipInitialFocus: true, | ||
648 | enableInactiveFocus: false, | ||
649 | captureDocumentHotkeys: true, | ||
650 | documentHotkeysFocusElementFilter: (e: HTMLElement) => { | ||
651 | const tagName = e.tagName.toLowerCase() | ||
652 | return e.id === 'content' || tagName === 'body' || tagName === 'video' | ||
653 | }, | ||
654 | |||
655 | enableVolumeScroll: false, | ||
656 | enableModifiersForNumbers: false, | ||
657 | |||
658 | rewindKey: function (event: KeyboardEvent) { | ||
659 | return isNaked(event, 'ArrowLeft') | ||
660 | }, | ||
661 | |||
662 | forwardKey: function (event: KeyboardEvent) { | ||
663 | return isNaked(event, 'ArrowRight') | ||
664 | }, | ||
665 | |||
666 | fullscreenKey: function (event: KeyboardEvent) { | ||
667 | // fullscreen with the f key or Ctrl+Enter | ||
668 | return isNaked(event, 'f') || (!event.altKey && event.ctrlKey && event.key === 'Enter') | ||
669 | }, | ||
670 | |||
671 | customKeys: { | ||
672 | increasePlaybackRateKey: { | ||
673 | key: function (event: KeyboardEvent) { | ||
674 | return isNaked(event, '>') | ||
675 | }, | ||
676 | handler: function (player: videojs.Player) { | ||
677 | const newValue = Math.min(player.playbackRate() + 0.1, 5) | ||
678 | player.playbackRate(parseFloat(newValue.toFixed(2))) | ||
679 | } | ||
680 | }, | ||
681 | decreasePlaybackRateKey: { | ||
682 | key: function (event: KeyboardEvent) { | ||
683 | return isNaked(event, '<') | ||
684 | }, | ||
685 | handler: function (player: videojs.Player) { | ||
686 | const newValue = Math.max(player.playbackRate() - 0.1, 0.10) | ||
687 | player.playbackRate(parseFloat(newValue.toFixed(2))) | ||
688 | } | ||
689 | }, | ||
690 | previousFrame: { | ||
691 | key: function (event: KeyboardEvent) { | ||
692 | return event.key === ',' | ||
693 | }, | ||
694 | handler: function (player: videojs.Player) { | ||
695 | player.pause() | ||
696 | // Calculate movement distance (assuming 30 fps) | ||
697 | const dist = 1 / 30 | ||
698 | player.currentTime(player.currentTime() - dist) | ||
699 | } | ||
700 | }, | ||
701 | nextFrame: { | ||
702 | key: function (event: KeyboardEvent) { | ||
703 | return event.key === '.' | ||
704 | }, | ||
705 | handler: function (player: videojs.Player) { | ||
706 | player.pause() | ||
707 | // Calculate movement distance (assuming 30 fps) | ||
708 | const dist = 1 / 30 | ||
709 | player.currentTime(player.currentTime() + dist) | ||
710 | } | ||
711 | } | ||
712 | } | ||
713 | } | ||
714 | }) | ||
715 | } | ||
716 | |||
717 | private static getAutoPlayValue (autoplay: any) { | 638 | private static getAutoPlayValue (autoplay: any) { |
718 | if (autoplay !== true) return autoplay | 639 | if (autoplay !== true) return autoplay |
719 | 640 | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index e4013d815..b20ef7a3b 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -41,8 +41,8 @@ declare module 'video.js' { | |||
41 | contextmenuUI (options: any): any | 41 | contextmenuUI (options: any): any |
42 | 42 | ||
43 | bezels (): void | 43 | bezels (): void |
44 | |||
45 | peertubeMobile (): void | 44 | peertubeMobile (): void |
45 | peerTubeHotkeysPlugin (): void | ||
46 | 46 | ||
47 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 47 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
48 | 48 | ||