]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/peertube-plugin.ts
Fix console error when watching a video
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / peertube-plugin.ts
CommitLineData
512decf3 1import videojs from 'video.js'
2adfc7ea 2import './videojs-components/settings-menu-button'
3b6f205c
C
3import {
4 PeerTubePluginOptions,
5 ResolutionUpdateData,
6 UserWatching,
f5fcd9f7 7 VideoJSCaption
3b6f205c 8} from './peertube-videojs-typings'
2adfc7ea
C
9import { isMobile, timeToInt } from './utils'
10import {
11 getStoredLastSubtitle,
12 getStoredMute,
13 getStoredVolume,
14 saveLastSubtitle,
15 saveMuteInStore,
58b9ce30 16 saveVideoWatchHistory,
2adfc7ea
C
17 saveVolumeInStore
18} from './peertube-player-local-storage'
19
f5fcd9f7
C
20const Plugin = videojs.getPlugin('plugin')
21
2adfc7ea 22class PeerTubePlugin extends Plugin {
2adfc7ea
C
23 private readonly videoViewUrl: string
24 private readonly videoDuration: number
25 private readonly CONSTANTS = {
26 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
27 }
28
2adfc7ea
C
29 private videoCaptions: VideoJSCaption[]
30 private defaultSubtitle: string
31
32 private videoViewInterval: any
33 private userWatchingVideoInterval: any
3b6f205c 34 private lastResolutionChange: ResolutionUpdateData
2adfc7ea 35
10f26f42
C
36 private isLive: boolean
37
d1f21ebb
C
38 private menuOpened = false
39 private mouseInControlBar = false
40 private readonly savedInactivityTimeout: number
41
7e37e111 42 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
f5fcd9f7 43 super(player)
2adfc7ea 44
2adfc7ea
C
45 this.videoViewUrl = options.videoViewUrl
46 this.videoDuration = options.videoDuration
47 this.videoCaptions = options.videoCaptions
10f26f42 48 this.isLive = options.isLive
2adfc7ea 49
d1f21ebb
C
50 this.savedInactivityTimeout = player.options_.inactivityTimeout
51
72efdda5 52 if (options.autoplay) this.player.addClass('vjs-has-autoplay')
6ec0b75b
C
53
54 this.player.on('autoplay-failure', () => {
55 this.player.removeClass('vjs-has-autoplay')
56 })
2adfc7ea
C
57
58 this.player.ready(() => {
59 const playerOptions = this.player.options_
60
09209296 61 if (options.mode === 'webtorrent') {
3b6f205c
C
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
09209296 66 if (options.mode === 'p2p-media-loader') {
3b6f205c
C
67 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
68 }
69
f5fcd9f7 70 this.player.tech(true).on('loadedqualitydata', () => {
3b6f205c
C
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
2adfc7ea
C
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
f0a39880
C
90 if (options.stopTime) {
91 const stopTime = timeToInt(options.stopTime)
e2f01c47 92 const self = this
f0a39880 93
e2f01c47
C
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 }
f0a39880
C
101 })
102 }
103
2adfc7ea 104 this.player.textTracks().on('change', () => {
f5fcd9f7 105 const showing = this.player.textTracks().tracks_.find(t => {
2adfc7ea
C
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
58b9ce30 124 this.runUserWatchVideo(options.userWatching, options.videoUUID)
2adfc7ea
C
125 })
126 }
127
128 dispose () {
f0a39880 129 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
2adfc7ea
C
130 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
131 }
132
d1f21ebb
C
133 onMenuOpen () {
134 this.menuOpened = false
135 this.alterInactivity()
136 }
137
138 onMenuClosed () {
139 this.menuOpened = true
140 this.alterInactivity()
141 }
142
2adfc7ea
C
143 private initializePlayer () {
144 if (isMobile()) this.player.addClass('vjs-is-mobile')
145
146 this.initSmoothProgressBar()
147
148 this.initCaptions()
149
d1f21ebb 150 this.listenControlBarMouse()
2adfc7ea
C
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
10f26f42
C
159 if (!this.isLive && this.videoDuration < minSecondsToView) {
160 minSecondsToView = (this.videoDuration * 3) / 4
161 }
2adfc7ea
C
162
163 let secondsViewed = 0
164 this.videoViewInterval = setInterval(() => {
165 if (this.player && !this.player.paused()) {
166 secondsViewed += 1
167
168 if (secondsViewed > minSecondsToView) {
10f26f42
C
169 // Restart the loop if this is a live
170 if (this.isLive) {
171 secondsViewed = 0
172 } else {
173 this.clearVideoViewInterval()
174 }
2adfc7ea
C
175
176 this.addViewToVideo().catch(err => console.error(err))
177 }
178 }
179 }, 1000)
180 }
181
58b9ce30 182 private runUserWatchVideo (options: UserWatching, videoUUID: string) {
2adfc7ea
C
183 let lastCurrentTime = 0
184
185 this.userWatchingVideoInterval = setInterval(() => {
186 const currentTime = Math.floor(this.player.currentTime())
187
188 if (currentTime - lastCurrentTime >= 1) {
189 lastCurrentTime = currentTime
190
58b9ce30 191 if (options) {
192 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
193 .catch(err => console.error('Cannot notify user is watching.', err))
194 } else {
195 saveVideoWatchHistory(videoUUID, currentTime)
196 }
2adfc7ea
C
197 }
198 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
199 }
200
201 private clearVideoViewInterval () {
202 if (this.videoViewInterval !== undefined) {
203 clearInterval(this.videoViewInterval)
204 this.videoViewInterval = undefined
205 }
206 }
207
208 private addViewToVideo () {
209 if (!this.videoViewUrl) return Promise.resolve(undefined)
210
211 return fetch(this.videoViewUrl, { method: 'POST' })
212 }
213
214 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
215 const body = new URLSearchParams()
216 body.append('currentTime', currentTime.toString())
217
218 const headers = new Headers({ 'Authorization': authorizationHeader })
219
220 return fetch(url, { method: 'PUT', body, headers })
221 }
222
3b6f205c
C
223 private handleResolutionChange (data: ResolutionUpdateData) {
224 this.lastResolutionChange = data
225
226 const qualityLevels = this.player.qualityLevels()
227
228 for (let i = 0; i < qualityLevels.length; i++) {
229 if (qualityLevels[i].height === data.resolutionId) {
230 data.id = qualityLevels[i].id
231 break
232 }
233 }
234
235 this.trigger('resolutionChange', data)
236 }
237
d1f21ebb
C
238 private listenControlBarMouse () {
239 this.player.controlBar.on('mouseenter', () => {
240 this.mouseInControlBar = true
241 this.alterInactivity()
242 })
2adfc7ea 243
d1f21ebb
C
244 this.player.controlBar.on('mouseleave', () => {
245 this.mouseInControlBar = false
246 this.alterInactivity()
247 })
248 }
2adfc7ea 249
d1f21ebb 250 private alterInactivity () {
35f0a5e6 251 if (this.menuOpened) {
d1f21ebb
C
252 this.player.options_.inactivityTimeout = this.savedInactivityTimeout
253 return
254 }
2adfc7ea 255
35f0a5e6
C
256 if (!this.mouseInControlBar && !this.isTouchEnabled()) {
257 this.player.options_.inactivityTimeout = 1
258 }
259 }
260
261 private isTouchEnabled () {
262 return ('ontouchstart' in window) ||
263 navigator.maxTouchPoints > 0 ||
264 navigator.msMaxTouchPoints > 0
2adfc7ea
C
265 }
266
267 private initCaptions () {
268 for (const caption of this.videoCaptions) {
269 this.player.addRemoteTextTrack({
270 kind: 'captions',
271 label: caption.label,
272 language: caption.language,
273 id: caption.language,
274 src: caption.src,
275 default: this.defaultSubtitle === caption.language
276 }, false)
277 }
278
279 this.player.trigger('captionsChanged')
280 }
281
282 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
283 private initSmoothProgressBar () {
f5fcd9f7 284 const SeekBar = videojs.getComponent('SeekBar') as any
2adfc7ea
C
285 SeekBar.prototype.getPercent = function getPercent () {
286 // Allows for smooth scrubbing, when player can't keep up.
287 // const time = (this.player_.scrubbing()) ?
288 // this.player_.getCache().currentTime :
289 // this.player_.currentTime()
290 const time = this.player_.currentTime()
291 const percent = time / this.player_.duration()
292 return percent >= 1 ? 1 : percent
293 }
294 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
295 let newTime = this.calculateDistance(event) * this.player_.duration()
296 if (newTime === this.player_.duration()) {
297 newTime = newTime - 0.1
298 }
299 this.player_.currentTime(newTime)
300 this.update()
301 }
302 }
303}
304
305videojs.registerPlugin('peertube', PeerTubePlugin)
306export { PeerTubePlugin }