2022-03-14 10:27:05 +01:00
|
|
|
import debug from 'debug'
|
2021-07-26 15:04:37 +02:00
|
|
|
import videojs from 'video.js'
|
2022-03-14 14:28:20 +01:00
|
|
|
import { isMobile } from '@root-helpers/web-browser'
|
2021-07-26 15:04:37 +02:00
|
|
|
import { timeToInt } from '@shared/core-utils'
|
2022-04-05 14:03:52 +02:00
|
|
|
import { VideoView, VideoViewEvent } from '@shared/models/videos'
|
2019-01-23 15:36:45 +01:00
|
|
|
import {
|
|
|
|
getStoredLastSubtitle,
|
|
|
|
getStoredMute,
|
|
|
|
getStoredVolume,
|
|
|
|
saveLastSubtitle,
|
|
|
|
saveMuteInStore,
|
2021-03-31 11:26:32 +02:00
|
|
|
saveVideoWatchHistory,
|
2019-01-23 15:36:45 +01:00
|
|
|
saveVolumeInStore
|
2022-03-14 14:28:20 +01:00
|
|
|
} from '../../peertube-player-local-storage'
|
2022-04-05 14:03:52 +02:00
|
|
|
import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
|
2022-03-14 14:28:20 +01:00
|
|
|
import { SettingsButton } from '../settings/settings-menu-button'
|
2022-01-11 11:26:35 +01:00
|
|
|
|
|
|
|
const logger = debug('peertube:player:peertube')
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2020-01-28 17:29:50 +01:00
|
|
|
const Plugin = videojs.getPlugin('plugin')
|
|
|
|
|
2019-01-23 15:36:45 +01:00
|
|
|
class PeerTubePlugin extends Plugin {
|
|
|
|
private readonly videoViewUrl: string
|
2022-04-05 14:03:52 +02:00
|
|
|
private readonly authorizationHeader: string
|
|
|
|
|
|
|
|
private readonly videoUUID: string
|
|
|
|
private readonly startTime: number
|
|
|
|
|
2019-01-23 15:36:45 +01:00
|
|
|
private readonly CONSTANTS = {
|
2022-04-05 14:03:52 +02:00
|
|
|
USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
|
2019-01-23 15:36:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private videoCaptions: VideoJSCaption[]
|
|
|
|
private defaultSubtitle: string
|
|
|
|
|
|
|
|
private videoViewInterval: any
|
2020-12-07 15:58:57 +01:00
|
|
|
|
2019-08-07 10:17:19 +02:00
|
|
|
private menuOpened = false
|
|
|
|
private mouseInControlBar = false
|
2021-12-09 10:33:32 +01:00
|
|
|
private mouseInSettings = false
|
|
|
|
private readonly initialInactivityTimeout: number
|
2019-08-07 10:17:19 +02:00
|
|
|
|
2020-04-17 11:20:12 +02:00
|
|
|
constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
|
2020-01-28 17:29:50 +01:00
|
|
|
super(player)
|
2019-01-23 15:36:45 +01:00
|
|
|
|
|
|
|
this.videoViewUrl = options.videoViewUrl
|
2022-04-05 14:03:52 +02:00
|
|
|
this.authorizationHeader = options.authorizationHeader
|
|
|
|
this.videoUUID = options.videoUUID
|
|
|
|
this.startTime = timeToInt(options.startTime)
|
|
|
|
|
2019-01-23 15:36:45 +01:00
|
|
|
this.videoCaptions = options.videoCaptions
|
2021-12-09 10:33:32 +01:00
|
|
|
this.initialInactivityTimeout = this.player.options_.inactivityTimeout
|
2019-08-07 10:17:19 +02:00
|
|
|
|
2020-05-11 17:48:25 +02:00
|
|
|
if (options.autoplay) this.player.addClass('vjs-has-autoplay')
|
2019-02-06 10:39:50 +01:00
|
|
|
|
|
|
|
this.player.on('autoplay-failure', () => {
|
|
|
|
this.player.removeClass('vjs-has-autoplay')
|
|
|
|
})
|
2019-01-23 15:36:45 +01:00
|
|
|
|
|
|
|
this.player.ready(() => {
|
|
|
|
const playerOptions = this.player.options_
|
|
|
|
|
|
|
|
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())
|
|
|
|
})
|
|
|
|
|
2019-03-07 17:06:00 +01:00
|
|
|
if (options.stopTime) {
|
|
|
|
const stopTime = timeToInt(options.stopTime)
|
2019-03-13 14:18:58 +01:00
|
|
|
const self = this
|
2019-03-07 17:06:00 +01:00
|
|
|
|
2019-03-13 14:18:58 +01:00
|
|
|
this.player.on('timeupdate', function onTimeUpdate () {
|
|
|
|
if (self.player.currentTime() > stopTime) {
|
|
|
|
self.player.pause()
|
|
|
|
self.player.trigger('stopped')
|
|
|
|
|
|
|
|
self.player.off('timeupdate', onTimeUpdate)
|
|
|
|
}
|
2019-03-07 17:06:00 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-06 16:08:59 +02:00
|
|
|
this.player.textTracks().addEventListener('change', () => {
|
2020-01-28 17:29:50 +01:00
|
|
|
const showing = this.player.textTracks().tracks_.find(t => {
|
2019-01-23 15:36:45 +01:00
|
|
|
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()
|
2022-04-05 14:03:52 +02:00
|
|
|
this.runUserViewing()
|
2019-01-23 15:36:45 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose () {
|
2019-03-07 17:06:00 +01:00
|
|
|
if (this.videoViewInterval) clearInterval(this.videoViewInterval)
|
2019-01-23 15:36:45 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 10:33:32 +01:00
|
|
|
onMenuOpened () {
|
|
|
|
this.menuOpened = true
|
2019-08-07 10:17:19 +02:00
|
|
|
this.alterInactivity()
|
|
|
|
}
|
|
|
|
|
|
|
|
onMenuClosed () {
|
2021-12-09 10:33:32 +01:00
|
|
|
this.menuOpened = false
|
2019-08-07 10:17:19 +02:00
|
|
|
this.alterInactivity()
|
|
|
|
}
|
|
|
|
|
2022-02-02 11:16:23 +01:00
|
|
|
displayFatalError () {
|
|
|
|
this.player.addClass('vjs-error-display-enabled')
|
|
|
|
}
|
|
|
|
|
|
|
|
hideFatalError () {
|
|
|
|
this.player.removeClass('vjs-error-display-enabled')
|
|
|
|
}
|
|
|
|
|
2019-01-23 15:36:45 +01:00
|
|
|
private initializePlayer () {
|
|
|
|
if (isMobile()) this.player.addClass('vjs-is-mobile')
|
|
|
|
|
|
|
|
this.initSmoothProgressBar()
|
|
|
|
|
|
|
|
this.initCaptions()
|
|
|
|
|
2019-08-07 10:17:19 +02:00
|
|
|
this.listenControlBarMouse()
|
2021-12-09 09:41:26 +01:00
|
|
|
|
|
|
|
this.listenFullScreenChange()
|
2019-01-23 15:36:45 +01:00
|
|
|
}
|
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
private runUserViewing () {
|
|
|
|
let lastCurrentTime = this.startTime
|
|
|
|
let lastViewEvent: VideoViewEvent
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
this.player.one('play', () => {
|
|
|
|
this.notifyUserIsWatching(this.startTime, lastViewEvent)
|
|
|
|
})
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
this.player.on('seeked', () => {
|
|
|
|
// Don't take into account small seek events
|
|
|
|
if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
lastViewEvent = 'seek'
|
|
|
|
})
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
this.player.one('ended', () => {
|
|
|
|
const currentTime = Math.floor(this.player.duration())
|
|
|
|
lastCurrentTime = currentTime
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
this.notifyUserIsWatching(currentTime, lastViewEvent)
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
lastViewEvent = undefined
|
|
|
|
})
|
|
|
|
|
|
|
|
this.videoViewInterval = setInterval(() => {
|
2019-01-23 15:36:45 +01:00
|
|
|
const currentTime = Math.floor(this.player.currentTime())
|
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
// No need to update
|
|
|
|
if (currentTime === lastCurrentTime) return
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
lastCurrentTime = currentTime
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
this.notifyUserIsWatching(currentTime, lastViewEvent)
|
|
|
|
.catch(err => console.error('Cannot notify user is watching.', err))
|
|
|
|
|
|
|
|
lastViewEvent = undefined
|
|
|
|
|
|
|
|
// Server won't save history, so save the video position in local storage
|
|
|
|
if (!this.authorizationHeader) {
|
|
|
|
saveVideoWatchHistory(this.videoUUID, currentTime)
|
|
|
|
}
|
|
|
|
}, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL)
|
2019-01-23 15:36:45 +01:00
|
|
|
}
|
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
|
2019-01-23 15:36:45 +01:00
|
|
|
if (!this.videoViewUrl) return Promise.resolve(undefined)
|
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
const body: VideoView = {
|
|
|
|
currentTime,
|
|
|
|
viewEvent
|
|
|
|
}
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
const headers = new Headers({
|
|
|
|
'Content-type': 'application/json; charset=UTF-8'
|
|
|
|
})
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader)
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2022-04-05 14:03:52 +02:00
|
|
|
return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
|
2019-01-23 15:36:45 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 09:41:26 +01:00
|
|
|
private listenFullScreenChange () {
|
|
|
|
this.player.on('fullscreenchange', () => {
|
|
|
|
if (this.player.isFullscreen()) this.player.focus()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-08-07 10:17:19 +02:00
|
|
|
private listenControlBarMouse () {
|
2021-12-09 10:33:32 +01:00
|
|
|
const controlBar = this.player.controlBar
|
|
|
|
const settingsButton: SettingsButton = (controlBar as any).settingsButton
|
|
|
|
|
|
|
|
controlBar.on('mouseenter', () => {
|
2019-08-07 10:17:19 +02:00
|
|
|
this.mouseInControlBar = true
|
|
|
|
this.alterInactivity()
|
|
|
|
})
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2021-12-09 10:33:32 +01:00
|
|
|
controlBar.on('mouseleave', () => {
|
2019-08-07 10:17:19 +02:00
|
|
|
this.mouseInControlBar = false
|
|
|
|
this.alterInactivity()
|
|
|
|
})
|
2021-12-09 10:33:32 +01:00
|
|
|
|
|
|
|
settingsButton.dialog.on('mouseenter', () => {
|
|
|
|
this.mouseInSettings = true
|
|
|
|
this.alterInactivity()
|
|
|
|
})
|
|
|
|
|
|
|
|
settingsButton.dialog.on('mouseleave', () => {
|
|
|
|
this.mouseInSettings = false
|
|
|
|
this.alterInactivity()
|
|
|
|
})
|
2019-08-07 10:17:19 +02:00
|
|
|
}
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2019-08-07 10:17:19 +02:00
|
|
|
private alterInactivity () {
|
2022-01-11 11:26:35 +01:00
|
|
|
if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
|
2021-12-09 10:33:32 +01:00
|
|
|
this.setInactivityTimeout(0)
|
2019-08-07 10:17:19 +02:00
|
|
|
return
|
|
|
|
}
|
2019-01-23 15:36:45 +01:00
|
|
|
|
2021-12-09 10:33:32 +01:00
|
|
|
this.setInactivityTimeout(this.initialInactivityTimeout)
|
|
|
|
this.player.reportUserActivity(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
private setInactivityTimeout (timeout: number) {
|
|
|
|
(this.player as any).cache_.inactivityTimeout = timeout
|
|
|
|
this.player.options_.inactivityTimeout = timeout
|
2022-01-11 11:26:35 +01:00
|
|
|
|
|
|
|
logger('Set player inactivity to ' + timeout)
|
2020-07-02 15:10:06 +02:00
|
|
|
}
|
|
|
|
|
2019-01-23 15:36:45 +01:00
|
|
|
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 () {
|
2020-01-28 17:29:50 +01:00
|
|
|
const SeekBar = videojs.getComponent('SeekBar') as any
|
2019-01-23 15:36:45 +01:00
|
|
|
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 }
|