From 092092969633bbcf6d4891a083ea497a7d5c3154 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 29 Jan 2019 08:37:25 +0100 Subject: Add hls support on server --- .../p2p-media-loader/p2p-media-loader-plugin.ts | 135 +++++++++++++++++++++ .../player/p2p-media-loader/segment-url-builder.ts | 28 +++++ .../player/p2p-media-loader/segment-validator.ts | 56 +++++++++ 3 files changed, 219 insertions(+) create mode 100644 client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts create mode 100644 client/src/assets/player/p2p-media-loader/segment-url-builder.ts create mode 100644 client/src/assets/player/p2p-media-loader/segment-validator.ts (limited to 'client/src/assets/player/p2p-media-loader') 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 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import * as videojs from 'video.js' +import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' +import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' +import { Events } from 'p2p-media-loader-core' + +// videojs-hlsjs-plugin needs videojs in window +window['videojs'] = videojs +require('@streamroot/videojs-hlsjs-plugin') + +const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') +class P2pMediaLoaderPlugin extends Plugin { + + private readonly CONSTANTS = { + INFO_SCHEDULER: 1000 // Don't change this + } + private readonly options: P2PMediaLoaderPluginOptions + + private hlsjs: any // Don't type hlsjs to not bundle the module + private p2pEngine: Engine + private statsP2PBytes = { + pendingDownload: [] as number[], + pendingUpload: [] as number[], + numPeers: 0, + totalDownload: 0, + totalUpload: 0 + } + private statsHTTPBytes = { + pendingDownload: [] as number[], + pendingUpload: [] as number[], + totalDownload: 0, + totalUpload: 0 + } + + private networkInfoInterval: any + + constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { + super(player, options) + + this.options = options + + videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { + this.hlsjs = hlsjs + }) + + initVideoJsContribHlsJsPlayer(player) + + player.src({ + type: options.type, + src: options.src + }) + + player.ready(() => this.initialize()) + } + + dispose () { + clearInterval(this.networkInfoInterval) + } + + private initialize () { + initHlsJsPlayer(this.hlsjs) + + this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine() + + // Avoid using constants to not import hls.hs + // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 + this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { + this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) + }) + + this.p2pEngine.on(Events.SegmentError, (segment, err) => { + console.error('Segment error.', segment, err) + }) + + this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length + + this.runStats() + } + + private runStats () { + this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { + const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes + + elem.pendingDownload.push(size) + elem.totalDownload += size + }) + + this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { + const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes + + elem.pendingUpload.push(size) + elem.totalUpload += size + }) + + this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) + this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) + + this.networkInfoInterval = setInterval(() => { + const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) + const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) + + const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) + const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) + + this.statsP2PBytes.pendingDownload = [] + this.statsP2PBytes.pendingUpload = [] + this.statsHTTPBytes.pendingDownload = [] + this.statsHTTPBytes.pendingUpload = [] + + return this.player.trigger('p2pInfo', { + http: { + downloadSpeed: httpDownloadSpeed, + uploadSpeed: httpUploadSpeed, + downloaded: this.statsHTTPBytes.totalDownload, + uploaded: this.statsHTTPBytes.totalUpload + }, + p2p: { + downloadSpeed: p2pDownloadSpeed, + uploadSpeed: p2pUploadSpeed, + numPeers: this.statsP2PBytes.numPeers, + downloaded: this.statsP2PBytes.totalDownload, + uploaded: this.statsP2PBytes.totalUpload + } + } as PlayerNetworkInfo) + }, this.CONSTANTS.INFO_SCHEDULER) + } + + private arraySum (data: number[]) { + return data.reduce((a: number, b: number) => a + b, 0) + } +} + +videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) +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 @@ +import { basename } from 'path' +import { Segment } from 'p2p-media-loader-core' + +function segmentUrlBuilderFactory (baseUrls: string[]) { + return function segmentBuilder (segment: Segment) { + const max = baseUrls.length + 1 + const i = getRandomInt(max) + + if (i === max - 1) return segment.url + + let newBaseUrl = baseUrls[i] + let middlePart = newBaseUrl.endsWith('/') ? '' : '/' + + return newBaseUrl + middlePart + basename(segment.url) + } +} + +// --------------------------------------------------------------------------- + +export { + segmentUrlBuilderFactory +} + +// --------------------------------------------------------------------------- + +function getRandomInt (max: number) { + return Math.floor(Math.random() * Math.floor(max)) +} 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 @@ +import { Segment } from 'p2p-media-loader-core' +import { basename } from 'path' + +function segmentValidatorFactory (segmentsSha256Url: string) { + const segmentsJSON = fetchSha256Segments(segmentsSha256Url) + + return async function segmentValidator (segment: Segment) { + const segmentName = basename(segment.url) + + const hashShouldBe = (await segmentsJSON)[segmentName] + if (hashShouldBe === undefined) { + throw new Error(`Unknown segment name ${segmentName} in segment validator`) + } + + const calculatedSha = bufferToEx(await sha256(segment.data)) + if (calculatedSha !== hashShouldBe) { + throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`) + } + } +} + +// --------------------------------------------------------------------------- + +export { + segmentValidatorFactory +} + +// --------------------------------------------------------------------------- + +function fetchSha256Segments (url: string) { + return fetch(url) + .then(res => res.json()) + .catch(err => { + console.error('Cannot get sha256 segments', err) + return {} + }) +} + +function sha256 (data?: ArrayBuffer) { + if (!data) return undefined + + return window.crypto.subtle.digest('SHA-256', data) +} + +// Thanks: https://stackoverflow.com/a/53307879 +function bufferToEx (buffer?: ArrayBuffer) { + if (!buffer) return '' + + let s = '' + const h = '0123456789abcdef' + const o = new Uint8Array(buffer) + + o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ]) + + return s +} -- cgit v1.2.3