mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2026-09-03 20:53:09 -05:00
750 lines
22 KiB
TypeScript
750 lines
22 KiB
TypeScript
import { timeToInt } from '@peertube/peertube-core-utils'
|
|
import { VideoStatsUserAgentDevice, VideoView, VideoViewEvent } from '@peertube/peertube-models'
|
|
import { logger } from '@root-helpers/logger'
|
|
import { isIOS, isMobile, isSafari } from '@root-helpers/web-browser'
|
|
import debug from 'debug'
|
|
import { UAParser } from 'ua-parser-js'
|
|
import videojs from 'video.js'
|
|
import type MediaError from 'video.js/dist/types/media-error'
|
|
import type ModalDialog from 'video.js/dist/types/modal-dialog'
|
|
import {
|
|
getPlayerSessionId,
|
|
getStoredLastSubtitle,
|
|
getStoredMute,
|
|
getStoredPlaybackRate,
|
|
getStoredVolume,
|
|
saveLastSubtitle,
|
|
saveMuteInStore,
|
|
savePlaybackRateInStore,
|
|
savePreferredSubtitle,
|
|
saveVideoWatchHistory,
|
|
saveVolumeInStore
|
|
} from '../../peertube-player-local-storage'
|
|
import type {
|
|
PeerTubePluginOptions,
|
|
VideojsAutoplay,
|
|
VideojsCaptionsButton,
|
|
VideojsPlayer,
|
|
VideojsPlayerOptions,
|
|
VideojsPlugin
|
|
} from '../../types'
|
|
import { SettingsButton } from '../settings/settings-menu-button'
|
|
|
|
const debugLogger = debug('peertube:player:peertube')
|
|
|
|
const Plugin = videojs.getPlugin('plugin') as typeof VideojsPlugin
|
|
|
|
class PeerTubePlugin extends Plugin {
|
|
declare private readonly videoViewUrl: () => string
|
|
declare private readonly authorizationHeader: () => string
|
|
declare private readonly initialInactivityTimeout: number
|
|
|
|
declare private readonly hasAutoplay: () => VideojsAutoplay
|
|
|
|
declare private currentSubtitle: string
|
|
declare private currentPlaybackRate: number
|
|
|
|
declare private videoViewInterval: any
|
|
|
|
declare private menuOpened: boolean
|
|
declare private mouseInControlBar: boolean
|
|
declare private mouseInSettings: boolean
|
|
|
|
declare private errorModal: ModalDialog
|
|
|
|
declare private hasInitialSeek: boolean
|
|
|
|
declare private videoViewOnPlayHandler: (...args: any[]) => void
|
|
declare private videoViewOnSeekedHandler: (...args: any[]) => void
|
|
declare private videoViewOnEndedHandler: (...args: any[]) => void
|
|
declare private adaptPosterForAudioOnlyPlayHandler: (...args: any[]) => void
|
|
|
|
declare private stopTimeHandler: (...args: any[]) => void
|
|
|
|
declare private resizeObserver: ResizeObserver
|
|
declare private userAgentInfo: {
|
|
client: string
|
|
device: VideoStatsUserAgentDevice
|
|
os: string
|
|
}
|
|
|
|
constructor (player: VideojsPlayer, private readonly options: PeerTubePluginOptions) {
|
|
super(player)
|
|
|
|
this.setUserAgentInfo()
|
|
|
|
this.menuOpened = false
|
|
this.mouseInControlBar = false
|
|
this.mouseInSettings = false
|
|
this.hasInitialSeek = false
|
|
|
|
this.videoViewUrl = options.videoViewUrl
|
|
this.authorizationHeader = options.authorizationHeader
|
|
this.hasAutoplay = options.hasAutoplay
|
|
|
|
this.initialInactivityTimeout = this.player.options_.inactivityTimeout
|
|
|
|
this.currentSubtitle = this.options.subtitle() || getStoredLastSubtitle()
|
|
|
|
this.initializePlayer()
|
|
this.initOnVideoChange()
|
|
|
|
this.player.removeClass('vjs-can-play')
|
|
|
|
this.deleteLegacyIndexedDB()
|
|
|
|
this.player.on('autoplay-failure', () => {
|
|
debugLogger('Autoplay failed')
|
|
|
|
this.player.removeClass('vjs-has-autoplay')
|
|
|
|
this.player.poster(options.poster())
|
|
|
|
// Fix a bug on iOS/Safari where the big play button is not displayed when autoplay fails
|
|
if (isIOS() || isSafari()) this.player.hasStarted(false)
|
|
})
|
|
|
|
this.player.one('canplay', () => {
|
|
const playerOptions = this.player.options_ as VideojsPlayerOptions
|
|
|
|
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)
|
|
|
|
const savedPlaybackRate = this.options.playbackRate ?? getStoredPlaybackRate()
|
|
if (savedPlaybackRate !== undefined) {
|
|
this.currentPlaybackRate = savedPlaybackRate
|
|
this.player.playbackRate(this.currentPlaybackRate)
|
|
this.player.defaultPlaybackRate(this.currentPlaybackRate)
|
|
}
|
|
|
|
this.player.addClass('vjs-can-play')
|
|
})
|
|
|
|
this.player.ready(() => {
|
|
this.player.on('volumechange', () => {
|
|
saveVolumeInStore(this.player.volume())
|
|
saveMuteInStore(this.player.muted())
|
|
})
|
|
|
|
this.player.on('ratechange', () => {
|
|
this.currentPlaybackRate = this.player.playbackRate()
|
|
this.player.defaultPlaybackRate(this.currentPlaybackRate)
|
|
|
|
// Don't save in store if the rate change is not a user action
|
|
if (this.currentPlaybackRate !== this.options.playbackRate) {
|
|
savePlaybackRateInStore(this.currentPlaybackRate)
|
|
}
|
|
})
|
|
|
|
this.player.textTracks().addEventListener('change', () => {
|
|
const showing = this.player.textTracks().tracks_.find(t => {
|
|
return t.kind === 'captions' && t.mode === 'showing'
|
|
})
|
|
|
|
if (!showing) {
|
|
saveLastSubtitle('off')
|
|
this.currentSubtitle = undefined
|
|
return
|
|
}
|
|
|
|
if (this.currentSubtitle === showing.language) return
|
|
|
|
this.currentSubtitle = showing.language
|
|
saveLastSubtitle(showing.language)
|
|
savePreferredSubtitle(showing.language)
|
|
})
|
|
|
|
this.player.on('video-change', () => {
|
|
this.initOnVideoChange()
|
|
|
|
this.hideFatalError()
|
|
})
|
|
|
|
this.updatePlayerSizeClasses()
|
|
|
|
if (typeof ResizeObserver !== 'undefined') {
|
|
this.resizeObserver = new ResizeObserver(() => {
|
|
this.updatePlayerSizeClasses()
|
|
})
|
|
|
|
this.resizeObserver.observe(this.player.el())
|
|
}
|
|
})
|
|
|
|
this.player.on('resolution-change', (_: any, { resolution }: { resolution: number }) => {
|
|
if (this.player.paused()) {
|
|
if (this.adaptPosterForAudioOnlyPlayHandler) {
|
|
this.player.off('play', this.adaptPosterForAudioOnlyPlayHandler)
|
|
this.adaptPosterForAudioOnlyPlayHandler = undefined
|
|
}
|
|
|
|
this.adaptPosterForAudioOnlyPlayHandler = () => this.adaptPosterForAudioOnly(resolution)
|
|
|
|
this.player.one('play', this.adaptPosterForAudioOnlyPlayHandler)
|
|
return
|
|
}
|
|
|
|
this.adaptPosterForAudioOnly(resolution)
|
|
})
|
|
|
|
this.initOnRatioChange()
|
|
}
|
|
|
|
dispose () {
|
|
if (this.videoViewInterval) clearInterval(this.videoViewInterval)
|
|
if (this.resizeObserver) this.resizeObserver.disconnect()
|
|
|
|
super.dispose()
|
|
}
|
|
|
|
onMenuOpened () {
|
|
this.menuOpened = true
|
|
this.alterInactivity()
|
|
}
|
|
|
|
onMenuClosed () {
|
|
this.menuOpened = false
|
|
this.alterInactivity()
|
|
}
|
|
|
|
displayFatalError (options: {
|
|
log?: boolean // default true
|
|
error?: Error | MediaError
|
|
isTechnicalError?: boolean // default true
|
|
} = {}) {
|
|
const { log = true, error = this.player.error(), isTechnicalError = true } = options
|
|
|
|
// Already displayed an error
|
|
if (this.errorModal) return
|
|
|
|
debugLogger('Display fatal error')
|
|
|
|
this.player.loadingSpinner.hide()
|
|
|
|
const buildModal = () => {
|
|
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)
|
|
|
|
if (isTechnicalError) {
|
|
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
|
|
}
|
|
|
|
this.errorModal = this.player.createModal(buildModal(), {
|
|
temporary: true,
|
|
uncloseable: true
|
|
})
|
|
this.errorModal.addClass('vjs-custom-error-display')
|
|
|
|
this.player.addClass('vjs-error-display-enabled')
|
|
|
|
if (log) {
|
|
// Google Bot may throw codecs, but it should not prevent indexing
|
|
if (/googlebot/i.test(navigator.userAgent)) {
|
|
console.error(error)
|
|
} else {
|
|
logger.error('Fatal error in player', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
hideFatalError () {
|
|
if (!this.errorModal) return
|
|
|
|
debugLogger('Hiding fatal error')
|
|
|
|
this.player.removeClass('vjs-error-display-enabled')
|
|
this.player.removeChild(this.errorModal)
|
|
this.errorModal.close()
|
|
this.errorModal = undefined
|
|
|
|
if (this.player.loadingSpinner) {
|
|
this.player.loadingSpinner.show()
|
|
}
|
|
}
|
|
|
|
private setUserAgentInfo () {
|
|
const userAgent = UAParser(window.navigator.userAgent)
|
|
const defaultDevice = isMobile()
|
|
? 'mobile'
|
|
: 'desktop'
|
|
|
|
this.userAgentInfo = {
|
|
client: userAgent.browser.name,
|
|
os: userAgent.os.name,
|
|
device: userAgent.device.type || defaultDevice
|
|
}
|
|
}
|
|
|
|
private initializePlayer () {
|
|
if (isMobile()) this.player.addClass('vjs-is-mobile')
|
|
|
|
this.patchMenuEscapeKey()
|
|
|
|
this.player.ready(() => {
|
|
this.listenControlBarMouse()
|
|
})
|
|
|
|
this.listenFullScreenChange()
|
|
}
|
|
|
|
private initOnVideoChange () {
|
|
if (this.hasAutoplay() !== false) this.player.addClass('vjs-has-autoplay')
|
|
else this.player.removeClass('vjs-has-autoplay')
|
|
|
|
if (this.currentPlaybackRate && this.currentPlaybackRate !== 1) {
|
|
debugLogger('Setting playback rate to ' + this.currentPlaybackRate)
|
|
|
|
this.player.playbackRate(this.currentPlaybackRate)
|
|
}
|
|
|
|
this.player.ready(() => {
|
|
this.initCaptions()
|
|
this.updateControlBar()
|
|
})
|
|
|
|
this.handleStartStopTime()
|
|
this.runUserViewing()
|
|
}
|
|
|
|
private initOnRatioChange () {
|
|
if (!this.options.autoPlayerRatio) return
|
|
|
|
const defaultRatio = getComputedStyle(this.player.el()).getPropertyValue(this.options.autoPlayerRatio.cssRatioVariable)
|
|
|
|
const tryToUpdateRatioFromOptions = () => {
|
|
if (!this.options.videoRatio()) return
|
|
|
|
this.adaptPlayerFromRatio({ ratio: this.options.videoRatio(), defaultRatio })
|
|
this.updatePlayerSizeClasses()
|
|
}
|
|
|
|
tryToUpdateRatioFromOptions()
|
|
|
|
this.player.on('video-change', () => tryToUpdateRatioFromOptions())
|
|
|
|
this.player.on('video-ratio-changed', (_event: any, data: { ratio: number }) => {
|
|
if (this.options.videoRatio()) return
|
|
|
|
this.adaptPlayerFromRatio({ ratio: data.ratio, defaultRatio })
|
|
this.updatePlayerSizeClasses()
|
|
})
|
|
}
|
|
|
|
private adaptPlayerFromRatio (options: {
|
|
ratio: number
|
|
defaultRatio: string
|
|
}) {
|
|
const { ratio, defaultRatio } = options
|
|
|
|
const el = this.player.el() as HTMLElement
|
|
|
|
// In portrait screen mode, we allow player with bigger height size than width
|
|
const portraitMode = getComputedStyle(el).getPropertyValue(this.options.autoPlayerRatio.cssPlayerPortraitModeVariable) === '1'
|
|
|
|
const currentRatio = isNaN(ratio) || (!portraitMode && ratio < 1)
|
|
? defaultRatio
|
|
: ratio
|
|
|
|
el.style.setProperty(this.options.autoPlayerRatio.cssRatioVariable, currentRatio + '')
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private runUserViewing () {
|
|
const startTime = timeToInt(this.options.startTime())
|
|
|
|
let lastCurrentTime = startTime
|
|
let lastViewEvent: VideoViewEvent
|
|
let ended = false // player.ended() is too "slow", so store ended state manually
|
|
|
|
this.disableUserViewing()
|
|
|
|
this.videoViewOnPlayHandler = () => {
|
|
debugLogger('Notify user is watching on play: ' + startTime)
|
|
|
|
this.notifyUserIsWatching(startTime, lastViewEvent)
|
|
}
|
|
|
|
this.videoViewOnSeekedHandler = () => {
|
|
// Bypass the first initial seek
|
|
if (this.hasInitialSeek) {
|
|
this.hasInitialSeek = false
|
|
return
|
|
}
|
|
|
|
const currentTime = Math.floor(this.player.currentTime())
|
|
if (currentTime === 0 && this.player.loop()) {
|
|
debugLogger('Disabling viewing notification after first video loop.')
|
|
this.disableUserViewing()
|
|
return
|
|
}
|
|
|
|
const diff = currentTime - lastCurrentTime
|
|
|
|
// Don't take into account small forwards
|
|
if (diff > 0 && diff < 3) return
|
|
|
|
debugLogger('Detected seek event for user watching')
|
|
|
|
lastViewEvent = 'seek'
|
|
}
|
|
|
|
this.videoViewOnEndedHandler = () => {
|
|
ended = true
|
|
|
|
if (this.options.isLive()) return
|
|
|
|
const currentTime = Math.floor(this.player.duration())
|
|
lastCurrentTime = currentTime
|
|
|
|
debugLogger('Notify user is watching on end: ' + currentTime)
|
|
|
|
this.notifyUserIsWatching(currentTime, lastViewEvent)
|
|
|
|
lastViewEvent = undefined
|
|
}
|
|
|
|
this.player.one('play', this.videoViewOnPlayHandler)
|
|
this.player.on('seeked', this.videoViewOnSeekedHandler)
|
|
this.player.one('ended', this.videoViewOnEndedHandler)
|
|
|
|
this.videoViewInterval = setInterval(() => {
|
|
const player = this.player
|
|
|
|
if (!player || ended) return
|
|
|
|
const currentTime = Math.floor(player.currentTime())
|
|
|
|
// No need to update
|
|
if (currentTime === lastCurrentTime) return
|
|
|
|
debugLogger('Notify user is watching: ' + currentTime)
|
|
|
|
lastCurrentTime = currentTime
|
|
|
|
this.notifyUserIsWatching(currentTime, lastViewEvent)
|
|
.catch(err => logger.error('Cannot notify user is watching.', err))
|
|
|
|
lastViewEvent = undefined
|
|
}, this.options.videoViewIntervalMs)
|
|
}
|
|
|
|
private disableUserViewing () {
|
|
if (this.videoViewInterval) {
|
|
clearInterval(this.videoViewInterval)
|
|
this.videoViewInterval = undefined
|
|
}
|
|
|
|
if (this.videoViewOnPlayHandler) {
|
|
this.player.off('play', this.videoViewOnPlayHandler)
|
|
this.videoViewOnPlayHandler = undefined
|
|
}
|
|
|
|
if (this.videoViewOnSeekedHandler) {
|
|
this.player.off('seeked', this.videoViewOnSeekedHandler)
|
|
this.videoViewOnSeekedHandler = undefined
|
|
}
|
|
|
|
if (this.videoViewOnEndedHandler) {
|
|
this.player.off('ended', this.videoViewOnEndedHandler)
|
|
this.videoViewOnEndedHandler = undefined
|
|
}
|
|
}
|
|
|
|
private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
|
|
// Server won't save history, so save the video position in local storage
|
|
if (!this.authorizationHeader()) {
|
|
saveVideoWatchHistory(this.options.videoUUID(), currentTime)
|
|
}
|
|
|
|
if (!this.videoViewUrl()) return Promise.resolve(true)
|
|
|
|
const sessionId = getPlayerSessionId()
|
|
|
|
const body: VideoView = {
|
|
currentTime,
|
|
viewEvent,
|
|
sessionId,
|
|
client: this.userAgentInfo.client || undefined,
|
|
device: this.userAgentInfo.device || undefined,
|
|
operatingSystem: this.userAgentInfo.os || undefined
|
|
}
|
|
|
|
const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
|
|
if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
|
|
|
|
return fetch(this.videoViewUrl(), { method: 'POST', body: JSON.stringify(body), headers })
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private adaptPosterForAudioOnly (resolution: number) {
|
|
debugLogger('Check if we need to adapt player for audio only', resolution)
|
|
|
|
if (resolution === 0) {
|
|
this.player.audioPosterMode(true)
|
|
this.player.poster(this.options.poster())
|
|
return
|
|
}
|
|
|
|
this.player.audioPosterMode(false)
|
|
this.player.poster('')
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private updatePlayerSizeClasses () {
|
|
requestAnimationFrame(() => {
|
|
if (!this.player) return
|
|
|
|
debugLogger('Updating player size classes')
|
|
|
|
const width = this.player.currentWidth()
|
|
|
|
const breakpoints = [ 350, 570, 750 ]
|
|
|
|
for (const breakpoint of breakpoints) {
|
|
if (width <= breakpoint) {
|
|
this.player.addClass('vjs-size-' + breakpoint)
|
|
} else {
|
|
this.player.removeClass('vjs-size-' + breakpoint)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
private listenFullScreenChange () {
|
|
this.player.on('fullscreenchange', () => {
|
|
if (this.player.isFullscreen()) this.player.focus()
|
|
})
|
|
}
|
|
|
|
private listenControlBarMouse () {
|
|
const controlBar = this.player.controlBar
|
|
const settingsButton = controlBar.settingsButton
|
|
|
|
controlBar.on('mouseenter', () => {
|
|
this.mouseInControlBar = true
|
|
this.alterInactivity()
|
|
})
|
|
|
|
controlBar.on('mouseleave', () => {
|
|
this.mouseInControlBar = false
|
|
this.alterInactivity()
|
|
})
|
|
|
|
settingsButton.dialog.on('mouseenter', () => {
|
|
this.mouseInSettings = true
|
|
this.alterInactivity()
|
|
})
|
|
|
|
settingsButton.dialog.on('mouseleave', () => {
|
|
this.mouseInSettings = false
|
|
this.alterInactivity()
|
|
})
|
|
}
|
|
|
|
private alterInactivity () {
|
|
if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
|
|
this.setInactivityTimeout(0)
|
|
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
|
|
}
|
|
|
|
private initCaptions () {
|
|
if (this.currentSubtitle) debugLogger('Init captions with current subtitle ' + this.currentSubtitle)
|
|
else debugLogger('Init captions without current subtitle')
|
|
|
|
this.player.tech(true).clearTracks('text')
|
|
|
|
this.player.removeClass('vjs-has-captions')
|
|
|
|
for (const caption of this.options.videoCaptions()) {
|
|
this.player.addRemoteTextTrack({
|
|
kind: 'captions',
|
|
|
|
label: caption.automaticallyGenerated
|
|
? this.player.localize('{1} (auto-generated)', [ caption.label ])
|
|
: caption.label,
|
|
|
|
language: caption.language,
|
|
id: caption.language,
|
|
src: caption.src,
|
|
default: this.currentSubtitle === caption.language
|
|
}, true)
|
|
|
|
this.player.addClass('vjs-has-captions')
|
|
}
|
|
|
|
this.player.trigger('captions-changed')
|
|
}
|
|
|
|
private updateControlBar () {
|
|
debugLogger('Updating control bar')
|
|
|
|
if (this.options.isLive() && this.options.liveDvrEnabled()) {
|
|
this.player.addClass('vjs-live-dvr')
|
|
} else {
|
|
this.player.removeClass('vjs-live-dvr')
|
|
}
|
|
|
|
if (this.options.isLive()) {
|
|
this.getPlaybackRateButton().hide()
|
|
this.player.controlBar.getChild('peerTubeLiveDisplay').show()
|
|
this.player.controlBar.getChild('currentTimeDisplay').hide()
|
|
this.player.controlBar.getChild('timeDivider').hide()
|
|
this.player.controlBar.getChild('durationDisplay').hide()
|
|
|
|
if (!this.options.liveDvrEnabled()) {
|
|
this.player.controlBar.getChild('progressControl').hide()
|
|
}
|
|
} else {
|
|
this.getPlaybackRateButton().show()
|
|
|
|
this.player.controlBar.getChild('progressControl').show()
|
|
this.player.controlBar.getChild('currentTimeDisplay').show()
|
|
this.player.controlBar.getChild('timeDivider').show()
|
|
this.player.controlBar.getChild('durationDisplay').show()
|
|
|
|
this.player.controlBar.getChild('peerTubeLiveDisplay').hide()
|
|
}
|
|
|
|
if (this.options.videoCaptions().length === 0) {
|
|
this.getCaptionsButton().hide()
|
|
} else {
|
|
this.getCaptionsButton().show()
|
|
}
|
|
}
|
|
|
|
private handleStartStopTime () {
|
|
this.player.duration(this.options.videoDuration())
|
|
|
|
if (this.stopTimeHandler) {
|
|
this.player.off('timeupdate', this.stopTimeHandler)
|
|
this.stopTimeHandler = undefined
|
|
}
|
|
|
|
// Prefer canplaythrough instead of canplay because Chrome has issues with the second one
|
|
this.player.one('canplaythrough', () => {
|
|
const startTime = this.options.startTime()
|
|
|
|
if (startTime !== null && startTime !== undefined) {
|
|
debugLogger('Start the video at ' + startTime)
|
|
|
|
this.hasInitialSeek = true
|
|
this.player.currentTime(timeToInt(startTime))
|
|
}
|
|
|
|
if (this.options.stopTime()) {
|
|
const stopTime = timeToInt(this.options.stopTime())
|
|
|
|
this.stopTimeHandler = () => {
|
|
if (this.player.currentTime() <= stopTime) return
|
|
|
|
debugLogger('Stopping the video at ' + this.options.stopTime())
|
|
|
|
// Time top stop
|
|
this.player.pause()
|
|
this.player.trigger('auto-stopped')
|
|
|
|
this.player.off('timeupdate', this.stopTimeHandler)
|
|
this.stopTimeHandler = undefined
|
|
}
|
|
|
|
this.player.on('timeupdate', this.stopTimeHandler)
|
|
}
|
|
})
|
|
}
|
|
|
|
private patchMenuEscapeKey () {
|
|
const Menu = videojs.getComponent('Menu') as any
|
|
|
|
const fn = Menu.prototype.handleKeyDown
|
|
Menu.prototype.handleKeyDown = function handleKeyDown (event: KeyboardEvent) {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
this.trigger('escaped-key')
|
|
return
|
|
}
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
this.trigger('arrow-right', event.target)
|
|
return
|
|
}
|
|
|
|
if (event.key === 'ArrowLeft') {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
this.trigger('arrow-left')
|
|
return
|
|
}
|
|
|
|
fn.call(this, event)
|
|
}
|
|
}
|
|
|
|
private getCaptionsButton () {
|
|
const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
|
|
|
|
return settingsButton.menu.getChild('captionsButton') as unknown as VideojsCaptionsButton
|
|
}
|
|
|
|
private getPlaybackRateButton () {
|
|
const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
|
|
|
|
return settingsButton.menu.getChild('playbackRateMenuButton')
|
|
}
|
|
|
|
// We don't use webtorrent anymore, so we can safely remove old chunks from IndexedDB
|
|
private deleteLegacyIndexedDB () {
|
|
try {
|
|
if (typeof window.indexedDB === 'undefined') return
|
|
if (!window.indexedDB) return
|
|
if (typeof window.indexedDB.databases !== 'function') return
|
|
|
|
window.indexedDB.databases()
|
|
.then(databases => {
|
|
for (const db of databases) {
|
|
window.indexedDB.deleteDatabase(db.name)
|
|
}
|
|
})
|
|
} catch (err) {
|
|
debugLogger('Cannot delete legacy indexed DB', err)
|
|
// Nothing to do
|
|
}
|
|
}
|
|
}
|
|
|
|
videojs.registerPlugin('peertube', PeerTubePlugin)
|
|
export { PeerTubePlugin }
|