aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/shared/peertube/peertube-plugin.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player/shared/peertube/peertube-plugin.ts')
-rw-r--r--client/src/assets/player/shared/peertube/peertube-plugin.ts248
1 files changed, 192 insertions, 56 deletions
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts
index af2147749..f52ec75f4 100644
--- a/client/src/assets/player/shared/peertube/peertube-plugin.ts
+++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts
@@ -1,7 +1,7 @@
1import debug from 'debug' 1import debug from 'debug'
2import videojs from 'video.js' 2import videojs from 'video.js'
3import { logger } from '@root-helpers/logger' 3import { logger } from '@root-helpers/logger'
4import { isMobile } from '@root-helpers/web-browser' 4import { isIOS, isMobile } from '@root-helpers/web-browser'
5import { timeToInt } from '@shared/core-utils' 5import { timeToInt } from '@shared/core-utils'
6import { VideoView, VideoViewEvent } from '@shared/models/videos' 6import { VideoView, VideoViewEvent } from '@shared/models/videos'
7import { 7import {
@@ -13,7 +13,7 @@ import {
13 saveVideoWatchHistory, 13 saveVideoWatchHistory,
14 saveVolumeInStore 14 saveVolumeInStore
15} from '../../peertube-player-local-storage' 15} from '../../peertube-player-local-storage'
16import { PeerTubePluginOptions, VideoJSCaption } from '../../types' 16import { PeerTubePluginOptions } from '../../types'
17import { SettingsButton } from '../settings/settings-menu-button' 17import { SettingsButton } from '../settings/settings-menu-button'
18 18
19const debugLogger = debug('peertube:player:peertube') 19const debugLogger = debug('peertube:player:peertube')
@@ -21,43 +21,59 @@ const debugLogger = debug('peertube:player:peertube')
21const Plugin = videojs.getPlugin('plugin') 21const Plugin = videojs.getPlugin('plugin')
22 22
23class PeerTubePlugin extends Plugin { 23class PeerTubePlugin extends Plugin {
24 private readonly videoViewUrl: string 24 private readonly videoViewUrl: () => string
25 private readonly authorizationHeader: () => string 25 private readonly authorizationHeader: () => string
26 private readonly initialInactivityTimeout: number
26 27
27 private readonly videoUUID: string 28 private readonly hasAutoplay: () => videojs.Autoplay
28 private readonly startTime: number
29
30 private readonly videoViewIntervalMs: number
31 29
32 private videoCaptions: VideoJSCaption[] 30 private currentSubtitle: string
33 private defaultSubtitle: string 31 private currentPlaybackRate: number
34 32
35 private videoViewInterval: any 33 private videoViewInterval: any
36 34
37 private menuOpened = false 35 private menuOpened = false
38 private mouseInControlBar = false 36 private mouseInControlBar = false
39 private mouseInSettings = false 37 private mouseInSettings = false
40 private readonly initialInactivityTimeout: number
41 38
42 constructor (player: videojs.Player, options?: PeerTubePluginOptions) { 39 private videoViewOnPlayHandler: (...args: any[]) => void
40 private videoViewOnSeekedHandler: (...args: any[]) => void
41 private videoViewOnEndedHandler: (...args: any[]) => void
42
43 private stopTimeHandler: (...args: any[]) => void
44
45 constructor (player: videojs.Player, private readonly options: PeerTubePluginOptions) {
43 super(player) 46 super(player)
44 47
45 this.videoViewUrl = options.videoViewUrl 48 this.videoViewUrl = options.videoViewUrl
46 this.authorizationHeader = options.authorizationHeader 49 this.authorizationHeader = options.authorizationHeader
47 this.videoUUID = options.videoUUID 50 this.hasAutoplay = options.hasAutoplay
48 this.startTime = timeToInt(options.startTime)
49 this.videoViewIntervalMs = options.videoViewIntervalMs
50 51
51 this.videoCaptions = options.videoCaptions
52 this.initialInactivityTimeout = this.player.options_.inactivityTimeout 52 this.initialInactivityTimeout = this.player.options_.inactivityTimeout
53 53
54 if (options.autoplay !== false) this.player.addClass('vjs-has-autoplay') 54 this.currentSubtitle = this.options.subtitle() || getStoredLastSubtitle()
55
56 this.initializePlayer()
57 this.initOnVideoChange()
58
59 this.deleteLegacyIndexedDB()
55 60
56 this.player.on('autoplay-failure', () => { 61 this.player.on('autoplay-failure', () => {
62 debugLogger('Autoplay failed')
63
57 this.player.removeClass('vjs-has-autoplay') 64 this.player.removeClass('vjs-has-autoplay')
65
66 // Fix a bug on iOS where the big play button is not displayed when autoplay fails
67 if (isIOS()) this.player.hasStarted(false)
58 }) 68 })
59 69
60 this.player.ready(() => { 70 this.player.on('ratechange', () => {
71 this.currentPlaybackRate = this.player.playbackRate()
72
73 this.player.defaultPlaybackRate(this.currentPlaybackRate)
74 })
75
76 this.player.one('canplay', () => {
61 const playerOptions = this.player.options_ 77 const playerOptions = this.player.options_
62 78
63 const volume = getStoredVolume() 79 const volume = getStoredVolume()
@@ -65,28 +81,15 @@ class PeerTubePlugin extends Plugin {
65 81
66 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() 82 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
67 if (muted !== undefined) this.player.muted(muted) 83 if (muted !== undefined) this.player.muted(muted)
84 })
68 85
69 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() 86 this.player.ready(() => {
70 87
71 this.player.on('volumechange', () => { 88 this.player.on('volumechange', () => {
72 saveVolumeInStore(this.player.volume()) 89 saveVolumeInStore(this.player.volume())
73 saveMuteInStore(this.player.muted()) 90 saveMuteInStore(this.player.muted())
74 }) 91 })
75 92
76 if (options.stopTime) {
77 const stopTime = timeToInt(options.stopTime)
78 const self = this
79
80 this.player.on('timeupdate', function onTimeUpdate () {
81 if (self.player.currentTime() > stopTime) {
82 self.player.pause()
83 self.player.trigger('stopped')
84
85 self.player.off('timeupdate', onTimeUpdate)
86 }
87 })
88 }
89
90 this.player.textTracks().addEventListener('change', () => { 93 this.player.textTracks().addEventListener('change', () => {
91 const showing = this.player.textTracks().tracks_.find(t => { 94 const showing = this.player.textTracks().tracks_.find(t => {
92 return t.kind === 'captions' && t.mode === 'showing' 95 return t.kind === 'captions' && t.mode === 'showing'
@@ -94,23 +97,24 @@ class PeerTubePlugin extends Plugin {
94 97
95 if (!showing) { 98 if (!showing) {
96 saveLastSubtitle('off') 99 saveLastSubtitle('off')
100 this.currentSubtitle = undefined
97 return 101 return
98 } 102 }
99 103
104 this.currentSubtitle = showing.language
100 saveLastSubtitle(showing.language) 105 saveLastSubtitle(showing.language)
101 }) 106 })
102 107
103 this.player.on('sourcechange', () => this.initCaptions()) 108 this.player.on('video-change', () => {
104 109 this.initOnVideoChange()
105 this.player.duration(options.videoDuration) 110 })
106
107 this.initializePlayer()
108 this.runUserViewing()
109 }) 111 })
110 } 112 }
111 113
112 dispose () { 114 dispose () {
113 if (this.videoViewInterval) clearInterval(this.videoViewInterval) 115 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
116
117 super.dispose()
114 } 118 }
115 119
116 onMenuOpened () { 120 onMenuOpened () {
@@ -162,40 +166,70 @@ class PeerTubePlugin extends Plugin {
162 166
163 this.initSmoothProgressBar() 167 this.initSmoothProgressBar()
164 168
165 this.initCaptions() 169 this.player.ready(() => {
166 170 this.listenControlBarMouse()
167 this.listenControlBarMouse() 171 })
168 172
169 this.listenFullScreenChange() 173 this.listenFullScreenChange()
170 } 174 }
171 175
176 private initOnVideoChange () {
177 if (this.hasAutoplay() !== false) this.player.addClass('vjs-has-autoplay')
178 else this.player.removeClass('vjs-has-autoplay')
179
180 if (this.currentPlaybackRate && this.currentPlaybackRate !== 1) {
181 debugLogger('Setting playback rate to ' + this.currentPlaybackRate)
182
183 this.player.playbackRate(this.currentPlaybackRate)
184 }
185
186 this.player.ready(() => {
187 this.initCaptions()
188 this.updateControlBar()
189 })
190
191 this.handleStartStopTime()
192 this.runUserViewing()
193 }
194
172 // --------------------------------------------------------------------------- 195 // ---------------------------------------------------------------------------
173 196
174 private runUserViewing () { 197 private runUserViewing () {
175 let lastCurrentTime = this.startTime 198 const startTime = timeToInt(this.options.startTime())
199
200 let lastCurrentTime = startTime
176 let lastViewEvent: VideoViewEvent 201 let lastViewEvent: VideoViewEvent
177 202
178 this.player.one('play', () => { 203 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
179 this.notifyUserIsWatching(this.startTime, lastViewEvent) 204 if (this.videoViewOnPlayHandler) this.player.off('play', this.videoViewOnPlayHandler)
180 }) 205 if (this.videoViewOnSeekedHandler) this.player.off('seeked', this.videoViewOnSeekedHandler)
206 if (this.videoViewOnEndedHandler) this.player.off('ended', this.videoViewOnEndedHandler)
181 207
182 this.player.on('seeked', () => { 208 this.videoViewOnPlayHandler = () => {
209 this.notifyUserIsWatching(startTime, lastViewEvent)
210 }
211
212 this.videoViewOnSeekedHandler = () => {
183 const diff = Math.floor(this.player.currentTime()) - lastCurrentTime 213 const diff = Math.floor(this.player.currentTime()) - lastCurrentTime
184 214
185 // Don't take into account small forwards 215 // Don't take into account small forwards
186 if (diff > 0 && diff < 3) return 216 if (diff > 0 && diff < 3) return
187 217
188 lastViewEvent = 'seek' 218 lastViewEvent = 'seek'
189 }) 219 }
190 220
191 this.player.one('ended', () => { 221 this.videoViewOnEndedHandler = () => {
192 const currentTime = Math.floor(this.player.duration()) 222 const currentTime = Math.floor(this.player.duration())
193 lastCurrentTime = currentTime 223 lastCurrentTime = currentTime
194 224
195 this.notifyUserIsWatching(currentTime, lastViewEvent) 225 this.notifyUserIsWatching(currentTime, lastViewEvent)
196 226
197 lastViewEvent = undefined 227 lastViewEvent = undefined
198 }) 228 }
229
230 this.player.one('play', this.videoViewOnPlayHandler)
231 this.player.on('seeked', this.videoViewOnSeekedHandler)
232 this.player.one('ended', this.videoViewOnEndedHandler)
199 233
200 this.videoViewInterval = setInterval(() => { 234 this.videoViewInterval = setInterval(() => {
201 const currentTime = Math.floor(this.player.currentTime()) 235 const currentTime = Math.floor(this.player.currentTime())
@@ -209,13 +243,13 @@ class PeerTubePlugin extends Plugin {
209 .catch(err => logger.error('Cannot notify user is watching.', err)) 243 .catch(err => logger.error('Cannot notify user is watching.', err))
210 244
211 lastViewEvent = undefined 245 lastViewEvent = undefined
212 }, this.videoViewIntervalMs) 246 }, this.options.videoViewIntervalMs)
213 } 247 }
214 248
215 private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { 249 private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
216 // Server won't save history, so save the video position in local storage 250 // Server won't save history, so save the video position in local storage
217 if (!this.authorizationHeader()) { 251 if (!this.authorizationHeader()) {
218 saveVideoWatchHistory(this.videoUUID, currentTime) 252 saveVideoWatchHistory(this.options.videoUUID(), currentTime)
219 } 253 }
220 254
221 if (!this.videoViewUrl) return Promise.resolve(true) 255 if (!this.videoViewUrl) return Promise.resolve(true)
@@ -225,7 +259,7 @@ class PeerTubePlugin extends Plugin {
225 const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) 259 const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
226 if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader()) 260 if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
227 261
228 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) 262 return fetch(this.videoViewUrl(), { method: 'POST', body: JSON.stringify(body), headers })
229 } 263 }
230 264
231 // --------------------------------------------------------------------------- 265 // ---------------------------------------------------------------------------
@@ -279,18 +313,89 @@ class PeerTubePlugin extends Plugin {
279 } 313 }
280 314
281 private initCaptions () { 315 private initCaptions () {
282 for (const caption of this.videoCaptions) { 316 debugLogger('Init captions with current subtitle ' + this.currentSubtitle)
317
318 this.player.tech(true).clearTracks('text')
319
320 for (const caption of this.options.videoCaptions()) {
283 this.player.addRemoteTextTrack({ 321 this.player.addRemoteTextTrack({
284 kind: 'captions', 322 kind: 'captions',
285 label: caption.label, 323 label: caption.label,
286 language: caption.language, 324 language: caption.language,
287 id: caption.language, 325 id: caption.language,
288 src: caption.src, 326 src: caption.src,
289 default: this.defaultSubtitle === caption.language 327 default: this.currentSubtitle === caption.language
290 }, false) 328 }, true)
329 }
330
331 this.player.trigger('captions-changed')
332 }
333
334 private updateControlBar () {
335 debugLogger('Updating control bar')
336
337 if (this.options.isLive()) {
338 this.getPlaybackRateButton().hide()
339
340 this.player.controlBar.getChild('progressControl').hide()
341 this.player.controlBar.getChild('currentTimeDisplay').hide()
342 this.player.controlBar.getChild('timeDivider').hide()
343 this.player.controlBar.getChild('durationDisplay').hide()
344
345 this.player.controlBar.getChild('peerTubeLiveDisplay').show()
346 } else {
347 this.getPlaybackRateButton().show()
348
349 this.player.controlBar.getChild('progressControl').show()
350 this.player.controlBar.getChild('currentTimeDisplay').show()
351 this.player.controlBar.getChild('timeDivider').show()
352 this.player.controlBar.getChild('durationDisplay').show()
353
354 this.player.controlBar.getChild('peerTubeLiveDisplay').hide()
291 } 355 }
292 356
293 this.player.trigger('captionsChanged') 357 if (this.options.videoCaptions().length === 0) {
358 this.getCaptionsButton().hide()
359 } else {
360 this.getCaptionsButton().show()
361 }
362 }
363
364 private handleStartStopTime () {
365 this.player.duration(this.options.videoDuration())
366
367 if (this.stopTimeHandler) {
368 this.player.off('timeupdate', this.stopTimeHandler)
369 this.stopTimeHandler = undefined
370 }
371
372 // Prefer canplaythrough instead of canplay because Chrome has issues with the second one
373 this.player.one('canplaythrough', () => {
374 if (this.options.startTime()) {
375 debugLogger('Start the video at ' + this.options.startTime())
376
377 this.player.currentTime(timeToInt(this.options.startTime()))
378 }
379
380 if (this.options.stopTime()) {
381 const stopTime = timeToInt(this.options.stopTime())
382
383 this.stopTimeHandler = () => {
384 if (this.player.currentTime() <= stopTime) return
385
386 debugLogger('Stopping the video at ' + this.options.stopTime())
387
388 // Time top stop
389 this.player.pause()
390 this.player.trigger('auto-stopped')
391
392 this.player.off('timeupdate', this.stopTimeHandler)
393 this.stopTimeHandler = undefined
394 }
395
396 this.player.on('timeupdate', this.stopTimeHandler)
397 }
398 })
294 } 399 }
295 400
296 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 401 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
@@ -314,6 +419,37 @@ class PeerTubePlugin extends Plugin {
314 this.update() 419 this.update()
315 } 420 }
316 } 421 }
422
423 private getCaptionsButton () {
424 const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
425
426 return settingsButton.menu.getChild('captionsButton') as videojs.CaptionsButton
427 }
428
429 private getPlaybackRateButton () {
430 const settingsButton = this.player.controlBar.getDescendant([ 'settingsButton' ]) as SettingsButton
431
432 return settingsButton.menu.getChild('playbackRateMenuButton')
433 }
434
435 // We don't use webtorrent anymore, so we can safely remove old chunks from IndexedDB
436 private deleteLegacyIndexedDB () {
437 try {
438 if (typeof window.indexedDB === 'undefined') return
439 if (!window.indexedDB) return
440 if (typeof window.indexedDB.databases !== 'function') return
441
442 window.indexedDB.databases()
443 .then(databases => {
444 for (const db of databases) {
445 window.indexedDB.deleteDatabase(db.name)
446 }
447 })
448 } catch (err) {
449 debugLogger('Cannot delete legacy indexed DB', err)
450 // Nothing to do
451 }
452 }
317} 453}
318 454
319videojs.registerPlugin('peertube', PeerTubePlugin) 455videojs.registerPlugin('peertube', PeerTubePlugin)