diff options
Diffstat (limited to 'client/src/assets/player/p2p-media-loader')
3 files changed, 219 insertions, 0 deletions
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts new file mode 100644 index 000000000..f9a2707fb --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -0,0 +1,135 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
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' | ||
7 | |||
8 | // videojs-hlsjs-plugin needs videojs in window | ||
9 | window['videojs'] = videojs | ||
10 | require('@streamroot/videojs-hlsjs-plugin') | ||
11 | |||
12 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
13 | class P2pMediaLoaderPlugin extends Plugin { | ||
14 | |||
15 | private readonly CONSTANTS = { | ||
16 | INFO_SCHEDULER: 1000 // Don't change this | ||
17 | } | ||
18 | private readonly options: P2PMediaLoaderPluginOptions | ||
19 | |||
20 | private hlsjs: any // Don't type hlsjs to not bundle the module | ||
21 | private p2pEngine: Engine | ||
22 | private statsP2PBytes = { | ||
23 | pendingDownload: [] as number[], | ||
24 | pendingUpload: [] as number[], | ||
25 | numPeers: 0, | ||
26 | totalDownload: 0, | ||
27 | totalUpload: 0 | ||
28 | } | ||
29 | private statsHTTPBytes = { | ||
30 | pendingDownload: [] as number[], | ||
31 | pendingUpload: [] as number[], | ||
32 | totalDownload: 0, | ||
33 | totalUpload: 0 | ||
34 | } | ||
35 | |||
36 | private networkInfoInterval: any | ||
37 | |||
38 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | ||
39 | super(player, options) | ||
40 | |||
41 | this.options = options | ||
42 | |||
43 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | ||
44 | this.hlsjs = hlsjs | ||
45 | }) | ||
46 | |||
47 | initVideoJsContribHlsJsPlayer(player) | ||
48 | |||
49 | player.src({ | ||
50 | type: options.type, | ||
51 | src: options.src | ||
52 | }) | ||
53 | |||
54 | player.ready(() => this.initialize()) | ||
55 | } | ||
56 | |||
57 | dispose () { | ||
58 | clearInterval(this.networkInfoInterval) | ||
59 | } | ||
60 | |||
61 | private initialize () { | ||
62 | initHlsJsPlayer(this.hlsjs) | ||
63 | |||
64 | this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine() | ||
65 | |||
66 | // Avoid using constants to not import hls.hs | ||
67 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | ||
68 | this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { | ||
69 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | ||
70 | }) | ||
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 | |||
78 | this.runStats() | ||
79 | } | ||
80 | |||
81 | private runStats () { | ||
82 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | ||
83 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
84 | |||
85 | elem.pendingDownload.push(size) | ||
86 | elem.totalDownload += size | ||
87 | }) | ||
88 | |||
89 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | ||
90 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
91 | |||
92 | elem.pendingUpload.push(size) | ||
93 | elem.totalUpload += size | ||
94 | }) | ||
95 | |||
96 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | ||
97 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | ||
98 | |||
99 | this.networkInfoInterval = setInterval(() => { | ||
100 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) | ||
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) | ||
105 | |||
106 | this.statsP2PBytes.pendingDownload = [] | ||
107 | this.statsP2PBytes.pendingUpload = [] | ||
108 | this.statsHTTPBytes.pendingDownload = [] | ||
109 | this.statsHTTPBytes.pendingUpload = [] | ||
110 | |||
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 | }, | ||
118 | p2p: { | ||
119 | downloadSpeed: p2pDownloadSpeed, | ||
120 | uploadSpeed: p2pUploadSpeed, | ||
121 | numPeers: this.statsP2PBytes.numPeers, | ||
122 | downloaded: this.statsP2PBytes.totalDownload, | ||
123 | uploaded: this.statsP2PBytes.totalUpload | ||
124 | } | ||
125 | } as PlayerNetworkInfo) | ||
126 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
127 | } | ||
128 | |||
129 | private arraySum (data: number[]) { | ||
130 | return data.reduce((a: number, b: number) => a + b, 0) | ||
131 | } | ||
132 | } | ||
133 | |||
134 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
135 | export { 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 | } | ||