Lazy import some modules
[oweals/peertube.git] / client / src / assets / player / peertube-plugin.ts
1 // FIXME: something weird with our path definition in tsconfig and typings
2 // @ts-ignore
3 import * as videojs from 'video.js'
4 import './videojs-components/settings-menu-button'
5 import {
6   PeerTubePluginOptions,
7   ResolutionUpdateData,
8   UserWatching,
9   VideoJSCaption,
10   VideoJSComponentInterface,
11   videojsUntyped
12 } from './peertube-videojs-typings'
13 import { isMobile, timeToInt } from './utils'
14 import {
15   getStoredLastSubtitle,
16   getStoredMute,
17   getStoredVolume,
18   saveLastSubtitle,
19   saveMuteInStore,
20   saveVolumeInStore
21 } from './peertube-player-local-storage'
22
23 const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
24 class PeerTubePlugin extends Plugin {
25   private readonly startTime: number = 0
26   private readonly videoViewUrl: string
27   private readonly videoDuration: number
28   private readonly CONSTANTS = {
29     USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
30   }
31
32   private player: any
33   private videoCaptions: VideoJSCaption[]
34   private defaultSubtitle: string
35
36   private videoViewInterval: any
37   private userWatchingVideoInterval: any
38   private qualityObservationTimer: any
39   private lastResolutionChange: ResolutionUpdateData
40
41   constructor (player: videojs.Player, options: PeerTubePluginOptions) {
42     super(player, options)
43
44     this.startTime = timeToInt(options.startTime)
45     this.videoViewUrl = options.videoViewUrl
46     this.videoDuration = options.videoDuration
47     this.videoCaptions = options.videoCaptions
48
49     if (options.autoplay === true) this.player.addClass('vjs-has-autoplay')
50
51     this.player.on('autoplay-failure', () => {
52       this.player.removeClass('vjs-has-autoplay')
53     })
54
55     this.player.ready(() => {
56       const playerOptions = this.player.options_
57
58       if (options.mode === 'webtorrent') {
59         this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
60         this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
61       }
62
63       if (options.mode === 'p2p-media-loader') {
64         this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
65       }
66
67       this.player.tech_.on('loadedqualitydata', () => {
68         setTimeout(() => {
69           // Replay a resolution change, now we loaded all quality data
70           if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
71         }, 0)
72       })
73
74       const volume = getStoredVolume()
75       if (volume !== undefined) this.player.volume(volume)
76
77       const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
78       if (muted !== undefined) this.player.muted(muted)
79
80       this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
81
82       this.player.on('volumechange', () => {
83         saveVolumeInStore(this.player.volume())
84         saveMuteInStore(this.player.muted())
85       })
86
87       this.player.textTracks().on('change', () => {
88         const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
89           return t.kind === 'captions' && t.mode === 'showing'
90         })
91
92         if (!showing) {
93           saveLastSubtitle('off')
94           return
95         }
96
97         saveLastSubtitle(showing.language)
98       })
99
100       this.player.on('sourcechange', () => this.initCaptions())
101
102       this.player.duration(options.videoDuration)
103
104       this.initializePlayer()
105       this.runViewAdd()
106
107       if (options.userWatching) this.runUserWatchVideo(options.userWatching)
108     })
109   }
110
111   dispose () {
112     clearTimeout(this.qualityObservationTimer)
113
114     clearInterval(this.videoViewInterval)
115
116     if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
117   }
118
119   private initializePlayer () {
120     if (isMobile()) this.player.addClass('vjs-is-mobile')
121
122     this.initSmoothProgressBar()
123
124     this.initCaptions()
125
126     this.alterInactivity()
127   }
128
129   private runViewAdd () {
130     this.clearVideoViewInterval()
131
132     // After 30 seconds (or 3/4 of the video), add a view to the video
133     let minSecondsToView = 30
134
135     if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
136
137     let secondsViewed = 0
138     this.videoViewInterval = setInterval(() => {
139       if (this.player && !this.player.paused()) {
140         secondsViewed += 1
141
142         if (secondsViewed > minSecondsToView) {
143           this.clearVideoViewInterval()
144
145           this.addViewToVideo().catch(err => console.error(err))
146         }
147       }
148     }, 1000)
149   }
150
151   private runUserWatchVideo (options: UserWatching) {
152     let lastCurrentTime = 0
153
154     this.userWatchingVideoInterval = setInterval(() => {
155       const currentTime = Math.floor(this.player.currentTime())
156
157       if (currentTime - lastCurrentTime >= 1) {
158         lastCurrentTime = currentTime
159
160         this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
161           .catch(err => console.error('Cannot notify user is watching.', err))
162       }
163     }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
164   }
165
166   private clearVideoViewInterval () {
167     if (this.videoViewInterval !== undefined) {
168       clearInterval(this.videoViewInterval)
169       this.videoViewInterval = undefined
170     }
171   }
172
173   private addViewToVideo () {
174     if (!this.videoViewUrl) return Promise.resolve(undefined)
175
176     return fetch(this.videoViewUrl, { method: 'POST' })
177   }
178
179   private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
180     const body = new URLSearchParams()
181     body.append('currentTime', currentTime.toString())
182
183     const headers = new Headers({ 'Authorization': authorizationHeader })
184
185     return fetch(url, { method: 'PUT', body, headers })
186   }
187
188   private handleResolutionChange (data: ResolutionUpdateData) {
189     this.lastResolutionChange = data
190
191     const qualityLevels = this.player.qualityLevels()
192
193     for (let i = 0; i < qualityLevels.length; i++) {
194       if (qualityLevels[i].height === data.resolutionId) {
195         data.id = qualityLevels[i].id
196         break
197       }
198     }
199
200     this.trigger('resolutionChange', data)
201   }
202
203   private alterInactivity () {
204     let saveInactivityTimeout: number
205
206     const disableInactivity = () => {
207       saveInactivityTimeout = this.player.options_.inactivityTimeout
208       this.player.options_.inactivityTimeout = 0
209     }
210     const enableInactivity = () => {
211       this.player.options_.inactivityTimeout = saveInactivityTimeout
212     }
213
214     const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
215
216     this.player.controlBar.on('mouseenter', () => disableInactivity())
217     settingsDialog.on('mouseenter', () => disableInactivity())
218     this.player.controlBar.on('mouseleave', () => enableInactivity())
219     settingsDialog.on('mouseleave', () => enableInactivity())
220   }
221
222   private initCaptions () {
223     for (const caption of this.videoCaptions) {
224       this.player.addRemoteTextTrack({
225         kind: 'captions',
226         label: caption.label,
227         language: caption.language,
228         id: caption.language,
229         src: caption.src,
230         default: this.defaultSubtitle === caption.language
231       }, false)
232     }
233
234     this.player.trigger('captionsChanged')
235   }
236
237   // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
238   private initSmoothProgressBar () {
239     const SeekBar = videojsUntyped.getComponent('SeekBar')
240     SeekBar.prototype.getPercent = function getPercent () {
241       // Allows for smooth scrubbing, when player can't keep up.
242       // const time = (this.player_.scrubbing()) ?
243       //   this.player_.getCache().currentTime :
244       //   this.player_.currentTime()
245       const time = this.player_.currentTime()
246       const percent = time / this.player_.duration()
247       return percent >= 1 ? 1 : percent
248     }
249     SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
250       let newTime = this.calculateDistance(event) * this.player_.duration()
251       if (newTime === this.player_.duration()) {
252         newTime = newTime - 0.1
253       }
254       this.player_.currentTime(newTime)
255       this.update()
256     }
257   }
258 }
259
260 videojs.registerPlugin('peertube', PeerTubePlugin)
261 export { PeerTubePlugin }