diff options
Diffstat (limited to 'client/src/assets/player/shared')
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 | ||
55 | function isSameOrigin (current: string, target: string) { | ||
56 | return new URL(current).origin === new URL(target).origin | ||
57 | } | ||
58 | |||
55 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
56 | 60 | ||
57 | export { | 61 | export { |
@@ -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 | ||
195 | videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin) | 199 | videojs.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' | |||
5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' | 5 | import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' |
6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' | 6 | import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' |
7 | import { PeertubePlayerManagerOptions } from '../../types/manager-options' | 7 | import { PeertubePlayerManagerOptions } from '../../types/manager-options' |
8 | import { getRtcConfig } from '../common' | 8 | import { getRtcConfig, isSameOrigin } from '../common' |
9 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' |
10 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' | 10 | import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' |
11 | import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' | 11 | import { 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 @@ | |||
1 | import { PeertubePlayerManagerOptions } from '../../types' | 1 | import { addQueryParams } from '../../../../../../shared/core-utils' |
2 | import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types' | ||
2 | 3 | ||
3 | export class WebTorrentOptionsBuilder { | 4 | export 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' | |||
2 | import { Segment } from '@peertube/p2p-media-loader-core' | 2 | import { Segment } from '@peertube/p2p-media-loader-core' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { wait } from '@root-helpers/utils' | 4 | import { wait } from '@root-helpers/utils' |
5 | import { isSameOrigin } from '../common' | ||
5 | 6 | ||
6 | type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } | 7 | type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } |
7 | 8 | ||
8 | const maxRetries = 3 | 9 | const maxRetries = 3 |
9 | 10 | ||
10 | function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { | 11 | function 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 | ||
71 | function fetchSha256Segments (url: string) { | 80 | function 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 | ||
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 | 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 | ||
87 | videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | 106 | videojs.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 | ||
9 | class ResolutionMenuItem extends MenuItem { | 9 | class 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' | |||
2 | import * as WebTorrent from 'webtorrent' | 2 | import * as WebTorrent from 'webtorrent' |
3 | import { logger } from '@root-helpers/logger' | 3 | import { logger } from '@root-helpers/logger' |
4 | import { isIOS } from '@root-helpers/web-browser' | 4 | import { isIOS } from '@root-helpers/web-browser' |
5 | import { timeToInt } from '@shared/core-utils' | 5 | import { addQueryParams, timeToInt } from '@shared/core-utils' |
6 | import { VideoFile } from '@shared/models' | 6 | import { VideoFile } from '@shared/models' |
7 | import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' | 7 | import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' |
8 | import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' | 8 | import { 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 | ||