import videojs from 'video.js'
import './videojs-components/settings-menu-button'
import {
PeerTubePluginOptions,
ResolutionUpdateData,
UserWatching,
VideoJSCaption
} from './peertube-videojs-typings'
import { isMobile, timeToInt } from './utils'
import {
getStoredLastSubtitle,
getStoredMute,
getStoredVolume,
saveLastSubtitle,
saveMuteInStore,
saveVideoWatchHistory,
saveVolumeInStore
} from './peertube-player-local-storage'
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
}
}
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 }