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

                                              





                        
                        

                                        

                                                                                                                      
 

                                          
                                     





                                                                                                




                                         
                                                    
 

                         



                                                 
                                                                         
                 
 


                                              
                                
 

                                                                   
                                                                  



                                                 



                                                
                                          



                                                                                                                        
                                                


                                                                                                               
                                                            





                                                                                               












                                                                                             

                                                    
                         
 






                                                               


          
                                                   
                                                                    

















                                                               
                                                                     



              
                                                                     


                                                                                     









                           






                                                         
                                







                                                                      


                                                                






                                                 





                                               






                                                                
                                                                        







                                                               





                                                                                          

























                                                                                                











                                                               
                                            


                                          




                                                   
 




                                                   
 
                              
                          


                                                                          
 








                                                            


















                                                                                   
                                                          





















                                                                               
import './videojs-components/settings-menu-button'
import videojs from 'video.js'
import { timeToInt } from '@shared/core-utils'
import {
  getStoredLastSubtitle,
  getStoredMute,
  getStoredVolume,
  saveLastSubtitle,
  saveMuteInStore,
  saveVideoWatchHistory,
  saveVolumeInStore
} from './peertube-player-local-storage'
import { PeerTubePluginOptions, ResolutionUpdateData, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
import { isMobile } from './utils'

const Plugin = videojs.getPlugin('plugin')

class PeerTubePlugin extends Plugin {
  private readonly videoViewUrl: string
  private readonly videoDuration: number
  private readonly CONSTANTS = {
    USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
  }

  private videoCaptions: VideoJSCaption[]
  private defaultSubtitle: string

  private videoViewInterval: any
  private userWatchingVideoInterval: any
  private lastResolutionChange: ResolutionUpdateData

  private isLive: boolean

  private menuOpened = false
  private mouseInControlBar = false
  private readonly savedInactivityTimeout: number

  constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
    super(player)

    this.videoViewUrl = options.videoViewUrl
    this.videoDuration = options.videoDuration
    this.videoCaptions = options.videoCaptions
    this.isLive = options.isLive

    this.savedInactivityTimeout = player.options_.inactivityTimeout

    if (options.autoplay) this.player.addClass('vjs-has-autoplay')

    this.player.on('autoplay-failure', () => {
      this.player.removeClass('vjs-has-autoplay')
    })

    this.player.ready(() => {
      const playerOptions = this.player.options_

      if (options.mode === 'webtorrent') {
        this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
        this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
      }

      if (options.mode === 'p2p-media-loader') {
        this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
      }

      this.player.tech(true).on('loadedqualitydata', () => {
        setTimeout(() => {
          // Replay a resolution change, now we loaded all quality data
          if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
        }, 0)
      })

      const volume = getStoredVolume()
      if (volume !== undefined) this.player.volume(volume)

      const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
      if (muted !== undefined) this.player.muted(muted)

      this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()

      this.player.on('volumechange', () => {
        saveVolumeInStore(this.player.volume())
        saveMuteInStore(this.player.muted())
      })

      if (options.stopTime) {
        const stopTime = timeToInt(options.stopTime)
        const self = this

        this.player.on('timeupdate', function onTimeUpdate () {
          if (self.player.currentTime() > stopTime) {
            self.player.pause()
            self.player.trigger('stopped')

            self.player.off('timeupdate', onTimeUpdate)
          }
        })
      }

      this.player.textTracks().on('change', () => {
        const showing = this.player.textTracks().tracks_.find(t => {
          return t.kind === 'captions' && t.mode === 'showing'
        })

        if (!showing) {
          saveLastSubtitle('off')
          return
        }

        saveLastSubtitle(showing.language)
      })

      this.player.on('sourcechange', () => this.initCaptions())

      this.player.duration(options.videoDuration)

      this.initializePlayer()
      this.runViewAdd()

      this.runUserWatchVideo(options.userWatching, options.videoUUID)
    })
  }

  dispose () {
    if (this.videoViewInterval) clearInterval(this.videoViewInterval)
    if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
  }

  onMenuOpen () {
    this.menuOpened = false
    this.alterInactivity()
  }

  onMenuClosed () {
    this.menuOpened = true
    this.alterInactivity()
  }

  private initializePlayer () {
    if (isMobile()) this.player.addClass('vjs-is-mobile')

    this.initSmoothProgressBar()

    this.initCaptions()

    this.listenControlBarMouse()
  }

  private runViewAdd () {
    this.clearVideoViewInterval()

    // After 30 seconds (or 3/4 of the video), add a view to the video
    let minSecondsToView = 30

    if (!this.isLive && this.videoDuration < minSecondsToView) {
      minSecondsToView = (this.videoDuration * 3) / 4
    }

    let secondsViewed = 0
    this.videoViewInterval = setInterval(() => {
      if (this.player && !this.player.paused()) {
        secondsViewed += 1

        if (secondsViewed > minSecondsToView) {
          // Restart the loop if this is a live
          if (this.isLive) {
            secondsViewed = 0
          } else {
            this.clearVideoViewInterval()
          }

          this.addViewToVideo().catch(err => console.error(err))
        }
      }
    }, 1000)
  }

  private runUserWatchVideo (options: UserWatching, videoUUID: string) {
    let lastCurrentTime = 0

    this.userWatchingVideoInterval = setInterval(() => {
      const currentTime = Math.floor(this.player.currentTime())

      if (currentTime - lastCurrentTime >= 1) {
        lastCurrentTime = currentTime

        if (options) {
          this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
            .catch(err => console.error('Cannot notify user is watching.', err))
        } else {
          saveVideoWatchHistory(videoUUID, currentTime)
        }
      }
    }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
  }

  private clearVideoViewInterval () {
    if (this.videoViewInterval !== undefined) {
      clearInterval(this.videoViewInterval)
      this.videoViewInterval = undefined
    }
  }

  private addViewToVideo () {
    if (!this.videoViewUrl) return Promise.resolve(undefined)

    return fetch(this.videoViewUrl, { method: 'POST' })
  }

  private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
    const body = new URLSearchParams()
    body.append('currentTime', currentTime.toString())

    const headers = new Headers({ 'Authorization': authorizationHeader })

    return fetch(url, { method: 'PUT', body, headers })
  }

  private handleResolutionChange (data: ResolutionUpdateData) {
    this.lastResolutionChange = data

    const qualityLevels = this.player.qualityLevels()

    for (let i = 0; i < qualityLevels.length; i++) {
      if (qualityLevels[i].height === data.resolutionId) {
        data.id = qualityLevels[i].id
        break
      }
    }

    console.log('Resolution changed.', data)
    this.trigger('resolutionChange', data)
  }

  private listenControlBarMouse () {
    this.player.controlBar.on('mouseenter', () => {
      this.mouseInControlBar = true
      this.alterInactivity()
    })

    this.player.controlBar.on('mouseleave', () => {
      this.mouseInControlBar = false
      this.alterInactivity()
    })
  }

  private alterInactivity () {
    if (this.menuOpened) {
      this.player.options_.inactivityTimeout = this.savedInactivityTimeout
      return
    }

    if (!this.mouseInControlBar && !this.isTouchEnabled()) {
      this.player.options_.inactivityTimeout = 1
    }
  }

  private isTouchEnabled () {
    return ('ontouchstart' in window) ||
      navigator.maxTouchPoints > 0 ||
      navigator.msMaxTouchPoints > 0
  }

  private initCaptions () {
    for (const caption of this.videoCaptions) {
      this.player.addRemoteTextTrack({
        kind: 'captions',
        label: caption.label,
        language: caption.language,
        id: caption.language,
        src: caption.src,
        default: this.defaultSubtitle === caption.language
      }, false)
    }

    this.player.trigger('captionsChanged')
  }

  // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
  private initSmoothProgressBar () {
    const SeekBar = videojs.getComponent('SeekBar') as any
    SeekBar.prototype.getPercent = function getPercent () {
      // Allows for smooth scrubbing, when player can't keep up.
      // const time = (this.player_.scrubbing()) ?
      //   this.player_.getCache().currentTime :
      //   this.player_.currentTime()
      const time = this.player_.currentTime()
      const percent = time / this.player_.duration()
      return percent >= 1 ? 1 : percent
    }
    SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
      let newTime = this.calculateDistance(event) * this.player_.duration()
      if (newTime === this.player_.duration()) {
        newTime = newTime - 0.1
      }
      this.player_.currentTime(newTime)
      this.update()
    }
  }
}

videojs.registerPlugin('peertube', PeerTubePlugin)
export { PeerTubePlugin }