diff options
Diffstat (limited to 'client/src/assets/player/p2p-media-loader')
3 files changed, 234 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..022a9c16f --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -0,0 +1,143 @@ | |||
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.on('play', () => { | ||
55 | player.addClass('vjs-has-big-play-button-clicked') | ||
56 | }) | ||
57 | |||
58 | player.ready(() => this.initialize()) | ||
59 | } | ||
60 | |||
61 | dispose () { | ||
62 | if (this.hlsjs) this.hlsjs.destroy() | ||
63 | if (this.p2pEngine) this.p2pEngine.destroy() | ||
64 | |||
65 | clearInterval(this.networkInfoInterval) | ||
66 | } | ||
67 | |||
68 | private initialize () { | ||
69 | initHlsJsPlayer(this.hlsjs) | ||
70 | |||
71 | const tech = this.player.tech_ | ||
72 | this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() | ||
73 | |||
74 | // Avoid using constants to not import hls.hs | ||
75 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | ||
76 | this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { | ||
77 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | ||
78 | }) | ||
79 | |||
80 | this.p2pEngine.on(Events.SegmentError, (segment, err) => { | ||
81 | console.error('Segment error.', segment, err) | ||
82 | }) | ||
83 | |||
84 | this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length | ||
85 | |||
86 | this.runStats() | ||
87 | } | ||
88 | |||
89 | private runStats () { | ||
90 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | ||
91 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
92 | |||
93 | elem.pendingDownload.push(size) | ||
94 | elem.totalDownload += size | ||
95 | }) | ||
96 | |||
97 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | ||
98 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
99 | |||
100 | elem.pendingUpload.push(size) | ||
101 | elem.totalUpload += size | ||
102 | }) | ||
103 | |||
104 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | ||
105 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | ||
106 | |||
107 | this.networkInfoInterval = setInterval(() => { | ||
108 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) | ||
109 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) | ||
110 | |||
111 | const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) | ||
112 | const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) | ||
113 | |||
114 | this.statsP2PBytes.pendingDownload = [] | ||
115 | this.statsP2PBytes.pendingUpload = [] | ||
116 | this.statsHTTPBytes.pendingDownload = [] | ||
117 | this.statsHTTPBytes.pendingUpload = [] | ||
118 | |||
119 | return this.player.trigger('p2pInfo', { | ||
120 | http: { | ||
121 | downloadSpeed: httpDownloadSpeed, | ||
122 | uploadSpeed: httpUploadSpeed, | ||
123 | downloaded: this.statsHTTPBytes.totalDownload, | ||
124 | uploaded: this.statsHTTPBytes.totalUpload | ||
125 | }, | ||
126 | p2p: { | ||
127 | downloadSpeed: p2pDownloadSpeed, | ||
128 | uploadSpeed: p2pUploadSpeed, | ||
129 | numPeers: this.statsP2PBytes.numPeers, | ||
130 | downloaded: this.statsP2PBytes.totalDownload, | ||
131 | uploaded: this.statsP2PBytes.totalUpload | ||
132 | } | ||
133 | } as PlayerNetworkInfo) | ||
134 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
135 | } | ||
136 | |||
137 | private arraySum (data: number[]) { | ||
138 | return data.reduce((a: number, b: number) => a + b, 0) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
143 | 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..72c32f9e0 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts | |||
@@ -0,0 +1,63 @@ | |||
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 | const regex = /bytes=(\d+)-(\d+)/ | ||
7 | |||
8 | return async function segmentValidator (segment: Segment) { | ||
9 | const filename = basename(segment.url) | ||
10 | const captured = regex.exec(segment.range) | ||
11 | |||
12 | const range = captured[1] + '-' + captured[2] | ||
13 | |||
14 | const hashShouldBe = (await segmentsJSON)[filename][range] | ||
15 | if (hashShouldBe === undefined) { | ||
16 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) | ||
17 | } | ||
18 | |||
19 | const calculatedSha = bufferToEx(await sha256(segment.data)) | ||
20 | if (calculatedSha !== hashShouldBe) { | ||
21 | throw new Error( | ||
22 | `Hashes does not correspond for segment ${filename}/${range}` + | ||
23 | `(expected: ${hashShouldBe} instead of ${calculatedSha})` | ||
24 | ) | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | segmentValidatorFactory | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function fetchSha256Segments (url: string) { | ||
38 | return fetch(url) | ||
39 | .then(res => res.json()) | ||
40 | .catch(err => { | ||
41 | console.error('Cannot get sha256 segments', err) | ||
42 | return {} | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | function sha256 (data?: ArrayBuffer) { | ||
47 | if (!data) return undefined | ||
48 | |||
49 | return window.crypto.subtle.digest('SHA-256', data) | ||
50 | } | ||
51 | |||
52 | // Thanks: https://stackoverflow.com/a/53307879 | ||
53 | function bufferToEx (buffer?: ArrayBuffer) { | ||
54 | if (!buffer) return '' | ||
55 | |||
56 | let s = '' | ||
57 | const h = '0123456789abcdef' | ||
58 | const o = new Uint8Array(buffer) | ||
59 | |||
60 | o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ]) | ||
61 | |||
62 | return s | ||
63 | } | ||