1 import videojs from 'video.js'
2 import { timeToInt } from '@shared/core-utils'
11 } from './peertube-player-local-storage'
12 import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
13 import { isMobile } from './utils'
15 const Plugin = videojs.getPlugin('plugin')
17 class PeerTubePlugin extends Plugin {
18 private readonly videoViewUrl: string
19 private readonly videoDuration: number
20 private readonly CONSTANTS = {
21 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
24 private videoCaptions: VideoJSCaption[]
25 private defaultSubtitle: string
27 private videoViewInterval: any
28 private userWatchingVideoInterval: any
30 private isLive: boolean
32 private menuOpened = false
33 private mouseInControlBar = false
34 private readonly savedInactivityTimeout: number
36 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
39 this.videoViewUrl = options.videoViewUrl
40 this.videoDuration = options.videoDuration
41 this.videoCaptions = options.videoCaptions
42 this.isLive = options.isLive
44 this.savedInactivityTimeout = player.options_.inactivityTimeout
46 if (options.autoplay) this.player.addClass('vjs-has-autoplay')
48 this.player.on('autoplay-failure', () => {
49 this.player.removeClass('vjs-has-autoplay')
52 this.player.ready(() => {
53 const playerOptions = this.player.options_
55 const volume = getStoredVolume()
56 if (volume !== undefined) this.player.volume(volume)
58 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
59 if (muted !== undefined) this.player.muted(muted)
61 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
63 this.player.on('volumechange', () => {
64 saveVolumeInStore(this.player.volume())
65 saveMuteInStore(this.player.muted())
68 if (options.stopTime) {
69 const stopTime = timeToInt(options.stopTime)
72 this.player.on('timeupdate', function onTimeUpdate () {
73 if (self.player.currentTime() > stopTime) {
75 self.player.trigger('stopped')
77 self.player.off('timeupdate', onTimeUpdate)
82 this.player.textTracks().addEventListener('change', () => {
83 const showing = this.player.textTracks().tracks_.find(t => {
84 return t.kind === 'captions' && t.mode === 'showing'
88 saveLastSubtitle('off')
92 saveLastSubtitle(showing.language)
95 this.player.on('sourcechange', () => this.initCaptions())
97 this.player.duration(options.videoDuration)
99 this.initializePlayer()
102 this.runUserWatchVideo(options.userWatching, options.videoUUID)
107 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
108 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
112 this.menuOpened = false
113 this.alterInactivity()
117 this.menuOpened = true
118 this.alterInactivity()
121 private initializePlayer () {
122 if (isMobile()) this.player.addClass('vjs-is-mobile')
124 this.initSmoothProgressBar()
128 this.listenControlBarMouse()
131 private runViewAdd () {
132 this.clearVideoViewInterval()
134 // After 30 seconds (or 3/4 of the video), add a view to the video
135 let minSecondsToView = 30
137 if (!this.isLive && this.videoDuration < minSecondsToView) {
138 minSecondsToView = (this.videoDuration * 3) / 4
141 let secondsViewed = 0
142 this.videoViewInterval = setInterval(() => {
143 if (this.player && !this.player.paused()) {
146 if (secondsViewed > minSecondsToView) {
147 // Restart the loop if this is a live
151 this.clearVideoViewInterval()
154 this.addViewToVideo().catch(err => console.error(err))
160 private runUserWatchVideo (options: UserWatching, videoUUID: string) {
161 let lastCurrentTime = 0
163 this.userWatchingVideoInterval = setInterval(() => {
164 const currentTime = Math.floor(this.player.currentTime())
166 if (currentTime - lastCurrentTime >= 1) {
167 lastCurrentTime = currentTime
170 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
171 .catch(err => console.error('Cannot notify user is watching.', err))
173 saveVideoWatchHistory(videoUUID, currentTime)
176 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
179 private clearVideoViewInterval () {
180 if (this.videoViewInterval !== undefined) {
181 clearInterval(this.videoViewInterval)
182 this.videoViewInterval = undefined
186 private addViewToVideo () {
187 if (!this.videoViewUrl) return Promise.resolve(undefined)
189 return fetch(this.videoViewUrl, { method: 'POST' })
192 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
193 const body = new URLSearchParams()
194 body.append('currentTime', currentTime.toString())
196 const headers = new Headers({ Authorization: authorizationHeader })
198 return fetch(url, { method: 'PUT', body, headers })
201 private listenControlBarMouse () {
202 this.player.controlBar.on('mouseenter', () => {
203 this.mouseInControlBar = true
204 this.alterInactivity()
207 this.player.controlBar.on('mouseleave', () => {
208 this.mouseInControlBar = false
209 this.alterInactivity()
213 private alterInactivity () {
214 if (this.menuOpened) {
215 this.player.options_.inactivityTimeout = this.savedInactivityTimeout
219 if (!this.mouseInControlBar && !this.isTouchEnabled()) {
220 this.player.options_.inactivityTimeout = 1
224 private isTouchEnabled () {
225 return ('ontouchstart' in window) ||
226 navigator.maxTouchPoints > 0 ||
227 navigator.msMaxTouchPoints > 0
230 private initCaptions () {
231 for (const caption of this.videoCaptions) {
232 this.player.addRemoteTextTrack({
234 label: caption.label,
235 language: caption.language,
236 id: caption.language,
238 default: this.defaultSubtitle === caption.language
242 this.player.trigger('captionsChanged')
245 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
246 private initSmoothProgressBar () {
247 const SeekBar = videojs.getComponent('SeekBar') as any
248 SeekBar.prototype.getPercent = function getPercent () {
249 // Allows for smooth scrubbing, when player can't keep up.
250 // const time = (this.player_.scrubbing()) ?
251 // this.player_.getCache().currentTime :
252 // this.player_.currentTime()
253 const time = this.player_.currentTime()
254 const percent = time / this.player_.duration()
255 return percent >= 1 ? 1 : percent
257 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
258 let newTime = this.calculateDistance(event) * this.player_.duration()
259 if (newTime === this.player_.duration()) {
260 newTime = newTime - 0.1
262 this.player_.currentTime(newTime)
268 videojs.registerPlugin('peertube', PeerTubePlugin)
269 export { PeerTubePlugin }