PeerTube/client/src/assets/player/shared/peertube/peertube-plugin.ts

326 lines
9.3 KiB
TypeScript
Raw Normal View History

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'
import { logger } from '@root-helpers/logger'
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'
import {
getStoredLastSubtitle,
getStoredMute,
getStoredVolume,
saveLastSubtitle,
saveMuteInStore,
saveVideoWatchHistory,
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 debugLogger = debug('peertube:player:peertube')
2020-01-28 17:29:50 +01:00
const Plugin = videojs.getPlugin('plugin')
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
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
}
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
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)
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)
this.videoCaptions = options.videoCaptions
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')
})
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 => {
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()
})
}
dispose () {
2019-03-07 17:06:00 +01:00
if (this.videoViewInterval) clearInterval(this.videoViewInterval)
}
onMenuOpened () {
this.menuOpened = true
2019-08-07 10:17:19 +02:00
this.alterInactivity()
}
onMenuClosed () {
this.menuOpened = false
2019-08-07 10:17:19 +02:00
this.alterInactivity()
}
2022-02-02 11:16:23 +01:00
displayFatalError () {
this.player.loadingSpinner.hide()
const buildModal = (error: MediaError) => {
const localize = this.player.localize.bind(this.player)
const wrapper = document.createElement('div')
const header = document.createElement('h1')
header.innerText = localize('Failed to play video')
wrapper.appendChild(header)
const desc = document.createElement('div')
desc.innerText = localize('The video failed to play due to technical issues.')
wrapper.appendChild(desc)
const details = document.createElement('p')
details.classList.add('error-details')
details.innerText = error.message
wrapper.appendChild(details)
return wrapper
}
const modal = this.player.createModal(buildModal(this.player.error()), {
temporary: false,
uncloseable: true
})
modal.addClass('vjs-custom-error-display')
2022-02-02 11:16:23 +01:00
this.player.addClass('vjs-error-display-enabled')
}
hideFatalError () {
this.player.removeClass('vjs-error-display-enabled')
}
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()
}
// ---------------------------------------------------------------------------
2022-04-05 14:03:52 +02:00
private runUserViewing () {
let lastCurrentTime = this.startTime
let lastViewEvent: VideoViewEvent
2022-04-05 14:03:52 +02:00
this.player.one('play', () => {
this.notifyUserIsWatching(this.startTime, lastViewEvent)
})
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
2022-04-05 14:03:52 +02:00
lastViewEvent = 'seek'
})
2022-04-05 14:03:52 +02:00
this.player.one('ended', () => {
2022-04-15 10:47:48 +02:00
const currentTime = Math.round(this.player.duration())
2022-04-05 14:03:52 +02:00
lastCurrentTime = currentTime
2022-04-05 14:03:52 +02:00
this.notifyUserIsWatching(currentTime, lastViewEvent)
2022-04-05 14:03:52 +02:00
lastViewEvent = undefined
})
this.videoViewInterval = setInterval(() => {
2022-04-15 10:47:48 +02:00
const currentTime = Math.round(this.player.currentTime())
2022-04-05 14:03:52 +02:00
// No need to update
if (currentTime === lastCurrentTime) return
2022-04-05 14:03:52 +02:00
lastCurrentTime = currentTime
2022-04-05 14:03:52 +02:00
this.notifyUserIsWatching(currentTime, lastViewEvent)
.catch(err => logger.error('Cannot notify user is watching.', err))
2022-04-05 14:03:52 +02:00
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)
}
2022-04-05 14:03:52 +02:00
private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
if (!this.videoViewUrl) return Promise.resolve(undefined)
2022-04-05 14:03:52 +02:00
const body: VideoView = {
currentTime,
viewEvent
}
2022-04-05 14:03:52 +02:00
const headers = new Headers({
'Content-type': 'application/json; charset=UTF-8'
})
2022-04-05 14:03:52 +02:00
if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader)
2022-04-05 14:03:52 +02:00
return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
}
// ---------------------------------------------------------------------------
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 () {
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()
})
controlBar.on('mouseleave', () => {
2019-08-07 10:17:19 +02:00
this.mouseInControlBar = false
this.alterInactivity()
})
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-08-07 10:17:19 +02:00
private alterInactivity () {
2022-01-11 11:26:35 +01:00
if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
this.setInactivityTimeout(0)
2019-08-07 10:17:19 +02:00
return
}
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
debugLogger('Set player inactivity to ' + timeout)
2020-07-02 15:10:06 +02: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
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 }