aboutsummaryrefslogblamecommitdiffhomepage
path: root/client/src/assets/player/shared/web-video/web-video-plugin.ts
blob: 930b5045a59641f5d1fd5cdc4ec2352137c79f23 (plain) (tree)














































































                                                                                                   


                                                                                         










































































































                                                                                              
import debug from 'debug'
import videojs from 'video.js'
import { logger } from '@root-helpers/logger'
import { addQueryParams } from '@shared/core-utils'
import { VideoFile } from '@shared/models'
import { PeerTubeResolution, PlayerNetworkInfo, WebVideoPluginOptions } from '../../types'

const debugLogger = debug('peertube:player:web-video-plugin')

const Plugin = videojs.getPlugin('plugin')

class WebVideoPlugin extends Plugin {
  private readonly videoFiles: VideoFile[]

  private currentVideoFile: VideoFile
  private videoFileToken: () => string

  private networkInfoInterval: any

  private onErrorHandler: () => void
  private onPlayHandler: () => void

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

    this.videoFiles = options.videoFiles
    this.videoFileToken = options.videoFileToken

    this.updateVideoFile({ videoFile: this.pickAverageVideoFile(), isUserResolutionChange: false })

    player.ready(() => {
      this.buildQualities()

      this.setupNetworkInfoInterval()

      if (this.videoFiles.length === 0) {
        this.player.addClass('disabled')
        return
      }
    })
  }

  dispose () {
    clearInterval(this.networkInfoInterval)

    if (this.onErrorHandler) this.player.off('error', this.onErrorHandler)
    if (this.onPlayHandler) this.player.off('canplay', this.onPlayHandler)

    super.dispose()
  }

  getCurrentResolutionId () {
    return this.currentVideoFile.resolution.id
  }

  updateVideoFile (options: {
    videoFile: VideoFile
    isUserResolutionChange: boolean
  }) {
    this.currentVideoFile = options.videoFile

    debugLogger('Updating web video file to ' + this.currentVideoFile.fileUrl)

    const paused = this.player.paused()
    const playbackRate = this.player.playbackRate()
    const currentTime = this.player.currentTime()

    // Enable error display now this is our last fallback
    this.onErrorHandler = () => this.player.peertube().displayFatalError()
    this.player.one('error', this.onErrorHandler)

    let httpUrl = this.currentVideoFile.fileUrl

    if (this.videoFileToken()) {
      httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() })
    }

    const oldAutoplayValue = this.player.autoplay()
    if (options.isUserResolutionChange) {
      // Prevent video source element displaying the poster when we change the resolution
      (this.player.el() as HTMLVideoElement).poster = ''

      this.player.autoplay(false)
      this.player.addClass('vjs-updating-resolution')
    }

    this.player.src(httpUrl)

    this.onPlayHandler = () => {
      this.player.playbackRate(playbackRate)
      this.player.currentTime(currentTime)

      this.adaptPosterForAudioOnly()

      if (options.isUserResolutionChange) {
        this.player.trigger('user-resolution-change')
        this.player.trigger('web-video-source-change')

        this.tryToPlay()
          .then(() => {
            if (paused) this.player.pause()

            this.player.autoplay(oldAutoplayValue)
          })
      }
    }

    this.player.one('canplay', this.onPlayHandler)
  }

  getCurrentVideoFile () {
    return this.currentVideoFile
  }

  private adaptPosterForAudioOnly () {
    // Audio-only (resolutionId === 0) gets special treatment
    if (this.currentVideoFile.resolution.id === 0) {
      this.player.audioPosterMode(true)
    } else {
      this.player.audioPosterMode(false)
    }
  }

  private tryToPlay () {
    debugLogger('Try to play manually the video')

    const playPromise = this.player.play()
    if (playPromise === undefined) return

    return playPromise
      .catch((err: Error) => {
        if (err.message.includes('The play() request was interrupted by a call to pause()')) {
          return
        }

        logger.warn(err)
        this.player.pause()
        this.player.posterImage.show()
        this.player.removeClass('vjs-has-autoplay')
        this.player.removeClass('vjs-playing-audio-only-content')
      })
      .finally(() => {
        this.player.removeClass('vjs-updating-resolution')
      })
  }

  private pickAverageVideoFile () {
    if (this.videoFiles.length === 1) return this.videoFiles[0]

    const files = this.videoFiles.filter(f => f.resolution.id !== 0)
    return files[Math.floor(files.length / 2)]
  }

  private buildQualities () {
    const resolutions: PeerTubeResolution[] = this.videoFiles.map(videoFile => ({
      id: videoFile.resolution.id,
      label: this.buildQualityLabel(videoFile),
      height: videoFile.resolution.id,
      selected: videoFile.id === this.currentVideoFile.id,
      selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true })
    }))

    this.player.peertubeResolutions().add(resolutions)
  }

  private buildQualityLabel (file: VideoFile) {
    let label = file.resolution.label

    if (file.fps && file.fps >= 50) {
      label += file.fps
    }

    return label
  }

  private setupNetworkInfoInterval () {
    this.networkInfoInterval = setInterval(() => {
      return this.player.trigger('http-info', {
        source: 'web-video',
        http: {
          downloaded: this.player.bufferedPercent() * this.currentVideoFile.size
        }
      } as PlayerNetworkInfo)
    }, 1000)
  }
}

videojs.registerPlugin('webVideo', WebVideoPlugin)
export { WebVideoPlugin }