aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/package.json1
-rw-r--r--client/src/assets/player/hotkeys/peertube-hotkeys-plugin.ts196
-rw-r--r--client/src/assets/player/peertube-player-manager.ts83
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts2
-rw-r--r--client/yarn.lock5
5 files changed, 199 insertions, 88 deletions
diff --git a/client/package.json b/client/package.json
index efefb1054..1f58f9cd0 100644
--- a/client/package.json
+++ b/client/package.json
@@ -131,7 +131,6 @@
131 "typescript": "~4.4.4", 131 "typescript": "~4.4.4",
132 "video.js": "^7", 132 "video.js": "^7",
133 "videojs-dock": "^3.0.0", 133 "videojs-dock": "^3.0.0",
134 "videojs-hotkeys": "^0.2.27",
135 "videostream": "~3.2.1", 134 "videostream": "~3.2.1",
136 "wdio-chromedriver-service": "^7.2.0", 135 "wdio-chromedriver-service": "^7.2.0",
137 "wdio-geckodriver-service": "^2.0.3", 136 "wdio-geckodriver-service": "^2.0.3",
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 @@
1import videojs from 'video.js'
2
3type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void }
4
5const Plugin = videojs.getPlugin('plugin')
6
7class 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
195videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin)
196export { 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 @@
1import 'videojs-hotkeys/videojs.hotkeys'
2import 'videojs-dock' 1import 'videojs-dock'
3import '@peertube/videojs-contextmenu' 2import '@peertube/videojs-contextmenu'
4import './upnext/end-card' 3import './upnext/end-card'
@@ -23,6 +22,7 @@ import './videojs-components/theater-button'
23import './playlist/playlist-plugin' 22import './playlist/playlist-plugin'
24import './mobile/peertube-mobile-plugin' 23import './mobile/peertube-mobile-plugin'
25import './mobile/peertube-mobile-buttons' 24import './mobile/peertube-mobile-buttons'
25import './hotkeys/peertube-hotkeys-plugin'
26import videojs from 'video.js' 26import videojs from 'video.js'
27import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs' 27import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
28import { PluginsManager } from '@root-helpers/plugins-manager' 28import { 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
diff --git a/client/yarn.lock b/client/yarn.lock
index 654f7f11f..750d3b328 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -11794,11 +11794,6 @@ videojs-font@3.2.0:
11794 resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232" 11794 resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232"
11795 integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA== 11795 integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==
11796 11796
11797videojs-hotkeys@^0.2.27:
11798 version "0.2.27"
11799 resolved "https://registry.yarnpkg.com/videojs-hotkeys/-/videojs-hotkeys-0.2.27.tgz#0df97952b9dff0e6cc1cf8a439fed7eac9c73f01"
11800 integrity sha512-pwtm1QocRmzJy1PWQsmFVHyeldYHHpLdeATK3FsFHVMmNpz6CROkAn8TFy2UILr8Ghgq134K8jEKNue8HWpudQ==
11801
11802videojs-vtt.js@^0.15.3: 11797videojs-vtt.js@^0.15.3:
11803 version "0.15.3" 11798 version "0.15.3"
11804 resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.3.tgz#84260393b79487fcf195d9372f812d7fab83a993" 11799 resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.3.tgz#84260393b79487fcf195d9372f812d7fab83a993"