--- /dev/null
+import videojs from 'video.js'
+
+type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void }
+
+const Plugin = videojs.getPlugin('plugin')
+
+class PeerTubeHotkeysPlugin extends Plugin {
+ private static readonly VOLUME_STEP = 0.1
+ private static readonly SEEK_STEP = 5
+
+ private readonly handleKeyFunction: (event: KeyboardEvent) => void
+
+ private readonly handlers: KeyHandler[]
+
+ constructor (player: videojs.Player, options: videojs.PlayerOptions) {
+ super(player, options)
+
+ this.handlers = this.buildHandlers()
+
+ this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event)
+ document.addEventListener('keydown', this.handleKeyFunction)
+ }
+
+ dispose () {
+ document.removeEventListener('keydown', this.handleKeyFunction)
+ }
+
+ private onKeyDown (event: KeyboardEvent) {
+ if (!this.isValidKeyTarget(event.target as HTMLElement)) return
+
+ for (const handler of this.handlers) {
+ if (handler.accept(event)) {
+ handler.cb(event)
+ return
+ }
+ }
+ }
+
+ private buildHandlers () {
+ const handlers: KeyHandler[] = [
+ // Play
+ {
+ accept: e => (e.key === ' ' || e.key === 'MediaPlayPause'),
+ cb: e => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ if (this.player.paused()) this.player.play()
+ else this.player.pause()
+ }
+ },
+
+ // Increase volume
+ {
+ accept: e => this.isNaked(e, 'ArrowUp'),
+ cb: e => {
+ e.preventDefault()
+ this.player.volume(this.player.volume() + PeerTubeHotkeysPlugin.VOLUME_STEP)
+ }
+ },
+
+ // Decrease volume
+ {
+ accept: e => this.isNaked(e, 'ArrowDown'),
+ cb: e => {
+ e.preventDefault()
+ this.player.volume(this.player.volume() - PeerTubeHotkeysPlugin.VOLUME_STEP)
+ }
+ },
+
+ // Rewind
+ {
+ accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'),
+ cb: e => {
+ e.preventDefault()
+
+ const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP)
+ this.player.currentTime(target)
+ }
+ },
+
+ // Forward
+ {
+ accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'),
+ cb: e => {
+ e.preventDefault()
+
+ const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP)
+ this.player.currentTime(target)
+ }
+ },
+
+ // Fullscreen
+ {
+ // f key or Ctrl + Enter
+ accept: e => this.isNaked(e, 'f') || (!e.altKey && e.ctrlKey && e.key === 'Enter'),
+ cb: e => {
+ e.preventDefault()
+
+ if (this.player.isFullscreen()) this.player.exitFullscreen()
+ else this.player.requestFullscreen()
+ }
+ },
+
+ // Mute
+ {
+ accept: e => this.isNaked(e, 'm'),
+ cb: e => {
+ e.preventDefault()
+
+ this.player.muted(!this.player.muted())
+ }
+ },
+
+ // Increase playback rate
+ {
+ accept: e => e.key === '>',
+ cb: () => {
+ const target = Math.min(this.player.playbackRate() + 0.1, 5)
+
+ this.player.playbackRate(parseFloat(target.toFixed(2)))
+ }
+ },
+
+ // Decrease playback rate
+ {
+ accept: e => e.key === '<',
+ cb: () => {
+ const target = Math.max(this.player.playbackRate() - 0.1, 0.10)
+
+ this.player.playbackRate(parseFloat(target.toFixed(2)))
+ }
+ },
+
+ // Previous frame
+ {
+ accept: e => e.key === ',',
+ cb: () => {
+ this.player.pause()
+
+ // Calculate movement distance (assuming 30 fps)
+ const dist = 1 / 30
+ this.player.currentTime(this.player.currentTime() - dist)
+ }
+ },
+
+ // Next frame
+ {
+ accept: e => e.key === '.',
+ cb: () => {
+ this.player.pause()
+
+ // Calculate movement distance (assuming 30 fps)
+ const dist = 1 / 30
+ this.player.currentTime(this.player.currentTime() + dist)
+ }
+ }
+ ]
+
+ // 0-9 key handlers
+ for (let i = 0; i < 10; i++) {
+ handlers.push({
+ accept: e => e.key === i + '',
+ cb: e => {
+ e.preventDefault()
+
+ this.player.currentTime(this.player.duration() * i * 0.1)
+ }
+ })
+ }
+
+ return handlers
+ }
+
+ private isValidKeyTarget (eventEl: HTMLElement) {
+ const playerEl = this.player.el()
+ const activeEl = document.activeElement
+ const currentElTagName = eventEl.tagName.toLowerCase()
+
+ return (
+ activeEl === playerEl ||
+ activeEl === playerEl.querySelector('.vjs-tech') ||
+ activeEl === playerEl.querySelector('.vjs-control-bar') ||
+ eventEl.id === 'content' ||
+ currentElTagName === 'body' ||
+ currentElTagName === 'video'
+ )
+ }
+
+ private isNaked (event: KeyboardEvent, key: string) {
+ return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key)
+ }
+}
+
+videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin)
+export { PeerTubeHotkeysPlugin }
-import 'videojs-hotkeys/videojs.hotkeys'
import 'videojs-dock'
import '@peertube/videojs-contextmenu'
import './upnext/end-card'
import './playlist/playlist-plugin'
import './mobile/peertube-mobile-plugin'
import './mobile/peertube-mobile-buttons'
+import './hotkeys/peertube-hotkeys-plugin'
import videojs from 'video.js'
import { HlsJsEngineSettings } from '@peertube/p2p-media-loader-hlsjs'
import { PluginsManager } from '@root-helpers/plugins-manager'
})
if (isMobile()) player.peertubeMobile()
+ if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin()
player.bezels()
plugins.playlist = commonOptions.playlist
}
- if (commonOptions.enableHotkeys === true) {
- PeertubePlayerManager.addHotkeysOptions(plugins)
- }
-
if (isHLS) {
const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
player.contextmenuUI({ content })
}
- private static addHotkeysOptions (plugins: VideoJSPluginOptions) {
- const isNaked = (event: KeyboardEvent, key: string) =>
- (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key)
-
- Object.assign(plugins, {
- hotkeys: {
- skipInitialFocus: true,
- enableInactiveFocus: false,
- captureDocumentHotkeys: true,
- documentHotkeysFocusElementFilter: (e: HTMLElement) => {
- const tagName = e.tagName.toLowerCase()
- return e.id === 'content' || tagName === 'body' || tagName === 'video'
- },
-
- enableVolumeScroll: false,
- enableModifiersForNumbers: false,
-
- rewindKey: function (event: KeyboardEvent) {
- return isNaked(event, 'ArrowLeft')
- },
-
- forwardKey: function (event: KeyboardEvent) {
- return isNaked(event, 'ArrowRight')
- },
-
- fullscreenKey: function (event: KeyboardEvent) {
- // fullscreen with the f key or Ctrl+Enter
- return isNaked(event, 'f') || (!event.altKey && event.ctrlKey && event.key === 'Enter')
- },
-
- customKeys: {
- increasePlaybackRateKey: {
- key: function (event: KeyboardEvent) {
- return isNaked(event, '>')
- },
- handler: function (player: videojs.Player) {
- const newValue = Math.min(player.playbackRate() + 0.1, 5)
- player.playbackRate(parseFloat(newValue.toFixed(2)))
- }
- },
- decreasePlaybackRateKey: {
- key: function (event: KeyboardEvent) {
- return isNaked(event, '<')
- },
- handler: function (player: videojs.Player) {
- const newValue = Math.max(player.playbackRate() - 0.1, 0.10)
- player.playbackRate(parseFloat(newValue.toFixed(2)))
- }
- },
- previousFrame: {
- key: function (event: KeyboardEvent) {
- return event.key === ','
- },
- handler: function (player: videojs.Player) {
- player.pause()
- // Calculate movement distance (assuming 30 fps)
- const dist = 1 / 30
- player.currentTime(player.currentTime() - dist)
- }
- },
- nextFrame: {
- key: function (event: KeyboardEvent) {
- return event.key === '.'
- },
- handler: function (player: videojs.Player) {
- player.pause()
- // Calculate movement distance (assuming 30 fps)
- const dist = 1 / 30
- player.currentTime(player.currentTime() + dist)
- }
- }
- }
- }
- })
- }
-
private static getAutoPlayValue (autoplay: any) {
if (autoplay !== true) return autoplay