201 lines
5.7 KiB
TypeScript
201 lines
5.7 KiB
TypeScript
import debug from 'debug'
|
|
import videojs from 'video.js'
|
|
import { logger } from '@root-helpers/logger'
|
|
import { addQueryParams } from '@peertube/peertube-core-utils'
|
|
import { VideoFile } from '@peertube/peertube-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
|
|
private onLoadedMetadata: () => void
|
|
|
|
constructor (player: videojs.Player, options?: WebVideoPluginOptions) {
|
|
super(player, options)
|
|
|
|
this.videoFiles = options.videoFiles
|
|
this.videoFileToken = options.videoFileToken
|
|
|
|
const videoFile = this.pickAverageVideoFile()
|
|
if (videoFile) this.updateVideoFile({ videoFile, isUserResolutionChange: false })
|
|
|
|
this.onLoadedMetadata = () => {
|
|
player.trigger('video-ratio-changed', { ratio: this.player.videoWidth() / this.player.videoHeight() })
|
|
}
|
|
|
|
player.on('loadedmetadata', this.onLoadedMetadata)
|
|
|
|
player.ready(() => {
|
|
if (this.videoFiles.length === 0) {
|
|
this.player.addClass('disabled')
|
|
return
|
|
}
|
|
|
|
this.buildQualities()
|
|
|
|
this.setupNetworkInfoInterval()
|
|
})
|
|
}
|
|
|
|
dispose () {
|
|
if (this.networkInfoInterval) clearInterval(this.networkInfoInterval)
|
|
|
|
if (this.onLoadedMetadata) this.player.off('loadedmetadata', this.onLoadedMetadata)
|
|
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()
|
|
|
|
if (!this.onErrorHandler) {
|
|
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 || this.videoFiles.length === 0) return undefined
|
|
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('network-info', {
|
|
source: 'web-video',
|
|
http: {
|
|
downloaded: this.player.bufferedPercent() * this.currentVideoFile?.size
|
|
}
|
|
} as PlayerNetworkInfo)
|
|
}, 1000)
|
|
}
|
|
}
|
|
|
|
videojs.registerPlugin('webVideo', WebVideoPlugin)
|
|
export { WebVideoPlugin }
|