diff options
Diffstat (limited to 'client/src/assets/player/shared/peertube/peertube-plugin.ts')
-rw-r--r-- | client/src/assets/player/shared/peertube/peertube-plugin.ts | 248 |
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 @@ | |||
1 | import debug from 'debug' | 1 | import debug from 'debug' |
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { isMobile } from '@root-helpers/web-browser' | 4 | import { isIOS, isMobile } from '@root-helpers/web-browser' |
5 | import { timeToInt } from '@shared/core-utils' | 5 | import { timeToInt } from '@shared/core-utils' |
6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' | 6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' |
7 | import { | 7 | import { |
@@ -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' |
16 | import { PeerTubePluginOptions, VideoJSCaption } from '../../types' | 16 | import { PeerTubePluginOptions } from '../../types' |
17 | import { SettingsButton } from '../settings/settings-menu-button' | 17 | import { SettingsButton } from '../settings/settings-menu-button' |
18 | 18 | ||
19 | const debugLogger = debug('peertube:player:peertube') | 19 | const debugLogger = debug('peertube:player:peertube') |
@@ -21,43 +21,59 @@ const debugLogger = debug('peertube:player:peertube') | |||
21 | const Plugin = videojs.getPlugin('plugin') | 21 | const Plugin = videojs.getPlugin('plugin') |
22 | 22 | ||
23 | class PeerTubePlugin extends Plugin { | 23 | class 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 | ||
319 | videojs.registerPlugin('peertube', PeerTubePlugin) | 455 | videojs.registerPlugin('peertube', PeerTubePlugin) |