1 import videojs from 'video.js'
3 type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void }
5 const Plugin = videojs.getPlugin('plugin')
7 class PeerTubeHotkeysPlugin extends Plugin {
8 private static readonly VOLUME_STEP = 0.1
9 private static readonly SEEK_STEP = 5
11 private readonly handleKeyFunction: (event: KeyboardEvent) => void
13 private readonly handlers: KeyHandler[]
15 constructor (player: videojs.Player, options: videojs.PlayerOptions) {
16 super(player, options)
18 this.handlers = this.buildHandlers()
20 this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event)
21 document.addEventListener('keydown', this.handleKeyFunction)
25 document.removeEventListener('keydown', this.handleKeyFunction)
28 private onKeyDown (event: KeyboardEvent) {
29 if (!this.isValidKeyTarget(event.target as HTMLElement)) return
31 for (const handler of this.handlers) {
32 if (handler.accept(event)) {
39 private buildHandlers () {
40 const handlers: KeyHandler[] = [
43 accept: e => (e.key === ' ' || e.key === 'MediaPlayPause'),
48 if (this.player.paused()) this.player.play()
49 else this.player.pause()
55 accept: e => this.isNaked(e, 'ArrowUp'),
58 this.player.volume(this.player.volume() + PeerTubeHotkeysPlugin.VOLUME_STEP)
64 accept: e => this.isNaked(e, 'ArrowDown'),
67 this.player.volume(this.player.volume() - PeerTubeHotkeysPlugin.VOLUME_STEP)
73 accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'),
77 const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP)
78 this.player.currentTime(target)
84 accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'),
88 const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP)
89 this.player.currentTime(target)
95 // f key or Ctrl + Enter
96 accept: e => this.isNaked(e, 'f') || (!e.altKey && e.ctrlKey && e.key === 'Enter'),
100 if (this.player.isFullscreen()) this.player.exitFullscreen()
101 else this.player.requestFullscreen()
107 accept: e => this.isNaked(e, 'm'),
111 this.player.muted(!this.player.muted())
115 // Increase playback rate
117 accept: e => e.key === '>',
119 const target = Math.min(this.player.playbackRate() + 0.1, 5)
121 this.player.playbackRate(parseFloat(target.toFixed(2)))
125 // Decrease playback rate
127 accept: e => e.key === '<',
129 const target = Math.max(this.player.playbackRate() - 0.1, 0.10)
131 this.player.playbackRate(parseFloat(target.toFixed(2)))
137 accept: e => e.key === ',',
141 // Calculate movement distance (assuming 30 fps)
143 this.player.currentTime(this.player.currentTime() - dist)
149 accept: e => e.key === '.',
153 // Calculate movement distance (assuming 30 fps)
155 this.player.currentTime(this.player.currentTime() + dist)
161 for (let i = 0; i < 10; i++) {
163 accept: e => this.isNakedOrShift(e, i + ''),
167 this.player.currentTime(this.player.duration() * i * 0.1)
175 private isValidKeyTarget (eventEl: HTMLElement) {
176 const playerEl = this.player.el()
177 const activeEl = document.activeElement
178 const currentElTagName = eventEl.tagName.toLowerCase()
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'
190 private isNaked (event: KeyboardEvent, key: string) {
191 return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key)
194 private isNakedOrShift (event: KeyboardEvent, key: string) {
195 return (!event.ctrlKey && !event.altKey && !event.metaKey && event.key === key)
199 videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin)
200 export { PeerTubeHotkeysPlugin }