1 // FIXME: something weird with our path definition in tsconfig and typings
3 import * as videojs from 'video.js'
4 import './videojs-components/settings-menu-button'
10 VideoJSComponentInterface,
12 } from './peertube-videojs-typings'
13 import { isMobile, timeToInt } from './utils'
15 getStoredLastSubtitle,
21 } from './peertube-player-local-storage'
23 const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
24 class PeerTubePlugin extends Plugin {
25 private readonly autoplay: boolean = false
26 private readonly startTime: number = 0
27 private readonly videoViewUrl: string
28 private readonly videoDuration: number
29 private readonly CONSTANTS = {
30 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
34 private videoCaptions: VideoJSCaption[]
35 private defaultSubtitle: string
37 private videoViewInterval: any
38 private userWatchingVideoInterval: any
39 private qualityObservationTimer: any
40 private lastResolutionChange: ResolutionUpdateData
42 constructor (player: videojs.Player, options: PeerTubePluginOptions) {
43 super(player, options)
45 this.startTime = timeToInt(options.startTime)
46 this.videoViewUrl = options.videoViewUrl
47 this.videoDuration = options.videoDuration
48 this.videoCaptions = options.videoCaptions
50 if (options.autoplay === true) this.player.addClass('vjs-has-autoplay')
52 this.player.on('autoplay-failure', () => {
53 this.player.removeClass('vjs-has-autoplay')
56 this.player.ready(() => {
57 const playerOptions = this.player.options_
59 if (options.mode === 'webtorrent') {
60 this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
61 this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
64 if (options.mode === 'p2p-media-loader') {
65 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
68 this.player.tech_.on('loadedqualitydata', () => {
70 // Replay a resolution change, now we loaded all quality data
71 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
75 const volume = getStoredVolume()
76 if (volume !== undefined) this.player.volume(volume)
78 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
79 if (muted !== undefined) this.player.muted(muted)
81 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
83 this.player.on('volumechange', () => {
84 saveVolumeInStore(this.player.volume())
85 saveMuteInStore(this.player.muted())
88 this.player.textTracks().on('change', () => {
89 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
90 return t.kind === 'captions' && t.mode === 'showing'
94 saveLastSubtitle('off')
98 saveLastSubtitle(showing.language)
101 this.player.on('sourcechange', () => this.initCaptions())
103 this.player.duration(options.videoDuration)
105 this.initializePlayer()
108 if (options.userWatching) this.runUserWatchVideo(options.userWatching)
113 clearTimeout(this.qualityObservationTimer)
115 clearInterval(this.videoViewInterval)
117 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
120 private initializePlayer () {
121 if (isMobile()) this.player.addClass('vjs-is-mobile')
123 this.initSmoothProgressBar()
127 this.alterInactivity()
130 private runViewAdd () {
131 this.clearVideoViewInterval()
133 // After 30 seconds (or 3/4 of the video), add a view to the video
134 let minSecondsToView = 30
136 if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
138 let secondsViewed = 0
139 this.videoViewInterval = setInterval(() => {
140 if (this.player && !this.player.paused()) {
143 if (secondsViewed > minSecondsToView) {
144 this.clearVideoViewInterval()
146 this.addViewToVideo().catch(err => console.error(err))
152 private runUserWatchVideo (options: UserWatching) {
153 let lastCurrentTime = 0
155 this.userWatchingVideoInterval = setInterval(() => {
156 const currentTime = Math.floor(this.player.currentTime())
158 if (currentTime - lastCurrentTime >= 1) {
159 lastCurrentTime = currentTime
161 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
162 .catch(err => console.error('Cannot notify user is watching.', err))
164 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
167 private clearVideoViewInterval () {
168 if (this.videoViewInterval !== undefined) {
169 clearInterval(this.videoViewInterval)
170 this.videoViewInterval = undefined
174 private addViewToVideo () {
175 if (!this.videoViewUrl) return Promise.resolve(undefined)
177 return fetch(this.videoViewUrl, { method: 'POST' })
180 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
181 const body = new URLSearchParams()
182 body.append('currentTime', currentTime.toString())
184 const headers = new Headers({ 'Authorization': authorizationHeader })
186 return fetch(url, { method: 'PUT', body, headers })
189 private handleResolutionChange (data: ResolutionUpdateData) {
190 this.lastResolutionChange = data
192 const qualityLevels = this.player.qualityLevels()
194 for (let i = 0; i < qualityLevels.length; i++) {
195 if (qualityLevels[i].height === data.resolutionId) {
196 data.id = qualityLevels[i].id
201 this.trigger('resolutionChange', data)
204 private alterInactivity () {
205 let saveInactivityTimeout: number
207 const disableInactivity = () => {
208 saveInactivityTimeout = this.player.options_.inactivityTimeout
209 this.player.options_.inactivityTimeout = 0
211 const enableInactivity = () => {
212 this.player.options_.inactivityTimeout = saveInactivityTimeout
215 const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
217 this.player.controlBar.on('mouseenter', () => disableInactivity())
218 settingsDialog.on('mouseenter', () => disableInactivity())
219 this.player.controlBar.on('mouseleave', () => enableInactivity())
220 settingsDialog.on('mouseleave', () => enableInactivity())
223 private initCaptions () {
224 for (const caption of this.videoCaptions) {
225 this.player.addRemoteTextTrack({
227 label: caption.label,
228 language: caption.language,
229 id: caption.language,
231 default: this.defaultSubtitle === caption.language
235 this.player.trigger('captionsChanged')
238 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
239 private initSmoothProgressBar () {
240 const SeekBar = videojsUntyped.getComponent('SeekBar')
241 SeekBar.prototype.getPercent = function getPercent () {
242 // Allows for smooth scrubbing, when player can't keep up.
243 // const time = (this.player_.scrubbing()) ?
244 // this.player_.getCache().currentTime :
245 // this.player_.currentTime()
246 const time = this.player_.currentTime()
247 const percent = time / this.player_.duration()
248 return percent >= 1 ? 1 : percent
250 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
251 let newTime = this.calculateDistance(event) * this.player_.duration()
252 if (newTime === this.player_.duration()) {
253 newTime = newTime - 0.1
255 this.player_.currentTime(newTime)
261 videojs.registerPlugin('peertube', PeerTubePlugin)
262 export { PeerTubePlugin }