aboutsummaryrefslogblamecommitdiffhomepage
path: root/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
blob: 2742b21a1cdce58aa41005fcacafee4768e02c53 (plain) (tree)
1
2
3
4
5
6
7
8
9
10





                                                                                               



                              







                                                                    


                                                                                         

                          

                                




















































                                                                                      

























                                                                                           

                                 









                                                                      

                                 









                                                                         

                                 











                                                                   

                                 








                                                                   

































                                                                                                                      


                                  
                                                    
                  

                                 

























                                                                   


                                                                                                                                    
   



                                                                                   






















                                                                                                                             



                                                                      
import videojs from 'video.js'

type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void }

const Plugin = videojs.getPlugin('plugin')

export type HotkeysOptions = {
  isLive: boolean
}

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[]

  private readonly isLive: boolean

  constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) {
    super(player, options)

    this.isLive = options.isLive

    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)
        }
      },

      // 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: () => {
          if (this.isLive) return

          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: () => {
          if (this.isLive) return

          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: () => {
          if (this.isLive) return

          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: () => {
          if (this.isLive) return

          this.player.pause()

          // Calculate movement distance (assuming 30 fps)
          const dist = 1 / 30
          this.player.currentTime(this.player.currentTime() + dist)
        }
      }
    ]

    if (this.isLive) return handlers

    return handlers.concat(this.buildVODHandlers())
  }

  private buildVODHandlers () {
    const handlers: KeyHandler[] = [
      // Rewind
      {
        accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'),
        cb: e => {
          if (this.isLive) return

          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 => {
          if (this.isLive) return

          e.preventDefault()

          const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP)
          this.player.currentTime(target)
        }
      }
    ]

    // 0-9 key handlers
    for (let i = 0; i < 10; i++) {
      handlers.push({
        accept: e => this.isNakedOrShift(e, i + ''),
        cb: e => {
          if (this.isLive) return

          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) {
    if (key.length === 1) key = key.toUpperCase()

    return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && this.getLatinKey(event.key, event.code) === key)
  }

  private isNakedOrShift (event: KeyboardEvent, key: string) {
    return (!event.ctrlKey && !event.altKey && !event.metaKey && event.key === key)
  }

  // Thanks Maciej Krawczyk
  // https://stackoverflow.com/questions/70211837/keyboard-shortcuts-commands-on-non-latin-alphabet-keyboards-javascript?rq=1
  private getLatinKey (key: string, code: string) {
    if (key.length !== 1) {
      return key
    }

    const capitalHetaCode = 880
    const isNonLatin = key.charCodeAt(0) >= capitalHetaCode

    if (isNonLatin) {
      if (code.indexOf('Key') === 0 && code.length === 4) { // i.e. 'KeyW'
        return code.charAt(3)
      }

      if (code.indexOf('Digit') === 0 && code.length === 6) { // i.e. 'Digit7'
        return code.charAt(5)
      }
    }

    return key.toUpperCase()
  }
}

videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin)
export { PeerTubeHotkeysPlugin }