1 import videojs from 'video.js'
3 type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void }
5 const Plugin = videojs.getPlugin('plugin')
7 export type HotkeysOptions = {
11 class PeerTubeHotkeysPlugin extends Plugin {
12 private static readonly VOLUME_STEP = 0.1
13 private static readonly SEEK_STEP = 5
15 private readonly handleKeyFunction: (event: KeyboardEvent) => void
17 private readonly handlers: KeyHandler[]
19 private readonly isLive: boolean
21 constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) {
22 super(player, options)
24 this.isLive = options.isLive
26 this.handlers = this.buildHandlers()
28 this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event)
29 document.addEventListener('keydown', this.handleKeyFunction)
33 document.removeEventListener('keydown', this.handleKeyFunction)
36 private onKeyDown (event: KeyboardEvent) {
37 if (!this.isValidKeyTarget(event.target as HTMLElement)) return
39 for (const handler of this.handlers) {
40 if (handler.accept(event)) {
47 private buildHandlers () {
48 const handlers: KeyHandler[] = [
51 accept: e => (e.key === ' ' || e.key === 'MediaPlayPause'),
56 if (this.player.paused()) this.player.play()
57 else this.player.pause()
63 accept: e => this.isNaked(e, 'ArrowUp'),
66 this.player.volume(this.player.volume() + PeerTubeHotkeysPlugin.VOLUME_STEP)
72 accept: e => this.isNaked(e, 'ArrowDown'),
75 this.player.volume(this.player.volume() - PeerTubeHotkeysPlugin.VOLUME_STEP)
81 // f key or Ctrl + Enter
82 accept: e => this.isNaked(e, 'f') || (!e.altKey && e.ctrlKey && e.key === 'Enter'),
86 if (this.player.isFullscreen()) this.player.exitFullscreen()
87 else this.player.requestFullscreen()
93 accept: e => this.isNaked(e, 'm'),
97 this.player.muted(!this.player.muted())
101 // Increase playback rate
103 accept: e => e.key === '>',
105 if (this.isLive) return
107 const target = Math.min(this.player.playbackRate() + 0.1, 5)
109 this.player.playbackRate(parseFloat(target.toFixed(2)))
113 // Decrease playback rate
115 accept: e => e.key === '<',
117 if (this.isLive) return
119 const target = Math.max(this.player.playbackRate() - 0.1, 0.10)
121 this.player.playbackRate(parseFloat(target.toFixed(2)))
127 accept: e => e.key === ',',
129 if (this.isLive) return
133 // Calculate movement distance (assuming 30 fps)
135 this.player.currentTime(this.player.currentTime() - dist)
141 accept: e => e.key === '.',
143 if (this.isLive) return
147 // Calculate movement distance (assuming 30 fps)
149 this.player.currentTime(this.player.currentTime() + dist)
154 if (this.isLive) return handlers
156 return handlers.concat(this.buildVODHandlers())
159 private buildVODHandlers () {
160 const handlers: KeyHandler[] = [
163 accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'),
165 if (this.isLive) return
169 const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP)
170 this.player.currentTime(target)
176 accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'),
178 if (this.isLive) return
182 const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP)
183 this.player.currentTime(target)
189 for (let i = 0; i < 10; i++) {
191 accept: e => this.isNakedOrShift(e, i + ''),
193 if (this.isLive) return
197 this.player.currentTime(this.player.duration() * i * 0.1)
205 private isValidKeyTarget (eventEl: HTMLElement) {
206 const playerEl = this.player.el()
207 const activeEl = document.activeElement
208 const currentElTagName = eventEl.tagName.toLowerCase()
211 activeEl === playerEl ||
212 activeEl === playerEl.querySelector('.vjs-tech') ||
213 activeEl === playerEl.querySelector('.vjs-control-bar') ||
214 eventEl.id === 'content' ||
215 currentElTagName === 'body' ||
216 currentElTagName === 'video'
220 private isNaked (event: KeyboardEvent, key: string) {
221 if (key.length === 1) key = key.toUpperCase()
223 return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && this.getLatinKey(event.key, event.code) === key)
226 private isNakedOrShift (event: KeyboardEvent, key: string) {
227 return (!event.ctrlKey && !event.altKey && !event.metaKey && event.key === key)
230 // Thanks Maciej Krawczyk
231 // https://stackoverflow.com/questions/70211837/keyboard-shortcuts-commands-on-non-latin-alphabet-keyboards-javascript?rq=1
232 private getLatinKey (key: string, code: string) {
233 if (key.length !== 1) {
237 const capitalHetaCode = 880
238 const isNonLatin = key.charCodeAt(0) >= capitalHetaCode
241 if (code.indexOf('Key') === 0 && code.length === 4) { // i.e. 'KeyW'
242 return code.charAt(3)
245 if (code.indexOf('Digit') === 0 && code.length === 6) { // i.e. 'Digit7'
246 return code.charAt(5)
250 return key.toUpperCase()
254 videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin)
255 export { PeerTubeHotkeysPlugin }