aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player/shared')
-rw-r--r--client/src/assets/player/shared/common/utils.ts8
-rw-r--r--client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts6
-rw-r--r--client/src/assets/player/shared/manager-options/hls-options-builder.ts27
-rw-r--r--client/src/assets/player/shared/manager-options/manager-options-builder.ts4
-rw-r--r--client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts15
-rw-r--r--client/src/assets/player/shared/metrics/metrics-plugin.ts2
-rw-r--r--client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts45
-rw-r--r--client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts24
-rw-r--r--client/src/assets/player/shared/p2p-media-loader/segment-validator.ts30
-rw-r--r--client/src/assets/player/shared/peertube/peertube-plugin.ts32
-rw-r--r--client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts5
-rw-r--r--client/src/assets/player/shared/settings/resolution-menu-button.ts19
-rw-r--r--client/src/assets/player/shared/settings/resolution-menu-item.ts2
-rw-r--r--client/src/assets/player/shared/upnext/upnext-plugin.ts3
-rw-r--r--client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts22
15 files changed, 208 insertions, 36 deletions
diff --git a/client/src/assets/player/shared/common/utils.ts b/client/src/assets/player/shared/common/utils.ts
index a010d9184..609240626 100644
--- a/client/src/assets/player/shared/common/utils.ts
+++ b/client/src/assets/player/shared/common/utils.ts
@@ -52,6 +52,10 @@ function getRtcConfig () {
52 } 52 }
53} 53}
54 54
55function isSameOrigin (current: string, target: string) {
56 return new URL(current).origin === new URL(target).origin
57}
58
55// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
56 60
57export { 61export {
@@ -60,5 +64,7 @@ export {
60 64
61 videoFileMaxByResolution, 65 videoFileMaxByResolution,
62 videoFileMinByResolution, 66 videoFileMinByResolution,
63 bytes 67 bytes,
68
69 isSameOrigin
64} 70}
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
index 8255245b5..ec1e1038b 100644
--- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
+++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
@@ -160,7 +160,7 @@ class PeerTubeHotkeysPlugin extends Plugin {
160 // 0-9 key handlers 160 // 0-9 key handlers
161 for (let i = 0; i < 10; i++) { 161 for (let i = 0; i < 10; i++) {
162 handlers.push({ 162 handlers.push({
163 accept: e => e.key === i + '' && !e.ctrlKey, // If using ctrl key, it's a web browser hotkey 163 accept: e => this.isNakedOrShift(e, i + ''),
164 cb: e => { 164 cb: e => {
165 e.preventDefault() 165 e.preventDefault()
166 166
@@ -190,6 +190,10 @@ class PeerTubeHotkeysPlugin extends Plugin {
190 private isNaked (event: KeyboardEvent, key: string) { 190 private isNaked (event: KeyboardEvent, key: string) {
191 return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key) 191 return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key)
192 } 192 }
193
194 private isNakedOrShift (event: KeyboardEvent, key: string) {
195 return (!event.ctrlKey && !event.altKey && !event.metaKey && event.key === key)
196 }
193} 197}
194 198
195videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin) 199videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin)
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/manager-options/hls-options-builder.ts
index a572febc2..497a97436 100644
--- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/hls-options-builder.ts
@@ -5,7 +5,7 @@ import { LiveVideoLatencyMode } from '@shared/models'
5import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' 5import { getAverageBandwidthInStore } from '../../peertube-player-local-storage'
6import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' 6import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types'
7import { PeertubePlayerManagerOptions } from '../../types/manager-options' 7import { PeertubePlayerManagerOptions } from '../../types/manager-options'
8import { getRtcConfig } from '../common' 8import { getRtcConfig, isSameOrigin } from '../common'
9import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' 9import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager'
10import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' 10import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder'
11import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' 11import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator'
@@ -19,15 +19,20 @@ export class HLSOptionsBuilder {
19 19
20 } 20 }
21 21
22 getPluginOptions () { 22 async getPluginOptions () {
23 const commonOptions = this.options.common 23 const commonOptions = this.options.common
24 24
25 const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls) 25 const redundancyUrlManager = new RedundancyUrlManager(this.options.p2pMediaLoader.redundancyBaseUrls)
26 26
27 const p2pMediaLoaderConfig = this.getP2PMediaLoaderOptions(redundancyUrlManager) 27 const p2pMediaLoaderConfig = await this.options.pluginsManager.runHook(
28 'filter:internal.player.p2p-media-loader.options.result',
29 this.getP2PMediaLoaderOptions(redundancyUrlManager)
30 )
28 const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader 31 const loader = new this.p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() as P2PMediaLoader
29 32
30 const p2pMediaLoader: P2PMediaLoaderPluginOptions = { 33 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
34 requiresAuth: commonOptions.requiresAuth,
35
31 redundancyUrlManager, 36 redundancyUrlManager,
32 type: 'application/x-mpegURL', 37 type: 'application/x-mpegURL',
33 startTime: commonOptions.startTime, 38 startTime: commonOptions.startTime,
@@ -81,7 +86,21 @@ export class HLSOptionsBuilder {
81 simultaneousHttpDownloads: 1, 86 simultaneousHttpDownloads: 1,
82 httpFailedSegmentTimeout: 1000, 87 httpFailedSegmentTimeout: 1000,
83 88
84 segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive), 89 xhrSetup: (xhr, url) => {
90 if (!this.options.common.requiresAuth) return
91 if (!isSameOrigin(this.options.common.serverUrl, url)) return
92
93 xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader())
94 },
95
96 segmentValidator: segmentValidatorFactory({
97 segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url,
98 isLive: this.options.common.isLive,
99 authorizationHeader: this.options.common.authorizationHeader,
100 requiresAuth: this.options.common.requiresAuth,
101 serverUrl: this.options.common.serverUrl
102 }),
103
85 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), 104 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
86 105
87 useP2P: this.options.common.p2pEnabled, 106 useP2P: this.options.common.p2pEnabled,
diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts
index 07678493d..2d96c9410 100644
--- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/manager-options-builder.ts
@@ -20,7 +20,7 @@ export class ManagerOptionsBuilder {
20 20
21 } 21 }
22 22
23 getVideojsOptions (alreadyPlayed: boolean): videojs.PlayerOptions { 23 async getVideojsOptions (alreadyPlayed: boolean): Promise<videojs.PlayerOptions> {
24 const commonOptions = this.options.common 24 const commonOptions = this.options.common
25 25
26 let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed) 26 let autoplay = this.getAutoPlayValue(commonOptions.autoplay, alreadyPlayed)
@@ -61,7 +61,7 @@ export class ManagerOptionsBuilder {
61 61
62 if (this.mode === 'p2p-media-loader') { 62 if (this.mode === 'p2p-media-loader') {
63 const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule) 63 const hlsOptionsBuilder = new HLSOptionsBuilder(this.options, this.p2pMediaLoaderModule)
64 const options = hlsOptionsBuilder.getPluginOptions() 64 const options = await hlsOptionsBuilder.getPluginOptions()
65 65
66 Object.assign(plugins, pick(options, [ 'hlsjs', 'p2pMediaLoader' ])) 66 Object.assign(plugins, pick(options, [ 'hlsjs', 'p2pMediaLoader' ]))
67 Object.assign(html5, options.html5) 67 Object.assign(html5, options.html5)
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
index 257cf1e05..b5bdcd4e6 100644
--- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
@@ -1,4 +1,5 @@
1import { PeertubePlayerManagerOptions } from '../../types' 1import { addQueryParams } from '../../../../../../shared/core-utils'
2import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types'
2 3
3export class WebTorrentOptionsBuilder { 4export class WebTorrentOptionsBuilder {
4 5
@@ -16,13 +17,23 @@ export class WebTorrentOptionsBuilder {
16 17
17 const autoplay = this.autoPlayValue === 'play' 18 const autoplay = this.autoPlayValue === 'play'
18 19
19 const webtorrent = { 20 const webtorrent: WebtorrentPluginOptions = {
20 autoplay, 21 autoplay,
21 22
22 playerRefusedP2P: commonOptions.p2pEnabled === false, 23 playerRefusedP2P: commonOptions.p2pEnabled === false,
23 videoDuration: commonOptions.videoDuration, 24 videoDuration: commonOptions.videoDuration,
24 playerElement: commonOptions.playerElement, 25 playerElement: commonOptions.playerElement,
25 26
27 videoFileToken: commonOptions.videoFileToken,
28
29 requiresAuth: commonOptions.requiresAuth,
30
31 buildWebSeedUrls: file => {
32 if (!commonOptions.requiresAuth) return []
33
34 return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ]
35 },
36
26 videoFiles: webtorrentOptions.videoFiles.length !== 0 37 videoFiles: webtorrentOptions.videoFiles.length !== 0
27 ? webtorrentOptions.videoFiles 38 ? webtorrentOptions.videoFiles
28 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode 39 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
diff --git a/client/src/assets/player/shared/metrics/metrics-plugin.ts b/client/src/assets/player/shared/metrics/metrics-plugin.ts
index 2844828da..0e296bef6 100644
--- a/client/src/assets/player/shared/metrics/metrics-plugin.ts
+++ b/client/src/assets/player/shared/metrics/metrics-plugin.ts
@@ -104,6 +104,8 @@ class MetricsPlugin extends Plugin {
104 104
105 private trackBytes () { 105 private trackBytes () {
106 this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => { 106 this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => {
107 if (!data) return
108
107 this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) 109 this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0)
108 this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0) 110 this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0)
109 111
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
index e49e5c694..a14beb347 100644
--- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
@@ -211,6 +211,28 @@ class Html5Hlsjs {
211 } 211 }
212 } 212 }
213 213
214 private _getHumanErrorMsg (error: { message: string, code?: number }) {
215 switch (error.code) {
216 default:
217 return error.message
218 }
219 }
220
221 private _handleUnrecovarableError (error: any) {
222 if (this.hls.levels.filter(l => l.id > -1).length > 1) {
223 this._removeQuality(this.hls.loadLevel)
224 return
225 }
226
227 this.hls.destroy()
228 logger.info('bubbling error up to VIDEOJS')
229 this.tech.error = () => ({
230 ...error,
231 message: this._getHumanErrorMsg(error)
232 })
233 this.tech.trigger('error')
234 }
235
214 private _handleMediaError (error: any) { 236 private _handleMediaError (error: any) {
215 if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) { 237 if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] === 1) {
216 logger.info('trying to recover media error') 238 logger.info('trying to recover media error')
@@ -226,14 +248,13 @@ class Html5Hlsjs {
226 } 248 }
227 249
228 if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) { 250 if (this.errorCounts[Hlsjs.ErrorTypes.MEDIA_ERROR] > 2) {
229 logger.info('bubbling media error up to VIDEOJS') 251 this._handleUnrecovarableError(error)
230 this.hls.destroy()
231 this.tech.error = () => error
232 this.tech.trigger('error')
233 } 252 }
234 } 253 }
235 254
236 private _handleNetworkError (error: any) { 255 private _handleNetworkError (error: any) {
256 if (navigator.onLine === false) return
257
237 if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) { 258 if (this.errorCounts[Hlsjs.ErrorTypes.NETWORK_ERROR] <= this.maxNetworkErrorRecovery) {
238 logger.info('trying to recover network error') 259 logger.info('trying to recover network error')
239 260
@@ -248,10 +269,7 @@ class Html5Hlsjs {
248 return 269 return
249 } 270 }
250 271
251 logger.info('bubbling network error up to VIDEOJS') 272 this._handleUnrecovarableError(error)
252 this.hls.destroy()
253 this.tech.error = () => error
254 this.tech.trigger('error')
255 } 273 }
256 274
257 private _onError (_event: any, data: ErrorData) { 275 private _onError (_event: any, data: ErrorData) {
@@ -273,10 +291,7 @@ class Html5Hlsjs {
273 error.code = 3 291 error.code = 3
274 this._handleMediaError(error) 292 this._handleMediaError(error)
275 } else if (data.fatal) { 293 } else if (data.fatal) {
276 this.hls.destroy() 294 this._handleUnrecovarableError(error)
277 logger.info('bubbling error up to VIDEOJS')
278 this.tech.error = () => error as any
279 this.tech.trigger('error')
280 } 295 }
281 } 296 }
282 297
@@ -292,6 +307,12 @@ class Html5Hlsjs {
292 return '0' 307 return '0'
293 } 308 }
294 309
310 private _removeQuality (index: number) {
311 this.hls.removeLevel(index)
312 this.player.peertubeResolutions().remove(index)
313 this.hls.currentLevel = -1
314 }
315
295 private _notifyVideoQualities () { 316 private _notifyVideoQualities () {
296 if (!this.metadata) return 317 if (!this.metadata) return
297 318
diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
index 56068e340..b608ee3e2 100644
--- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -44,13 +44,31 @@ class P2pMediaLoaderPlugin extends Plugin {
44 if (!(videojs as any).Html5Hlsjs) { 44 if (!(videojs as any).Html5Hlsjs) {
45 logger.warn('HLS.js does not seem to be supported. Try to fallback to built in HLS.') 45 logger.warn('HLS.js does not seem to be supported. Try to fallback to built in HLS.')
46 46
47 let message: string
47 if (!player.canPlayType('application/vnd.apple.mpegurl')) { 48 if (!player.canPlayType('application/vnd.apple.mpegurl')) {
48 const message = 'Cannot fallback to built-in HLS' 49 message = 'Cannot fallback to built-in HLS'
50 } else if (options.requiresAuth) {
51 message = 'Video requires auth which is not compatible to build-in HLS player'
52 }
53
54 if (message) {
49 logger.warn(message) 55 logger.warn(message)
50 56
51 player.ready(() => player.trigger('error', new Error(message))) 57 const error: MediaError = {
58 code: MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
59 message,
60 MEDIA_ERR_ABORTED: MediaError.MEDIA_ERR_ABORTED,
61 MEDIA_ERR_DECODE: MediaError.MEDIA_ERR_DECODE,
62 MEDIA_ERR_NETWORK: MediaError.MEDIA_ERR_NETWORK,
63 MEDIA_ERR_SRC_NOT_SUPPORTED: MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
64 }
65
66 player.ready(() => player.error(error))
52 return 67 return
53 } 68 }
69
70 // Workaround to force video.js to not re create a video element
71 (this.player as any).playerElIngest_ = this.player.el().parentNode
54 } else { 72 } else {
55 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 73 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
56 (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { 74 (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
@@ -115,6 +133,8 @@ class P2pMediaLoaderPlugin extends Plugin {
115 this.p2pEngine = this.options.loader.getEngine() 133 this.p2pEngine = this.options.loader.getEngine()
116 134
117 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { 135 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
136 if (navigator.onLine === false) return
137
118 logger.error(`Segment ${segment.id} error.`, err) 138 logger.error(`Segment ${segment.id} error.`, err)
119 139
120 this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl) 140 this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
index 18cb6750f..a7ee91950 100644
--- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
@@ -2,13 +2,22 @@ import { basename } from 'path'
2import { Segment } from '@peertube/p2p-media-loader-core' 2import { Segment } from '@peertube/p2p-media-loader-core'
3import { logger } from '@root-helpers/logger' 3import { logger } from '@root-helpers/logger'
4import { wait } from '@root-helpers/utils' 4import { wait } from '@root-helpers/utils'
5import { isSameOrigin } from '../common'
5 6
6type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } 7type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } }
7 8
8const maxRetries = 3 9const maxRetries = 3
9 10
10function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { 11function segmentValidatorFactory (options: {
11 let segmentsJSON = fetchSha256Segments(segmentsSha256Url) 12 serverUrl: string
13 segmentsSha256Url: string
14 isLive: boolean
15 authorizationHeader: () => string
16 requiresAuth: boolean
17}) {
18 const { serverUrl, segmentsSha256Url, isLive, authorizationHeader, requiresAuth } = options
19
20 let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
12 const regex = /bytes=(\d+)-(\d+)/ 21 const regex = /bytes=(\d+)-(\d+)/
13 22
14 return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { 23 return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) {
@@ -28,7 +37,7 @@ function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) {
28 37
29 await wait(1000) 38 await wait(1000)
30 39
31 segmentsJSON = fetchSha256Segments(segmentsSha256Url) 40 segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
32 await segmentValidator(segment, _method, _peerId, retry + 1) 41 await segmentValidator(segment, _method, _peerId, retry + 1)
33 42
34 return 43 return
@@ -68,8 +77,19 @@ export {
68 77
69// --------------------------------------------------------------------------- 78// ---------------------------------------------------------------------------
70 79
71function fetchSha256Segments (url: string) { 80function fetchSha256Segments (options: {
72 return fetch(url) 81 serverUrl: string
82 segmentsSha256Url: string
83 authorizationHeader: () => string
84 requiresAuth: boolean
85}) {
86 const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options
87
88 const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url)
89 ? { Authorization: authorizationHeader() }
90 : {}
91
92 return fetch(segmentsSha256Url, { headers })
73 .then(res => res.json() as Promise<SegmentsJSON>) 93 .then(res => res.json() as Promise<SegmentsJSON>)
74 .catch(err => { 94 .catch(err => {
75 logger.error('Cannot get sha256 segments', err) 95 logger.error('Cannot get sha256 segments', err)
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts
index 83c32415e..56de66998 100644
--- a/client/src/assets/player/shared/peertube/peertube-plugin.ts
+++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts
@@ -22,7 +22,7 @@ const 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 26
27 private readonly videoUUID: string 27 private readonly videoUUID: string
28 private readonly startTime: number 28 private readonly startTime: number
@@ -125,6 +125,32 @@ class PeerTubePlugin extends Plugin {
125 } 125 }
126 126
127 displayFatalError () { 127 displayFatalError () {
128 this.player.loadingSpinner.hide()
129
130 const buildModal = (error: MediaError) => {
131 const localize = this.player.localize.bind(this.player)
132
133 const wrapper = document.createElement('div')
134 const header = document.createElement('h1')
135 header.innerText = localize('Failed to play video')
136 wrapper.appendChild(header)
137 const desc = document.createElement('div')
138 desc.innerText = localize('The video failed to play due to technical issues.')
139 wrapper.appendChild(desc)
140 const details = document.createElement('p')
141 details.classList.add('error-details')
142 details.innerText = error.message
143 wrapper.appendChild(details)
144
145 return wrapper
146 }
147
148 const modal = this.player.createModal(buildModal(this.player.error()), {
149 temporary: false,
150 uncloseable: true
151 })
152 modal.addClass('vjs-custom-error-display')
153
128 this.player.addClass('vjs-error-display-enabled') 154 this.player.addClass('vjs-error-display-enabled')
129 } 155 }
130 156
@@ -184,7 +210,7 @@ class PeerTubePlugin extends Plugin {
184 lastViewEvent = undefined 210 lastViewEvent = undefined
185 211
186 // Server won't save history, so save the video position in local storage 212 // Server won't save history, so save the video position in local storage
187 if (!this.authorizationHeader) { 213 if (!this.authorizationHeader()) {
188 saveVideoWatchHistory(this.videoUUID, currentTime) 214 saveVideoWatchHistory(this.videoUUID, currentTime)
189 } 215 }
190 }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL) 216 }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL)
@@ -202,7 +228,7 @@ class PeerTubePlugin extends Plugin {
202 'Content-type': 'application/json; charset=UTF-8' 228 'Content-type': 'application/json; charset=UTF-8'
203 }) 229 })
204 230
205 if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader) 231 if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
206 232
207 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) 233 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
208 } 234 }
diff --git a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
index e7899ac71..4fafd27b1 100644
--- a/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
+++ b/client/src/assets/player/shared/resolutions/peertube-resolutions-plugin.ts
@@ -21,6 +21,11 @@ class PeerTubeResolutionsPlugin extends Plugin {
21 this.trigger('resolutionsAdded') 21 this.trigger('resolutionsAdded')
22 } 22 }
23 23
24 remove (resolutionIndex: number) {
25 this.resolutions = this.resolutions.filter(r => r.id !== resolutionIndex)
26 this.trigger('resolutionRemoved')
27 }
28
24 getResolutions () { 29 getResolutions () {
25 return this.resolutions 30 return this.resolutions
26 } 31 }
diff --git a/client/src/assets/player/shared/settings/resolution-menu-button.ts b/client/src/assets/player/shared/settings/resolution-menu-button.ts
index a0b349f67..672411c11 100644
--- a/client/src/assets/player/shared/settings/resolution-menu-button.ts
+++ b/client/src/assets/player/shared/settings/resolution-menu-button.ts
@@ -12,6 +12,7 @@ class ResolutionMenuButton extends MenuButton {
12 this.controlText('Quality') 12 this.controlText('Quality')
13 13
14 player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities()) 14 player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
15 player.peertubeResolutions().on('resolutionRemoved', () => this.cleanupQualities())
15 16
16 // For parent 17 // For parent
17 player.peertubeResolutions().on('resolutionChanged', () => { 18 player.peertubeResolutions().on('resolutionChanged', () => {
@@ -82,6 +83,24 @@ class ResolutionMenuButton extends MenuButton {
82 83
83 this.trigger('menuChanged') 84 this.trigger('menuChanged')
84 } 85 }
86
87 private cleanupQualities () {
88 const resolutions = this.player().peertubeResolutions().getResolutions()
89
90 this.menu.children().forEach((children: ResolutionMenuItem) => {
91 if (children.resolutionId === undefined) {
92 return
93 }
94
95 if (resolutions.find(r => r.id === children.resolutionId)) {
96 return
97 }
98
99 this.menu.removeChild(children)
100 })
101
102 this.trigger('menuChanged')
103 }
85} 104}
86 105
87videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) 106videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/shared/settings/resolution-menu-item.ts b/client/src/assets/player/shared/settings/resolution-menu-item.ts
index 678eb368b..c59b8b891 100644
--- a/client/src/assets/player/shared/settings/resolution-menu-item.ts
+++ b/client/src/assets/player/shared/settings/resolution-menu-item.ts
@@ -7,7 +7,7 @@ export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
7} 7}
8 8
9class ResolutionMenuItem extends MenuItem { 9class ResolutionMenuItem extends MenuItem {
10 private readonly resolutionId: number 10 readonly resolutionId: number
11 private readonly label: string 11 private readonly label: string
12 12
13 private autoResolutionEnabled: boolean 13 private autoResolutionEnabled: boolean
diff --git a/client/src/assets/player/shared/upnext/upnext-plugin.ts b/client/src/assets/player/shared/upnext/upnext-plugin.ts
index db969024d..e12e8c503 100644
--- a/client/src/assets/player/shared/upnext/upnext-plugin.ts
+++ b/client/src/assets/player/shared/upnext/upnext-plugin.ts
@@ -19,6 +19,9 @@ class UpNextPlugin extends Plugin {
19 19
20 super(player) 20 super(player)
21 21
22 // UpNext plugin can be called later, so ensure the player is not disposed
23 if (this.player.isDisposed()) return
24
22 this.player.ready(() => { 25 this.player.ready(() => {
23 player.addClass('vjs-upnext') 26 player.addClass('vjs-upnext')
24 }) 27 })
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
index fa3f48a9a..658b7c867 100644
--- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
@@ -2,7 +2,7 @@ import videojs from 'video.js'
2import * as WebTorrent from 'webtorrent' 2import * as WebTorrent from 'webtorrent'
3import { logger } from '@root-helpers/logger' 3import { logger } from '@root-helpers/logger'
4import { isIOS } from '@root-helpers/web-browser' 4import { isIOS } from '@root-helpers/web-browser'
5import { timeToInt } from '@shared/core-utils' 5import { addQueryParams, timeToInt } from '@shared/core-utils'
6import { VideoFile } from '@shared/models' 6import { VideoFile } from '@shared/models'
7import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' 7import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage'
8import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' 8import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types'
@@ -38,6 +38,8 @@ class WebTorrentPlugin extends Plugin {
38 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth 38 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
39 } 39 }
40 40
41 private readonly buildWebSeedUrls: (file: VideoFile) => string[]
42
41 private readonly webtorrent = new WebTorrent({ 43 private readonly webtorrent = new WebTorrent({
42 tracker: { 44 tracker: {
43 rtcConfig: getRtcConfig() 45 rtcConfig: getRtcConfig()
@@ -57,6 +59,9 @@ class WebTorrentPlugin extends Plugin {
57 private isAutoResolutionObservation = false 59 private isAutoResolutionObservation = false
58 private playerRefusedP2P = false 60 private playerRefusedP2P = false
59 61
62 private requiresAuth: boolean
63 private videoFileToken: () => string
64
60 private torrentInfoInterval: any 65 private torrentInfoInterval: any
61 private autoQualityInterval: any 66 private autoQualityInterval: any
62 private addTorrentDelay: any 67 private addTorrentDelay: any
@@ -81,6 +86,11 @@ class WebTorrentPlugin extends Plugin {
81 this.savePlayerSrcFunction = this.player.src 86 this.savePlayerSrcFunction = this.player.src
82 this.playerElement = options.playerElement 87 this.playerElement = options.playerElement
83 88
89 this.requiresAuth = options.requiresAuth
90 this.videoFileToken = options.videoFileToken
91
92 this.buildWebSeedUrls = options.buildWebSeedUrls
93
84 this.player.ready(() => { 94 this.player.ready(() => {
85 const playerOptions = this.player.options_ 95 const playerOptions = this.player.options_
86 96
@@ -268,7 +278,8 @@ class WebTorrentPlugin extends Plugin {
268 return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { 278 return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), {
269 max: 100 279 max: 100
270 }) 280 })
271 } 281 },
282 urlList: this.buildWebSeedUrls(this.currentVideoFile)
272 } 283 }
273 284
274 this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { 285 this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => {
@@ -533,7 +544,12 @@ class WebTorrentPlugin extends Plugin {
533 // Enable error display now this is our last fallback 544 // Enable error display now this is our last fallback
534 this.player.one('error', () => this.player.peertube().displayFatalError()) 545 this.player.one('error', () => this.player.peertube().displayFatalError())
535 546
536 const httpUrl = this.currentVideoFile.fileUrl 547 let httpUrl = this.currentVideoFile.fileUrl
548
549 if (this.requiresAuth && this.videoFileToken) {
550 httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() })
551 }
552
537 this.player.src = this.savePlayerSrcFunction 553 this.player.src = this.savePlayerSrcFunction
538 this.player.src(httpUrl) 554 this.player.src(httpUrl)
539 555