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