diff options
Diffstat (limited to 'client/src')
14 files changed, 278 insertions, 64 deletions
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 0b3511e8e..021b1feb4 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -22,7 +22,9 @@ export abstract class UserEdit extends FormReactive { | |||
22 | } | 22 | } |
23 | 23 | ||
24 | computeQuotaWithTranscoding () { | 24 | computeQuotaWithTranscoding () { |
25 | const resolutions = this.serverService.getConfig().transcoding.enabledResolutions | 25 | const transcodingConfig = this.serverService.getConfig().transcoding |
26 | |||
27 | const resolutions = transcodingConfig.enabledResolutions | ||
26 | const higherResolution = VideoResolution.H_1080P | 28 | const higherResolution = VideoResolution.H_1080P |
27 | let multiplier = 0 | 29 | let multiplier = 0 |
28 | 30 | ||
@@ -30,6 +32,8 @@ export abstract class UserEdit extends FormReactive { | |||
30 | multiplier += resolution / higherResolution | 32 | multiplier += resolution / higherResolution |
31 | } | 33 | } |
32 | 34 | ||
35 | if (transcodingConfig.hls.enabled) multiplier *= 2 | ||
36 | |||
33 | return multiplier * parseInt(this.form.value['videoQuota'], 10) | 37 | return multiplier * parseInt(this.form.value['videoQuota'], 10) |
34 | } | 38 | } |
35 | 39 | ||
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 4ae72427b..c868ccdcc 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -51,7 +51,10 @@ export class ServerService { | |||
51 | requiresEmailVerification: false | 51 | requiresEmailVerification: false |
52 | }, | 52 | }, |
53 | transcoding: { | 53 | transcoding: { |
54 | enabledResolutions: [] | 54 | enabledResolutions: [], |
55 | hls: { | ||
56 | enabled: false | ||
57 | } | ||
55 | }, | 58 | }, |
56 | avatar: { | 59 | avatar: { |
57 | file: { | 60 | file: { |
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index fa4ca7f93..f44b4138b 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core' | |||
3 | import { Video } from '../../shared/video/video.model' | 3 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 4 | import { Account } from '@app/shared/account/account.model' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
6 | import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model' | ||
7 | import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type' | ||
6 | 8 | ||
7 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 9 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
8 | descriptionPath: string | 10 | descriptionPath: string |
@@ -19,6 +21,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
19 | likesPercent: number | 21 | likesPercent: number |
20 | dislikesPercent: number | 22 | dislikesPercent: number |
21 | 23 | ||
24 | trackerUrls: string[] | ||
25 | |||
26 | streamingPlaylists: VideoStreamingPlaylist[] | ||
27 | |||
22 | constructor (hash: VideoDetailsServerModel, translations = {}) { | 28 | constructor (hash: VideoDetailsServerModel, translations = {}) { |
23 | super(hash, translations) | 29 | super(hash, translations) |
24 | 30 | ||
@@ -30,6 +36,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
30 | this.support = hash.support | 36 | this.support = hash.support |
31 | this.commentsEnabled = hash.commentsEnabled | 37 | this.commentsEnabled = hash.commentsEnabled |
32 | 38 | ||
39 | this.trackerUrls = hash.trackerUrls | ||
40 | this.streamingPlaylists = hash.streamingPlaylists | ||
41 | |||
33 | this.buildLikeAndDislikePercents() | 42 | this.buildLikeAndDislikePercents() |
34 | } | 43 | } |
35 | 44 | ||
@@ -53,4 +62,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
53 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 | 62 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 |
54 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 | 63 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 |
55 | } | 64 | } |
65 | |||
66 | getHlsPlaylist () { | ||
67 | return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
68 | } | ||
56 | } | 69 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 6e38af195..f77316712 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -23,7 +23,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
23 | import { environment } from '../../../environments/environment' | 23 | import { environment } from '../../../environments/environment' |
24 | import { VideoCaptionService } from '@app/shared/video-caption' | 24 | import { VideoCaptionService } from '@app/shared/video-caption' |
25 | import { MarkdownService } from '@app/shared/renderer' | 25 | import { MarkdownService } from '@app/shared/renderer' |
26 | import { PeertubePlayerManager } from '../../../assets/player/peertube-player-manager' | 26 | import { P2PMediaLoaderOptions, PeertubePlayerManager, PlayerMode, WebtorrentOptions } from '../../../assets/player/peertube-player-manager' |
27 | 27 | ||
28 | @Component({ | 28 | @Component({ |
29 | selector: 'my-video-watch', | 29 | selector: 'my-video-watch', |
@@ -424,15 +424,33 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
424 | serverUrl: environment.apiUrl, | 424 | serverUrl: environment.apiUrl, |
425 | 425 | ||
426 | videoCaptions: playerCaptions | 426 | videoCaptions: playerCaptions |
427 | }, | 427 | } |
428 | } | ||
428 | 429 | ||
429 | webtorrent: { | 430 | let mode: PlayerMode |
431 | const hlsPlaylist = this.video.getHlsPlaylist() | ||
432 | if (hlsPlaylist) { | ||
433 | mode = 'p2p-media-loader' | ||
434 | const p2pMediaLoader = { | ||
435 | playlistUrl: hlsPlaylist.playlistUrl, | ||
436 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
437 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
438 | trackerAnnounce: this.video.trackerUrls, | ||
430 | videoFiles: this.video.files | 439 | videoFiles: this.video.files |
431 | } | 440 | } as P2PMediaLoaderOptions |
441 | |||
442 | Object.assign(options, { p2pMediaLoader }) | ||
443 | } else { | ||
444 | mode = 'webtorrent' | ||
445 | const webtorrent = { | ||
446 | videoFiles: this.video.files | ||
447 | } as WebtorrentOptions | ||
448 | |||
449 | Object.assign(options, { webtorrent }) | ||
432 | } | 450 | } |
433 | 451 | ||
434 | this.zone.runOutsideAngular(async () => { | 452 | this.zone.runOutsideAngular(async () => { |
435 | this.player = await PeertubePlayerManager.initialize('webtorrent', options) | 453 | this.player = await PeertubePlayerManager.initialize(mode, options) |
436 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) | 454 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) |
437 | }) | 455 | }) |
438 | 456 | ||
diff --git a/client/src/assets/player/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts index a5b20219f..f9a2707fb 100644 --- a/client/src/assets/player/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -1,21 +1,21 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | // FIXME: something weird with our path definition in tsconfig and typings |
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from './peertube-videojs-typings' | 4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' |
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
6 | import { Events } from 'p2p-media-loader-core' | ||
5 | 7 | ||
6 | // videojs-hlsjs-plugin needs videojs in window | 8 | // videojs-hlsjs-plugin needs videojs in window |
7 | window['videojs'] = videojs | 9 | window['videojs'] = videojs |
8 | require('@streamroot/videojs-hlsjs-plugin') | 10 | require('@streamroot/videojs-hlsjs-plugin') |
9 | 11 | ||
10 | import { Engine, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
11 | import { Events } from 'p2p-media-loader-core' | ||
12 | |||
13 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 12 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
14 | class P2pMediaLoaderPlugin extends Plugin { | 13 | class P2pMediaLoaderPlugin extends Plugin { |
15 | 14 | ||
16 | private readonly CONSTANTS = { | 15 | private readonly CONSTANTS = { |
17 | INFO_SCHEDULER: 1000 // Don't change this | 16 | INFO_SCHEDULER: 1000 // Don't change this |
18 | } | 17 | } |
18 | private readonly options: P2PMediaLoaderPluginOptions | ||
19 | 19 | ||
20 | private hlsjs: any // Don't type hlsjs to not bundle the module | 20 | private hlsjs: any // Don't type hlsjs to not bundle the module |
21 | private p2pEngine: Engine | 21 | private p2pEngine: Engine |
@@ -26,16 +26,22 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
26 | totalDownload: 0, | 26 | totalDownload: 0, |
27 | totalUpload: 0 | 27 | totalUpload: 0 |
28 | } | 28 | } |
29 | private statsHTTPBytes = { | ||
30 | pendingDownload: [] as number[], | ||
31 | pendingUpload: [] as number[], | ||
32 | totalDownload: 0, | ||
33 | totalUpload: 0 | ||
34 | } | ||
29 | 35 | ||
30 | private networkInfoInterval: any | 36 | private networkInfoInterval: any |
31 | 37 | ||
32 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | 38 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { |
33 | super(player, options) | 39 | super(player, options) |
34 | 40 | ||
41 | this.options = options | ||
42 | |||
35 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | 43 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { |
36 | this.hlsjs = hlsjs | 44 | this.hlsjs = hlsjs |
37 | |||
38 | this.initialize() | ||
39 | }) | 45 | }) |
40 | 46 | ||
41 | initVideoJsContribHlsJsPlayer(player) | 47 | initVideoJsContribHlsJsPlayer(player) |
@@ -44,6 +50,8 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
44 | type: options.type, | 50 | type: options.type, |
45 | src: options.src | 51 | src: options.src |
46 | }) | 52 | }) |
53 | |||
54 | player.ready(() => this.initialize()) | ||
47 | } | 55 | } |
48 | 56 | ||
49 | dispose () { | 57 | dispose () { |
@@ -51,6 +59,8 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
51 | } | 59 | } |
52 | 60 | ||
53 | private initialize () { | 61 | private initialize () { |
62 | initHlsJsPlayer(this.hlsjs) | ||
63 | |||
54 | this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine() | 64 | this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine() |
55 | 65 | ||
56 | // Avoid using constants to not import hls.hs | 66 | // Avoid using constants to not import hls.hs |
@@ -59,38 +69,55 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
59 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | 69 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) |
60 | }) | 70 | }) |
61 | 71 | ||
72 | this.p2pEngine.on(Events.SegmentError, (segment, err) => { | ||
73 | console.error('Segment error.', segment, err) | ||
74 | }) | ||
75 | |||
76 | this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length | ||
77 | |||
62 | this.runStats() | 78 | this.runStats() |
63 | } | 79 | } |
64 | 80 | ||
65 | private runStats () { | 81 | private runStats () { |
66 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | 82 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { |
67 | if (method === 'p2p') { | 83 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes |
68 | this.statsP2PBytes.pendingDownload.push(size) | 84 | |
69 | this.statsP2PBytes.totalDownload += size | 85 | elem.pendingDownload.push(size) |
70 | } | 86 | elem.totalDownload += size |
71 | }) | 87 | }) |
72 | 88 | ||
73 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | 89 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { |
74 | if (method === 'p2p') { | 90 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes |
75 | this.statsP2PBytes.pendingUpload.push(size) | 91 | |
76 | this.statsP2PBytes.totalUpload += size | 92 | elem.pendingUpload.push(size) |
77 | } | 93 | elem.totalUpload += size |
78 | }) | 94 | }) |
79 | 95 | ||
80 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | 96 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) |
81 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | 97 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) |
82 | 98 | ||
83 | this.networkInfoInterval = setInterval(() => { | 99 | this.networkInfoInterval = setInterval(() => { |
84 | let downloadSpeed = this.statsP2PBytes.pendingDownload.reduce((a: number, b: number) => a + b, 0) | 100 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) |
85 | let uploadSpeed = this.statsP2PBytes.pendingUpload.reduce((a: number, b: number) => a + b, 0) | 101 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) |
102 | |||
103 | const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) | ||
104 | const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) | ||
86 | 105 | ||
87 | this.statsP2PBytes.pendingDownload = [] | 106 | this.statsP2PBytes.pendingDownload = [] |
88 | this.statsP2PBytes.pendingUpload = [] | 107 | this.statsP2PBytes.pendingUpload = [] |
108 | this.statsHTTPBytes.pendingDownload = [] | ||
109 | this.statsHTTPBytes.pendingUpload = [] | ||
89 | 110 | ||
90 | return this.player.trigger('p2pInfo', { | 111 | return this.player.trigger('p2pInfo', { |
112 | http: { | ||
113 | downloadSpeed: httpDownloadSpeed, | ||
114 | uploadSpeed: httpUploadSpeed, | ||
115 | downloaded: this.statsHTTPBytes.totalDownload, | ||
116 | uploaded: this.statsHTTPBytes.totalUpload | ||
117 | }, | ||
91 | p2p: { | 118 | p2p: { |
92 | downloadSpeed, | 119 | downloadSpeed: p2pDownloadSpeed, |
93 | uploadSpeed, | 120 | uploadSpeed: p2pUploadSpeed, |
94 | numPeers: this.statsP2PBytes.numPeers, | 121 | numPeers: this.statsP2PBytes.numPeers, |
95 | downloaded: this.statsP2PBytes.totalDownload, | 122 | downloaded: this.statsP2PBytes.totalDownload, |
96 | uploaded: this.statsP2PBytes.totalUpload | 123 | uploaded: this.statsP2PBytes.totalUpload |
@@ -98,6 +125,10 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
98 | } as PlayerNetworkInfo) | 125 | } as PlayerNetworkInfo) |
99 | }, this.CONSTANTS.INFO_SCHEDULER) | 126 | }, this.CONSTANTS.INFO_SCHEDULER) |
100 | } | 127 | } |
128 | |||
129 | private arraySum (data: number[]) { | ||
130 | return data.reduce((a: number, b: number) => a + b, 0) | ||
131 | } | ||
101 | } | 132 | } |
102 | 133 | ||
103 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | 134 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) |
diff --git a/client/src/assets/player/p2p-media-loader/segment-url-builder.ts b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts new file mode 100644 index 000000000..32e7ce4f2 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { basename } from 'path' | ||
2 | import { Segment } from 'p2p-media-loader-core' | ||
3 | |||
4 | function segmentUrlBuilderFactory (baseUrls: string[]) { | ||
5 | return function segmentBuilder (segment: Segment) { | ||
6 | const max = baseUrls.length + 1 | ||
7 | const i = getRandomInt(max) | ||
8 | |||
9 | if (i === max - 1) return segment.url | ||
10 | |||
11 | let newBaseUrl = baseUrls[i] | ||
12 | let middlePart = newBaseUrl.endsWith('/') ? '' : '/' | ||
13 | |||
14 | return newBaseUrl + middlePart + basename(segment.url) | ||
15 | } | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | segmentUrlBuilderFactory | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function getRandomInt (max: number) { | ||
27 | return Math.floor(Math.random() * Math.floor(max)) | ||
28 | } | ||
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts new file mode 100644 index 000000000..8f4922daa --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts | |||
@@ -0,0 +1,56 @@ | |||
1 | import { Segment } from 'p2p-media-loader-core' | ||
2 | import { basename } from 'path' | ||
3 | |||
4 | function segmentValidatorFactory (segmentsSha256Url: string) { | ||
5 | const segmentsJSON = fetchSha256Segments(segmentsSha256Url) | ||
6 | |||
7 | return async function segmentValidator (segment: Segment) { | ||
8 | const segmentName = basename(segment.url) | ||
9 | |||
10 | const hashShouldBe = (await segmentsJSON)[segmentName] | ||
11 | if (hashShouldBe === undefined) { | ||
12 | throw new Error(`Unknown segment name ${segmentName} in segment validator`) | ||
13 | } | ||
14 | |||
15 | const calculatedSha = bufferToEx(await sha256(segment.data)) | ||
16 | if (calculatedSha !== hashShouldBe) { | ||
17 | throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`) | ||
18 | } | ||
19 | } | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | segmentValidatorFactory | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | function fetchSha256Segments (url: string) { | ||
31 | return fetch(url) | ||
32 | .then(res => res.json()) | ||
33 | .catch(err => { | ||
34 | console.error('Cannot get sha256 segments', err) | ||
35 | return {} | ||
36 | }) | ||
37 | } | ||
38 | |||
39 | function sha256 (data?: ArrayBuffer) { | ||
40 | if (!data) return undefined | ||
41 | |||
42 | return window.crypto.subtle.digest('SHA-256', data) | ||
43 | } | ||
44 | |||
45 | // Thanks: https://stackoverflow.com/a/53307879 | ||
46 | function bufferToEx (buffer?: ArrayBuffer) { | ||
47 | if (!buffer) return '' | ||
48 | |||
49 | let s = '' | ||
50 | const h = '0123456789abcdef' | ||
51 | const o = new Uint8Array(buffer) | ||
52 | |||
53 | o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ]) | ||
54 | |||
55 | return s | ||
56 | } | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 91ca6a2aa..3fdba6fdf 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -13,8 +13,10 @@ import './videojs-components/p2p-info-button' | |||
13 | import './videojs-components/peertube-load-progress-bar' | 13 | import './videojs-components/peertube-load-progress-bar' |
14 | import './videojs-components/theater-button' | 14 | import './videojs-components/theater-button' |
15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | 15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' |
16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | 16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' |
17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | 17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' |
18 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | ||
19 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | ||
18 | 20 | ||
19 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | 21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) |
20 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | 22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' |
@@ -31,7 +33,10 @@ export type WebtorrentOptions = { | |||
31 | 33 | ||
32 | export type P2PMediaLoaderOptions = { | 34 | export type P2PMediaLoaderOptions = { |
33 | playlistUrl: string | 35 | playlistUrl: string |
36 | segmentsSha256Url: string | ||
34 | trackerAnnounce: string[] | 37 | trackerAnnounce: string[] |
38 | redundancyBaseUrls: string[] | ||
39 | videoFiles: VideoFile[] | ||
35 | } | 40 | } |
36 | 41 | ||
37 | export type CommonOptions = { | 42 | export type CommonOptions = { |
@@ -90,11 +95,11 @@ export class PeertubePlayerManager { | |||
90 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | 95 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { |
91 | let p2pMediaLoader: any | 96 | let p2pMediaLoader: any |
92 | 97 | ||
93 | if (mode === 'webtorrent') await import('./webtorrent-plugin') | 98 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') |
94 | if (mode === 'p2p-media-loader') { | 99 | if (mode === 'p2p-media-loader') { |
95 | [ p2pMediaLoader ] = await Promise.all([ | 100 | [ p2pMediaLoader ] = await Promise.all([ |
96 | import('p2p-media-loader-hlsjs'), | 101 | import('p2p-media-loader-hlsjs'), |
97 | import('./p2p-media-loader-plugin') | 102 | import('./p2p-media-loader/p2p-media-loader-plugin') |
98 | ]) | 103 | ]) |
99 | } | 104 | } |
100 | 105 | ||
@@ -144,11 +149,14 @@ export class PeertubePlayerManager { | |||
144 | const commonOptions = options.common | 149 | const commonOptions = options.common |
145 | const webtorrentOptions = options.webtorrent | 150 | const webtorrentOptions = options.webtorrent |
146 | const p2pMediaLoaderOptions = options.p2pMediaLoader | 151 | const p2pMediaLoaderOptions = options.p2pMediaLoader |
152 | |||
153 | let autoplay = options.common.autoplay | ||
147 | let html5 = {} | 154 | let html5 = {} |
148 | 155 | ||
149 | const plugins: VideoJSPluginOptions = { | 156 | const plugins: VideoJSPluginOptions = { |
150 | peertube: { | 157 | peertube: { |
151 | autoplay: commonOptions.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | 158 | mode, |
159 | autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
152 | videoViewUrl: commonOptions.videoViewUrl, | 160 | videoViewUrl: commonOptions.videoViewUrl, |
153 | videoDuration: commonOptions.videoDuration, | 161 | videoDuration: commonOptions.videoDuration, |
154 | startTime: commonOptions.startTime, | 162 | startTime: commonOptions.startTime, |
@@ -160,19 +168,35 @@ export class PeertubePlayerManager { | |||
160 | 168 | ||
161 | if (p2pMediaLoaderOptions) { | 169 | if (p2pMediaLoaderOptions) { |
162 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | 170 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { |
171 | redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls, | ||
163 | type: 'application/x-mpegURL', | 172 | type: 'application/x-mpegURL', |
164 | src: p2pMediaLoaderOptions.playlistUrl | 173 | src: p2pMediaLoaderOptions.playlistUrl |
165 | } | 174 | } |
166 | 175 | ||
176 | const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce | ||
177 | .filter(t => t.startsWith('ws')) | ||
178 | |||
167 | const p2pMediaLoaderConfig = { | 179 | const p2pMediaLoaderConfig = { |
168 | // loader: { | 180 | loader: { |
169 | // trackerAnnounce: p2pMediaLoaderOptions.trackerAnnounce | 181 | trackerAnnounce, |
170 | // }, | 182 | segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url), |
183 | rtcConfig: getRtcConfig(), | ||
184 | requiredSegmentsPriority: 5, | ||
185 | segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls) | ||
186 | }, | ||
171 | segments: { | 187 | segments: { |
172 | swarmId: p2pMediaLoaderOptions.playlistUrl | 188 | swarmId: p2pMediaLoaderOptions.playlistUrl |
173 | } | 189 | } |
174 | } | 190 | } |
175 | const streamrootHls = { | 191 | const streamrootHls = { |
192 | levelLabelHandler: (level: { height: number, width: number }) => { | ||
193 | const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) | ||
194 | |||
195 | let label = file.resolution.label | ||
196 | if (file.fps >= 50) label += file.fps | ||
197 | |||
198 | return label | ||
199 | }, | ||
176 | html5: { | 200 | html5: { |
177 | hlsjsConfig: { | 201 | hlsjsConfig: { |
178 | liveSyncDurationCount: 7, | 202 | liveSyncDurationCount: 7, |
@@ -187,12 +211,15 @@ export class PeertubePlayerManager { | |||
187 | 211 | ||
188 | if (webtorrentOptions) { | 212 | if (webtorrentOptions) { |
189 | const webtorrent = { | 213 | const webtorrent = { |
190 | autoplay: commonOptions.autoplay, | 214 | autoplay, |
191 | videoDuration: commonOptions.videoDuration, | 215 | videoDuration: commonOptions.videoDuration, |
192 | playerElement: commonOptions.playerElement, | 216 | playerElement: commonOptions.playerElement, |
193 | videoFiles: webtorrentOptions.videoFiles | 217 | videoFiles: webtorrentOptions.videoFiles |
194 | } | 218 | } |
195 | Object.assign(plugins, { webtorrent }) | 219 | Object.assign(plugins, { webtorrent }) |
220 | |||
221 | // WebTorrent plugin handles autoplay, because we do some hackish stuff in there | ||
222 | autoplay = false | ||
196 | } | 223 | } |
197 | 224 | ||
198 | const videojsOptions = { | 225 | const videojsOptions = { |
@@ -208,7 +235,7 @@ export class PeertubePlayerManager { | |||
208 | : undefined, // Undefined so the player knows it has to check the local storage | 235 | : undefined, // Undefined so the player knows it has to check the local storage |
209 | 236 | ||
210 | poster: commonOptions.poster, | 237 | poster: commonOptions.poster, |
211 | autoplay: false, | 238 | autoplay, |
212 | inactivityTimeout: commonOptions.inactivityTimeout, | 239 | inactivityTimeout: commonOptions.inactivityTimeout, |
213 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | 240 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], |
214 | plugins, | 241 | plugins, |
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts index f83d9094a..aacbf5f6e 100644 --- a/client/src/assets/player/peertube-plugin.ts +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -52,12 +52,12 @@ class PeerTubePlugin extends Plugin { | |||
52 | this.player.ready(() => { | 52 | this.player.ready(() => { |
53 | const playerOptions = this.player.options_ | 53 | const playerOptions = this.player.options_ |
54 | 54 | ||
55 | if (this.player.webtorrent) { | 55 | if (options.mode === 'webtorrent') { |
56 | this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | 56 | this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) |
57 | this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d)) | 57 | this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d)) |
58 | } | 58 | } |
59 | 59 | ||
60 | if (this.player.p2pMediaLoader) { | 60 | if (options.mode === 'p2p-media-loader') { |
61 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | 61 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) |
62 | } | 62 | } |
63 | 63 | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index fff992a6f..79a5a6c4d 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -4,12 +4,15 @@ import * as videojs from 'video.js' | |||
4 | 4 | ||
5 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
6 | import { PeerTubePlugin } from './peertube-plugin' | 6 | import { PeerTubePlugin } from './peertube-plugin' |
7 | import { WebTorrentPlugin } from './webtorrent-plugin' | 7 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' |
8 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | ||
9 | import { PlayerMode } from './peertube-player-manager' | ||
8 | 10 | ||
9 | declare namespace videojs { | 11 | declare namespace videojs { |
10 | interface Player { | 12 | interface Player { |
11 | peertube (): PeerTubePlugin | 13 | peertube (): PeerTubePlugin |
12 | webtorrent (): WebTorrentPlugin | 14 | webtorrent (): WebTorrentPlugin |
15 | p2pMediaLoader (): P2pMediaLoaderPlugin | ||
13 | } | 16 | } |
14 | } | 17 | } |
15 | 18 | ||
@@ -33,6 +36,8 @@ type UserWatching = { | |||
33 | } | 36 | } |
34 | 37 | ||
35 | type PeerTubePluginOptions = { | 38 | type PeerTubePluginOptions = { |
39 | mode: PlayerMode | ||
40 | |||
36 | autoplay: boolean | 41 | autoplay: boolean |
37 | videoViewUrl: string | 42 | videoViewUrl: string |
38 | videoDuration: number | 43 | videoDuration: number |
@@ -54,6 +59,7 @@ type WebtorrentPluginOptions = { | |||
54 | } | 59 | } |
55 | 60 | ||
56 | type P2PMediaLoaderPluginOptions = { | 61 | type P2PMediaLoaderPluginOptions = { |
62 | redundancyBaseUrls: string[] | ||
57 | type: string | 63 | type: string |
58 | src: string | 64 | src: string |
59 | } | 65 | } |
@@ -91,6 +97,13 @@ type AutoResolutionUpdateData = { | |||
91 | } | 97 | } |
92 | 98 | ||
93 | type PlayerNetworkInfo = { | 99 | type PlayerNetworkInfo = { |
100 | http: { | ||
101 | downloadSpeed: number | ||
102 | uploadSpeed: number | ||
103 | downloaded: number | ||
104 | uploaded: number | ||
105 | } | ||
106 | |||
94 | p2p: { | 107 | p2p: { |
95 | downloadSpeed: number | 108 | downloadSpeed: number |
96 | uploadSpeed: number | 109 | uploadSpeed: number |
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 8b9f34b99..8d87567c2 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -112,9 +112,23 @@ function videoFileMinByResolution (files: VideoFile[]) { | |||
112 | return min | 112 | return min |
113 | } | 113 | } |
114 | 114 | ||
115 | function getRtcConfig () { | ||
116 | return { | ||
117 | iceServers: [ | ||
118 | { | ||
119 | urls: 'stun:stun.stunprotocol.org' | ||
120 | }, | ||
121 | { | ||
122 | urls: 'stun:stun.framasoft.org' | ||
123 | } | ||
124 | ] | ||
125 | } | ||
126 | } | ||
127 | |||
115 | // --------------------------------------------------------------------------- | 128 | // --------------------------------------------------------------------------- |
116 | 129 | ||
117 | export { | 130 | export { |
131 | getRtcConfig, | ||
118 | toTitleCase, | 132 | toTitleCase, |
119 | timeToInt, | 133 | timeToInt, |
120 | buildVideoLink, | 134 | buildVideoLink, |
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index 2fc4c4562..6424787b2 100644 --- a/client/src/assets/player/videojs-components/p2p-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts | |||
@@ -75,11 +75,12 @@ class P2pInfoButton extends Button { | |||
75 | } | 75 | } |
76 | 76 | ||
77 | const p2pStats = data.p2p | 77 | const p2pStats = data.p2p |
78 | const httpStats = data.http | ||
78 | 79 | ||
79 | const downloadSpeed = bytes(p2pStats.downloadSpeed) | 80 | const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) |
80 | const uploadSpeed = bytes(p2pStats.uploadSpeed) | 81 | const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) |
81 | const totalDownloaded = bytes(p2pStats.downloaded) | 82 | const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) |
82 | const totalUploaded = bytes(p2pStats.uploaded) | 83 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) |
83 | const numPeers = p2pStats.numPeers | 84 | const numPeers = p2pStats.numPeers |
84 | 85 | ||
85 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + | 86 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + |
@@ -92,7 +93,7 @@ class P2pInfoButton extends Button { | |||
92 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] | 93 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] |
93 | 94 | ||
94 | peersNumber.textContent = numPeers | 95 | peersNumber.textContent = numPeers |
95 | peersText.textContent = ' ' + this.player_.localize('peers') | 96 | peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) |
96 | 97 | ||
97 | subDivHttp.className = 'vjs-peertube-hidden' | 98 | subDivHttp.className = 'vjs-peertube-hidden' |
98 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 99 | subDivWebtorrent.className = 'vjs-peertube-displayed' |
diff --git a/client/src/assets/player/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 47f169e24..c69bf31fa 100644 --- a/client/src/assets/player/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -3,18 +3,18 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../../shared/models/videos/video.model' |
7 | import { renderVideo } from './webtorrent/video-renderer' | 7 | import { renderVideo } from './video-renderer' |
8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings' | 8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
9 | import { videoFileMaxByResolution, videoFileMinByResolution } from './utils' | 9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
10 | import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store' | 10 | import { PeertubeChunkStore } from './peertube-chunk-store' |
11 | import { | 11 | import { |
12 | getAverageBandwidthInStore, | 12 | getAverageBandwidthInStore, |
13 | getStoredMute, | 13 | getStoredMute, |
14 | getStoredVolume, | 14 | getStoredVolume, |
15 | getStoredWebTorrentEnabled, | 15 | getStoredWebTorrentEnabled, |
16 | saveAverageBandwidth | 16 | saveAverageBandwidth |
17 | } from './peertube-player-local-storage' | 17 | } from '../peertube-player-local-storage' |
18 | 18 | ||
19 | const CacheChunkStore = require('cache-chunk-store') | 19 | const CacheChunkStore = require('cache-chunk-store') |
20 | 20 | ||
@@ -44,16 +44,7 @@ class WebTorrentPlugin extends Plugin { | |||
44 | 44 | ||
45 | private readonly webtorrent = new WebTorrent({ | 45 | private readonly webtorrent = new WebTorrent({ |
46 | tracker: { | 46 | tracker: { |
47 | rtcConfig: { | 47 | rtcConfig: getRtcConfig() |
48 | iceServers: [ | ||
49 | { | ||
50 | urls: 'stun:stun.stunprotocol.org' | ||
51 | }, | ||
52 | { | ||
53 | urls: 'stun:stun.framasoft.org' | ||
54 | } | ||
55 | ] | ||
56 | } | ||
57 | }, | 48 | }, |
58 | dht: false | 49 | dht: false |
59 | }) | 50 | }) |
@@ -472,6 +463,12 @@ class WebTorrentPlugin extends Plugin { | |||
472 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | 463 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) |
473 | 464 | ||
474 | return this.player.trigger('p2pInfo', { | 465 | return this.player.trigger('p2pInfo', { |
466 | http: { | ||
467 | downloadSpeed: 0, | ||
468 | uploadSpeed: 0, | ||
469 | downloaded: 0, | ||
470 | uploaded: 0 | ||
471 | }, | ||
475 | p2p: { | 472 | p2p: { |
476 | downloadSpeed: this.torrent.downloadSpeed, | 473 | downloadSpeed: this.torrent.downloadSpeed, |
477 | numPeers: this.torrent.numPeers, | 474 | numPeers: this.torrent.numPeers, |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 6dd9a3d76..1e58d42d9 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -23,7 +23,13 @@ import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' | |||
23 | import { PeerTubeResolution } from '../player/definitions' | 23 | import { PeerTubeResolution } from '../player/definitions' |
24 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' | 24 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' |
25 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 25 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
26 | import { PeertubePlayerManager, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' | 26 | import { |
27 | P2PMediaLoaderOptions, | ||
28 | PeertubePlayerManager, | ||
29 | PeertubePlayerManagerOptions, | ||
30 | PlayerMode | ||
31 | } from '../../assets/player/peertube-player-manager' | ||
32 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | ||
27 | 33 | ||
28 | /** | 34 | /** |
29 | * Embed API exposes control of the embed player to the outside world via | 35 | * Embed API exposes control of the embed player to the outside world via |
@@ -319,13 +325,16 @@ class PeerTubeEmbed { | |||
319 | } | 325 | } |
320 | 326 | ||
321 | if (this.mode === 'p2p-media-loader') { | 327 | if (this.mode === 'p2p-media-loader') { |
328 | const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
329 | |||
322 | Object.assign(options, { | 330 | Object.assign(options, { |
323 | p2pMediaLoader: { | 331 | p2pMediaLoader: { |
324 | // playlistUrl: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8' | 332 | playlistUrl: hlsPlaylist.playlistUrl, |
325 | // playlistUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8' | 333 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, |
326 | // trackerAnnounce: [ window.location.origin.replace(/^http/, 'ws') + '/tracker/socket' ], | 334 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), |
327 | playlistUrl: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8' | 335 | trackerAnnounce: videoInfo.trackerUrls, |
328 | } | 336 | videoFiles: videoInfo.files |
337 | } as P2PMediaLoaderOptions | ||
329 | }) | 338 | }) |
330 | } else { | 339 | } else { |
331 | Object.assign(options, { | 340 | Object.assign(options, { |