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