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