import debug from 'debug'
import videojs from 'video.js'
+import { logger } from '@root-helpers/logger'
import { isMobile } from '@root-helpers/web-browser'
import { timeToInt } from '@shared/core-utils'
+import { VideoView, VideoViewEvent } from '@shared/models/videos'
import {
getStoredLastSubtitle,
getStoredMute,
saveVideoWatchHistory,
saveVolumeInStore
} from '../../peertube-player-local-storage'
-import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from '../../types'
+import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
import { SettingsButton } from '../settings/settings-menu-button'
-const logger = debug('peertube:player:peertube')
+const debugLogger = debug('peertube:player:peertube')
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 readonly authorizationHeader: () => string
+
+ private readonly videoUUID: string
+ private readonly startTime: number
+
+ private readonly videoViewIntervalMs: number
private videoCaptions: VideoJSCaption[]
private defaultSubtitle: string
private videoViewInterval: any
- private userWatchingVideoInterval: any
-
- private isLive: boolean
private menuOpened = false
private mouseInControlBar = false
super(player)
this.videoViewUrl = options.videoViewUrl
- this.videoDuration = options.videoDuration
+ this.authorizationHeader = options.authorizationHeader
+ this.videoUUID = options.videoUUID
+ this.startTime = timeToInt(options.startTime)
+ this.videoViewIntervalMs = options.videoViewIntervalMs
+
this.videoCaptions = options.videoCaptions
- this.isLive = options.isLive
this.initialInactivityTimeout = this.player.options_.inactivityTimeout
- if (options.autoplay) this.player.addClass('vjs-has-autoplay')
+ if (options.autoplay !== false) this.player.addClass('vjs-has-autoplay')
this.player.on('autoplay-failure', () => {
this.player.removeClass('vjs-has-autoplay')
this.player.duration(options.videoDuration)
this.initializePlayer()
- this.runViewAdd()
-
- this.runUserWatchVideo(options.userWatching, options.videoUUID)
+ this.runUserViewing()
})
}
dispose () {
if (this.videoViewInterval) clearInterval(this.videoViewInterval)
- if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
}
onMenuOpened () {
}
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')
+
this.player.addClass('vjs-error-display-enabled')
}
this.listenFullScreenChange()
}
- private runViewAdd () {
- this.clearVideoViewInterval()
+ // ---------------------------------------------------------------------------
- // After 30 seconds (or 3/4 of the video), add a view to the video
- let minSecondsToView = 30
+ private runUserViewing () {
+ let lastCurrentTime = this.startTime
+ let lastViewEvent: VideoViewEvent
- if (!this.isLive && this.videoDuration < minSecondsToView) {
- minSecondsToView = (this.videoDuration * 3) / 4
- }
+ this.player.one('play', () => {
+ this.notifyUserIsWatching(this.startTime, lastViewEvent)
+ })
- 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.player.on('seeked', () => {
+ const diff = Math.floor(this.player.currentTime()) - lastCurrentTime
- this.addViewToVideo().catch(err => console.error(err))
- }
- }
- }, 1000)
- }
+ // Don't take into account small forwards
+ if (diff > 0 && diff < 3) return
+
+ lastViewEvent = 'seek'
+ })
+
+ this.player.one('ended', () => {
+ const currentTime = Math.floor(this.player.duration())
+ lastCurrentTime = currentTime
- private runUserWatchVideo (options: UserWatching, videoUUID: string) {
- let lastCurrentTime = 0
+ this.notifyUserIsWatching(currentTime, lastViewEvent)
- this.userWatchingVideoInterval = setInterval(() => {
+ lastViewEvent = undefined
+ })
+
+ this.videoViewInterval = setInterval(() => {
const currentTime = Math.floor(this.player.currentTime())
- if (currentTime - lastCurrentTime >= 1) {
- lastCurrentTime = currentTime
+ // No need to update
+ if (currentTime === lastCurrentTime) return
- 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)
- }
+ lastCurrentTime = currentTime
- private clearVideoViewInterval () {
- if (this.videoViewInterval !== undefined) {
- clearInterval(this.videoViewInterval)
- this.videoViewInterval = undefined
- }
+ this.notifyUserIsWatching(currentTime, lastViewEvent)
+ .catch(err => logger.error('Cannot notify user is watching.', err))
+
+ lastViewEvent = undefined
+ }, this.videoViewIntervalMs)
}
- private addViewToVideo () {
- if (!this.videoViewUrl) return Promise.resolve(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.videoUUID, currentTime)
+ }
- return fetch(this.videoViewUrl, { method: 'POST' })
- }
+ if (!this.videoViewUrl) return Promise.resolve(true)
- private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
- const body = new URLSearchParams()
- body.append('currentTime', currentTime.toString())
+ const body: VideoView = { currentTime, viewEvent }
- const headers = new Headers({ Authorization: authorizationHeader })
+ const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
+ if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
- return fetch(url, { method: 'PUT', body, headers })
+ return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
}
+ // ---------------------------------------------------------------------------
+
private listenFullScreenChange () {
this.player.on('fullscreenchange', () => {
if (this.player.isFullscreen()) this.player.focus()
(this.player as any).cache_.inactivityTimeout = timeout
this.player.options_.inactivityTimeout = timeout
- logger('Set player inactivity to ' + timeout)
+ debugLogger('Set player inactivity to ' + timeout)
}
private initCaptions () {