From c78c60ca31f10784b8574a4e5649750b5faa7ec7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 21 Aug 2023 14:21:30 +0300 Subject: [PATCH 001/127] Fix issue with wrong segment endTime. --- packages/p2p-media-loader-core/src/core.ts | 5 ++++- packages/p2p-media-loader-shaka/src/segment-manager.ts | 10 +++++----- packages/p2p-media-loader-shaka/src/stream-utils.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 82d5a501..a8c2dcca 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,6 +1,7 @@ import { Loader } from "./loader"; import { Stream, StreamWithSegments, Segment, SegmentResponse } from "./types"; import { Playback } from "./internal-types"; +import * as Utils from "./utils"; export class Core { private readonly streams = new Map< @@ -15,7 +16,9 @@ export class Core { } hasSegment(segmentLocalId: string): boolean { - return this.streams.has(segmentLocalId); + const { segment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentLocalId) ?? {}; + return !!segment; } getStream(streamLocalId: string): StreamWithSegments | undefined { diff --git a/packages/p2p-media-loader-shaka/src/segment-manager.ts b/packages/p2p-media-loader-shaka/src/segment-manager.ts index 6eb60ca7..cca76503 100644 --- a/packages/p2p-media-loader-shaka/src/segment-manager.ts +++ b/packages/p2p-media-loader-shaka/src/segment-manager.ts @@ -9,17 +9,17 @@ import { export class SegmentManager { private readonly core: Core; - private readonly isHls: boolean; + private streamInfo: Readonly; constructor(streamInfo: Readonly, core: Core) { this.core = core; - this.isHls = streamInfo.protocol === "hls"; + this.streamInfo = streamInfo; } setStream(shakaStream: HookedStream, type: StreamType, index = -1) { const localId = Utils.getStreamLocalIdFromShakaStream( shakaStream, - this.isHls + this.streamInfo.protocol === "hls" ); this.core.addStreamIfNoneExists({ @@ -42,14 +42,14 @@ export class SegmentManager { const { segmentIndex } = stream.shakaStream; if (!segmentReferences && segmentIndex) { try { - return [...segmentIndex]; + segmentReferences = [...segmentIndex]; } catch (err) { return; } } if (!segmentReferences) return; - if (this.isHls) { + if (this.streamInfo.protocol === "hls") { this.processHlsSegmentReferences(stream, segmentReferences); } else { this.processDashSegmentReferences(stream, segmentReferences); diff --git a/packages/p2p-media-loader-shaka/src/stream-utils.ts b/packages/p2p-media-loader-shaka/src/stream-utils.ts index 080bc4a6..08043144 100644 --- a/packages/p2p-media-loader-shaka/src/stream-utils.ts +++ b/packages/p2p-media-loader-shaka/src/stream-utils.ts @@ -61,7 +61,7 @@ export function getSegmentInfoFromReference( const start = segmentReference.getStartByte(); const end = segmentReference.getEndByte() ?? undefined; const startTime = segmentReference.getStartTime(); - const endTime = segmentReference.getStartTime(); + const endTime = segmentReference.getEndTime(); return { byteRange: end !== undefined ? { start, end } : undefined, From eb216136970389dae685917e738a53b0757b4204 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 21 Aug 2023 15:55:47 +0300 Subject: [PATCH 002/127] Fix issue with hls wrong segment local id. --- packages/p2p-media-loader-hlsjs/src/segment-mananger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts index 768027cc..944ca1f9 100644 --- a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts +++ b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts @@ -59,7 +59,7 @@ export class SegmentManager { start, end !== undefined ? end - 1 : undefined ); - const segmentLocalId = Utils.getSegmentLocalId(url, byteRange); + const segmentLocalId = Utils.getSegmentLocalId(responseUrl, byteRange); segmentToRemoveIds.delete(segmentLocalId); if (playlist.segments.has(segmentLocalId)) return; From 99c234d5f873061609fdd08ec21c2e93cb41e7bc Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 23 Aug 2023 17:11:21 +0300 Subject: [PATCH 003/127] Add linked-map, load queue and http-loader. --- packages/p2p-media-loader-core/src/core.ts | 24 ++- .../p2p-media-loader-core/src/http-loader.ts | 47 +++++ .../p2p-media-loader-core/src/linked-map.ts | 94 +++++++++ packages/p2p-media-loader-core/src/loader.ts | 180 +++++++++++++----- packages/p2p-media-loader-core/src/types.ts | 9 +- packages/p2p-media-loader-core/src/utils.ts | 12 +- 6 files changed, 307 insertions(+), 59 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/http-loader.ts create mode 100644 packages/p2p-media-loader-core/src/linked-map.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index a8c2dcca..dd1a6760 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -2,12 +2,10 @@ import { Loader } from "./loader"; import { Stream, StreamWithSegments, Segment, SegmentResponse } from "./types"; import { Playback } from "./internal-types"; import * as Utils from "./utils"; +import { LinkedMap } from "./linked-map"; export class Core { - private readonly streams = new Map< - string, - StreamWithSegments> - >(); + private readonly streams = new Map>(); private readonly playback: Playback = { position: 0, rate: 1 }; private readonly loader: Loader = new Loader(this.streams); @@ -29,7 +27,7 @@ export class Core { if (this.streams.has(stream.localId)) return; this.streams.set(stream.localId, { ...stream, - segments: new Map(), + segments: new LinkedMap(), }); } @@ -62,3 +60,19 @@ export class Core { this.streams.clear(); } } + +class StreamContainer { + private readonly streams = new Map(); + + updateStream( + streamLocalId: string, + addSegments?: Segment[], + removeSegmentIds?: string[] + ): void { + const stream = this.streams.get(streamLocalId); + if (!stream) return; + + addSegments?.forEach((s) => stream.segments.set(s.localId, s)); + removeSegmentIds?.forEach((s) => stream.segments.delete(s)); + } +} diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts new file mode 100644 index 00000000..87657b9e --- /dev/null +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -0,0 +1,47 @@ +import { Segment } from "./types"; +import { FetchError } from "./errors"; + +type RequestContext = { + abortController: AbortController; +}; + +export class HttpLoader { + private readonly segmentRequestContext = new Map(); + + async load(segment: Segment) { + const headers = new Headers(); + const { url, byteRange } = segment; + + if (byteRange) { + const { start, end } = byteRange; + const byteRangeString = `bytes=${start}-${end}`; + headers.set("Range", byteRangeString); + } + const requestContext: RequestContext = { + abortController: new AbortController(), + }; + this.segmentRequestContext.set(segment.localId, requestContext); + const response = await fetch(url, { + headers, + signal: requestContext.abortController.signal, + }); + if (!response.ok) { + throw new FetchError( + response.statusText ?? "Fetch, bad network response", + response.status, + response + ); + } + const data = await response.arrayBuffer(); + return { + ok: response.ok, + status: response.status, + data, + url: response.url, + }; + } + + abort(segmentId: string) { + this.segmentRequestContext.get(segmentId)?.abortController.abort(); + } +} diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts new file mode 100644 index 00000000..a15de8cc --- /dev/null +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -0,0 +1,94 @@ +type LinkedObject = { + value: V; + prev?: LinkedObject; + next?: LinkedObject; +}; + +export class LinkedMap { + private readonly map = new Map>(); + private _first?: LinkedObject; + private _last?: LinkedObject; + + get first() { + return this._first?.value; + } + + get last() { + return this._last?.value; + } + + get size() { + return this.map.size; + } + + get(key: K): V | undefined { + return this.map.get(key)?.value; + } + + has(key: K): boolean { + return this.map.has(key); + } + + addToEnd(key: K, value: V) { + const item: LinkedObject = { value }; + if (this._last) item.prev = this._last; + this._last = item; + this.map.set(key, item); + } + + addToStart(key: K, value: V) { + const item: LinkedObject = { value }; + if (this._first) item.next = this._first; + this._first = item; + this.map.set(key, item); + } + + delete(key: K) { + if (!this.map.size) return; + const item = this.map.get(key); + if (!item) return; + + const { next, prev } = item; + if (this._first?.value === item.value) this._first = next; + if (this._last?.value === item.value) this._last = prev; + if (prev) prev.next = next; + if (next) next.prev = prev; + this.map.delete(key); + } + + clear() { + this._first = undefined; + this._last = undefined; + this.map.clear(); + } + + *values(): Generator { + let item = this._first; + while (item !== undefined) { + yield item.value; + item = item.next; + } + } + + *valuesBackwardsFrom(key: K): Generator { + let value = this.map.get(key); + if (value === undefined) return; + while (value?.value !== undefined) { + yield value.value; + value = value.prev; + } + } + + *valuesFrom(key: K): Generator { + let value = this.map.get(key); + if (value === undefined) return; + while (value?.value !== undefined) { + yield value.value; + value = value.next; + } + } + + getNextTo(key: K): V | undefined { + return this.map.get(key)?.next?.value; + } +} diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index b9e127c1..881114cf 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -1,14 +1,19 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import { getStreamExternalId } from "./utils"; -import { FetchError } from "./errors"; +import * as Utils from "./utils"; +import { LinkedMap } from "./linked-map"; +import { HttpLoader } from "./http-loader"; export class Loader { private manifestResponseUrl?: string; private readonly streams: Map; - private readonly segmentRequestContext = new Map(); + private readonly mainQueue: LoadQueue; + private readonly secondaryQueue: LoadQueue; + private readonly httpLoader = new HttpLoader(); constructor(streams: Map) { this.streams = streams; + this.mainQueue = new LoadQueue(this.streams); + this.secondaryQueue = new LoadQueue(this.streams); } setManifestResponseUrl(url: string) { @@ -16,14 +21,20 @@ export class Loader { } async loadSegment(segmentId: string): Promise { - const segment = this.identifySegment(segmentId); + const { segment, stream } = this.identifySegment(segmentId); + + const queue = stream.type === "main" ? this.mainQueue : this.secondaryQueue; + queue.requestByPlayer(segment.localId); const [response, loadingDuration] = await trackTime( - () => this.fetchSegment(segment), + () => this.httpLoader.load(segment), "s" ); + queue.removeLoadedSegment(segment.localId); + const { data, url, ok, status } = response; const bits = data.byteLength * 8; + const bandwidth = bits / loadingDuration; return { @@ -36,9 +47,7 @@ export class Loader { } abortSegment(segmentId: string) { - const requestContext = this.segmentRequestContext.get(segmentId); - if (!requestContext) return; - requestContext.abortController.abort(); + this.httpLoader.abort(segmentId); } private identifySegment(segmentId: string) { @@ -46,61 +55,25 @@ export class Loader { throw new Error("Manifest response url is undefined"); } - const stream = this.streams.get(segmentId); - const segment = stream?.segments.get(segmentId); + const { stream, segment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; if (!segment || !stream) { throw new Error(`Not found segment with id: ${segmentId}`); } - console.log("\nloading segment:"); - console.log("Index: ", segment.externalId); - const streamEternalId = getStreamExternalId( + // console.log("\nloading segment:"); + // console.log("Index: ", segment.externalId); + const streamEternalId = Utils.getStreamExternalId( stream, this.manifestResponseUrl ); - console.log("Stream: ", streamEternalId); - - return segment; - } - - private async fetchSegment(segment: Segment) { - const headers = new Headers(); - const { url, byteRange } = segment; + // console.log("Stream: ", streamEternalId); + console.log(this.mainQueue); - if (byteRange) { - const { start, end } = byteRange; - const byteRangeString = `bytes=${start}-${end}`; - headers.set("Range", byteRangeString); - } - const requestContext: RequestContext = { - abortController: new AbortController(), - }; - this.segmentRequestContext.set(segment.localId, requestContext); - const response = await fetch(url, { - headers, - signal: requestContext.abortController.signal, - }); - if (!response.ok) { - throw new FetchError( - response.statusText ?? "Fetch, bad network response", - response.status, - response - ); - } - const data = await response.arrayBuffer(); - return { - ok: response.ok, - status: response.status, - data, - url: response.url, - }; + return { segment, stream }; } } -type RequestContext = { - abortController: AbortController; -}; - async function trackTime( action: () => T, unit: "s" | "ms" = "s" @@ -110,3 +83,106 @@ async function trackTime( const duration = performance.now() - start; return [result, unit === "ms" ? duration : duration / 1000]; } + +class LoadQueue { + private queue = new LinkedMap(); + private readonly streams: Map; + private activeStream?: StreamWithSegments; + private lastReqByPlayer?: Segment; + private readonly isSegmentLoaded!: (segmentId: string) => boolean; + + constructor(streams: Map) { + this.streams = streams; + } + + reqSegment(segmentId: string) { + const { stream, segment: requestedSegment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; + } + + requestByPlayer(segmentId: string) { + const { stream, segment: requestedSegment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; + if (!stream || !requestedSegment) return; + + const prevReqByPlayer = this.lastReqByPlayer; + this.lastReqByPlayer = requestedSegment; + if (this.activeStream !== stream) { + this.activeStream = stream; + this.streamChanged(this.activeStream, requestedSegment); + return; + } + if (!prevReqByPlayer) return; + + const next = this.activeStream?.segments.getNextTo(prevReqByPlayer.localId); + if (next === requestedSegment) return; + + if (requestedSegment.startTime > prevReqByPlayer.startTime) { + this.movedForward(requestedSegment); + } else if (requestedSegment.startTime < prevReqByPlayer.startTime) { + this.movedBackward(this.activeStream, requestedSegment); + } + } + + private streamChanged( + activeStream: StreamWithSegments, + requestedSegment: Segment + ) { + this.queue.clear(); + const { localId: segmentId } = requestedSegment; + + for (const segment of activeStream.segments.valuesFrom(segmentId)) { + if (!this.isSegmentLoaded(segmentId)) { + this.queue.addToEnd(segment.localId, segment); + } + } + } + + private movedForward(requestedSegment: Segment) { + const { localId: segmentId } = requestedSegment; + for (const segment of this.queue.valuesBackwardsFrom(segmentId)) { + this.queue.delete(segment.localId); + } + } + + private movedBackward( + activeStream: StreamWithSegments, + requestedSegment: Segment + ) { + const { segments } = activeStream; + const { localId: segmentId } = requestedSegment; + for (const segment of segments.valuesBackwardsFrom(segmentId)) { + if (!this.isSegmentLoaded(segment.localId)) { + this.queue.addToStart(segment.localId, segment); + } + if (segment.localId === segmentId) break; + } + } + + removeLoadedSegment(segmentId: string) { + this.queue.delete(segmentId); + } + + // refreshQueue() { + // if (!this.activeStream) return; + // + // for (const loadedSegmentId of this.loadedSegmentIds) { + // if (!this.activeStream.segments.has(loadedSegmentId)) { + // this.loadedSegmentIds.delete(loadedSegmentId); + // } + // } + // + // const last = this.queue[this.queue.length - 1]; + // for (const segment of this.activeStream.segments.values()) { + // if (!this.loadedSegmentIds.has(segment.localId)) this.queue.push(segment); + // } + // } +} + +class Request { + segment: Segment; + + constructor(segment: Segment) { + this.segment = segment; + } +} diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index e37bc1ce..cd42587f 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -1,3 +1,5 @@ +import { LinkedMap } from "./linked-map"; + export type StreamType = "main" | "secondary"; export type ByteRange = { start: number; end: number }; @@ -17,9 +19,14 @@ export type Stream = { readonly index: number; }; +export type ReadonlyLinkedMap = Pick< + LinkedMap, + "has" +>; + export type StreamWithSegments< TStream extends Stream = Stream, - TMap extends ReadonlyMap = ReadonlyMap + TMap extends ReadonlyLinkedMap = LinkedMap > = TStream & { readonly segments: TMap; }; diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index d82d6592..363dac44 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -1,4 +1,4 @@ -import { Stream } from "./index"; +import { Segment, Stream, StreamWithSegments } from "./index"; export function getStreamExternalId( stream: Stream, @@ -7,3 +7,13 @@ export function getStreamExternalId( const { type, index } = stream; return `${manifestResponseUrl}-${type}-${index}`; } + +export function getSegmentFromStreamsMap( + streams: Map, + segmentId: string +): { segment: Segment; stream: StreamWithSegments } | undefined { + for (const stream of streams.values()) { + const segment = stream.segments.get(segmentId); + if (segment) return { segment, stream }; + } +} From 40881d6cd7a61aed44cbe0d2fc6eb372345e3062 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Sun, 27 Aug 2023 23:01:30 +0300 Subject: [PATCH 004/127] Add load queue class. --- packages/p2p-media-loader-core/src/core.ts | 39 ++-- .../src/internal-types.ts | 1 + .../p2p-media-loader-core/src/linked-map.ts | 24 +-- .../p2p-media-loader-core/src/load-queue.ts | 191 ++++++++++++++++++ packages/p2p-media-loader-core/src/loader.ts | 115 +---------- .../p2p-media-loader-core/src/playback.ts | 59 ++++++ packages/p2p-media-loader-hlsjs/src/engine.ts | 5 + 7 files changed, 295 insertions(+), 139 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/load-queue.ts create mode 100644 packages/p2p-media-loader-core/src/playback.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index dd1a6760..b5557dff 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,13 +1,23 @@ import { Loader } from "./loader"; import { Stream, StreamWithSegments, Segment, SegmentResponse } from "./types"; -import { Playback } from "./internal-types"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; +import { LoadQueue } from "./load-queue"; +import { Playback } from "./playback"; export class Core { private readonly streams = new Map>(); - private readonly playback: Playback = { position: 0, rate: 1 }; - private readonly loader: Loader = new Loader(this.streams); + private readonly playback: Playback = new Playback({ + highDemandBufferLength: 60, + lowDemandBufferLength: 600, + }); + private readonly mainQueue = new LoadQueue(this.streams); + private readonly secondaryQueue: LoadQueue = new LoadQueue(this.streams); + private readonly loader: Loader = new Loader( + this.streams, + this.mainQueue, + this.secondaryQueue + ); setManifestResponseUrl(url: string): void { this.loader.setManifestResponseUrl(url.split("?")[0]); @@ -39,7 +49,7 @@ export class Core { const stream = this.streams.get(streamLocalId); if (!stream) return; - addSegments?.forEach((s) => stream.segments.set(s.localId, s)); + addSegments?.forEach((s) => stream.segments.addToEnd(s.localId, s)); removeSegmentIds?.forEach((id) => stream.segments.delete(id)); } @@ -52,27 +62,16 @@ export class Core { } updatePlayback({ position, rate }: Partial): void { + if (position === undefined && rate === undefined) return; + if (position !== undefined) this.playback.position = position; if (rate !== undefined) this.playback.rate = rate; + + this.mainQueue.onPlaybackUpdate(); + this.secondaryQueue.onPlaybackUpdate(); } destroy(): void { this.streams.clear(); } } - -class StreamContainer { - private readonly streams = new Map(); - - updateStream( - streamLocalId: string, - addSegments?: Segment[], - removeSegmentIds?: string[] - ): void { - const stream = this.streams.get(streamLocalId); - if (!stream) return; - - addSegments?.forEach((s) => stream.segments.set(s.localId, s)); - removeSegmentIds?.forEach((s) => stream.segments.delete(s)); - } -} diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 8ba147ef..275c1091 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -1,4 +1,5 @@ export type Playback = { position: number; rate: number; + lastPositionUpdate: "moved-forward" | "moved-backward"; }; diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts index a15de8cc..0a4768e9 100644 --- a/packages/p2p-media-loader-core/src/linked-map.ts +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -36,7 +36,7 @@ export class LinkedMap { this.map.set(key, item); } - addToStart(key: K, value: V) { + addToStart(items: [K, V] | [K, V][]) { const item: LinkedObject = { value }; if (this._first) item.next = this._first; this._first = item; @@ -62,16 +62,8 @@ export class LinkedMap { this.map.clear(); } - *values(): Generator { - let item = this._first; - while (item !== undefined) { - yield item.value; - item = item.next; - } - } - - *valuesBackwardsFrom(key: K): Generator { - let value = this.map.get(key); + *valuesBackwards(key?: K): Generator { + let value = key ? this.map.get(key) : this._last; if (value === undefined) return; while (value?.value !== undefined) { yield value.value; @@ -79,8 +71,8 @@ export class LinkedMap { } } - *valuesFrom(key: K): Generator { - let value = this.map.get(key); + *values(key?: K): Generator { + let value = key ? this.map.get(key) : this._first; if (value === undefined) return; while (value?.value !== undefined) { yield value.value; @@ -88,6 +80,12 @@ export class LinkedMap { } } + forEach(callback: (item: V) => void) { + for (const item of this.values()) { + callback(item); + } + } + getNextTo(key: K): V | undefined { return this.map.get(key)?.next?.value; } diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts new file mode 100644 index 00000000..de6826d8 --- /dev/null +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -0,0 +1,191 @@ +import { Segment, StreamWithSegments } from "./types"; +import { LinkedMap } from "./linked-map"; +import { Playback } from "./playback"; +import * as Utils from "./utils"; + +export class LoadQueue { + private readonly streams: Map; + private activeStream?: StreamWithSegments; + private readonly isSegmentLoaded!: (segmentId: string) => boolean; + private readonly highDemandQueue = new LinkedMap(); + private readonly lowDemandQueue = new LinkedMap(); + private readonly playback: Playback; + + constructor(streams: Map, playback: Playback) { + this.streams = streams; + this.playback = playback; + } + + requestByPlayer(segmentId: string) { + const { stream, segment: requestedSegment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; + if (!stream || !requestedSegment) return; + + if (this.activeStream !== stream) { + this.activeStream = stream; + this.abortAndClear(); + } + + this.addRequests(requestedSegment); + } + + addRequests(requestedSegment: Segment) { + if (!this.activeStream) return; + + const highDemandAddToStart: SegmentRequest[] = []; + const lowDemandAddToStart: SegmentRequest[] = []; + for (const segment of this.activeStream.segments.values( + requestedSegment.localId + )) { + const status = this.getSegmentStatus(segment); + + if ( + status === "not-actual" || + this.highDemandQueue.has(segment.localId) || + this.lowDemandQueue.has(segment.localId) + ) { + break; + } + if (this.isSegmentLoaded(segment.localId)) continue; + + const request = this.createSegmentRequest(segment); + if (status === "high-demand") { + highDemandAddToStart.push(request); + } else if (status === "low-demand") { + lowDemandAddToStart.push(request); + } + } + + const lowDemandLast = this.lowDemandQueue.last?.segment; + if (!lowDemandLast) return; + + for (const segment of this.activeStream.segments.values( + lowDemandLast.localId + )) { + const status = this.getSegmentStatus(segment); + if (status === "not-actual") break; + + const request = this.createSegmentRequest(segment); + this.lowDemandQueue.addToEnd(segment.localId, request); + } + } + + onPlaybackUpdate() { + if (!this.activeStream) return; + + // remove not actual values (if exist) from high demand queue start + for (const request of this.highDemandQueue.values()) { + const { segment } = request; + const status = this.getSegmentStatus(segment); + if (status === "high-demand") break; + + request.abort(); + this.lowDemandQueue.delete(segment.localId); + } + + // remove not actual values (if exist) from low demand queue end + for (const request of this.lowDemandQueue.valuesBackwards()) { + const { segment } = request; + const status = this.getSegmentStatus(segment); + if (status === "low-demand") break; + + request.abort(); + this.lowDemandQueue.delete(segment.localId); + } + + // move low demand values (if exist) from high demand queue + for (const request of this.highDemandQueue.valuesBackwards()) { + const { segment } = request; + const status = this.getSegmentStatus(segment); + if (status === "high-demand") break; + + if (status === "low-demand") { + this.lowDemandQueue.addToStart(segment.localId, request); + } + this.lowDemandQueue.delete(segment.localId); + } + + // move high demand values (if exist) from low demand queue + for (const request of this.lowDemandQueue.values()) { + const { segment } = request; + const status = this.getSegmentStatus(segment); + if (status === "low-demand") break; + + if (status === "high-demand") { + this.highDemandQueue.addToEnd(segment.localId, request); + } + this.lowDemandQueue.delete(segment.localId); + } + } + + private abortAndClear() { + this.highDemandQueue.forEach((r) => r.abort()); + this.lowDemandQueue.forEach((r) => r.abort()); + this.highDemandQueue.clear(); + this.lowDemandQueue.clear(); + } + + private createSegmentRequest(segment: Segment) { + const request = new SegmentRequest(segment); + request.setLoadedHandler(() => { + this.highDemandQueue.delete(segment.localId); + this.lowDemandQueue.delete(segment.localId); + }); + return request; + } + + private getSegmentStatus(segment: Segment) { + const { position, highDemandMargin, lowDemandMargin } = this.playback; + const { startTime } = segment; + if (startTime >= position && startTime < highDemandMargin) { + return "high-demand"; + } + if (startTime >= highDemandMargin && startTime < lowDemandMargin) { + return "low-demand"; + } + return "not-actual"; + } + + // refreshQueue() { + // if (!this.activeStream) return; + // + // for (const loadedSegmentId of this.loadedSegmentIds) { + // if (!this.activeStream.segments.has(loadedSegmentId)) { + // this.loadedSegmentIds.delete(loadedSegmentId); + // } + // } + // + // const last = this.queue[this.queue.length - 1]; + // for (const segment of this.activeStream.segments.values()) { + // if (!this.loadedSegmentIds.has(segment.localId)) this.queue.push(segment); + // } + // } +} + +class SegmentRequest { + segment: Segment; + private status: "not-started" | "pending" | "completed" = "not-started"; + private abortHandler?: () => void; + private loadedHandler?: () => void; + + constructor(segment: Segment) { + this.segment = segment; + } + + setAbortHandler(handler: () => void) { + this.abortHandler = handler; + } + + setLoadedHandler(handler: () => void) { + this.loadedHandler = handler; + } + + loaded() { + this.status = "completed"; + this.loadedHandler?.(); + } + + abort() { + this.abortHandler?.(); + } +} diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index 881114cf..4bb32c40 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -2,6 +2,8 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; import { HttpLoader } from "./http-loader"; +import { Playback } from "./internal-types"; +import { LoadQueue } from "./load-queue"; export class Loader { private manifestResponseUrl?: string; @@ -10,10 +12,14 @@ export class Loader { private readonly secondaryQueue: LoadQueue; private readonly httpLoader = new HttpLoader(); - constructor(streams: Map) { + constructor( + streams: Map, + mainQueue: LoadQueue, + secondaryQueue: LoadQueue + ) { this.streams = streams; - this.mainQueue = new LoadQueue(this.streams); - this.secondaryQueue = new LoadQueue(this.streams); + this.mainQueue = mainQueue; + this.secondaryQueue = secondaryQueue; } setManifestResponseUrl(url: string) { @@ -83,106 +89,3 @@ async function trackTime( const duration = performance.now() - start; return [result, unit === "ms" ? duration : duration / 1000]; } - -class LoadQueue { - private queue = new LinkedMap(); - private readonly streams: Map; - private activeStream?: StreamWithSegments; - private lastReqByPlayer?: Segment; - private readonly isSegmentLoaded!: (segmentId: string) => boolean; - - constructor(streams: Map) { - this.streams = streams; - } - - reqSegment(segmentId: string) { - const { stream, segment: requestedSegment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - } - - requestByPlayer(segmentId: string) { - const { stream, segment: requestedSegment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - if (!stream || !requestedSegment) return; - - const prevReqByPlayer = this.lastReqByPlayer; - this.lastReqByPlayer = requestedSegment; - if (this.activeStream !== stream) { - this.activeStream = stream; - this.streamChanged(this.activeStream, requestedSegment); - return; - } - if (!prevReqByPlayer) return; - - const next = this.activeStream?.segments.getNextTo(prevReqByPlayer.localId); - if (next === requestedSegment) return; - - if (requestedSegment.startTime > prevReqByPlayer.startTime) { - this.movedForward(requestedSegment); - } else if (requestedSegment.startTime < prevReqByPlayer.startTime) { - this.movedBackward(this.activeStream, requestedSegment); - } - } - - private streamChanged( - activeStream: StreamWithSegments, - requestedSegment: Segment - ) { - this.queue.clear(); - const { localId: segmentId } = requestedSegment; - - for (const segment of activeStream.segments.valuesFrom(segmentId)) { - if (!this.isSegmentLoaded(segmentId)) { - this.queue.addToEnd(segment.localId, segment); - } - } - } - - private movedForward(requestedSegment: Segment) { - const { localId: segmentId } = requestedSegment; - for (const segment of this.queue.valuesBackwardsFrom(segmentId)) { - this.queue.delete(segment.localId); - } - } - - private movedBackward( - activeStream: StreamWithSegments, - requestedSegment: Segment - ) { - const { segments } = activeStream; - const { localId: segmentId } = requestedSegment; - for (const segment of segments.valuesBackwardsFrom(segmentId)) { - if (!this.isSegmentLoaded(segment.localId)) { - this.queue.addToStart(segment.localId, segment); - } - if (segment.localId === segmentId) break; - } - } - - removeLoadedSegment(segmentId: string) { - this.queue.delete(segmentId); - } - - // refreshQueue() { - // if (!this.activeStream) return; - // - // for (const loadedSegmentId of this.loadedSegmentIds) { - // if (!this.activeStream.segments.has(loadedSegmentId)) { - // this.loadedSegmentIds.delete(loadedSegmentId); - // } - // } - // - // const last = this.queue[this.queue.length - 1]; - // for (const segment of this.activeStream.segments.values()) { - // if (!this.loadedSegmentIds.has(segment.localId)) this.queue.push(segment); - // } - // } -} - -class Request { - segment: Segment; - - constructor(segment: Segment) { - this.segment = segment; - } -} diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts new file mode 100644 index 00000000..3c75cbf7 --- /dev/null +++ b/packages/p2p-media-loader-core/src/playback.ts @@ -0,0 +1,59 @@ +export class Playback { + private _rate = 0; + private _position = 0; + private readonly settings: { + readonly highDemandBufferLength: number; + readonly lowDemandBufferLength: number; + }; + private _highDemandMargin: number = 0; + private _lowDemandMargin: number = 0; + + constructor(settings: { + readonly highDemandBufferLength: number; + readonly lowDemandBufferLength: number; + }) { + this.settings = settings; + } + + set position(value: number) { + this._position = value; + this._highDemandMargin = this.getHighDemandMargin(); + this._lowDemandMargin = this.getLowDemandMargin(); + } + + get position() { + return this._position; + } + + set rate(value: number) { + this._rate = value; + this._highDemandMargin = this.getHighDemandMargin(); + this._lowDemandMargin = this.getLowDemandMargin(); + } + + get rate() { + return this._rate; + } + + get highDemandMargin() { + return this._highDemandMargin; + } + + get lowDemandMargin() { + return this._lowDemandMargin; + } + + private getHighDemandMargin() { + const { highDemandBufferLength } = this.settings; + return this._position + highDemandBufferLength * this._rate; + } + + private getLowDemandMargin() { + const { lowDemandBufferLength } = this.settings; + return this._position + lowDemandBufferLength * this._rate; + } + + getTimeTo(time: number) { + return (time - this._position) / this._rate; + } +} diff --git a/packages/p2p-media-loader-hlsjs/src/engine.ts b/packages/p2p-media-loader-hlsjs/src/engine.ts index ef4da3ae..e79aa2be 100644 --- a/packages/p2p-media-loader-hlsjs/src/engine.ts +++ b/packages/p2p-media-loader-hlsjs/src/engine.ts @@ -65,6 +65,11 @@ export class Engine { this.core.updatePlayback({ position: media.currentTime }); }); + media.addEventListener("seeking", () => { + console.log("playhead time: ", media.currentTime); + this.core.updatePlayback({ position: media.currentTime }); + }); + media.addEventListener("ratechange", () => { console.log("playback rate: ", media.playbackRate); this.core.updatePlayback({ rate: media.playbackRate }); From 02a07fe74e0b0746836b3d3621375f8f9f6df42b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 28 Aug 2023 16:32:07 +0300 Subject: [PATCH 005/127] Add segments storage. --- packages/p2p-media-loader-core/src/core.ts | 37 +++--- .../p2p-media-loader-core/src/http-loader.ts | 10 +- .../p2p-media-loader-core/src/linked-map.ts | 86 +++++++++----- .../p2p-media-loader-core/src/load-queue.ts | 107 ++++++++++++------ packages/p2p-media-loader-core/src/loader.ts | 33 +++--- .../p2p-media-loader-core/src/playback.ts | 10 +- .../src/segments-storage.ts | 87 ++++++++++++++ packages/p2p-media-loader-core/src/types.ts | 2 +- packages/p2p-media-loader-hlsjs/src/engine.ts | 12 +- 9 files changed, 276 insertions(+), 108 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/segments-storage.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index b5557dff..f57010f7 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -4,19 +4,33 @@ import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; import { LoadQueue } from "./load-queue"; import { Playback } from "./playback"; +import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { private readonly streams = new Map>(); private readonly playback: Playback = new Playback({ - highDemandBufferLength: 60, - lowDemandBufferLength: 600, + highDemandBufferLength: 15, + lowDemandBufferLength: 60, }); - private readonly mainQueue = new LoadQueue(this.streams); - private readonly secondaryQueue: LoadQueue = new LoadQueue(this.streams); + private readonly segmentStorage = new SegmentsMemoryStorage(this.playback, { + cachedSegmentExpiration: 120, + cachedSegmentsCount: 50, + }); + private readonly mainQueue = new LoadQueue( + this.streams, + this.playback, + this.segmentStorage + ); + private readonly secondaryQueue: LoadQueue = new LoadQueue( + this.streams, + this.playback, + this.segmentStorage + ); private readonly loader: Loader = new Loader( this.streams, this.mainQueue, - this.secondaryQueue + this.secondaryQueue, + this.segmentStorage ); setManifestResponseUrl(url: string): void { @@ -61,14 +75,11 @@ export class Core { return this.loader.abortSegment(segmentId); } - updatePlayback({ position, rate }: Partial): void { - if (position === undefined && rate === undefined) return; - - if (position !== undefined) this.playback.position = position; - if (rate !== undefined) this.playback.rate = rate; - - this.mainQueue.onPlaybackUpdate(); - this.secondaryQueue.onPlaybackUpdate(); + updatePlayback(position: number, rate: number): void { + this.playback.position = position; + this.playback.rate = rate; + this.mainQueue.removeNotInLoadTimeRange(); + this.secondaryQueue.removeNotInLoadTimeRange(); } destroy(): void { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 87657b9e..e0e044f0 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,5 +1,5 @@ -import { Segment } from "./types"; import { FetchError } from "./errors"; +import { SegmentRequest } from "./load-queue"; type RequestContext = { abortController: AbortController; @@ -8,8 +8,13 @@ type RequestContext = { export class HttpLoader { private readonly segmentRequestContext = new Map(); - async load(segment: Segment) { + async load(request: SegmentRequest) { + const { segment } = request; + request.setAbortHandler(() => { + this.abort(segment.localId); + }); const headers = new Headers(); + const { url, byteRange } = segment; if (byteRange) { @@ -32,6 +37,7 @@ export class HttpLoader { response ); } + request.loaded(); const data = await response.arrayBuffer(); return { ok: response.ok, diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts index 0a4768e9..31a3a425 100644 --- a/packages/p2p-media-loader-core/src/linked-map.ts +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -1,20 +1,20 @@ -type LinkedObject = { - value: V; - prev?: LinkedObject; - next?: LinkedObject; +type LinkedObject = { + item: [K, V]; + prev?: LinkedObject; + next?: LinkedObject; }; export class LinkedMap { - private readonly map = new Map>(); - private _first?: LinkedObject; - private _last?: LinkedObject; + private readonly map = new Map>(); + private _first?: LinkedObject; + private _last?: LinkedObject; get first() { - return this._first?.value; + return this._first?.item; } get last() { - return this._last?.value; + return this._last?.item; } get size() { @@ -22,7 +22,7 @@ export class LinkedMap { } get(key: K): V | undefined { - return this.map.get(key)?.value; + return this.map.get(key)?.item[1]; } has(key: K): boolean { @@ -30,27 +30,42 @@ export class LinkedMap { } addToEnd(key: K, value: V) { - const item: LinkedObject = { value }; - if (this._last) item.prev = this._last; + const item: LinkedObject = { item: [key, value] }; + if (this._last) { + this._last.next = item; + item.prev = this._last; + } this._last = item; + if (!this._first) this._first = item; this.map.set(key, item); } - addToStart(items: [K, V] | [K, V][]) { - const item: LinkedObject = { value }; - if (this._first) item.next = this._first; + addToStart(key: K, value: V) { + const item: LinkedObject = { item: [key, value] }; + if (this._first) { + this._first.prev = item; + item.next = this._first; + } this._first = item; + if (!this._last) this._last = item; this.map.set(key, item); } + addListToStart(items: [K, V][]) { + for (let i = items.length - 1; i >= 0; i--) { + const [key, value] = items[i]; + this.addToStart(key, value); + } + } + delete(key: K) { if (!this.map.size) return; - const item = this.map.get(key); - if (!item) return; + const value = this.map.get(key); + if (!value) return; - const { next, prev } = item; - if (this._first?.value === item.value) this._first = next; - if (this._last?.value === item.value) this._last = prev; + const { next, prev } = value; + if (this._first?.item[0] === key) this._first = next; + if (this._last?.item[0] === key) this._last = prev; if (prev) prev.next = next; if (next) next.prev = prev; this.map.delete(key); @@ -62,31 +77,40 @@ export class LinkedMap { this.map.clear(); } - *valuesBackwards(key?: K): Generator { + *valuesBackwards(key?: K): Generator<[K, V]> { let value = key ? this.map.get(key) : this._last; if (value === undefined) return; - while (value?.value !== undefined) { - yield value.value; + while (value?.item !== undefined) { + yield value.item; value = value.prev; } } - *values(key?: K): Generator { + *entries(key?: K): Generator<[K, V]> { let value = key ? this.map.get(key) : this._first; if (value === undefined) return; - while (value?.value !== undefined) { - yield value.value; + while (value?.item !== undefined) { + yield value.item; + value = value.next; + } + } + + *keys(): Generator { + let value = this._first; + if (value === undefined) return; + while (value?.item !== undefined) { + yield value.item[0]; value = value.next; } } - forEach(callback: (item: V) => void) { - for (const item of this.values()) { - callback(item); + forEach(callback: (item: [K, V]) => void) { + for (const value of this.entries()) { + callback(value); } } - getNextTo(key: K): V | undefined { - return this.map.get(key)?.next?.value; + getNextTo(key: K): [K, V] | undefined { + return this.map.get(key)?.next?.item; } } diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index de6826d8..d62e958e 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -2,18 +2,37 @@ import { Segment, StreamWithSegments } from "./types"; import { LinkedMap } from "./linked-map"; import { Playback } from "./playback"; import * as Utils from "./utils"; +import { SegmentsMemoryStorage } from "./segments-storage"; export class LoadQueue { - private readonly streams: Map; private activeStream?: StreamWithSegments; - private readonly isSegmentLoaded!: (segmentId: string) => boolean; private readonly highDemandQueue = new LinkedMap(); private readonly lowDemandQueue = new LinkedMap(); - private readonly playback: Playback; - constructor(streams: Map, playback: Playback) { - this.streams = streams; - this.playback = playback; + constructor( + private readonly streams: Map, + private readonly playback: Playback, + private readonly segmentStorage: SegmentsMemoryStorage + ) {} + + getRequestById(id: string) { + return this.highDemandQueue.get(id) ?? this.lowDemandQueue.get(id); + } + + getNextForLoading() { + if (this.highDemandQueue.size) { + for (const [, request] of this.highDemandQueue.entries()) { + if (request.status === "not-started") return request; + } + } + if (this.lowDemandQueue.size) { + const randomIndex = getRandomInt(0, this.lowDemandQueue.size); + let i = 0; + for (const [, request] of this.lowDemandQueue.entries()) { + if (i === randomIndex) return request; + i++; + } + } } requestByPlayer(segmentId: string) { @@ -29,52 +48,58 @@ export class LoadQueue { this.addRequests(requestedSegment); } - addRequests(requestedSegment: Segment) { + private addRequests(requestedSegment: Segment) { if (!this.activeStream) return; - const highDemandAddToStart: SegmentRequest[] = []; - const lowDemandAddToStart: SegmentRequest[] = []; - for (const segment of this.activeStream.segments.values( + const highDemandAddToStart: [string, SegmentRequest][] = []; + const lowDemandAddToStart: [string, SegmentRequest][] = []; + for (const [segmentId, segment] of this.activeStream.segments.entries( requestedSegment.localId )) { const status = this.getSegmentStatus(segment); - if ( status === "not-actual" || - this.highDemandQueue.has(segment.localId) || - this.lowDemandQueue.has(segment.localId) + this.highDemandQueue.has(segmentId) || + this.lowDemandQueue.has(segmentId) ) { break; } - if (this.isSegmentLoaded(segment.localId)) continue; + if (this.isSegmentAlreadyLoaded(segmentId)) continue; const request = this.createSegmentRequest(segment); if (status === "high-demand") { - highDemandAddToStart.push(request); + highDemandAddToStart.push([segmentId, request]); } else if (status === "low-demand") { - lowDemandAddToStart.push(request); + lowDemandAddToStart.push([segmentId, request]); } } + this.highDemandQueue.addListToStart(highDemandAddToStart); + this.lowDemandQueue.addListToStart(lowDemandAddToStart); - const lowDemandLast = this.lowDemandQueue.last?.segment; - if (!lowDemandLast) return; + let queue: LinkedMap | undefined; + if (this.lowDemandQueue.last) queue = this.lowDemandQueue; + else if (this.highDemandQueue.last) queue = this.highDemandQueue; + if (!queue) return; - for (const segment of this.activeStream.segments.values( - lowDemandLast.localId + for (const [segmentId, segment] of this.activeStream.segments.entries( + queue.last?.[0] )) { const status = this.getSegmentStatus(segment); if (status === "not-actual") break; + if (queue.has(segmentId) || this.isSegmentAlreadyLoaded(segmentId)) { + continue; + } const request = this.createSegmentRequest(segment); - this.lowDemandQueue.addToEnd(segment.localId, request); + queue.addToEnd(segmentId, request); } } - onPlaybackUpdate() { + removeNotInLoadTimeRange() { if (!this.activeStream) return; - // remove not actual values (if exist) from high demand queue start - for (const request of this.highDemandQueue.values()) { + // remove not actual requests (if exist) from high demand queue start + for (const [, request] of this.highDemandQueue.entries()) { const { segment } = request; const status = this.getSegmentStatus(segment); if (status === "high-demand") break; @@ -83,8 +108,8 @@ export class LoadQueue { this.lowDemandQueue.delete(segment.localId); } - // remove not actual values (if exist) from low demand queue end - for (const request of this.lowDemandQueue.valuesBackwards()) { + // remove not actual requests (if exist) from low demand queue end + for (const [, request] of this.lowDemandQueue.valuesBackwards()) { const { segment } = request; const status = this.getSegmentStatus(segment); if (status === "low-demand") break; @@ -93,8 +118,8 @@ export class LoadQueue { this.lowDemandQueue.delete(segment.localId); } - // move low demand values (if exist) from high demand queue - for (const request of this.highDemandQueue.valuesBackwards()) { + // move low demand requests (if exist) from high demand queue + for (const [, request] of this.highDemandQueue.valuesBackwards()) { const { segment } = request; const status = this.getSegmentStatus(segment); if (status === "high-demand") break; @@ -105,8 +130,8 @@ export class LoadQueue { this.lowDemandQueue.delete(segment.localId); } - // move high demand values (if exist) from low demand queue - for (const request of this.lowDemandQueue.values()) { + // move high demand requests (if exist) from low demand queue + for (const [, request] of this.lowDemandQueue.entries()) { const { segment } = request; const status = this.getSegmentStatus(segment); if (status === "low-demand") break; @@ -119,8 +144,8 @@ export class LoadQueue { } private abortAndClear() { - this.highDemandQueue.forEach((r) => r.abort()); - this.lowDemandQueue.forEach((r) => r.abort()); + this.highDemandQueue.forEach(([, r]) => r.abort()); + this.lowDemandQueue.forEach(([, r]) => r.abort()); this.highDemandQueue.clear(); this.lowDemandQueue.clear(); } @@ -146,6 +171,10 @@ export class LoadQueue { return "not-actual"; } + private isSegmentAlreadyLoaded(segmentId: string) { + return this.segmentStorage.hasSegment(segmentId); + } + // refreshQueue() { // if (!this.activeStream) return; // @@ -162,9 +191,9 @@ export class LoadQueue { // } } -class SegmentRequest { +export class SegmentRequest { segment: Segment; - private status: "not-started" | "pending" | "completed" = "not-started"; + private _status: "not-started" | "pending" | "completed" = "not-started"; private abortHandler?: () => void; private loadedHandler?: () => void; @@ -172,6 +201,10 @@ class SegmentRequest { this.segment = segment; } + get status() { + return this._status; + } + setAbortHandler(handler: () => void) { this.abortHandler = handler; } @@ -181,7 +214,7 @@ class SegmentRequest { } loaded() { - this.status = "completed"; + this._status = "completed"; this.loadedHandler?.(); } @@ -189,3 +222,7 @@ class SegmentRequest { this.abortHandler?.(); } } + +function getRandomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index 4bb32c40..ec9831e8 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -1,26 +1,19 @@ -import { Segment, SegmentResponse, StreamWithSegments } from "./index"; +import { SegmentResponse, StreamWithSegments } from "./index"; import * as Utils from "./utils"; -import { LinkedMap } from "./linked-map"; import { HttpLoader } from "./http-loader"; -import { Playback } from "./internal-types"; import { LoadQueue } from "./load-queue"; +import { SegmentsMemoryStorage } from "./segments-storage"; export class Loader { private manifestResponseUrl?: string; - private readonly streams: Map; - private readonly mainQueue: LoadQueue; - private readonly secondaryQueue: LoadQueue; private readonly httpLoader = new HttpLoader(); constructor( - streams: Map, - mainQueue: LoadQueue, - secondaryQueue: LoadQueue - ) { - this.streams = streams; - this.mainQueue = mainQueue; - this.secondaryQueue = secondaryQueue; - } + private readonly streams: Map, + private readonly mainQueue: LoadQueue, + private readonly secondaryQueue: LoadQueue, + private readonly segmentsMemoryStorage: SegmentsMemoryStorage + ) {} setManifestResponseUrl(url: string) { this.manifestResponseUrl = url; @@ -31,12 +24,16 @@ export class Loader { const queue = stream.type === "main" ? this.mainQueue : this.secondaryQueue; queue.requestByPlayer(segment.localId); + const request = queue.getRequestById(segment.localId); + + if (!request) { + throw new Error("No segment found"); + } const [response, loadingDuration] = await trackTime( - () => this.httpLoader.load(segment), + () => this.httpLoader.load(request), "s" ); - queue.removeLoadedSegment(segment.localId); const { data, url, ok, status } = response; const bits = data.byteLength * 8; @@ -74,7 +71,9 @@ export class Loader { this.manifestResponseUrl ); // console.log("Stream: ", streamEternalId); - console.log(this.mainQueue); + // console.log(this.mainQueue.highDemandQueue); + // // console.log(this.mainQueue.lowDemandQueue); + // console.log((this.mainQueue as any).playback); return { segment, stream }; } diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts index 3c75cbf7..5fad61d3 100644 --- a/packages/p2p-media-loader-core/src/playback.ts +++ b/packages/p2p-media-loader-core/src/playback.ts @@ -1,21 +1,24 @@ export class Playback { - private _rate = 0; + private _rate = 1; private _position = 0; private readonly settings: { readonly highDemandBufferLength: number; readonly lowDemandBufferLength: number; }; - private _highDemandMargin: number = 0; - private _lowDemandMargin: number = 0; + private _highDemandMargin = 0; + private _lowDemandMargin = 0; constructor(settings: { readonly highDemandBufferLength: number; readonly lowDemandBufferLength: number; }) { this.settings = settings; + this._highDemandMargin = this.getHighDemandMargin(); + this._lowDemandMargin = this.getLowDemandMargin(); } set position(value: number) { + if (value === this._position) return; this._position = value; this._highDemandMargin = this.getHighDemandMargin(); this._lowDemandMargin = this.getLowDemandMargin(); @@ -26,6 +29,7 @@ export class Playback { } set rate(value: number) { + if (value === this._rate) return; this._rate = value; this._highDemandMargin = this.getHighDemandMargin(); this._lowDemandMargin = this.getLowDemandMargin(); diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts new file mode 100644 index 00000000..67344d78 --- /dev/null +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -0,0 +1,87 @@ +import { Playback } from "./playback"; +import { Segment } from "./types"; + +export class SegmentsMemoryStorage { + private cache = new Map< + string, + { segment: Segment; data: ArrayBuffer; lastAccessed: number } + >(); + + constructor( + private readonly playback: Playback, + private settings: { + cachedSegmentExpiration: number; + cachedSegmentsCount: number; + } + ) {} + + storeSegment(segment: Segment, data: ArrayBuffer) { + this.cache.set(segment.localId, { + segment, + data, + lastAccessed: performance.now(), + }); + } + + getSegment(segmentId: string): ArrayBuffer | undefined { + const cacheItem = this.cache.get(segmentId); + if (cacheItem === undefined) return undefined; + + cacheItem.lastAccessed = performance.now(); + return cacheItem.data; + } + + hasSegment(segmentId: string) { + return this.cache.has(segmentId); + } + + private isSegmentLocked(segment: Segment) { + const { position, lowDemandMargin } = this.playback; + const { startTime, endTime } = segment; + return !( + (startTime < position && endTime < position) || + (startTime > lowDemandMargin && endTime > lowDemandMargin) + ); + } + + async clean(): Promise { + const segmentsToDelete: string[] = []; + const remainingSegments: { + lastAccessed: number; + segment: Segment; + }[] = []; + + // Delete old segments + const now = performance.now(); + + for (const [segmentId, { lastAccessed, segment }] of this.cache.entries()) { + if (now - lastAccessed > this.settings.cachedSegmentExpiration) { + segmentsToDelete.push(segmentId); + } else { + remainingSegments.push({ segment, lastAccessed }); + } + } + + // Delete segments over cached count + let countOverhead = + remainingSegments.length - this.settings.cachedSegmentsCount; + if (countOverhead > 0) { + remainingSegments.sort((a, b) => a.lastAccessed - b.lastAccessed); + + for (const cachedSegment of remainingSegments) { + if (this.isSegmentLocked(cachedSegment.segment)) { + segmentsToDelete.push(cachedSegment.segment.localId); + countOverhead--; + if (countOverhead === 0) break; + } + } + } + + segmentsToDelete.forEach((id) => this.cache.delete(id)); + return segmentsToDelete.length > 0; + } + + public async destroy() { + this.cache.clear(); + } +} diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index cd42587f..a5585169 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -21,7 +21,7 @@ export type Stream = { export type ReadonlyLinkedMap = Pick< LinkedMap, - "has" + "has" | "keys" >; export type StreamWithSegments< diff --git a/packages/p2p-media-loader-hlsjs/src/engine.ts b/packages/p2p-media-loader-hlsjs/src/engine.ts index e79aa2be..4f620135 100644 --- a/packages/p2p-media-loader-hlsjs/src/engine.ts +++ b/packages/p2p-media-loader-hlsjs/src/engine.ts @@ -61,18 +61,18 @@ export class Engine { hls.on("hlsMediaAttached" as Events.MEDIA_ATTACHED, (event, data) => { const { media } = data; media.addEventListener("timeupdate", () => { - console.log("playhead time: ", media.currentTime); - this.core.updatePlayback({ position: media.currentTime }); + // console.log("playhead time: ", media.currentTime); + this.core.updatePlayback(media.currentTime, media.playbackRate); }); media.addEventListener("seeking", () => { - console.log("playhead time: ", media.currentTime); - this.core.updatePlayback({ position: media.currentTime }); + // console.log("playhead time: ", media.currentTime); + this.core.updatePlayback(media.currentTime, media.playbackRate); }); media.addEventListener("ratechange", () => { - console.log("playback rate: ", media.playbackRate); - this.core.updatePlayback({ rate: media.playbackRate }); + // console.log("playback rate: ", media.playbackRate); + this.core.updatePlayback(media.currentTime, media.playbackRate); }); }); } From 30ed915fade281936e64b4318cf8537e4b420794 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 30 Aug 2023 18:08:36 +0300 Subject: [PATCH 006/127] Add segments load logic. --- packages/p2p-media-loader-core/src/core.ts | 31 +-- .../p2p-media-loader-core/src/http-loader.ts | 51 ++-- .../src/internal-types.ts | 5 + .../p2p-media-loader-core/src/linked-map.ts | 25 +- .../p2p-media-loader-core/src/load-queue.ts | 249 ++++++------------ packages/p2p-media-loader-core/src/loader.ts | 171 +++++++++--- .../p2p-media-loader-core/src/playback.ts | 63 +++-- .../src/segments-storage.ts | 9 +- packages/p2p-media-loader-core/src/utils.ts | 22 ++ 9 files changed, 346 insertions(+), 280 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index f57010f7..f2ad7028 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -2,35 +2,25 @@ import { Loader } from "./loader"; import { Stream, StreamWithSegments, Segment, SegmentResponse } from "./types"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; -import { LoadQueue } from "./load-queue"; import { Playback } from "./playback"; import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { private readonly streams = new Map>(); - private readonly playback: Playback = new Playback({ + private readonly playback = new Playback({ highDemandBufferLength: 15, - lowDemandBufferLength: 60, + httpDownloadBufferLength: 60, + p2pDownloadBufferLength: 80, }); private readonly segmentStorage = new SegmentsMemoryStorage(this.playback, { cachedSegmentExpiration: 120, cachedSegmentsCount: 50, }); - private readonly mainQueue = new LoadQueue( + private readonly loader = new Loader( this.streams, + this.segmentStorage, this.playback, - this.segmentStorage - ); - private readonly secondaryQueue: LoadQueue = new LoadQueue( - this.streams, - this.playback, - this.segmentStorage - ); - private readonly loader: Loader = new Loader( - this.streams, - this.mainQueue, - this.secondaryQueue, - this.segmentStorage + { simultaneousHttpDownloads: 3 } ); setManifestResponseUrl(url: string): void { @@ -65,10 +55,15 @@ export class Core { addSegments?.forEach((s) => stream.segments.addToEnd(s.localId, s)); removeSegmentIds?.forEach((id) => stream.segments.delete(id)); + + const firstSegment = stream.segments.first?.[1]; + if (firstSegment && firstSegment.startTime > this.playback.position) { + this.playback.position = firstSegment.startTime; + } } loadSegment(segmentLocalId: string): Promise { - return this.loader.loadSegment(segmentLocalId); + return this.loader.requestSegmentByPlugin(segmentLocalId); } abortSegmentLoading(segmentId: string): void { @@ -78,8 +73,6 @@ export class Core { updatePlayback(position: number, rate: number): void { this.playback.position = position; this.playback.rate = rate; - this.mainQueue.removeNotInLoadTimeRange(); - this.secondaryQueue.removeNotInLoadTimeRange(); } destroy(): void { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index e0e044f0..65f3c879 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,20 +1,34 @@ import { FetchError } from "./errors"; -import { SegmentRequest } from "./load-queue"; +import { Segment } from "./types"; -type RequestContext = { +type Request = { + promise: Promise<{ + ok: boolean; + status: number; + data: ArrayBuffer; + url: string; + }>; abortController: AbortController; }; export class HttpLoader { - private readonly segmentRequestContext = new Map(); + private readonly requests = new Map(); - async load(request: SegmentRequest) { - const { segment } = request; - request.setAbortHandler(() => { - this.abort(segment.localId); - }); - const headers = new Headers(); + async load(segment: Segment) { + const abortController = new AbortController(); + const promise = this.fetch(segment, abortController); + const requestContext: Request = { + abortController, + promise, + }; + this.requests.set(segment.localId, requestContext); + await promise; + this.requests.delete(segment.localId); + return promise; + } + private async fetch(segment: Segment, abortController: AbortController) { + const headers = new Headers(); const { url, byteRange } = segment; if (byteRange) { @@ -22,13 +36,9 @@ export class HttpLoader { const byteRangeString = `bytes=${start}-${end}`; headers.set("Range", byteRangeString); } - const requestContext: RequestContext = { - abortController: new AbortController(), - }; - this.segmentRequestContext.set(segment.localId, requestContext); const response = await fetch(url, { headers, - signal: requestContext.abortController.signal, + signal: abortController.signal, }); if (!response.ok) { throw new FetchError( @@ -37,7 +47,7 @@ export class HttpLoader { response ); } - request.loaded(); + const data = await response.arrayBuffer(); return { ok: response.ok, @@ -48,6 +58,15 @@ export class HttpLoader { } abort(segmentId: string) { - this.segmentRequestContext.get(segmentId)?.abortController.abort(); + this.requests.get(segmentId)?.abortController.abort(); + this.requests.delete(segmentId); + } + + getLoadingsAmount() { + return this.requests.size; + } + + getRequest(segmentId: string) { + return this.requests.get(segmentId)?.promise; } } diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 275c1091..c5b843f1 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -3,3 +3,8 @@ export type Playback = { rate: number; lastPositionUpdate: "moved-forward" | "moved-backward"; }; + +export type SegmentLoadStatus = + | "high-demand" + | "http-downloadable" + | "p2p-downloadable"; diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts index 31a3a425..7ce9a801 100644 --- a/packages/p2p-media-loader-core/src/linked-map.ts +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -58,6 +58,21 @@ export class LinkedMap { } } + addAfter(prevKey: K, key: K, value: V) { + const prev = this.map.get(prevKey); + if (!prev) return; + + const newItem: LinkedObject = { + item: [key, value], + prev, + next: prev.next, + }; + prev.next = newItem; + if (this._last === prev) this._last = newItem; + + this.map.set(key, newItem); + } + delete(key: K) { if (!this.map.size) return; const value = this.map.get(key); @@ -77,7 +92,7 @@ export class LinkedMap { this.map.clear(); } - *valuesBackwards(key?: K): Generator<[K, V]> { + *entriesBackwards(key?: K): Generator<[K, V]> { let value = key ? this.map.get(key) : this._last; if (value === undefined) return; while (value?.item !== undefined) { @@ -110,6 +125,14 @@ export class LinkedMap { } } + filter(callback: (item: [K, V]) => boolean) { + const list: [K, V][] = []; + for (const value of this.entries()) { + if (callback(value)) list.push(value); + } + return list; + } + getNextTo(key: K): [K, V] | undefined { return this.map.get(key)?.next?.item; } diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index d62e958e..dac0befd 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -3,223 +3,128 @@ import { LinkedMap } from "./linked-map"; import { Playback } from "./playback"; import * as Utils from "./utils"; import { SegmentsMemoryStorage } from "./segments-storage"; +import { SegmentLoadStatus } from "./internal-types"; + +export type QueueItem = { + segment: Segment; + statuses: Set; + isLoading?: boolean; + loadingType?: "http" | "p2p"; +}; export class LoadQueue { + private readonly queue = new LinkedMap(); private activeStream?: StreamWithSegments; - private readonly highDemandQueue = new LinkedMap(); - private readonly lowDemandQueue = new LinkedMap(); constructor( - private readonly streams: Map, private readonly playback: Playback, private readonly segmentStorage: SegmentsMemoryStorage ) {} - getRequestById(id: string) { - return this.highDemandQueue.get(id) ?? this.lowDemandQueue.get(id); + *getSegmentsToLoad() { + for (const [segmentId, segmentInfo] of this.queue.entries()) { + if (this.queue.get(segmentId)?.isLoading) continue; + yield segmentInfo; + } } - getNextForLoading() { - if (this.highDemandQueue.size) { - for (const [, request] of this.highDemandQueue.entries()) { - if (request.status === "not-started") return request; - } - } - if (this.lowDemandQueue.size) { - const randomIndex = getRandomInt(0, this.lowDemandQueue.size); - let i = 0; - for (const [, request] of this.lowDemandQueue.entries()) { - if (i === randomIndex) return request; - i++; + getRandomHttpLoadableSegment() { + const notLoadingSegments = this.queue.filter( + ([, { isLoading, statuses }]) => + !isLoading && statuses.has("http-downloadable") + ); + if (!notLoadingSegments.length) return undefined; + const randomIndex = getRandomInt(0, notLoadingSegments.length - 1); + return notLoadingSegments[randomIndex][1]; + } + + getLastHttpLoadingItemAfter(segmentId: string) { + for (const [itemSegmentId, item] of this.queue.entriesBackwards()) { + if (itemSegmentId === segmentId) break; + if (item.isLoading && item.loadingType === "http") { + return item; } } } - requestByPlayer(segmentId: string) { - const { stream, segment: requestedSegment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - if (!stream || !requestedSegment) return; - + update(segment: Segment, stream: StreamWithSegments) { + const segmentsToAbortIds: string[] = []; if (this.activeStream !== stream) { this.activeStream = stream; - this.abortAndClear(); + this.queue.forEach(([segmentId, { isLoading }]) => { + if (isLoading) segmentsToAbortIds.push(segmentId); + }); + this.queue.clear(); } - this.addRequests(requestedSegment); + this.addNewSegmentsToQueue(segment); + + return { segmentsToAbortIds }; } - private addRequests(requestedSegment: Segment) { + addNewSegmentsToQueue(requestedSegment: Segment) { if (!this.activeStream) return; - const highDemandAddToStart: [string, SegmentRequest][] = []; - const lowDemandAddToStart: [string, SegmentRequest][] = []; + let prevSegmentId: string | undefined; for (const [segmentId, segment] of this.activeStream.segments.entries( requestedSegment.localId )) { - const status = this.getSegmentStatus(segment); - if ( - status === "not-actual" || - this.highDemandQueue.has(segmentId) || - this.lowDemandQueue.has(segmentId) - ) { - break; - } - if (this.isSegmentAlreadyLoaded(segmentId)) continue; - - const request = this.createSegmentRequest(segment); - if (status === "high-demand") { - highDemandAddToStart.push([segmentId, request]); - } else if (status === "low-demand") { - lowDemandAddToStart.push([segmentId, request]); - } - } - this.highDemandQueue.addListToStart(highDemandAddToStart); - this.lowDemandQueue.addListToStart(lowDemandAddToStart); - - let queue: LinkedMap | undefined; - if (this.lowDemandQueue.last) queue = this.lowDemandQueue; - else if (this.highDemandQueue.last) queue = this.highDemandQueue; - if (!queue) return; - - for (const [segmentId, segment] of this.activeStream.segments.entries( - queue.last?.[0] - )) { - const status = this.getSegmentStatus(segment); - if (status === "not-actual") break; - if (queue.has(segmentId) || this.isSegmentAlreadyLoaded(segmentId)) { + if (this.segmentStorage.hasSegment(segmentId)) continue; + if (this.queue.has(segmentId)) { + prevSegmentId = segmentId; continue; } + const statuses = Utils.getSegmentLoadStatuses(segment, this.playback); + if (!statuses) break; - const request = this.createSegmentRequest(segment); - queue.addToEnd(segmentId, request); + const info = { segment, statuses }; + if (prevSegmentId) this.queue.addAfter(prevSegmentId, segmentId, info); + else this.queue.addToStart(segmentId, info); + prevSegmentId = segmentId; } } - removeNotInLoadTimeRange() { - if (!this.activeStream) return; - - // remove not actual requests (if exist) from high demand queue start - for (const [, request] of this.highDemandQueue.entries()) { - const { segment } = request; - const status = this.getSegmentStatus(segment); - if (status === "high-demand") break; - - request.abort(); - this.lowDemandQueue.delete(segment.localId); - } - - // remove not actual requests (if exist) from low demand queue end - for (const [, request] of this.lowDemandQueue.valuesBackwards()) { - const { segment } = request; - const status = this.getSegmentStatus(segment); - if (status === "low-demand") break; - - request.abort(); - this.lowDemandQueue.delete(segment.localId); - } - - // move low demand requests (if exist) from high demand queue - for (const [, request] of this.highDemandQueue.valuesBackwards()) { - const { segment } = request; - const status = this.getSegmentStatus(segment); - if (status === "high-demand") break; - - if (status === "low-demand") { - this.lowDemandQueue.addToStart(segment.localId, request); + clearNotInLoadRangeSegments() { + const segmentsToAbortIds: string[] = []; + for (const [segmentId, segmentInfo] of this.queue.entries()) { + const statuses = Utils.getSegmentLoadStatuses( + segmentInfo.segment, + this.playback + ); + if (!statuses) { + segmentsToAbortIds.push(segmentId); + this.queue.delete(segmentId); + } else { + segmentInfo.statuses = statuses; } - this.lowDemandQueue.delete(segment.localId); - } - - // move high demand requests (if exist) from low demand queue - for (const [, request] of this.lowDemandQueue.entries()) { - const { segment } = request; - const status = this.getSegmentStatus(segment); - if (status === "low-demand") break; - - if (status === "high-demand") { - this.highDemandQueue.addToEnd(segment.localId, request); - } - this.lowDemandQueue.delete(segment.localId); - } - } - - private abortAndClear() { - this.highDemandQueue.forEach(([, r]) => r.abort()); - this.lowDemandQueue.forEach(([, r]) => r.abort()); - this.highDemandQueue.clear(); - this.lowDemandQueue.clear(); - } - - private createSegmentRequest(segment: Segment) { - const request = new SegmentRequest(segment); - request.setLoadedHandler(() => { - this.highDemandQueue.delete(segment.localId); - this.lowDemandQueue.delete(segment.localId); - }); - return request; - } - - private getSegmentStatus(segment: Segment) { - const { position, highDemandMargin, lowDemandMargin } = this.playback; - const { startTime } = segment; - if (startTime >= position && startTime < highDemandMargin) { - return "high-demand"; } - if (startTime >= highDemandMargin && startTime < lowDemandMargin) { - return "low-demand"; - } - return "not-actual"; - } - private isSegmentAlreadyLoaded(segmentId: string) { - return this.segmentStorage.hasSegment(segmentId); + segmentsToAbortIds.forEach((id) => this.queue.delete(id)); + return { segmentsToAbortIds }; } - // refreshQueue() { - // if (!this.activeStream) return; - // - // for (const loadedSegmentId of this.loadedSegmentIds) { - // if (!this.activeStream.segments.has(loadedSegmentId)) { - // this.loadedSegmentIds.delete(loadedSegmentId); - // } - // } - // - // const last = this.queue[this.queue.length - 1]; - // for (const segment of this.activeStream.segments.values()) { - // if (!this.loadedSegmentIds.has(segment.localId)) this.queue.push(segment); - // } - // } -} + markSegmentAsLoading(segmentId: string, loadingType: "http" | "p2p") { + const segmentInfo = this.queue.get(segmentId); + if (!segmentInfo) return; -export class SegmentRequest { - segment: Segment; - private _status: "not-started" | "pending" | "completed" = "not-started"; - private abortHandler?: () => void; - private loadedHandler?: () => void; - - constructor(segment: Segment) { - this.segment = segment; + segmentInfo.isLoading = true; + segmentInfo.loadingType = loadingType; } - get status() { - return this._status; - } - - setAbortHandler(handler: () => void) { - this.abortHandler = handler; - } + markSegmentAsNotLoading(segmentId: string) { + const segmentInfo = this.queue.get(segmentId); + if (!segmentInfo) return; - setLoadedHandler(handler: () => void) { - this.loadedHandler = handler; + delete segmentInfo.isLoading; + delete segmentInfo.loadingType; } - loaded() { - this._status = "completed"; - this.loadedHandler?.(); + removeLoadedSegment(segmentId: string) { + this.queue.delete(segmentId); } - abort() { - this.abortHandler?.(); + get length() { + return this.queue.size; } } diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index ec9831e8..83401900 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -1,56 +1,143 @@ -import { SegmentResponse, StreamWithSegments } from "./index"; +import { Segment, SegmentResponse, StreamWithSegments } from "./index"; import * as Utils from "./utils"; import { HttpLoader } from "./http-loader"; -import { LoadQueue } from "./load-queue"; +import { LoadQueue, QueueItem } from "./load-queue"; import { SegmentsMemoryStorage } from "./segments-storage"; +import { Playback } from "./playback"; export class Loader { private manifestResponseUrl?: string; private readonly httpLoader = new HttpLoader(); + private readonly mainQueue: LoadQueue; + private readonly secondaryQueue: LoadQueue; constructor( private readonly streams: Map, - private readonly mainQueue: LoadQueue, - private readonly secondaryQueue: LoadQueue, - private readonly segmentsMemoryStorage: SegmentsMemoryStorage - ) {} + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly playback: Playback, + private readonly settings: { simultaneousHttpDownloads: number } + ) { + this.mainQueue = new LoadQueue(this.playback, this.segmentStorage); + this.secondaryQueue = new LoadQueue(this.playback, this.segmentStorage); + this.playback.subscribeToUpdate(this.onPlaybackUpdate.bind(this)); + setInterval(() => this.loadRandomSegmentThroughHttp(), 1000); + } setManifestResponseUrl(url: string) { this.manifestResponseUrl = url; } - async loadSegment(segmentId: string): Promise { + async requestSegmentByPlugin(segmentId: string): Promise { + console.log("requested", segmentId); const { segment, stream } = this.identifySegment(segmentId); const queue = stream.type === "main" ? this.mainQueue : this.secondaryQueue; - queue.requestByPlayer(segment.localId); - const request = queue.getRequestById(segment.localId); + const { segmentsToAbortIds } = queue.update(segment, stream); + this.abortSegments(segmentsToAbortIds); + this.processQueues(); + + console.log("queue size: ", queue.length); + console.log("request size: ", this.httpLoader.getLoadingsAmount()); + let data = this.segmentStorage.getSegment(segmentId); + if (!data) { + const request = this.httpLoader.getRequest(segmentId); + const response = await request; + if (!response) throw new Error("No data"); + data = response.data; + } + console.log("loaded", segmentId); + return { + url: segment.url, + data, + bandwidth: 9999999, + status: 200, + ok: true, + }; + } - if (!request) { - throw new Error("No segment found"); + private processQueues() { + const { simultaneousHttpDownloads } = this.settings; + + for (const [ + { segment, statuses }, + queue, + ] of this.getQueuesSegmentsToLoad()) { + if (statuses.has("high-demand")) { + if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { + void this.loadSegmentThroughHttp(segment, queue); + continue; + } + const lastItem = queue.getLastHttpLoadingItemAfter(segment.localId); + if (lastItem) { + this.httpLoader.abort(lastItem.segment.localId); + queue.markSegmentAsNotLoading(lastItem.segment.localId); + void this.loadSegmentThroughHttp(segment, queue); + } + } } + } - const [response, loadingDuration] = await trackTime( - () => this.httpLoader.load(request), - "s" - ); + private *getQueuesSegmentsToLoad() { + const mainGen = this.mainQueue.getSegmentsToLoad(); + const secondaryGen = this.secondaryQueue.getSegmentsToLoad(); + let item1: QueueItem | undefined; + let item2: QueueItem | undefined; + + const retrieveMinTimeItem = (): [QueueItem, LoadQueue] | undefined => { + item1 = item1 ?? (mainGen.next().value as QueueItem | undefined); + item2 = item2 ?? (secondaryGen.next().value as QueueItem | undefined); + + if (!item1 && !item2) return undefined; + if (item1 && item2) { + if (item1.segment.startTime < item2.segment.startTime) { + const result = item1; + item1 = undefined; + return [result, this.mainQueue]; + } else { + const result = item2; + item2 = undefined; + return [result, this.secondaryQueue]; + } + } + if (item1) { + const result: [QueueItem, LoadQueue] = [item1, this.mainQueue]; + item1 = undefined; + return result; + } + if (item2) { + const result: [QueueItem, LoadQueue] = [item2, this.secondaryQueue]; + item2 = undefined; + return result; + } + }; - const { data, url, ok, status } = response; - const bits = data.byteLength * 8; + let item: [QueueItem, LoadQueue] | undefined; + do { + item = retrieveMinTimeItem(); + if (item) yield item; + } while (item); + } - const bandwidth = bits / loadingDuration; + private async loadSegmentThroughHttp(segment: Segment, queue: LoadQueue) { + queue.markSegmentAsLoading(segment.localId, "http"); + const response = await this.httpLoader.load(segment); + this.segmentStorage.storeSegment(segment, response.data); + queue.removeLoadedSegment(segment.localId); - return { - url, - data, - bandwidth, - status, - ok, - }; + console.log("queue cleared", queue.length); } - abortSegment(segmentId: string) { - this.httpLoader.abort(segmentId); + private async loadRandomSegmentThroughHttp() { + if ( + this.httpLoader.getLoadingsAmount() > + this.settings.simultaneousHttpDownloads + ) { + return; + } + const randomSegmentInfo = this.mainQueue.getRandomHttpLoadableSegment(); + if (!randomSegmentInfo) return; + + void this.loadSegmentThroughHttp(randomSegmentInfo.segment, this.mainQueue); } private identifySegment(segmentId: string) { @@ -64,19 +151,27 @@ export class Loader { throw new Error(`Not found segment with id: ${segmentId}`); } - // console.log("\nloading segment:"); - // console.log("Index: ", segment.externalId); - const streamEternalId = Utils.getStreamExternalId( - stream, - this.manifestResponseUrl - ); - // console.log("Stream: ", streamEternalId); - // console.log(this.mainQueue.highDemandQueue); - // // console.log(this.mainQueue.lowDemandQueue); - // console.log((this.mainQueue as any).playback); - return { segment, stream }; } + + private onPlaybackUpdate() { + console.log("playback update"); + const { segmentsToAbortIds: mainIds } = + this.mainQueue.clearNotInLoadRangeSegments(); + const { segmentsToAbortIds: secondaryIds } = + this.secondaryQueue.clearNotInLoadRangeSegments(); + + this.abortSegments(mainIds); + this.abortSegments(secondaryIds); + } + + abortSegment(segmentId: string) { + this.httpLoader.abort(segmentId); + } + + private abortSegments(segmentIds: string[]) { + segmentIds.forEach((id) => this.abortSegment(id)); + } } async function trackTime( diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts index 5fad61d3..3958398a 100644 --- a/packages/p2p-media-loader-core/src/playback.ts +++ b/packages/p2p-media-loader-core/src/playback.ts @@ -1,27 +1,26 @@ export class Playback { private _rate = 1; private _position = 0; - private readonly settings: { - readonly highDemandBufferLength: number; - readonly lowDemandBufferLength: number; - }; private _highDemandMargin = 0; - private _lowDemandMargin = 0; + private _httpDownloadMargin = 0; + private _p2pDownloadMargin = 0; + private onUpdateSubscriptions: (() => void)[] = []; - constructor(settings: { - readonly highDemandBufferLength: number; - readonly lowDemandBufferLength: number; - }) { - this.settings = settings; - this._highDemandMargin = this.getHighDemandMargin(); - this._lowDemandMargin = this.getLowDemandMargin(); + constructor( + private readonly settings: { + readonly highDemandBufferLength: number; + readonly httpDownloadBufferLength: number; + readonly p2pDownloadBufferLength: number; + } + ) { + this.updateMargins(); } set position(value: number) { if (value === this._position) return; this._position = value; - this._highDemandMargin = this.getHighDemandMargin(); - this._lowDemandMargin = this.getLowDemandMargin(); + this.updateMargins(); + this.onUpdateSubscriptions.forEach((s) => s()); } get position() { @@ -31,33 +30,41 @@ export class Playback { set rate(value: number) { if (value === this._rate) return; this._rate = value; - this._highDemandMargin = this.getHighDemandMargin(); - this._lowDemandMargin = this.getLowDemandMargin(); - } - - get rate() { - return this._rate; + this.updateMargins(); + this.onUpdateSubscriptions.forEach((s) => s()); } get highDemandMargin() { return this._highDemandMargin; } - get lowDemandMargin() { - return this._lowDemandMargin; + get httpDownloadMargin() { + return this._httpDownloadMargin; } - private getHighDemandMargin() { - const { highDemandBufferLength } = this.settings; - return this._position + highDemandBufferLength * this._rate; + get p2pDownloadMargin() { + return this._p2pDownloadMargin; } - private getLowDemandMargin() { - const { lowDemandBufferLength } = this.settings; - return this._position + lowDemandBufferLength * this._rate; + private updateMargins() { + const { + highDemandBufferLength, + httpDownloadBufferLength, + p2pDownloadBufferLength, + } = this.settings; + this._highDemandMargin = + this._position + highDemandBufferLength * this._rate; + this._httpDownloadMargin = + this._position + httpDownloadBufferLength * this._rate; + this._p2pDownloadMargin = + this._position + p2pDownloadBufferLength * this._rate; } getTimeTo(time: number) { return (time - this._position) / this._rate; } + + subscribeToUpdate(handler: () => void) { + this.onUpdateSubscriptions.push(handler); + } } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 67344d78..503b09c7 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -1,5 +1,6 @@ import { Playback } from "./playback"; import { Segment } from "./types"; +import * as Utils from "./utils"; export class SegmentsMemoryStorage { private cache = new Map< @@ -36,12 +37,8 @@ export class SegmentsMemoryStorage { } private isSegmentLocked(segment: Segment) { - const { position, lowDemandMargin } = this.playback; - const { startTime, endTime } = segment; - return !( - (startTime < position && endTime < position) || - (startTime > lowDemandMargin && endTime > lowDemandMargin) - ); + const statuses = Utils.getSegmentLoadStatuses(segment, this.playback); + return !!statuses; } async clean(): Promise { diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 363dac44..75783ad3 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -1,4 +1,6 @@ import { Segment, Stream, StreamWithSegments } from "./index"; +import { Playback } from "./playback"; +import { SegmentLoadStatus } from "./internal-types"; export function getStreamExternalId( stream: Stream, @@ -17,3 +19,23 @@ export function getSegmentFromStreamsMap( if (segment) return { segment, stream }; } } + +export function getSegmentLoadStatuses( + segment: Segment, + playback: Playback +): Set | undefined { + const { position, highDemandMargin, httpDownloadMargin, p2pDownloadMargin } = + playback; + const { startTime } = segment; + const statuses = new Set(); + if (startTime >= position && startTime < highDemandMargin) { + statuses.add("high-demand"); + } + if (startTime >= position && startTime < httpDownloadMargin) { + statuses.add("http-downloadable"); + } + if (startTime >= position && startTime < p2pDownloadMargin) { + statuses.add("p2p-downloadable"); + } + if (statuses.size) return statuses; +} From 2dc902976d09c8bf4e305dce03bfbcf1367e89be Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 31 Aug 2023 14:30:55 +0300 Subject: [PATCH 007/127] Add plugin requests promises map. --- packages/p2p-media-loader-core/src/loader.ts | 69 ++++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index 83401900..c28ce12c 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -10,6 +10,7 @@ export class Loader { private readonly httpLoader = new HttpLoader(); private readonly mainQueue: LoadQueue; private readonly secondaryQueue: LoadQueue; + private readonly pluginRequests = new Map(); constructor( private readonly streams: Map, @@ -20,7 +21,7 @@ export class Loader { this.mainQueue = new LoadQueue(this.playback, this.segmentStorage); this.secondaryQueue = new LoadQueue(this.playback, this.segmentStorage); this.playback.subscribeToUpdate(this.onPlaybackUpdate.bind(this)); - setInterval(() => this.loadRandomSegmentThroughHttp(), 1000); + // setInterval(() => this.loadRandomSegmentThroughHttp(), 1000); } setManifestResponseUrl(url: string) { @@ -28,31 +29,26 @@ export class Loader { } async requestSegmentByPlugin(segmentId: string): Promise { - console.log("requested", segmentId); const { segment, stream } = this.identifySegment(segmentId); const queue = stream.type === "main" ? this.mainQueue : this.secondaryQueue; + const { segmentsToAbortIds } = queue.update(segment, stream); this.abortSegments(segmentsToAbortIds); this.processQueues(); - console.log("queue size: ", queue.length); - console.log("request size: ", this.httpLoader.getLoadingsAmount()); - let data = this.segmentStorage.getSegment(segmentId); - if (!data) { - const request = this.httpLoader.getRequest(segmentId); - const response = await request; - if (!response) throw new Error("No data"); - data = response.data; + const data = this.segmentStorage.getSegment(segmentId); + if (data) { + return { + url: segment.url, + data, + bandwidth: 999999999, + status: 200, + ok: true, + }; } - console.log("loaded", segmentId); - return { - url: segment.url, - data, - bandwidth: 9999999, - status: 200, - ok: true, - }; + const request = this.createPluginSegmentRequest(segment); + return request.promise; } private processQueues() { @@ -74,6 +70,7 @@ export class Loader { void this.loadSegmentThroughHttp(segment, queue); } } + break; } } @@ -122,9 +119,17 @@ export class Loader { queue.markSegmentAsLoading(segment.localId, "http"); const response = await this.httpLoader.load(segment); this.segmentStorage.storeSegment(segment, response.data); + const request = this.pluginRequests.get(segment.localId); + if (request) { + request.onSuccess({ + bandwidth: 9999999999, + data: response.data, + ok: response.ok, + status: response.status, + url: response.url, + }); + } queue.removeLoadedSegment(segment.localId); - - console.log("queue cleared", queue.length); } private async loadRandomSegmentThroughHttp() { @@ -155,7 +160,6 @@ export class Loader { } private onPlaybackUpdate() { - console.log("playback update"); const { segmentsToAbortIds: mainIds } = this.mainQueue.clearNotInLoadRangeSegments(); const { segmentsToAbortIds: secondaryIds } = @@ -163,6 +167,7 @@ export class Loader { this.abortSegments(mainIds); this.abortSegments(secondaryIds); + this.processQueues(); } abortSegment(segmentId: string) { @@ -172,8 +177,30 @@ export class Loader { private abortSegments(segmentIds: string[]) { segmentIds.forEach((id) => this.abortSegment(id)); } + + private createPluginSegmentRequest(segment: Segment) { + let onSuccess: Request["onSuccess"]; + const promise = new Promise((resolve, reject) => { + onSuccess = resolve; + }); + const request: Request = { + promise, + onSuccess: (res: SegmentResponse) => { + console.log("success"); + onSuccess(res)!; + }, + }; + + this.pluginRequests.set(segment.localId, request); + return request; + } } +type Request = { + promise: Promise; + onSuccess: (response: SegmentResponse) => void; +}; + async function trackTime( action: () => T, unit: "s" | "ms" = "s" From 3ea3362d74065515e4948c970e485f3e1c6e1121 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 31 Aug 2023 18:59:40 +0300 Subject: [PATCH 008/127] Rewrite load queue. --- .../p2p-media-loader-core/src/load-queue.ts | 100 ++++++------------ packages/p2p-media-loader-core/src/loader.ts | 4 +- 2 files changed, 36 insertions(+), 68 deletions(-) diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index dac0befd..8f767cbb 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -8,68 +8,54 @@ import { SegmentLoadStatus } from "./internal-types"; export type QueueItem = { segment: Segment; statuses: Set; - isLoading?: boolean; - loadingType?: "http" | "p2p"; }; export class LoadQueue { private readonly queue = new LinkedMap(); private activeStream?: StreamWithSegments; + private isSegmentLoaded?: (segmentId: string) => boolean; + private lastRequestedSegment?: Segment; + private prevPosition?: number; + private prevRate?: number; + private segmentDuration = 0; + private updateHandler?: () => void; - constructor( - private readonly playback: Playback, - private readonly segmentStorage: SegmentsMemoryStorage - ) {} + constructor(private readonly playback: Playback) {} - *getSegmentsToLoad() { - for (const [segmentId, segmentInfo] of this.queue.entries()) { - if (this.queue.get(segmentId)?.isLoading) continue; - yield segmentInfo; - } - } - - getRandomHttpLoadableSegment() { - const notLoadingSegments = this.queue.filter( - ([, { isLoading, statuses }]) => - !isLoading && statuses.has("http-downloadable") - ); - if (!notLoadingSegments.length) return undefined; - const randomIndex = getRandomInt(0, notLoadingSegments.length - 1); - return notLoadingSegments[randomIndex][1]; - } - - getLastHttpLoadingItemAfter(segmentId: string) { - for (const [itemSegmentId, item] of this.queue.entriesBackwards()) { - if (itemSegmentId === segmentId) break; - if (item.isLoading && item.loadingType === "http") { - return item; - } - } - } - - update(segment: Segment, stream: StreamWithSegments) { + updateOnStreamChange(segment: Segment, stream: StreamWithSegments) { const segmentsToAbortIds: string[] = []; if (this.activeStream !== stream) { this.activeStream = stream; - this.queue.forEach(([segmentId, { isLoading }]) => { - if (isLoading) segmentsToAbortIds.push(segmentId); - }); + this.queue.forEach(([segmentId]) => segmentsToAbortIds.push(segmentId)); this.queue.clear(); } + this.lastRequestedSegment = segment; + this.addNewSegmentsToQueue(); + } - this.addNewSegmentsToQueue(segment); - - return { segmentsToAbortIds }; + playbackUpdate(position: number, rate: number) { + const isRateChanged = this.prevRate === undefined || rate !== this.prevRate; + const isPositionSignificantlyChanged = + this.prevPosition === undefined || + Math.abs(position - this.prevPosition) / this.segmentDuration < 0.8; + if (!isRateChanged && !isPositionSignificantlyChanged) { + return; + } + if (isRateChanged) this.prevRate = rate; + if (isPositionSignificantlyChanged) this.prevPosition = position; + this.clearNotActualSegmentsUpdateStatuses(); + this.addNewSegmentsToQueue(); } - addNewSegmentsToQueue(requestedSegment: Segment) { - if (!this.activeStream) return; + addNewSegmentsToQueue() { + if (!this.activeStream || !this.lastRequestedSegment) return; + let newQueueSegmentsCount = 0; let prevSegmentId: string | undefined; for (const [segmentId, segment] of this.activeStream.segments.entries( - requestedSegment.localId + this.lastRequestedSegment.localId )) { - if (this.segmentStorage.hasSegment(segmentId)) continue; + if (this.isSegmentLoaded?.(segmentId)) continue; if (this.queue.has(segmentId)) { prevSegmentId = segmentId; continue; @@ -80,43 +66,27 @@ export class LoadQueue { const info = { segment, statuses }; if (prevSegmentId) this.queue.addAfter(prevSegmentId, segmentId, info); else this.queue.addToStart(segmentId, info); + newQueueSegmentsCount++; prevSegmentId = segmentId; } + + return newQueueSegmentsCount; } - clearNotInLoadRangeSegments() { - const segmentsToAbortIds: string[] = []; + private clearNotActualSegmentsUpdateStatuses() { + const notActualSegments: string[] = []; for (const [segmentId, segmentInfo] of this.queue.entries()) { const statuses = Utils.getSegmentLoadStatuses( segmentInfo.segment, this.playback ); if (!statuses) { - segmentsToAbortIds.push(segmentId); + notActualSegments.push(segmentId); this.queue.delete(segmentId); } else { segmentInfo.statuses = statuses; } } - - segmentsToAbortIds.forEach((id) => this.queue.delete(id)); - return { segmentsToAbortIds }; - } - - markSegmentAsLoading(segmentId: string, loadingType: "http" | "p2p") { - const segmentInfo = this.queue.get(segmentId); - if (!segmentInfo) return; - - segmentInfo.isLoading = true; - segmentInfo.loadingType = loadingType; - } - - markSegmentAsNotLoading(segmentId: string) { - const segmentInfo = this.queue.get(segmentId); - if (!segmentInfo) return; - - delete segmentInfo.isLoading; - delete segmentInfo.loadingType; } removeLoadedSegment(segmentId: string) { diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index c28ce12c..7f61dc1a 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -33,7 +33,7 @@ export class Loader { const queue = stream.type === "main" ? this.mainQueue : this.secondaryQueue; - const { segmentsToAbortIds } = queue.update(segment, stream); + const { segmentsToAbortIds } = queue.updateOnStreamChange(segment, stream); this.abortSegments(segmentsToAbortIds); this.processQueues(); @@ -66,7 +66,6 @@ export class Loader { const lastItem = queue.getLastHttpLoadingItemAfter(segment.localId); if (lastItem) { this.httpLoader.abort(lastItem.segment.localId); - queue.markSegmentAsNotLoading(lastItem.segment.localId); void this.loadSegmentThroughHttp(segment, queue); } } @@ -116,7 +115,6 @@ export class Loader { } private async loadSegmentThroughHttp(segment: Segment, queue: LoadQueue) { - queue.markSegmentAsLoading(segment.localId, "http"); const response = await this.httpLoader.load(segment); this.segmentStorage.storeSegment(segment, response.data); const request = this.pluginRequests.get(segment.localId); From 2ff4aa6fe7ff8cf49c6f4a3d1857491c3e849286 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 31 Aug 2023 23:04:08 +0300 Subject: [PATCH 009/127] Move segment queue status determination to queue class. --- .../p2p-media-loader-core/src/load-queue.ts | 125 +++++++++++++----- packages/p2p-media-loader-core/src/utils.ts | 20 --- 2 files changed, 93 insertions(+), 52 deletions(-) diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index 8f767cbb..7ac64ba1 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -1,26 +1,34 @@ import { Segment, StreamWithSegments } from "./types"; import { LinkedMap } from "./linked-map"; -import { Playback } from "./playback"; -import * as Utils from "./utils"; -import { SegmentsMemoryStorage } from "./segments-storage"; import { SegmentLoadStatus } from "./internal-types"; -export type QueueItem = { +export type LoadQueueItem = { segment: Segment; statuses: Set; }; export class LoadQueue { - private readonly queue = new LinkedMap(); + private readonly queue = new LinkedMap(); private activeStream?: StreamWithSegments; private isSegmentLoaded?: (segmentId: string) => boolean; private lastRequestedSegment?: Segment; - private prevPosition?: number; - private prevRate?: number; + private position = 0; + private rate = 1; private segmentDuration = 0; - private updateHandler?: () => void; + private updateHandlers: ((removedSegmentIds: string[]) => void)[] = []; + private highDemandBufferMargin!: number; + private httpBufferMargin!: number; + private p2pBufferMargin!: number; - constructor(private readonly playback: Playback) {} + constructor( + private readonly settings: { + highDemandBufferLength: number; + httpBufferLength: number; + p2pBufferLength: number; + } + ) { + this.updateBufferMargins(); + } updateOnStreamChange(segment: Segment, stream: StreamWithSegments) { const segmentsToAbortIds: string[] = []; @@ -34,23 +42,32 @@ export class LoadQueue { } playbackUpdate(position: number, rate: number) { - const isRateChanged = this.prevRate === undefined || rate !== this.prevRate; + const isRateChanged = this.rate === undefined || rate !== this.rate; const isPositionSignificantlyChanged = - this.prevPosition === undefined || - Math.abs(position - this.prevPosition) / this.segmentDuration < 0.8; - if (!isRateChanged && !isPositionSignificantlyChanged) { - return; + this.position === undefined || + Math.abs(position - this.position) / this.segmentDuration < 0.8; + if (!isRateChanged && !isPositionSignificantlyChanged) return; + if (isRateChanged) this.rate = rate; + if (isPositionSignificantlyChanged) this.position = position; + this.updateBufferMargins(); + + const { removedSegmentIds, statusChangedSegmentIds } = + this.clearNotActualSegmentsUpdateStatuses(); + const newSegmentIds = this.addNewSegmentsToQueue(); + + if ( + removedSegmentIds.length || + statusChangedSegmentIds.length || + newSegmentIds?.length + ) { + this.updateHandlers.forEach((handler) => handler(removedSegmentIds)); } - if (isRateChanged) this.prevRate = rate; - if (isPositionSignificantlyChanged) this.prevPosition = position; - this.clearNotActualSegmentsUpdateStatuses(); - this.addNewSegmentsToQueue(); } - addNewSegmentsToQueue() { + private addNewSegmentsToQueue() { if (!this.activeStream || !this.lastRequestedSegment) return; - let newQueueSegmentsCount = 0; + const newSegmentIds: string[] = []; let prevSegmentId: string | undefined; for (const [segmentId, segment] of this.activeStream.segments.entries( this.lastRequestedSegment.localId @@ -60,44 +77,88 @@ export class LoadQueue { prevSegmentId = segmentId; continue; } - const statuses = Utils.getSegmentLoadStatuses(segment, this.playback); + const statuses = this.getSegmentStatuses(segment); if (!statuses) break; const info = { segment, statuses }; if (prevSegmentId) this.queue.addAfter(prevSegmentId, segmentId, info); else this.queue.addToStart(segmentId, info); - newQueueSegmentsCount++; + newSegmentIds.push(segmentId); prevSegmentId = segmentId; } - return newQueueSegmentsCount; + return newSegmentIds; } private clearNotActualSegmentsUpdateStatuses() { - const notActualSegments: string[] = []; - for (const [segmentId, segmentInfo] of this.queue.entries()) { - const statuses = Utils.getSegmentLoadStatuses( - segmentInfo.segment, - this.playback - ); + const removedSegmentIds: string[] = []; + const statusChangedSegmentIds: string[] = []; + for (const [segmentId, item] of this.queue.entries()) { + const statuses = this.getSegmentStatuses(item.segment); if (!statuses) { - notActualSegments.push(segmentId); + removedSegmentIds.push(segmentId); this.queue.delete(segmentId); - } else { - segmentInfo.statuses = statuses; + } else if (areSetsEqual(item.statuses, statuses)) { + item.statuses = statuses; + statusChangedSegmentIds.push(segmentId); } } + return { removedSegmentIds, statusChangedSegmentIds }; + } + + private updateBufferMargins() { + if (this.position === undefined || this.rate === undefined) return; + const { highDemandBufferLength, p2pBufferLength, httpBufferLength } = + this.settings; + + this.highDemandBufferMargin = + this.position + highDemandBufferLength * this.rate; + this.httpBufferMargin = this.position + httpBufferLength * this.rate; + this.p2pBufferMargin = this.position + p2pBufferLength * this.rate; + } + + private getSegmentStatuses(segment: Segment) { + const { + highDemandBufferMargin, + httpBufferMargin, + p2pBufferMargin, + position, + } = this; + const { startTime } = segment; + const statuses = new Set(); + if (startTime >= position && startTime < highDemandBufferMargin) { + statuses.add("high-demand"); + } + if (startTime >= position && startTime < httpBufferMargin) { + statuses.add("http-downloadable"); + } + if (startTime >= position && startTime < p2pBufferMargin) { + statuses.add("p2p-downloadable"); + } + if (statuses.size) return statuses; } removeLoadedSegment(segmentId: string) { this.queue.delete(segmentId); } + subscribeToUpdate(handler: () => void) { + this.updateHandlers.push(handler); + } + get length() { return this.queue.size; } } +function areSetsEqual(set1: Set, set2: Set): boolean { + if (set1.size !== set2.size) return false; + for (const item of set1) { + if (!set2.has(item)) return false; + } + return true; +} + function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 75783ad3..b7066ff6 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -19,23 +19,3 @@ export function getSegmentFromStreamsMap( if (segment) return { segment, stream }; } } - -export function getSegmentLoadStatuses( - segment: Segment, - playback: Playback -): Set | undefined { - const { position, highDemandMargin, httpDownloadMargin, p2pDownloadMargin } = - playback; - const { startTime } = segment; - const statuses = new Set(); - if (startTime >= position && startTime < highDemandMargin) { - statuses.add("high-demand"); - } - if (startTime >= position && startTime < httpDownloadMargin) { - statuses.add("http-downloadable"); - } - if (startTime >= position && startTime < p2pDownloadMargin) { - statuses.add("p2p-downloadable"); - } - if (statuses.size) return statuses; -} From 77f68047f0d991196e671545cb3bcbd6ec170a3c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 1 Sep 2023 13:00:17 +0300 Subject: [PATCH 010/127] Separate main and secondary stream to different contexts. --- packages/p2p-media-loader-core/src/core.ts | 24 +- .../p2p-media-loader-core/src/http-loader.ts | 19 +- .../src/internal-types.ts | 1 - .../p2p-media-loader-core/src/load-queue.ts | 35 ++- packages/p2p-media-loader-core/src/loader.ts | 254 ++++++++---------- .../p2p-media-loader-core/src/playback.ts | 70 ----- packages/p2p-media-loader-core/src/types.ts | 3 - 7 files changed, 164 insertions(+), 242 deletions(-) delete mode 100644 packages/p2p-media-loader-core/src/playback.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index f2ad7028..e1c418d2 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,27 +1,23 @@ import { Loader } from "./loader"; import { Stream, StreamWithSegments, Segment, SegmentResponse } from "./types"; +import { Playback } from "./internal-types"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; -import { Playback } from "./playback"; import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { private readonly streams = new Map>(); - private readonly playback = new Playback({ - highDemandBufferLength: 15, - httpDownloadBufferLength: 60, - p2pDownloadBufferLength: 80, - }); + private readonly playback: Playback = { position: 0, rate: 1 }; private readonly segmentStorage = new SegmentsMemoryStorage(this.playback, { cachedSegmentExpiration: 120, cachedSegmentsCount: 50, }); - private readonly loader = new Loader( - this.streams, - this.segmentStorage, - this.playback, - { simultaneousHttpDownloads: 3 } - ); + private readonly loader = new Loader(this.streams, this.segmentStorage, { + simultaneousHttpDownloads: 3, + highDemandBufferLength: 20, + httpBufferLength: 60, + p2pBufferLength: 60, + }); setManifestResponseUrl(url: string): void { this.loader.setManifestResponseUrl(url.split("?")[0]); @@ -59,11 +55,12 @@ export class Core { const firstSegment = stream.segments.first?.[1]; if (firstSegment && firstSegment.startTime > this.playback.position) { this.playback.position = firstSegment.startTime; + this.loader.onPlaybackUpdate(this.playback); } } loadSegment(segmentLocalId: string): Promise { - return this.loader.requestSegmentByPlugin(segmentLocalId); + return this.loader.loadSegment(segmentLocalId); } abortSegmentLoading(segmentId: string): void { @@ -73,6 +70,7 @@ export class Core { updatePlayback(position: number, rate: number): void { this.playback.position = position; this.playback.rate = rate; + this.loader.onPlaybackUpdate(this.playback); } destroy(): void { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 65f3c879..27eac480 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -2,12 +2,7 @@ import { FetchError } from "./errors"; import { Segment } from "./types"; type Request = { - promise: Promise<{ - ok: boolean; - status: number; - data: ArrayBuffer; - url: string; - }>; + promise: Promise; abortController: AbortController; }; @@ -48,13 +43,11 @@ export class HttpLoader { ); } - const data = await response.arrayBuffer(); - return { - ok: response.ok, - status: response.status, - data, - url: response.url, - }; + return response.arrayBuffer(); + } + + isLoading(segmentId: string) { + return this.requests.has(segmentId); } abort(segmentId: string) { diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index c5b843f1..9261a960 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -1,7 +1,6 @@ export type Playback = { position: number; rate: number; - lastPositionUpdate: "moved-forward" | "moved-backward"; }; export type SegmentLoadStatus = diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index 7ac64ba1..140829bb 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -14,7 +14,7 @@ export class LoadQueue { private lastRequestedSegment?: Segment; private position = 0; private rate = 1; - private segmentDuration = 0; + private segmentDuration?: number; private updateHandlers: ((removedSegmentIds: string[]) => void)[] = []; private highDemandBufferMargin!: number; private httpBufferMargin!: number; @@ -30,7 +30,19 @@ export class LoadQueue { this.updateBufferMargins(); } - updateOnStreamChange(segment: Segment, stream: StreamWithSegments) { + *items() { + for (const [, item] of this.queue.entries()) { + yield item; + } + } + + *itemsBackwards() { + for (const [, item] of this.queue.entriesBackwards()) { + yield item; + } + } + + updateIfStreamChanged(segment: Segment, stream: StreamWithSegments) { const segmentsToAbortIds: string[] = []; if (this.activeStream !== stream) { this.activeStream = stream; @@ -42,10 +54,11 @@ export class LoadQueue { } playbackUpdate(position: number, rate: number) { + const avgSegmentDuration = this.getAvgSegmentDuration(); const isRateChanged = this.rate === undefined || rate !== this.rate; const isPositionSignificantlyChanged = this.position === undefined || - Math.abs(position - this.position) / this.segmentDuration < 0.8; + Math.abs(position - this.position) / avgSegmentDuration > 0.5; if (!isRateChanged && !isPositionSignificantlyChanged) return; if (isRateChanged) this.rate = rate; if (isPositionSignificantlyChanged) this.position = position; @@ -142,10 +155,24 @@ export class LoadQueue { this.queue.delete(segmentId); } - subscribeToUpdate(handler: () => void) { + subscribeToUpdate(handler: (removedSegmentIds: string[]) => void) { this.updateHandlers.push(handler); } + setIsSegmentLoadedPredicate(predicate: (segmentId: string) => boolean) { + this.isSegmentLoaded = predicate; + } + + private getAvgSegmentDuration() { + if (this.segmentDuration) return this.segmentDuration; + let sum = 0; + this.queue.forEach( + ([, { segment }]) => (sum += segment.endTime - segment.startTime) + ); + this.segmentDuration = sum / this.queue.size; + return this.segmentDuration; + } + get length() { return this.queue.size; } diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index 7f61dc1a..dbd555c2 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -1,192 +1,179 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; import * as Utils from "./utils"; import { HttpLoader } from "./http-loader"; -import { LoadQueue, QueueItem } from "./load-queue"; +import { LoadQueue } from "./load-queue"; import { SegmentsMemoryStorage } from "./segments-storage"; -import { Playback } from "./playback"; +import { Playback } from "./internal-types"; export class Loader { private manifestResponseUrl?: string; - private readonly httpLoader = new HttpLoader(); - private readonly mainQueue: LoadQueue; - private readonly secondaryQueue: LoadQueue; - private readonly pluginRequests = new Map(); + private readonly mainStreamLoader: StreamLoader; + private readonly secondaryStreamLoader: StreamLoader; constructor( private readonly streams: Map, private readonly segmentStorage: SegmentsMemoryStorage, - private readonly playback: Playback, - private readonly settings: { simultaneousHttpDownloads: number } + private readonly settings: { + simultaneousHttpDownloads: number; + highDemandBufferLength: number; + httpBufferLength: number; + p2pBufferLength: number; + } ) { - this.mainQueue = new LoadQueue(this.playback, this.segmentStorage); - this.secondaryQueue = new LoadQueue(this.playback, this.segmentStorage); - this.playback.subscribeToUpdate(this.onPlaybackUpdate.bind(this)); - // setInterval(() => this.loadRandomSegmentThroughHttp(), 1000); + this.mainStreamLoader = new StreamLoader( + this.segmentStorage, + this.settings + ); + this.secondaryStreamLoader = new StreamLoader( + this.segmentStorage, + this.settings + ); } setManifestResponseUrl(url: string) { this.manifestResponseUrl = url; } - async requestSegmentByPlugin(segmentId: string): Promise { + async loadSegment(segmentId: string): Promise { const { segment, stream } = this.identifySegment(segmentId); - const queue = stream.type === "main" ? this.mainQueue : this.secondaryQueue; + const loader = + stream.type === "main" + ? this.mainStreamLoader + : this.secondaryStreamLoader; + return loader.loadSegment(segment, stream); + } + + private identifySegment(segmentId: string) { + if (!this.manifestResponseUrl) { + throw new Error("Manifest response url is undefined"); + } + + const { stream, segment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; + if (!segment || !stream) { + throw new Error(`Not found segment with id: ${segmentId}`); + } + + return { segment, stream }; + } + + onPlaybackUpdate(playback: Playback) { + const { position, rate } = playback; + this.mainStreamLoader.onPlaybackUpdate(position, rate); + this.secondaryStreamLoader.onPlaybackUpdate(position, rate); + } - const { segmentsToAbortIds } = queue.updateOnStreamChange(segment, stream); - this.abortSegments(segmentsToAbortIds); - this.processQueues(); + abortSegment(segmentId: string) { + this.mainStreamLoader.abortSegment(segmentId); + this.secondaryStreamLoader.abortSegment(segmentId); + } +} + +class StreamLoader { + private readonly queue: LoadQueue; + private readonly httpLoader = new HttpLoader(); + private readonly pluginRequests = new Map(); - const data = this.segmentStorage.getSegment(segmentId); - if (data) { + constructor( + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly settings: { + highDemandBufferLength: number; + httpBufferLength: number; + p2pBufferLength: number; + simultaneousHttpDownloads: number; + } + ) { + this.queue = new LoadQueue(this.settings); + this.queue.subscribeToUpdate(this.onQueueChanged.bind(this)); + this.queue.setIsSegmentLoadedPredicate(this.isSegmentLoaded.bind(this)); + } + + async loadSegment( + segment: Segment, + stream: StreamWithSegments + ): Promise { + this.queue.updateIfStreamChanged(segment, stream); + const storageData = this.segmentStorage.getSegment(segment.localId); + if (storageData) { return { - url: segment.url, - data, - bandwidth: 999999999, - status: 200, - ok: true, + data: storageData, + bandwidth: 99999999, }; } const request = this.createPluginSegmentRequest(segment); - return request.promise; + return request.responsePromise; + } + + abortSegment(segmentId: string) { + this.httpLoader.abort(segmentId); } - private processQueues() { + private processQueue() { const { simultaneousHttpDownloads } = this.settings; - for (const [ - { segment, statuses }, - queue, - ] of this.getQueuesSegmentsToLoad()) { + for (const { segment, statuses } of this.queue.items()) { if (statuses.has("high-demand")) { if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { - void this.loadSegmentThroughHttp(segment, queue); + void this.loadSegmentThroughHttp(segment); continue; } - const lastItem = queue.getLastHttpLoadingItemAfter(segment.localId); - if (lastItem) { - this.httpLoader.abort(lastItem.segment.localId); - void this.loadSegmentThroughHttp(segment, queue); + this.abortLastHttpLoadingAfter(segment.localId); + if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { + void this.loadSegmentThroughHttp(segment); } } break; } } - private *getQueuesSegmentsToLoad() { - const mainGen = this.mainQueue.getSegmentsToLoad(); - const secondaryGen = this.secondaryQueue.getSegmentsToLoad(); - let item1: QueueItem | undefined; - let item2: QueueItem | undefined; - - const retrieveMinTimeItem = (): [QueueItem, LoadQueue] | undefined => { - item1 = item1 ?? (mainGen.next().value as QueueItem | undefined); - item2 = item2 ?? (secondaryGen.next().value as QueueItem | undefined); - - if (!item1 && !item2) return undefined; - if (item1 && item2) { - if (item1.segment.startTime < item2.segment.startTime) { - const result = item1; - item1 = undefined; - return [result, this.mainQueue]; - } else { - const result = item2; - item2 = undefined; - return [result, this.secondaryQueue]; - } - } - if (item1) { - const result: [QueueItem, LoadQueue] = [item1, this.mainQueue]; - item1 = undefined; - return result; - } - if (item2) { - const result: [QueueItem, LoadQueue] = [item2, this.secondaryQueue]; - item2 = undefined; - return result; - } - }; - - let item: [QueueItem, LoadQueue] | undefined; - do { - item = retrieveMinTimeItem(); - if (item) yield item; - } while (item); - } - - private async loadSegmentThroughHttp(segment: Segment, queue: LoadQueue) { - const response = await this.httpLoader.load(segment); - this.segmentStorage.storeSegment(segment, response.data); + private async loadSegmentThroughHttp(segment: Segment) { + const data = await this.httpLoader.load(segment); + this.segmentStorage.storeSegment(segment, data); const request = this.pluginRequests.get(segment.localId); if (request) { request.onSuccess({ bandwidth: 9999999999, - data: response.data, - ok: response.ok, - status: response.status, - url: response.url, + data, }); } - queue.removeLoadedSegment(segment.localId); - } - - private async loadRandomSegmentThroughHttp() { - if ( - this.httpLoader.getLoadingsAmount() > - this.settings.simultaneousHttpDownloads - ) { - return; - } - const randomSegmentInfo = this.mainQueue.getRandomHttpLoadableSegment(); - if (!randomSegmentInfo) return; - - void this.loadSegmentThroughHttp(randomSegmentInfo.segment, this.mainQueue); + this.queue.removeLoadedSegment(segment.localId); } - private identifySegment(segmentId: string) { - if (!this.manifestResponseUrl) { - throw new Error("Manifest response url is undefined"); - } - - const { stream, segment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - if (!segment || !stream) { - throw new Error(`Not found segment with id: ${segmentId}`); + private abortLastHttpLoadingAfter(segmentId: string) { + for (const { segment } of this.queue.itemsBackwards()) { + if (segment.localId === segmentId) break; + if (this.httpLoader.isLoading(segment.localId)) { + this.httpLoader.abort(segment.localId); + break; + } } - - return { segment, stream }; } - private onPlaybackUpdate() { - const { segmentsToAbortIds: mainIds } = - this.mainQueue.clearNotInLoadRangeSegments(); - const { segmentsToAbortIds: secondaryIds } = - this.secondaryQueue.clearNotInLoadRangeSegments(); - - this.abortSegments(mainIds); - this.abortSegments(secondaryIds); - this.processQueues(); + onPlaybackUpdate(position: number, rate: number) { + this.queue.playbackUpdate(position, rate); } - abortSegment(segmentId: string) { - this.httpLoader.abort(segmentId); + private onQueueChanged(removedSegmentIds: string[]) { + removedSegmentIds.forEach((id) => this.httpLoader.abort(id)); + this.processQueue(); } - private abortSegments(segmentIds: string[]) { - segmentIds.forEach((id) => this.abortSegment(id)); + private isSegmentLoaded(segmentId: string): boolean { + return this.segmentStorage.hasSegment(segmentId); } private createPluginSegmentRequest(segment: Segment) { let onSuccess: Request["onSuccess"]; - const promise = new Promise((resolve, reject) => { + let onError: Request["onError"]; + const responsePromise = new Promise((resolve, reject) => { onSuccess = resolve; + onError = reject; }); const request: Request = { - promise, - onSuccess: (res: SegmentResponse) => { - console.log("success"); - onSuccess(res)!; - }, + responsePromise, + onSuccess: onSuccess!, + onError: onError!, }; this.pluginRequests.set(segment.localId, request); @@ -195,16 +182,7 @@ export class Loader { } type Request = { - promise: Promise; + responsePromise: Promise; onSuccess: (response: SegmentResponse) => void; + onError: (reason?: unknown) => void; }; - -async function trackTime( - action: () => T, - unit: "s" | "ms" = "s" -): Promise<[Awaited, number]> { - const start = performance.now(); - const result = await action(); - const duration = performance.now() - start; - return [result, unit === "ms" ? duration : duration / 1000]; -} diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts deleted file mode 100644 index 3958398a..00000000 --- a/packages/p2p-media-loader-core/src/playback.ts +++ /dev/null @@ -1,70 +0,0 @@ -export class Playback { - private _rate = 1; - private _position = 0; - private _highDemandMargin = 0; - private _httpDownloadMargin = 0; - private _p2pDownloadMargin = 0; - private onUpdateSubscriptions: (() => void)[] = []; - - constructor( - private readonly settings: { - readonly highDemandBufferLength: number; - readonly httpDownloadBufferLength: number; - readonly p2pDownloadBufferLength: number; - } - ) { - this.updateMargins(); - } - - set position(value: number) { - if (value === this._position) return; - this._position = value; - this.updateMargins(); - this.onUpdateSubscriptions.forEach((s) => s()); - } - - get position() { - return this._position; - } - - set rate(value: number) { - if (value === this._rate) return; - this._rate = value; - this.updateMargins(); - this.onUpdateSubscriptions.forEach((s) => s()); - } - - get highDemandMargin() { - return this._highDemandMargin; - } - - get httpDownloadMargin() { - return this._httpDownloadMargin; - } - - get p2pDownloadMargin() { - return this._p2pDownloadMargin; - } - - private updateMargins() { - const { - highDemandBufferLength, - httpDownloadBufferLength, - p2pDownloadBufferLength, - } = this.settings; - this._highDemandMargin = - this._position + highDemandBufferLength * this._rate; - this._httpDownloadMargin = - this._position + httpDownloadBufferLength * this._rate; - this._p2pDownloadMargin = - this._position + p2pDownloadBufferLength * this._rate; - } - - getTimeTo(time: number) { - return (time - this._position) / this._rate; - } - - subscribeToUpdate(handler: () => void) { - this.onUpdateSubscriptions.push(handler); - } -} diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index a5585169..5bca44ef 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -33,8 +33,5 @@ export type StreamWithSegments< export type SegmentResponse = { data: ArrayBuffer; - url: string; bandwidth: number; - status: number; - ok: boolean; }; From d933e7aaae47cd68af7bcf3764b8644f3a25e50b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 1 Sep 2023 13:34:52 +0300 Subject: [PATCH 011/127] Remove unnecessary loader layer. --- packages/p2p-media-loader-core/src/core.ts | 50 +++++++++++++-- packages/p2p-media-loader-core/src/loader.ts | 67 -------------------- 2 files changed, 43 insertions(+), 74 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index e1c418d2..c3e2fbdf 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -6,21 +6,30 @@ import { LinkedMap } from "./linked-map"; import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { + private manifestResponseUrl?: string; private readonly streams = new Map>(); private readonly playback: Playback = { position: 0, rate: 1 }; private readonly segmentStorage = new SegmentsMemoryStorage(this.playback, { cachedSegmentExpiration: 120, cachedSegmentsCount: 50, }); - private readonly loader = new Loader(this.streams, this.segmentStorage, { + private readonly settings = { simultaneousHttpDownloads: 3, highDemandBufferLength: 20, httpBufferLength: 60, p2pBufferLength: 60, - }); + }; + private readonly mainStreamLoader = new Loader( + this.segmentStorage, + this.settings + ); + private readonly secondaryStreamLoader = new Loader( + this.segmentStorage, + this.settings + ); setManifestResponseUrl(url: string): void { - this.loader.setManifestResponseUrl(url.split("?")[0]); + this.manifestResponseUrl = url.split("?")[0]; } hasSegment(segmentLocalId: string): boolean { @@ -55,25 +64,52 @@ export class Core { const firstSegment = stream.segments.first?.[1]; if (firstSegment && firstSegment.startTime > this.playback.position) { this.playback.position = firstSegment.startTime; - this.loader.onPlaybackUpdate(this.playback); + this.onPlaybackUpdate(this.playback); } } loadSegment(segmentLocalId: string): Promise { - return this.loader.loadSegment(segmentLocalId); + const { segment, stream } = this.identifySegment(segmentLocalId); + + const loader = + stream.type === "main" + ? this.mainStreamLoader + : this.secondaryStreamLoader; + return loader.loadSegment(segment, stream); } abortSegmentLoading(segmentId: string): void { - return this.loader.abortSegment(segmentId); + this.mainStreamLoader.abortSegment(segmentId); + this.secondaryStreamLoader.abortSegment(segmentId); } updatePlayback(position: number, rate: number): void { this.playback.position = position; this.playback.rate = rate; - this.loader.onPlaybackUpdate(this.playback); + this.onPlaybackUpdate(this.playback); } destroy(): void { this.streams.clear(); } + + private identifySegment(segmentId: string) { + if (!this.manifestResponseUrl) { + throw new Error("Manifest response url is undefined"); + } + + const { stream, segment } = + Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; + if (!segment || !stream) { + throw new Error(`Not found segment with id: ${segmentId}`); + } + + return { segment, stream }; + } + + private onPlaybackUpdate(playback: Playback) { + const { position, rate } = playback; + this.mainStreamLoader.onPlaybackUpdate(position, rate); + this.secondaryStreamLoader.onPlaybackUpdate(position, rate); + } } diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index dbd555c2..66c3d94f 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -1,76 +1,9 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import * as Utils from "./utils"; import { HttpLoader } from "./http-loader"; import { LoadQueue } from "./load-queue"; import { SegmentsMemoryStorage } from "./segments-storage"; -import { Playback } from "./internal-types"; export class Loader { - private manifestResponseUrl?: string; - private readonly mainStreamLoader: StreamLoader; - private readonly secondaryStreamLoader: StreamLoader; - - constructor( - private readonly streams: Map, - private readonly segmentStorage: SegmentsMemoryStorage, - private readonly settings: { - simultaneousHttpDownloads: number; - highDemandBufferLength: number; - httpBufferLength: number; - p2pBufferLength: number; - } - ) { - this.mainStreamLoader = new StreamLoader( - this.segmentStorage, - this.settings - ); - this.secondaryStreamLoader = new StreamLoader( - this.segmentStorage, - this.settings - ); - } - - setManifestResponseUrl(url: string) { - this.manifestResponseUrl = url; - } - - async loadSegment(segmentId: string): Promise { - const { segment, stream } = this.identifySegment(segmentId); - - const loader = - stream.type === "main" - ? this.mainStreamLoader - : this.secondaryStreamLoader; - return loader.loadSegment(segment, stream); - } - - private identifySegment(segmentId: string) { - if (!this.manifestResponseUrl) { - throw new Error("Manifest response url is undefined"); - } - - const { stream, segment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - if (!segment || !stream) { - throw new Error(`Not found segment with id: ${segmentId}`); - } - - return { segment, stream }; - } - - onPlaybackUpdate(playback: Playback) { - const { position, rate } = playback; - this.mainStreamLoader.onPlaybackUpdate(position, rate); - this.secondaryStreamLoader.onPlaybackUpdate(position, rate); - } - - abortSegment(segmentId: string) { - this.mainStreamLoader.abortSegment(segmentId); - this.secondaryStreamLoader.abortSegment(segmentId); - } -} - -class StreamLoader { private readonly queue: LoadQueue; private readonly httpLoader = new HttpLoader(); private readonly pluginRequests = new Map(); From 72d33eb8441bdb093d915c2eb62841416c47da4d Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 4 Sep 2023 16:05:33 +0300 Subject: [PATCH 012/127] Fix issue with not loading after moving playhead to random position. --- p2p-media-loader-demo/src/App.tsx | 2 - packages/p2p-media-loader-core/src/core.ts | 50 +++---- .../p2p-media-loader-core/src/load-queue.ts | 123 ++++++++---------- packages/p2p-media-loader-core/src/loader.ts | 54 +++++--- .../p2p-media-loader-core/src/playback.ts | 56 ++++++++ .../src/segments-storage.ts | 15 +-- packages/p2p-media-loader-core/src/types.ts | 9 ++ packages/p2p-media-loader-core/src/utils.ts | 32 ++++- .../src/fragment-loader.ts | 9 +- 9 files changed, 220 insertions(+), 130 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/playback.ts diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index ff9b78c2..da462e5d 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -39,8 +39,6 @@ const streamUrl = { "https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/level_4.m3u8", dashLiveWithSeparateVideoAudio: "https://livesim.dashif.org/livesim/testpic_2s/Manifest.mpd", - hlsAkamaiLive: - "https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8", mss: "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest", audioOnly: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/a1/prog_index.m3u8", diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index c3e2fbdf..59364c1e 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,32 +1,28 @@ -import { Loader } from "./loader"; -import { Stream, StreamWithSegments, Segment, SegmentResponse } from "./types"; -import { Playback } from "./internal-types"; +import { HybridLoader } from "./loader"; +import { + Stream, + StreamWithSegments, + Segment, + SegmentResponse, + Settings, +} from "./types"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; -import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { private manifestResponseUrl?: string; private readonly streams = new Map>(); - private readonly playback: Playback = { position: 0, rate: 1 }; - private readonly segmentStorage = new SegmentsMemoryStorage(this.playback, { - cachedSegmentExpiration: 120, - cachedSegmentsCount: 50, - }); - private readonly settings = { + private readonly settings: Settings = { simultaneousHttpDownloads: 3, highDemandBufferLength: 20, httpBufferLength: 60, p2pBufferLength: 60, + cachedSegmentExpiration: 120, + cachedSegmentsCount: 50, }; - private readonly mainStreamLoader = new Loader( - this.segmentStorage, - this.settings - ); - private readonly secondaryStreamLoader = new Loader( - this.segmentStorage, - this.settings - ); + private position = 0; + private readonly mainStreamLoader = new HybridLoader(this.settings); + private readonly secondaryStreamLoader = new HybridLoader(this.settings); setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; @@ -62,9 +58,10 @@ export class Core { removeSegmentIds?.forEach((id) => stream.segments.delete(id)); const firstSegment = stream.segments.first?.[1]; - if (firstSegment && firstSegment.startTime > this.playback.position) { - this.playback.position = firstSegment.startTime; - this.onPlaybackUpdate(this.playback); + if (firstSegment && firstSegment.startTime > this.position) { + this.position = firstSegment.startTime; + this.mainStreamLoader.updatePlayback(firstSegment.startTime); + this.secondaryStreamLoader.updatePlayback(firstSegment.startTime); } } @@ -84,9 +81,8 @@ export class Core { } updatePlayback(position: number, rate: number): void { - this.playback.position = position; - this.playback.rate = rate; - this.onPlaybackUpdate(this.playback); + this.mainStreamLoader.updatePlayback(position, rate); + this.secondaryStreamLoader.updatePlayback(position, rate); } destroy(): void { @@ -106,10 +102,4 @@ export class Core { return { segment, stream }; } - - private onPlaybackUpdate(playback: Playback) { - const { position, rate } = playback; - this.mainStreamLoader.onPlaybackUpdate(position, rate); - this.secondaryStreamLoader.onPlaybackUpdate(position, rate); - } } diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index 140829bb..582a6e3d 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -1,6 +1,8 @@ import { Segment, StreamWithSegments } from "./types"; import { LinkedMap } from "./linked-map"; import { SegmentLoadStatus } from "./internal-types"; +import * as Utils from "./utils"; +import { Playback } from "./playback"; export type LoadQueueItem = { segment: Segment; @@ -9,26 +11,14 @@ export type LoadQueueItem = { export class LoadQueue { private readonly queue = new LinkedMap(); - private activeStream?: StreamWithSegments; + private _activeStream?: StreamWithSegments; private isSegmentLoaded?: (segmentId: string) => boolean; private lastRequestedSegment?: Segment; - private position = 0; - private rate = 1; private segmentDuration?: number; - private updateHandlers: ((removedSegmentIds: string[]) => void)[] = []; - private highDemandBufferMargin!: number; - private httpBufferMargin!: number; - private p2pBufferMargin!: number; - - constructor( - private readonly settings: { - highDemandBufferLength: number; - httpBufferLength: number; - p2pBufferLength: number; - } - ) { - this.updateBufferMargins(); - } + private updateHandlers: ((removedSegmentIds?: string[]) => void)[] = []; + private prevUpdatePosition?: number; + + constructor(private readonly playback: Playback) {} *items() { for (const [, item] of this.queue.entries()) { @@ -44,25 +34,30 @@ export class LoadQueue { updateIfStreamChanged(segment: Segment, stream: StreamWithSegments) { const segmentsToAbortIds: string[] = []; - if (this.activeStream !== stream) { - this.activeStream = stream; + if (this._activeStream !== stream) { + this._activeStream = stream; this.queue.forEach(([segmentId]) => segmentsToAbortIds.push(segmentId)); this.queue.clear(); } this.lastRequestedSegment = segment; - this.addNewSegmentsToQueue(); + const newSegments = this.addNewSegmentsToQueue(); + + if (newSegments?.length) { + this.updateHandlers.forEach((handler) => handler()); + } } - playbackUpdate(position: number, rate: number) { + playbackUpdate() { + const { position, rate } = this.playback; const avgSegmentDuration = this.getAvgSegmentDuration(); - const isRateChanged = this.rate === undefined || rate !== this.rate; + const isRateChanged = + this.playback.rate === undefined || rate !== this.playback.rate; const isPositionSignificantlyChanged = - this.position === undefined || - Math.abs(position - this.position) / avgSegmentDuration > 0.5; + this.prevUpdatePosition === undefined || + Math.abs(position - this.prevUpdatePosition) / avgSegmentDuration >= 0.5; + if (!isRateChanged && !isPositionSignificantlyChanged) return; - if (isRateChanged) this.rate = rate; - if (isPositionSignificantlyChanged) this.position = position; - this.updateBufferMargins(); + this.prevUpdatePosition = this.playback.position; const { removedSegmentIds, statusChangedSegmentIds } = this.clearNotActualSegmentsUpdateStatuses(); @@ -82,6 +77,15 @@ export class LoadQueue { const newSegmentIds: string[] = []; let prevSegmentId: string | undefined; + + const nextToLastRequested = this.activeStream.segments.getNextTo( + this.lastRequestedSegment.localId + )?.[1]; + const nextSegmentStatuses = + nextToLastRequested && + Utils.getSegmentLoadStatuses(nextToLastRequested, this.playback); + + let i = 0; for (const [segmentId, segment] of this.activeStream.segments.entries( this.lastRequestedSegment.localId )) { @@ -90,14 +94,20 @@ export class LoadQueue { prevSegmentId = segmentId; continue; } - const statuses = this.getSegmentStatuses(segment); - if (!statuses) break; + const statuses = Utils.getSegmentLoadStatuses(segment, this.playback); + if (!statuses && !(i === 0 && nextSegmentStatuses)) { + break; + } - const info = { segment, statuses }; - if (prevSegmentId) this.queue.addAfter(prevSegmentId, segmentId, info); - else this.queue.addToStart(segmentId, info); + const item: LoadQueueItem = { + segment, + statuses: statuses ?? new Set(["high-demand"]), + }; + if (prevSegmentId) this.queue.addAfter(prevSegmentId, segmentId, item); + else this.queue.addToStart(segmentId, item); newSegmentIds.push(segmentId); prevSegmentId = segmentId; + i++; } return newSegmentIds; @@ -107,11 +117,15 @@ export class LoadQueue { const removedSegmentIds: string[] = []; const statusChangedSegmentIds: string[] = []; for (const [segmentId, item] of this.queue.entries()) { - const statuses = this.getSegmentStatuses(item.segment); + const statuses = Utils.getSegmentLoadStatuses( + item.segment, + this.playback + ); + if (!statuses) { removedSegmentIds.push(segmentId); this.queue.delete(segmentId); - } else if (areSetsEqual(item.statuses, statuses)) { + } else if (!areSetsEqual(item.statuses, statuses)) { item.statuses = statuses; statusChangedSegmentIds.push(segmentId); } @@ -119,43 +133,11 @@ export class LoadQueue { return { removedSegmentIds, statusChangedSegmentIds }; } - private updateBufferMargins() { - if (this.position === undefined || this.rate === undefined) return; - const { highDemandBufferLength, p2pBufferLength, httpBufferLength } = - this.settings; - - this.highDemandBufferMargin = - this.position + highDemandBufferLength * this.rate; - this.httpBufferMargin = this.position + httpBufferLength * this.rate; - this.p2pBufferMargin = this.position + p2pBufferLength * this.rate; - } - - private getSegmentStatuses(segment: Segment) { - const { - highDemandBufferMargin, - httpBufferMargin, - p2pBufferMargin, - position, - } = this; - const { startTime } = segment; - const statuses = new Set(); - if (startTime >= position && startTime < highDemandBufferMargin) { - statuses.add("high-demand"); - } - if (startTime >= position && startTime < httpBufferMargin) { - statuses.add("http-downloadable"); - } - if (startTime >= position && startTime < p2pBufferMargin) { - statuses.add("p2p-downloadable"); - } - if (statuses.size) return statuses; - } - removeLoadedSegment(segmentId: string) { this.queue.delete(segmentId); } - subscribeToUpdate(handler: (removedSegmentIds: string[]) => void) { + subscribeToUpdate(handler: (removedSegmentIds?: string[]) => void) { this.updateHandlers.push(handler); } @@ -176,6 +158,10 @@ export class LoadQueue { get length() { return this.queue.size; } + + get activeStream() { + return this._activeStream; + } } function areSetsEqual(set1: Set, set2: Set): boolean { @@ -183,6 +169,9 @@ function areSetsEqual(set1: Set, set2: Set): boolean { for (const item of set1) { if (!set2.has(item)) return false; } + for (const item of set2) { + if (!set1.has(item)) return false; + } return true; } diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/loader.ts index 66c3d94f..b1cb4e02 100644 --- a/packages/p2p-media-loader-core/src/loader.ts +++ b/packages/p2p-media-loader-core/src/loader.ts @@ -2,24 +2,30 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; import { HttpLoader } from "./http-loader"; import { LoadQueue } from "./load-queue"; import { SegmentsMemoryStorage } from "./segments-storage"; +import { Settings } from "./types"; +import { Playback } from "./playback"; +import * as Utils from "./utils"; -export class Loader { +export class HybridLoader { private readonly queue: LoadQueue; private readonly httpLoader = new HttpLoader(); private readonly pluginRequests = new Map(); + private readonly segmentStorage: SegmentsMemoryStorage; + private readonly playback: Playback; - constructor( - private readonly segmentStorage: SegmentsMemoryStorage, - private readonly settings: { - highDemandBufferLength: number; - httpBufferLength: number; - p2pBufferLength: number; - simultaneousHttpDownloads: number; - } - ) { - this.queue = new LoadQueue(this.settings); - this.queue.subscribeToUpdate(this.onQueueChanged.bind(this)); + constructor(private readonly settings: Settings) { + this.segmentStorage = new SegmentsMemoryStorage(this.settings); + this.playback = new Playback(this.settings); + this.queue = new LoadQueue(this.playback); + this.queue.subscribeToUpdate(this.onQueueUpdated.bind(this)); this.queue.setIsSegmentLoadedPredicate(this.isSegmentLoaded.bind(this)); + this.segmentStorage.setIsSegmentLockedPredicate((segment) => { + const stream = this.queue.activeStream; + return !!( + stream?.segments.has(segment.localId) && + Utils.getSegmentLoadStatuses(segment, this.playback) + ); + }); } async loadSegment( @@ -31,7 +37,7 @@ export class Loader { if (storageData) { return { data: storageData, - bandwidth: 99999999, + bandwidth: 9999999999, }; } const request = this.createPluginSegmentRequest(segment); @@ -44,8 +50,8 @@ export class Loader { private processQueue() { const { simultaneousHttpDownloads } = this.settings; - for (const { segment, statuses } of this.queue.items()) { + if (this.httpLoader.isLoading(segment.localId)) continue; if (statuses.has("high-demand")) { if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { void this.loadSegmentThroughHttp(segment); @@ -63,6 +69,7 @@ export class Loader { private async loadSegmentThroughHttp(segment: Segment) { const data = await this.httpLoader.load(segment); this.segmentStorage.storeSegment(segment, data); + this.queue.removeLoadedSegment(segment.localId); const request = this.pluginRequests.get(segment.localId); if (request) { request.onSuccess({ @@ -70,7 +77,7 @@ export class Loader { data, }); } - this.queue.removeLoadedSegment(segment.localId); + this.pluginRequests.delete(segment.localId); } private abortLastHttpLoadingAfter(segmentId: string) { @@ -83,12 +90,19 @@ export class Loader { } } - onPlaybackUpdate(position: number, rate: number) { - this.queue.playbackUpdate(position, rate); + updatePlayback(position: number, rate?: number) { + this.playback.position = position; + if (rate !== undefined) this.playback.rate = rate; + this.queue.playbackUpdate(); } - private onQueueChanged(removedSegmentIds: string[]) { - removedSegmentIds.forEach((id) => this.httpLoader.abort(id)); + private onQueueUpdated(removedSegmentIds?: string[]) { + removedSegmentIds?.forEach((id) => { + this.httpLoader.abort(id); + const request = this.pluginRequests.get(id); + if (request) request.onError("aborted"); + this.pluginRequests.delete(id); + }); this.processQueue(); } @@ -105,7 +119,9 @@ export class Loader { }); const request: Request = { responsePromise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion onSuccess: onSuccess!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion onError: onError!, }; diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts new file mode 100644 index 00000000..132f97a2 --- /dev/null +++ b/packages/p2p-media-loader-core/src/playback.ts @@ -0,0 +1,56 @@ +export class Playback { + private _position = 0; + private _rate = 1; + private _highDemandMargin = 0; + private _httpMargin = 0; + private _p2pMargin = 0; + + constructor( + private readonly settings: { + highDemandBufferLength: number; + httpBufferLength: number; + p2pBufferLength: number; + } + ) { + this.updateMargins(); + } + + set position(value: number) { + if (this._position === value) return; + this._position = value; + this.updateMargins(); + } + + get position() { + return this._position; + } + + set rate(value: number) { + if (this._rate === value) return; + this._rate = value; + this.updateMargins(); + } + + get rate() { + return this._rate; + } + + private updateMargins() { + if (this._position === undefined || this._rate === undefined) return; + + const { highDemandBufferLength, httpBufferLength, p2pBufferLength } = + this.settings; + this._highDemandMargin = + this._position + highDemandBufferLength * this._rate; + this._httpMargin = this._position + httpBufferLength * this._rate; + this._p2pMargin = this._position + p2pBufferLength * this._rate; + } + + get margins() { + return { + highDemand: this._highDemandMargin, + http: this._httpMargin, + p2p: this._p2pMargin, + }; + } +} diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 503b09c7..5073977d 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -1,21 +1,23 @@ -import { Playback } from "./playback"; import { Segment } from "./types"; -import * as Utils from "./utils"; export class SegmentsMemoryStorage { private cache = new Map< string, { segment: Segment; data: ArrayBuffer; lastAccessed: number } >(); + private isSegmentLockedPredicate?: (segment: Segment) => boolean; constructor( - private readonly playback: Playback, private settings: { cachedSegmentExpiration: number; cachedSegmentsCount: number; } ) {} + setIsSegmentLockedPredicate(predicate: (segment: Segment) => boolean) { + this.isSegmentLockedPredicate = predicate; + } + storeSegment(segment: Segment, data: ArrayBuffer) { this.cache.set(segment.localId, { segment, @@ -36,11 +38,6 @@ export class SegmentsMemoryStorage { return this.cache.has(segmentId); } - private isSegmentLocked(segment: Segment) { - const statuses = Utils.getSegmentLoadStatuses(segment, this.playback); - return !!statuses; - } - async clean(): Promise { const segmentsToDelete: string[] = []; const remainingSegments: { @@ -66,7 +63,7 @@ export class SegmentsMemoryStorage { remainingSegments.sort((a, b) => a.lastAccessed - b.lastAccessed); for (const cachedSegment of remainingSegments) { - if (this.isSegmentLocked(cachedSegment.segment)) { + if (this.isSegmentLockedPredicate?.(cachedSegment.segment)) { segmentsToDelete.push(cachedSegment.segment.localId); countOverhead--; if (countOverhead === 0) break; diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 5bca44ef..1217b613 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -35,3 +35,12 @@ export type SegmentResponse = { data: ArrayBuffer; bandwidth: number; }; + +export type Settings = { + highDemandBufferLength: number; + httpBufferLength: number; + p2pBufferLength: number; + simultaneousHttpDownloads: number; + cachedSegmentExpiration: number; + cachedSegmentsCount: number; +}; diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index b7066ff6..d4d05d7b 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -1,6 +1,6 @@ import { Segment, Stream, StreamWithSegments } from "./index"; -import { Playback } from "./playback"; import { SegmentLoadStatus } from "./internal-types"; +import { Playback } from "./playback"; export function getStreamExternalId( stream: Stream, @@ -19,3 +19,33 @@ export function getSegmentFromStreamsMap( if (segment) return { segment, stream }; } } + +export function getSegmentLoadStatuses(segment: Segment, playback: Playback) { + const { position } = playback; + const { highDemand, http, p2p } = playback.margins; + const { startTime, endTime } = segment; + + const statuses = new Set(); + const isValueBetween = (value: number, from: number, to: number) => + value >= from && value < to; + + if ( + isValueBetween(startTime, position, highDemand) || + isValueBetween(endTime, position, highDemand) + ) { + statuses.add("high-demand"); + } + if ( + isValueBetween(startTime, position, http) || + isValueBetween(endTime, position, http) + ) { + statuses.add("http-downloadable"); + } + if ( + isValueBetween(startTime, position, p2p) || + isValueBetween(endTime, position, p2p) + ) { + statuses.add("p2p-downloadable"); + } + if (statuses.size) return statuses; +} diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index b45f16b3..af8d2f38 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -81,7 +81,12 @@ export class FragmentLoaderBase implements Loader { }); stats.total = stats.loaded = loadedBytes; - callbacks.onSuccess(this.response, this.stats, context, this.response); + callbacks.onSuccess( + { data: this.response.data, url: context.url }, + this.stats, + context, + this.response + ); } private handleError(thrownError: unknown) { @@ -98,7 +103,7 @@ export class FragmentLoaderBase implements Loader { } private abortInternal() { - if (!this.response?.ok && this.segmentId) { + if (!this.response && this.segmentId) { this.core.abortSegmentLoading(this.segmentId); this.stats.aborted = true; } From f6713b702a2c67180fe16d9054c3df8993048e5c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 4 Sep 2023 16:23:28 +0300 Subject: [PATCH 013/127] Rename hybrid loader file. --- packages/p2p-media-loader-core/src/core.ts | 2 +- .../p2p-media-loader-core/src/{loader.ts => hybrid-loader.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/p2p-media-loader-core/src/{loader.ts => hybrid-loader.ts} (100%) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 59364c1e..99c71648 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,4 +1,4 @@ -import { HybridLoader } from "./loader"; +import { HybridLoader } from "./hybrid-loader"; import { Stream, StreamWithSegments, diff --git a/packages/p2p-media-loader-core/src/loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts similarity index 100% rename from packages/p2p-media-loader-core/src/loader.ts rename to packages/p2p-media-loader-core/src/hybrid-loader.ts From ff79e374f224a7bbd19cff091c2699f2b4e2c0ac Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 4 Sep 2023 17:19:21 +0300 Subject: [PATCH 014/127] Initialize playback on first segment request by plugin. --- .../src/hybrid-loader.ts | 3 +++ .../p2p-media-loader-core/src/load-queue.ts | 1 - .../p2p-media-loader-core/src/playback.ts | 22 +++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index b1cb4e02..4a97611b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -32,6 +32,9 @@ export class HybridLoader { segment: Segment, stream: StreamWithSegments ): Promise { + if (!this.playback.isInitialized()) { + this.playback.position = segment.startTime; + } this.queue.updateIfStreamChanged(segment, stream); const storageData = this.segmentStorage.getSegment(segment.localId); if (storageData) { diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index 582a6e3d..b640ecb2 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -109,7 +109,6 @@ export class LoadQueue { prevSegmentId = segmentId; i++; } - return newSegmentIds; } diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts index 132f97a2..cf009b50 100644 --- a/packages/p2p-media-loader-core/src/playback.ts +++ b/packages/p2p-media-loader-core/src/playback.ts @@ -1,9 +1,9 @@ export class Playback { - private _position = 0; + private _position?: number; private _rate = 1; - private _highDemandMargin = 0; - private _httpMargin = 0; - private _p2pMargin = 0; + private _highDemandMargin?: number; + private _httpMargin?: number; + private _p2pMargin?: number; constructor( private readonly settings: { @@ -15,6 +15,10 @@ export class Playback { this.updateMargins(); } + isInitialized() { + return this._position !== undefined; + } + set position(value: number) { if (this._position === value) return; this._position = value; @@ -22,7 +26,7 @@ export class Playback { } get position() { - return this._position; + return this._position ?? 0; } set rate(value: number) { @@ -32,7 +36,7 @@ export class Playback { } get rate() { - return this._rate; + return this._rate ?? 0; } private updateMargins() { @@ -48,9 +52,9 @@ export class Playback { get margins() { return { - highDemand: this._highDemandMargin, - http: this._httpMargin, - p2p: this._p2pMargin, + highDemand: this._highDemandMargin ?? 0, + http: this._httpMargin ?? 0, + p2p: this._p2pMargin ?? 0, }; } } From fe3ea0e162004cd2ceec57e8db9e8c3a0277e47b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 4 Sep 2023 18:00:30 +0300 Subject: [PATCH 015/127] Make storage async. Add core destroy logic. --- packages/p2p-media-loader-core/src/core.ts | 4 ++++ .../p2p-media-loader-core/src/http-loader.ts | 7 ++++++ .../src/hybrid-loader.ts | 23 +++++++++++++++++-- .../p2p-media-loader-core/src/load-queue.ts | 8 +++++++ .../p2p-media-loader-core/src/playback.ts | 8 +++++++ .../src/segments-storage.ts | 12 ++++++---- 6 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 99c71648..feefc5f4 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -87,6 +87,10 @@ export class Core { destroy(): void { this.streams.clear(); + this.position = 0; + this.mainStreamLoader.clear(); + this.secondaryStreamLoader.clear(); + this.manifestResponseUrl = undefined; } private identifySegment(segmentId: string) { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 27eac480..de5dec5f 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -62,4 +62,11 @@ export class HttpLoader { getRequest(segmentId: string) { return this.requests.get(segmentId)?.promise; } + + abortAll() { + for (const request of this.requests.values()) { + request.abortController.abort(); + } + this.requests.clear(); + } } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 4a97611b..62accf3d 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -11,6 +11,7 @@ export class HybridLoader { private readonly httpLoader = new HttpLoader(); private readonly pluginRequests = new Map(); private readonly segmentStorage: SegmentsMemoryStorage; + private storageCleanUpIntervalId?: number; private readonly playback: Playback; constructor(private readonly settings: Settings) { @@ -26,6 +27,11 @@ export class HybridLoader { Utils.getSegmentLoadStatuses(segment, this.playback) ); }); + + this.storageCleanUpIntervalId = setInterval( + () => this.segmentStorage.clear(), + 1000 + ); } async loadSegment( @@ -36,7 +42,7 @@ export class HybridLoader { this.playback.position = segment.startTime; } this.queue.updateIfStreamChanged(segment, stream); - const storageData = this.segmentStorage.getSegment(segment.localId); + const storageData = await this.segmentStorage.getSegment(segment.localId); if (storageData) { return { data: storageData, @@ -71,7 +77,7 @@ export class HybridLoader { private async loadSegmentThroughHttp(segment: Segment) { const data = await this.httpLoader.load(segment); - this.segmentStorage.storeSegment(segment, data); + void this.segmentStorage.storeSegment(segment, data); this.queue.removeLoadedSegment(segment.localId); const request = this.pluginRequests.get(segment.localId); if (request) { @@ -131,6 +137,19 @@ export class HybridLoader { this.pluginRequests.set(segment.localId, request); return request; } + + clear() { + this.queue.clear(); + clearInterval(this.storageCleanUpIntervalId); + this.storageCleanUpIntervalId = undefined; + void this.segmentStorage.clear(); + this.httpLoader.abortAll(); + for (const request of this.pluginRequests.values()) { + request.onError("Aborted"); + } + this.pluginRequests.clear(); + this.playback.clear(); + } } type Request = { diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index b640ecb2..88f6ba62 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -161,6 +161,14 @@ export class LoadQueue { get activeStream() { return this._activeStream; } + + clear() { + this.queue.clear(); + this._activeStream = undefined; + this.lastRequestedSegment = undefined; + this.segmentDuration = undefined; + this.prevUpdatePosition = undefined; + } } function areSetsEqual(set1: Set, set2: Set): boolean { diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts index cf009b50..93a5f510 100644 --- a/packages/p2p-media-loader-core/src/playback.ts +++ b/packages/p2p-media-loader-core/src/playback.ts @@ -57,4 +57,12 @@ export class Playback { p2p: this._p2pMargin ?? 0, }; } + + clear() { + this._rate = 1; + this._position = undefined; + this._highDemandMargin = undefined; + this._httpMargin = undefined; + this._p2pMargin = undefined; + } } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 5073977d..d75268f3 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -18,7 +18,7 @@ export class SegmentsMemoryStorage { this.isSegmentLockedPredicate = predicate; } - storeSegment(segment: Segment, data: ArrayBuffer) { + async storeSegment(segment: Segment, data: ArrayBuffer) { this.cache.set(segment.localId, { segment, data, @@ -26,7 +26,7 @@ export class SegmentsMemoryStorage { }); } - getSegment(segmentId: string): ArrayBuffer | undefined { + async getSegment(segmentId: string): Promise { const cacheItem = this.cache.get(segmentId); if (cacheItem === undefined) return undefined; @@ -38,7 +38,7 @@ export class SegmentsMemoryStorage { return this.cache.has(segmentId); } - async clean(): Promise { + async clear(): Promise { const segmentsToDelete: string[] = []; const remainingSegments: { lastAccessed: number; @@ -50,7 +50,9 @@ export class SegmentsMemoryStorage { for (const [segmentId, { lastAccessed, segment }] of this.cache.entries()) { if (now - lastAccessed > this.settings.cachedSegmentExpiration) { - segmentsToDelete.push(segmentId); + if (!this.isSegmentLockedPredicate?.(segment)) { + segmentsToDelete.push(segmentId); + } } else { remainingSegments.push({ segment, lastAccessed }); } @@ -63,7 +65,7 @@ export class SegmentsMemoryStorage { remainingSegments.sort((a, b) => a.lastAccessed - b.lastAccessed); for (const cachedSegment of remainingSegments) { - if (this.isSegmentLockedPredicate?.(cachedSegment.segment)) { + if (!this.isSegmentLockedPredicate?.(cachedSegment.segment)) { segmentsToDelete.push(cachedSegment.segment.localId); countOverhead--; if (countOverhead === 0) break; From e13d3589c28239276f7d95ebac3cfe2cd3894830 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 5 Sep 2023 10:55:49 +0300 Subject: [PATCH 016/127] Use StreamWithReadonlySegments type in plugins. --- packages/p2p-media-loader-core/src/linked-map.ts | 9 +++++++++ packages/p2p-media-loader-core/src/types.ts | 5 ++++- packages/p2p-media-loader-shaka/src/loading-handler.ts | 5 ++--- packages/p2p-media-loader-shaka/src/segment-manager.ts | 6 +++--- packages/p2p-media-loader-shaka/src/stream-utils.ts | 8 ++++++-- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts index 7ce9a801..d0e78bac 100644 --- a/packages/p2p-media-loader-core/src/linked-map.ts +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -110,6 +110,15 @@ export class LinkedMap { } } + *values() { + let value = this._first; + if (value === undefined) return; + while (value?.item !== undefined) { + yield value.item[1]; + value = value.next; + } + } + *keys(): Generator { let value = this._first; if (value === undefined) return; diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 1217b613..38aefe61 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -21,7 +21,7 @@ export type Stream = { export type ReadonlyLinkedMap = Pick< LinkedMap, - "has" | "keys" + "has" | "keys" | "values" | "size" >; export type StreamWithSegments< @@ -31,6 +31,9 @@ export type StreamWithSegments< readonly segments: TMap; }; +export type StreamWithReadonlySegments = + StreamWithSegments>; + export type SegmentResponse = { data: ArrayBuffer; bandwidth: number; diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index 5918c612..8b2ef37c 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -85,12 +85,11 @@ export class LoadingHandler implements LoadingHandlerInterface { const loadSegment = async (): Promise => { const response = await this.core.loadSegment(segmentId); - const { data, url, status, bandwidth } = response; + const { data, bandwidth } = response; return { data, headers: {}, - status, - uri: url, + uri: segmentUrl, originalUri: segmentUrl, timeMs: getLoadingDurationBasedOnBandwidth(bandwidth, data.byteLength), }; diff --git a/packages/p2p-media-loader-shaka/src/segment-manager.ts b/packages/p2p-media-loader-shaka/src/segment-manager.ts index cca76503..b99258d8 100644 --- a/packages/p2p-media-loader-shaka/src/segment-manager.ts +++ b/packages/p2p-media-loader-shaka/src/segment-manager.ts @@ -2,7 +2,7 @@ import * as Utils from "./stream-utils"; import { HookedStream, StreamInfo, Stream } from "./types"; import { Core, - StreamWithSegments, + StreamWithReadonlySegments, Segment, StreamType, } from "p2p-media-loader-core"; @@ -57,7 +57,7 @@ export class SegmentManager { } private processDashSegmentReferences( - managerStream: StreamWithSegments, + managerStream: StreamWithReadonlySegments, segmentReferences: shaka.media.SegmentReference[] ) { const staleSegmentsIds = new Set(managerStream.segments.keys()); @@ -83,7 +83,7 @@ export class SegmentManager { } private processHlsSegmentReferences( - managerStream: StreamWithSegments, + managerStream: StreamWithReadonlySegments, segmentReferences: shaka.media.SegmentReference[] ) { const segments = [...managerStream.segments.values()]; diff --git a/packages/p2p-media-loader-shaka/src/stream-utils.ts b/packages/p2p-media-loader-shaka/src/stream-utils.ts index 08043144..cc5a2897 100644 --- a/packages/p2p-media-loader-shaka/src/stream-utils.ts +++ b/packages/p2p-media-loader-shaka/src/stream-utils.ts @@ -1,5 +1,9 @@ import { HookedStream, Stream } from "./types"; -import { Segment, StreamWithSegments, ByteRange } from "p2p-media-loader-core"; +import { + Segment, + StreamWithReadonlySegments, + ByteRange, +} from "p2p-media-loader-core"; export function createSegment({ segmentReference, @@ -72,7 +76,7 @@ export function getSegmentInfoFromReference( } export function getStreamLastMediaSequence( - stream: StreamWithSegments + stream: StreamWithReadonlySegments ): number | undefined { const { shakaStream } = stream; const map = shakaStream.mediaSequenceTimeMap; From 7c4197d77b27fa0d8c5174af574bbad16e8e72f7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 5 Sep 2023 11:31:14 +0300 Subject: [PATCH 017/127] Add bandwidth approximator. --- .../src/bandwidth-approximator.ts | 58 +++++++++++++++++++ packages/p2p-media-loader-core/src/core.ts | 14 ++++- .../src/hybrid-loader.ts | 11 +++- 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/bandwidth-approximator.ts diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts new file mode 100644 index 00000000..7e7381d6 --- /dev/null +++ b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts @@ -0,0 +1,58 @@ +const SMOOTH_INTERVAL = 15 * 1000; +const MEASURE_INTERVAL = 60 * 1000; + +type NumberWithTime = { + readonly value: number; + readonly timeStamp: number; +}; + +export class BandwidthApproximator { + private lastBytes: NumberWithTime[] = []; + private currentBytesSum = 0; + private lastBandwidth: NumberWithTime[] = []; + + addBytes(bytes: number): void { + const timeStamp = performance.now(); + this.lastBytes.push({ value: bytes, timeStamp }); + this.currentBytesSum += bytes; + + while (timeStamp - this.lastBytes[0].timeStamp > SMOOTH_INTERVAL) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.currentBytesSum -= this.lastBytes.shift()!.value; + } + + const interval = Math.min(SMOOTH_INTERVAL, timeStamp); + this.lastBandwidth.push({ + value: (this.currentBytesSum * 8000) / interval, + timeStamp, + }); + } + + // in bits per seconds + getBandwidth(): number { + const timeStamp = performance.now(); + while ( + this.lastBandwidth.length !== 0 && + timeStamp - this.lastBandwidth[0].timeStamp > MEASURE_INTERVAL + ) { + this.lastBandwidth.shift(); + } + + let maxBandwidth = 0; + for (const bandwidth of this.lastBandwidth) { + if (bandwidth.value > maxBandwidth) { + maxBandwidth = bandwidth.value; + } + } + + return maxBandwidth; + } + + getSmoothInterval(): number { + return SMOOTH_INTERVAL; + } + + getMeasureInterval(): number { + return MEASURE_INTERVAL; + } +} diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index feefc5f4..8b223868 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -8,21 +8,29 @@ import { } from "./types"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; +import { BandwidthApproximator } from "./bandwidth-approximator"; export class Core { private manifestResponseUrl?: string; private readonly streams = new Map>(); private readonly settings: Settings = { simultaneousHttpDownloads: 3, - highDemandBufferLength: 20, + highDemandBufferLength: 25, httpBufferLength: 60, p2pBufferLength: 60, cachedSegmentExpiration: 120, cachedSegmentsCount: 50, }; private position = 0; - private readonly mainStreamLoader = new HybridLoader(this.settings); - private readonly secondaryStreamLoader = new HybridLoader(this.settings); + private readonly bandwidthApproximator = new BandwidthApproximator(); + private readonly mainStreamLoader = new HybridLoader( + this.settings, + this.bandwidthApproximator + ); + private readonly secondaryStreamLoader = new HybridLoader( + this.settings, + this.bandwidthApproximator + ); setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 62accf3d..b3599231 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -5,6 +5,7 @@ import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { Playback } from "./playback"; import * as Utils from "./utils"; +import { BandwidthApproximator } from "./bandwidth-approximator"; export class HybridLoader { private readonly queue: LoadQueue; @@ -14,7 +15,10 @@ export class HybridLoader { private storageCleanUpIntervalId?: number; private readonly playback: Playback; - constructor(private readonly settings: Settings) { + constructor( + private readonly settings: Settings, + private readonly bandwidthApproximator: BandwidthApproximator + ) { this.segmentStorage = new SegmentsMemoryStorage(this.settings); this.playback = new Playback(this.settings); this.queue = new LoadQueue(this.playback); @@ -46,7 +50,7 @@ export class HybridLoader { if (storageData) { return { data: storageData, - bandwidth: 9999999999, + bandwidth: this.bandwidthApproximator.getBandwidth(), }; } const request = this.createPluginSegmentRequest(segment); @@ -77,12 +81,13 @@ export class HybridLoader { private async loadSegmentThroughHttp(segment: Segment) { const data = await this.httpLoader.load(segment); + this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); this.queue.removeLoadedSegment(segment.localId); const request = this.pluginRequests.get(segment.localId); if (request) { request.onSuccess({ - bandwidth: 9999999999, + bandwidth: this.bandwidthApproximator.getBandwidth(), data, }); } From 72e426ac0607d968834696e22d003e552820bd88 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 5 Sep 2023 12:27:43 +0300 Subject: [PATCH 018/127] Refactor load queue code. --- .../src/hybrid-loader.ts | 4 +-- .../p2p-media-loader-core/src/linked-map.ts | 15 --------- .../p2p-media-loader-core/src/load-queue.ts | 33 +++++++++---------- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index b3599231..f523c2e5 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -45,7 +45,7 @@ export class HybridLoader { if (!this.playback.isInitialized()) { this.playback.position = segment.startTime; } - this.queue.updateIfStreamChanged(segment, stream); + this.queue.updateOnSegmentRequest(segment, stream); const storageData = await this.segmentStorage.getSegment(segment.localId); if (storageData) { return { @@ -107,7 +107,7 @@ export class HybridLoader { updatePlayback(position: number, rate?: number) { this.playback.position = position; if (rate !== undefined) this.playback.rate = rate; - this.queue.playbackUpdate(); + this.queue.updateOnPlaybackChange(); } private onQueueUpdated(removedSegmentIds?: string[]) { diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts index d0e78bac..ca2b88fa 100644 --- a/packages/p2p-media-loader-core/src/linked-map.ts +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -51,13 +51,6 @@ export class LinkedMap { this.map.set(key, item); } - addListToStart(items: [K, V][]) { - for (let i = items.length - 1; i >= 0; i--) { - const [key, value] = items[i]; - this.addToStart(key, value); - } - } - addAfter(prevKey: K, key: K, value: V) { const prev = this.map.get(prevKey); if (!prev) return; @@ -134,14 +127,6 @@ export class LinkedMap { } } - filter(callback: (item: [K, V]) => boolean) { - const list: [K, V][] = []; - for (const value of this.entries()) { - if (callback(value)) list.push(value); - } - return list; - } - getNextTo(key: K): [K, V] | undefined { return this.map.get(key)?.next?.item; } diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts index 88f6ba62..f66531b4 100644 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ b/packages/p2p-media-loader-core/src/load-queue.ts @@ -32,7 +32,7 @@ export class LoadQueue { } } - updateIfStreamChanged(segment: Segment, stream: StreamWithSegments) { + updateOnSegmentRequest(segment: Segment, stream: StreamWithSegments) { const segmentsToAbortIds: string[] = []; if (this._activeStream !== stream) { this._activeStream = stream; @@ -40,53 +40,55 @@ export class LoadQueue { this.queue.clear(); } this.lastRequestedSegment = segment; - const newSegments = this.addNewSegmentsToQueue(); + const addedSegments = this.addActualSegmentsToQueue(); - if (newSegments?.length) { + if (addedSegments?.length) { this.updateHandlers.forEach((handler) => handler()); } } - playbackUpdate() { + updateOnPlaybackChange() { const { position, rate } = this.playback; const avgSegmentDuration = this.getAvgSegmentDuration(); const isRateChanged = this.playback.rate === undefined || rate !== this.playback.rate; const isPositionSignificantlyChanged = this.prevUpdatePosition === undefined || - Math.abs(position - this.prevUpdatePosition) / avgSegmentDuration >= 0.5; + Math.abs(position - this.prevUpdatePosition) / avgSegmentDuration >= 0.45; if (!isRateChanged && !isPositionSignificantlyChanged) return; - this.prevUpdatePosition = this.playback.position; + this.prevUpdatePosition = position; const { removedSegmentIds, statusChangedSegmentIds } = - this.clearNotActualSegmentsUpdateStatuses(); - const newSegmentIds = this.addNewSegmentsToQueue(); + this.removeNotActualSegmentsAndUpdateStatuses(); + const addedSegments = this.addActualSegmentsToQueue(); if ( removedSegmentIds.length || statusChangedSegmentIds.length || - newSegmentIds?.length + addedSegments?.length ) { this.updateHandlers.forEach((handler) => handler(removedSegmentIds)); } } - private addNewSegmentsToQueue() { - if (!this.activeStream || !this.lastRequestedSegment) return; + private addActualSegmentsToQueue() { + if (!this._activeStream || !this.lastRequestedSegment) return; const newSegmentIds: string[] = []; let prevSegmentId: string | undefined; - const nextToLastRequested = this.activeStream.segments.getNextTo( + const nextToLastRequested = this._activeStream.segments.getNextTo( this.lastRequestedSegment.localId )?.[1]; + // it's needed when the segment requested by the plugin + // (lastRequestedSegment) ends before current playhead position const nextSegmentStatuses = nextToLastRequested && Utils.getSegmentLoadStatuses(nextToLastRequested, this.playback); let i = 0; - for (const [segmentId, segment] of this.activeStream.segments.entries( + for (const [segmentId, segment] of this._activeStream.segments.entries( this.lastRequestedSegment.localId )) { if (this.isSegmentLoaded?.(segmentId)) continue; @@ -112,7 +114,7 @@ export class LoadQueue { return newSegmentIds; } - private clearNotActualSegmentsUpdateStatuses() { + private removeNotActualSegmentsAndUpdateStatuses() { const removedSegmentIds: string[] = []; const statusChangedSegmentIds: string[] = []; for (const [segmentId, item] of this.queue.entries()) { @@ -176,9 +178,6 @@ function areSetsEqual(set1: Set, set2: Set): boolean { for (const item of set1) { if (!set2.has(item)) return false; } - for (const item of set2) { - if (!set1.has(item)) return false; - } return true; } From 03a388b1ea0b7487d6b6ac2e78d51962f456ee15 Mon Sep 17 00:00:00 2001 From: i-zolotarenko <86921321+i-zolotarenko@users.noreply.github.com> Date: Thu, 7 Sep 2023 16:32:24 +0300 Subject: [PATCH 019/127] Load controlling: pure functions style. (#302) * Refactor code, use functional style. * Add shaka playback update handling. * Set playback after first segment is requested. * Remove unnecessary linked map methods. * Use is loaded callback in queue generating function instead of segment storage instance. --- packages/p2p-media-loader-core/src/core.ts | 9 +- .../p2p-media-loader-core/src/http-loader.ts | 4 + .../src/hybrid-loader.ts | 142 ++++++++----- .../src/internal-types.ts | 15 ++ .../p2p-media-loader-core/src/linked-map.ts | 52 +---- .../p2p-media-loader-core/src/load-queue.ts | 186 ------------------ .../p2p-media-loader-core/src/playback.ts | 68 ------- .../src/segments-storage.ts | 2 +- packages/p2p-media-loader-core/src/utils.ts | 120 +++++++++-- packages/p2p-media-loader-shaka/src/engine.ts | 16 +- 10 files changed, 231 insertions(+), 383 deletions(-) delete mode 100644 packages/p2p-media-loader-core/src/load-queue.ts delete mode 100644 packages/p2p-media-loader-core/src/playback.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 8b223868..55593034 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -64,13 +64,6 @@ export class Core { addSegments?.forEach((s) => stream.segments.addToEnd(s.localId, s)); removeSegmentIds?.forEach((id) => stream.segments.delete(id)); - - const firstSegment = stream.segments.first?.[1]; - if (firstSegment && firstSegment.startTime > this.position) { - this.position = firstSegment.startTime; - this.mainStreamLoader.updatePlayback(firstSegment.startTime); - this.secondaryStreamLoader.updatePlayback(firstSegment.startTime); - } } loadSegment(segmentLocalId: string): Promise { @@ -91,6 +84,8 @@ export class Core { updatePlayback(position: number, rate: number): void { this.mainStreamLoader.updatePlayback(position, rate); this.secondaryStreamLoader.updatePlayback(position, rate); + + // TODO: update playback position when the live stream is updated } destroy(): void { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index de5dec5f..258f152c 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -59,6 +59,10 @@ export class HttpLoader { return this.requests.size; } + getLoadingSegmentIds() { + return this.requests.keys(); + } + getRequest(segmentId: string) { return this.requests.get(segmentId)?.promise; } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index f523c2e5..7b10abf8 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,35 +1,35 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; import { HttpLoader } from "./http-loader"; -import { LoadQueue } from "./load-queue"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; -import { Playback } from "./playback"; -import * as Utils from "./utils"; import { BandwidthApproximator } from "./bandwidth-approximator"; +import { Playback, QueueItem } from "./internal-types"; +import * as Utils from "./utils"; export class HybridLoader { - private readonly queue: LoadQueue; private readonly httpLoader = new HttpLoader(); private readonly pluginRequests = new Map(); private readonly segmentStorage: SegmentsMemoryStorage; private storageCleanUpIntervalId?: number; - private readonly playback: Playback; + private activeStream?: Readonly; + private lastRequestedSegment?: Readonly; + private playback?: Playback; + private segmentAvgLength?: number; constructor( private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator ) { this.segmentStorage = new SegmentsMemoryStorage(this.settings); - this.playback = new Playback(this.settings); - this.queue = new LoadQueue(this.playback); - this.queue.subscribeToUpdate(this.onQueueUpdated.bind(this)); - this.queue.setIsSegmentLoadedPredicate(this.isSegmentLoaded.bind(this)); this.segmentStorage.setIsSegmentLockedPredicate((segment) => { - const stream = this.queue.activeStream; - return !!( - stream?.segments.has(segment.localId) && - Utils.getSegmentLoadStatuses(segment, this.playback) + if (!this.playback || !this.activeStream?.segments.has(segment.localId)) { + return false; + } + const bufferRanges = Utils.getLoadBufferRanges( + this.playback, + this.settings ); + return Utils.isSegmentActual(segment, bufferRanges); }); this.storageCleanUpIntervalId = setInterval( @@ -39,13 +39,19 @@ export class HybridLoader { } async loadSegment( - segment: Segment, - stream: StreamWithSegments + segment: Readonly, + stream: Readonly ): Promise { - if (!this.playback.isInitialized()) { - this.playback.position = segment.startTime; + if (!this.playback) { + this.playback = { position: segment.startTime, rate: 1 }; } - this.queue.updateOnSegmentRequest(segment, stream); + if (stream !== this.activeStream) { + this.segmentAvgLength = computeSegmentAvgLength(stream); + this.activeStream = stream; + } + this.lastRequestedSegment = segment; + this.processQueue(); + const storageData = await this.segmentStorage.getSegment(segment.localId); if (storageData) { return { @@ -57,20 +63,43 @@ export class HybridLoader { return request.responsePromise; } - abortSegment(segmentId: string) { - this.httpLoader.abort(segmentId); - } - private processQueue() { + if (!this.activeStream || !this.lastRequestedSegment || !this.playback) { + return; + } + + const { queue, queueSegmentIds } = Utils.generateQueue({ + segment: this.lastRequestedSegment, + stream: this.activeStream, + playback: this.playback, + settings: this.settings, + isSegmentLoaded: (segmentId) => this.segmentStorage.has(segmentId), + }); + + const bufferRanges = Utils.getLoadBufferRanges( + this.playback, + this.settings + ); + for (const segmentId of this.getLoadingSegmentIds()) { + const segment = this.activeStream.segments.get(segmentId); + if ( + !queueSegmentIds.has(segmentId) && + !this.pluginRequests.has(segmentId) && + !(segment && Utils.isSegmentActual(segment, bufferRanges)) + ) { + this.abortSegment(segmentId); + } + } + const { simultaneousHttpDownloads } = this.settings; - for (const { segment, statuses } of this.queue.items()) { + for (const { segment, statuses } of queue) { if (this.httpLoader.isLoading(segment.localId)) continue; if (statuses.has("high-demand")) { if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { void this.loadSegmentThroughHttp(segment); continue; } - this.abortLastHttpLoadingAfter(segment.localId); + this.abortLastHttpLoadingAfter(queue, segment.localId); if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { void this.loadSegmentThroughHttp(segment); } @@ -79,11 +108,28 @@ export class HybridLoader { } } + getLoadingSegmentIds() { + return this.httpLoader.getLoadingSegmentIds(); + } + + abortSegment(segmentId: string) { + this.httpLoader.abort(segmentId); + const request = this.pluginRequests.get(segmentId); + if (!request) return; + request.onError("Abort"); + this.pluginRequests.delete(segmentId); + } + private async loadSegmentThroughHttp(segment: Segment) { - const data = await this.httpLoader.load(segment); + let data: ArrayBuffer | undefined; + try { + data = await this.httpLoader.load(segment); + } catch (err) { + // TODO: handle abort + } + if (!data) return; this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); - this.queue.removeLoadedSegment(segment.localId); const request = this.pluginRequests.get(segment.localId); if (request) { request.onSuccess({ @@ -94,34 +140,30 @@ export class HybridLoader { this.pluginRequests.delete(segment.localId); } - private abortLastHttpLoadingAfter(segmentId: string) { - for (const { segment } of this.queue.itemsBackwards()) { + private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { + for (let i = queue.length - 1; i >= 0; i--) { + const { segment } = queue[i]; if (segment.localId === segmentId) break; if (this.httpLoader.isLoading(segment.localId)) { - this.httpLoader.abort(segment.localId); + this.abortSegment(segment.localId); break; } } } updatePlayback(position: number, rate?: number) { - this.playback.position = position; - if (rate !== undefined) this.playback.rate = rate; - this.queue.updateOnPlaybackChange(); - } + if (!this.playback) return; + const isRateChanged = rate && this.playback.rate !== rate; + const isPositionSignificantlyChanged = + this.segmentAvgLength === undefined || + Math.abs(position - this.playback.position) / this.segmentAvgLength >= + 0.45; - private onQueueUpdated(removedSegmentIds?: string[]) { - removedSegmentIds?.forEach((id) => { - this.httpLoader.abort(id); - const request = this.pluginRequests.get(id); - if (request) request.onError("aborted"); - this.pluginRequests.delete(id); - }); - this.processQueue(); - } + if (!isRateChanged && !isPositionSignificantlyChanged) return; - private isSegmentLoaded(segmentId: string): boolean { - return this.segmentStorage.hasSegment(segmentId); + if (isPositionSignificantlyChanged) this.playback.position = position; + if (isRateChanged) this.playback.rate = rate; + this.processQueue(); } private createPluginSegmentRequest(segment: Segment) { @@ -144,7 +186,6 @@ export class HybridLoader { } clear() { - this.queue.clear(); clearInterval(this.storageCleanUpIntervalId); this.storageCleanUpIntervalId = undefined; void this.segmentStorage.clear(); @@ -153,7 +194,7 @@ export class HybridLoader { request.onError("Aborted"); } this.pluginRequests.clear(); - this.playback.clear(); + this.playback = undefined; } } @@ -162,3 +203,12 @@ type Request = { onSuccess: (response: SegmentResponse) => void; onError: (reason?: unknown) => void; }; + +function computeSegmentAvgLength(stream: StreamWithSegments) { + if (!stream.segments.size) return; + let sum = 0; + for (const segment of stream.segments.values()) { + sum += segment.endTime - segment.startTime; + } + return sum / stream.segments.size; +} diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 9261a960..813c57a7 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -1,3 +1,5 @@ +import { Segment } from "./types"; + export type Playback = { position: number; rate: number; @@ -7,3 +9,16 @@ export type SegmentLoadStatus = | "high-demand" | "http-downloadable" | "p2p-downloadable"; + +export type NumberRange = { + from: number; + to: number; +}; + +export type LoadBufferRanges = { + highDemand: NumberRange; + http: NumberRange; + p2p: NumberRange; +}; + +export type QueueItem = { segment: Segment; statuses: Set }; diff --git a/packages/p2p-media-loader-core/src/linked-map.ts b/packages/p2p-media-loader-core/src/linked-map.ts index ca2b88fa..f1827daf 100644 --- a/packages/p2p-media-loader-core/src/linked-map.ts +++ b/packages/p2p-media-loader-core/src/linked-map.ts @@ -40,32 +40,6 @@ export class LinkedMap { this.map.set(key, item); } - addToStart(key: K, value: V) { - const item: LinkedObject = { item: [key, value] }; - if (this._first) { - this._first.prev = item; - item.next = this._first; - } - this._first = item; - if (!this._last) this._last = item; - this.map.set(key, item); - } - - addAfter(prevKey: K, key: K, value: V) { - const prev = this.map.get(prevKey); - if (!prev) return; - - const newItem: LinkedObject = { - item: [key, value], - prev, - next: prev.next, - }; - prev.next = newItem; - if (this._last === prev) this._last = newItem; - - this.map.set(key, newItem); - } - delete(key: K) { if (!this.map.size) return; const value = this.map.get(key); @@ -85,27 +59,9 @@ export class LinkedMap { this.map.clear(); } - *entriesBackwards(key?: K): Generator<[K, V]> { - let value = key ? this.map.get(key) : this._last; - if (value === undefined) return; - while (value?.item !== undefined) { - yield value.item; - value = value.prev; - } - } - - *entries(key?: K): Generator<[K, V]> { + *values(key?: K) { let value = key ? this.map.get(key) : this._first; if (value === undefined) return; - while (value?.item !== undefined) { - yield value.item; - value = value.next; - } - } - - *values() { - let value = this._first; - if (value === undefined) return; while (value?.item !== undefined) { yield value.item[1]; value = value.next; @@ -121,12 +77,6 @@ export class LinkedMap { } } - forEach(callback: (item: [K, V]) => void) { - for (const value of this.entries()) { - callback(value); - } - } - getNextTo(key: K): [K, V] | undefined { return this.map.get(key)?.next?.item; } diff --git a/packages/p2p-media-loader-core/src/load-queue.ts b/packages/p2p-media-loader-core/src/load-queue.ts deleted file mode 100644 index f66531b4..00000000 --- a/packages/p2p-media-loader-core/src/load-queue.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Segment, StreamWithSegments } from "./types"; -import { LinkedMap } from "./linked-map"; -import { SegmentLoadStatus } from "./internal-types"; -import * as Utils from "./utils"; -import { Playback } from "./playback"; - -export type LoadQueueItem = { - segment: Segment; - statuses: Set; -}; - -export class LoadQueue { - private readonly queue = new LinkedMap(); - private _activeStream?: StreamWithSegments; - private isSegmentLoaded?: (segmentId: string) => boolean; - private lastRequestedSegment?: Segment; - private segmentDuration?: number; - private updateHandlers: ((removedSegmentIds?: string[]) => void)[] = []; - private prevUpdatePosition?: number; - - constructor(private readonly playback: Playback) {} - - *items() { - for (const [, item] of this.queue.entries()) { - yield item; - } - } - - *itemsBackwards() { - for (const [, item] of this.queue.entriesBackwards()) { - yield item; - } - } - - updateOnSegmentRequest(segment: Segment, stream: StreamWithSegments) { - const segmentsToAbortIds: string[] = []; - if (this._activeStream !== stream) { - this._activeStream = stream; - this.queue.forEach(([segmentId]) => segmentsToAbortIds.push(segmentId)); - this.queue.clear(); - } - this.lastRequestedSegment = segment; - const addedSegments = this.addActualSegmentsToQueue(); - - if (addedSegments?.length) { - this.updateHandlers.forEach((handler) => handler()); - } - } - - updateOnPlaybackChange() { - const { position, rate } = this.playback; - const avgSegmentDuration = this.getAvgSegmentDuration(); - const isRateChanged = - this.playback.rate === undefined || rate !== this.playback.rate; - const isPositionSignificantlyChanged = - this.prevUpdatePosition === undefined || - Math.abs(position - this.prevUpdatePosition) / avgSegmentDuration >= 0.45; - - if (!isRateChanged && !isPositionSignificantlyChanged) return; - this.prevUpdatePosition = position; - - const { removedSegmentIds, statusChangedSegmentIds } = - this.removeNotActualSegmentsAndUpdateStatuses(); - const addedSegments = this.addActualSegmentsToQueue(); - - if ( - removedSegmentIds.length || - statusChangedSegmentIds.length || - addedSegments?.length - ) { - this.updateHandlers.forEach((handler) => handler(removedSegmentIds)); - } - } - - private addActualSegmentsToQueue() { - if (!this._activeStream || !this.lastRequestedSegment) return; - - const newSegmentIds: string[] = []; - let prevSegmentId: string | undefined; - - const nextToLastRequested = this._activeStream.segments.getNextTo( - this.lastRequestedSegment.localId - )?.[1]; - // it's needed when the segment requested by the plugin - // (lastRequestedSegment) ends before current playhead position - const nextSegmentStatuses = - nextToLastRequested && - Utils.getSegmentLoadStatuses(nextToLastRequested, this.playback); - - let i = 0; - for (const [segmentId, segment] of this._activeStream.segments.entries( - this.lastRequestedSegment.localId - )) { - if (this.isSegmentLoaded?.(segmentId)) continue; - if (this.queue.has(segmentId)) { - prevSegmentId = segmentId; - continue; - } - const statuses = Utils.getSegmentLoadStatuses(segment, this.playback); - if (!statuses && !(i === 0 && nextSegmentStatuses)) { - break; - } - - const item: LoadQueueItem = { - segment, - statuses: statuses ?? new Set(["high-demand"]), - }; - if (prevSegmentId) this.queue.addAfter(prevSegmentId, segmentId, item); - else this.queue.addToStart(segmentId, item); - newSegmentIds.push(segmentId); - prevSegmentId = segmentId; - i++; - } - return newSegmentIds; - } - - private removeNotActualSegmentsAndUpdateStatuses() { - const removedSegmentIds: string[] = []; - const statusChangedSegmentIds: string[] = []; - for (const [segmentId, item] of this.queue.entries()) { - const statuses = Utils.getSegmentLoadStatuses( - item.segment, - this.playback - ); - - if (!statuses) { - removedSegmentIds.push(segmentId); - this.queue.delete(segmentId); - } else if (!areSetsEqual(item.statuses, statuses)) { - item.statuses = statuses; - statusChangedSegmentIds.push(segmentId); - } - } - return { removedSegmentIds, statusChangedSegmentIds }; - } - - removeLoadedSegment(segmentId: string) { - this.queue.delete(segmentId); - } - - subscribeToUpdate(handler: (removedSegmentIds?: string[]) => void) { - this.updateHandlers.push(handler); - } - - setIsSegmentLoadedPredicate(predicate: (segmentId: string) => boolean) { - this.isSegmentLoaded = predicate; - } - - private getAvgSegmentDuration() { - if (this.segmentDuration) return this.segmentDuration; - let sum = 0; - this.queue.forEach( - ([, { segment }]) => (sum += segment.endTime - segment.startTime) - ); - this.segmentDuration = sum / this.queue.size; - return this.segmentDuration; - } - - get length() { - return this.queue.size; - } - - get activeStream() { - return this._activeStream; - } - - clear() { - this.queue.clear(); - this._activeStream = undefined; - this.lastRequestedSegment = undefined; - this.segmentDuration = undefined; - this.prevUpdatePosition = undefined; - } -} - -function areSetsEqual(set1: Set, set2: Set): boolean { - if (set1.size !== set2.size) return false; - for (const item of set1) { - if (!set2.has(item)) return false; - } - return true; -} - -function getRandomInt(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} diff --git a/packages/p2p-media-loader-core/src/playback.ts b/packages/p2p-media-loader-core/src/playback.ts deleted file mode 100644 index 93a5f510..00000000 --- a/packages/p2p-media-loader-core/src/playback.ts +++ /dev/null @@ -1,68 +0,0 @@ -export class Playback { - private _position?: number; - private _rate = 1; - private _highDemandMargin?: number; - private _httpMargin?: number; - private _p2pMargin?: number; - - constructor( - private readonly settings: { - highDemandBufferLength: number; - httpBufferLength: number; - p2pBufferLength: number; - } - ) { - this.updateMargins(); - } - - isInitialized() { - return this._position !== undefined; - } - - set position(value: number) { - if (this._position === value) return; - this._position = value; - this.updateMargins(); - } - - get position() { - return this._position ?? 0; - } - - set rate(value: number) { - if (this._rate === value) return; - this._rate = value; - this.updateMargins(); - } - - get rate() { - return this._rate ?? 0; - } - - private updateMargins() { - if (this._position === undefined || this._rate === undefined) return; - - const { highDemandBufferLength, httpBufferLength, p2pBufferLength } = - this.settings; - this._highDemandMargin = - this._position + highDemandBufferLength * this._rate; - this._httpMargin = this._position + httpBufferLength * this._rate; - this._p2pMargin = this._position + p2pBufferLength * this._rate; - } - - get margins() { - return { - highDemand: this._highDemandMargin ?? 0, - http: this._httpMargin ?? 0, - p2p: this._p2pMargin ?? 0, - }; - } - - clear() { - this._rate = 1; - this._position = undefined; - this._highDemandMargin = undefined; - this._httpMargin = undefined; - this._p2pMargin = undefined; - } -} diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index d75268f3..c743d51c 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -34,7 +34,7 @@ export class SegmentsMemoryStorage { return cacheItem.data; } - hasSegment(segmentId: string) { + has(segmentId: string) { return this.cache.has(segmentId); } diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index d4d05d7b..66a56eff 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -1,6 +1,11 @@ -import { Segment, Stream, StreamWithSegments } from "./index"; -import { SegmentLoadStatus } from "./internal-types"; -import { Playback } from "./playback"; +import { Segment, Settings, Stream, StreamWithSegments } from "./index"; +import { + SegmentLoadStatus, + Playback, + LoadBufferRanges, + QueueItem, + NumberRange, +} from "./internal-types"; export function getStreamExternalId( stream: Stream, @@ -20,32 +25,111 @@ export function getSegmentFromStreamsMap( } } -export function getSegmentLoadStatuses(segment: Segment, playback: Playback) { - const { position } = playback; - const { highDemand, http, p2p } = playback.margins; +export function generateQueue({ + segment, + stream, + playback, + settings, + isSegmentLoaded, +}: { + stream: Readonly; + segment: Readonly; + playback: Readonly; + isSegmentLoaded: (segmentId: string) => boolean; + settings: Pick< + Settings, + "highDemandBufferLength" | "httpBufferLength" | "p2pBufferLength" + >; +}) { + const bufferRanges = getLoadBufferRanges(playback, settings); + const { localId: requestedSegmentId } = segment; + + const queue: QueueItem[] = []; + const queueSegmentIds = new Set(); + + const nextSegment = stream.segments.getNextTo(segment.localId)?.[1]; + const isNextSegmentHighDemand = !!( + nextSegment && + getSegmentLoadStatuses(nextSegment, bufferRanges)?.has("high-demand") + ); + + let i = 0; + for (const segment of stream.segments.values(requestedSegmentId)) { + const statuses = getSegmentLoadStatuses(segment, bufferRanges); + if (!statuses && !(i === 0 && isNextSegmentHighDemand)) break; + if (isSegmentLoaded(segment.localId)) continue; + + queueSegmentIds.add(segment.localId); + queue.push({ segment, statuses: statuses ?? new Set(["high-demand"]) }); + i++; + } + + return { queue, queueSegmentIds }; +} + +export function getLoadBufferRanges( + playback: Readonly, + settings: Pick< + Settings, + "highDemandBufferLength" | "httpBufferLength" | "p2pBufferLength" + > +): LoadBufferRanges { + const { position, rate } = playback; + const { highDemandBufferLength, httpBufferLength, p2pBufferLength } = + settings; + + const getRange = (position: number, rate: number, bufferLength: number) => { + return { + from: position, + to: position + rate * bufferLength, + }; + }; + return { + highDemand: getRange(position, rate, highDemandBufferLength), + http: getRange(position, rate, httpBufferLength), + p2p: getRange(position, rate, p2pBufferLength), + }; +} + +export function getSegmentLoadStatuses( + segment: Readonly, + loadBufferRanges: LoadBufferRanges +): Set | undefined { + const { highDemand, http, p2p } = loadBufferRanges; const { startTime, endTime } = segment; const statuses = new Set(); - const isValueBetween = (value: number, from: number, to: number) => - value >= from && value < to; + const isValueInRange = (value: number, range: NumberRange) => + value >= range.from && value < range.to; if ( - isValueBetween(startTime, position, highDemand) || - isValueBetween(endTime, position, highDemand) + isValueInRange(startTime, highDemand) || + isValueInRange(endTime, highDemand) ) { statuses.add("high-demand"); } - if ( - isValueBetween(startTime, position, http) || - isValueBetween(endTime, position, http) - ) { + if (isValueInRange(startTime, http) || isValueInRange(endTime, http)) { statuses.add("http-downloadable"); } - if ( - isValueBetween(startTime, position, p2p) || - isValueBetween(endTime, position, p2p) - ) { + if (isValueInRange(startTime, p2p) || isValueInRange(endTime, p2p)) { statuses.add("p2p-downloadable"); } if (statuses.size) return statuses; } + +export function isSegmentActual( + segment: Readonly, + bufferRanges: LoadBufferRanges +) { + const { startTime, endTime } = segment; + const { highDemand, p2p, http } = bufferRanges; + + const isInRange = (value: number) => { + return ( + value > highDemand.from && + (value < highDemand.to || value < http.to || value < p2p.to) + ); + }; + + return isInRange(startTime) || isInRange(endTime); +} diff --git a/packages/p2p-media-loader-shaka/src/engine.ts b/packages/p2p-media-loader-shaka/src/engine.ts index af84b6eb..0970bcb9 100644 --- a/packages/p2p-media-loader-shaka/src/engine.ts +++ b/packages/p2p-media-loader-shaka/src/engine.ts @@ -40,15 +40,19 @@ export class Engine { }); player.addEventListener("loaded", () => { - const mediaElement = player.getMediaElement(); - if (!mediaElement) return; + const media = player.getMediaElement(); + if (!media) return; - mediaElement.addEventListener("timeupdate", () => { - console.log("playhead time: ", mediaElement.currentTime); + media.addEventListener("timeupdate", () => { + this.core.updatePlayback(media.currentTime, media.playbackRate); }); - mediaElement.addEventListener("ratechange", () => { - console.log("playback rate: ", mediaElement.playbackRate); + media.addEventListener("ratechange", () => { + this.core.updatePlayback(media.currentTime, media.playbackRate); + }); + + media.addEventListener("seeking", () => { + this.core.updatePlayback(media.currentTime, media.playbackRate); }); }); } From 9ac874ff8bb4ac14131ccd0869c8c5146bec7b72 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 12 Sep 2023 14:04:48 +0300 Subject: [PATCH 020/127] Install bittorrent-tracker and ripemd160 to core. --- packages/p2p-media-loader-core/package.json | 7 + pnpm-lock.yaml | 314 +++++++++++++++++++- 2 files changed, 315 insertions(+), 6 deletions(-) diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index 4b5d7367..b42d0390 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -26,5 +26,12 @@ "lint": "eslint . --ext .ts", "clean": "rimraf lib dist build", "type-check": "npx tsc --noEmit" + }, + "dependencies": { + "bittorrent-tracker": "^10.0.12", + "ripemd160": "^2.0.2" + }, + "devDependencies": { + "@types/ripemd160": "^2.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0a4b610..54c6c991 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,18 @@ importers: specifier: ^0.4.1 version: 0.4.1(eslint@8.39.0) - packages/p2p-media-loader-core: {} + packages/p2p-media-loader-core: + dependencies: + bittorrent-tracker: + specifier: ^10.0.12 + version: 10.0.12 + ripemd160: + specifier: ^2.0.2 + version: 2.0.2 + devDependencies: + '@types/ripemd160': + specifier: ^2.0.0 + version: 2.0.0 packages/p2p-media-loader-hlsjs: dependencies: @@ -668,6 +679,33 @@ packages: dev: true optional: true + /@thaunknown/simple-peer@9.12.1: + resolution: {integrity: sha512-IS5BXvXx7cvBAzaxqotJf4s4rJCPk5JABLK6Gbnn7oAmWVcH4hYABabBBrvvJtv/xyUqR4v/H3LalnGRJJfEog==} + dependencies: + debug: 4.3.4 + err-code: 3.0.1 + get-browser-rtc: 1.1.0 + queue-microtask: 1.2.3 + streamx: 2.15.1 + uint8-util: 2.2.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@thaunknown/simple-websocket@9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-vzQloFWRodRZqZhpxMpBljFtISesY8TihA8T5uKwCYdj2I1ImMhE/gAeTCPsCGOtxJfGKu3hw/is6MXauWLjOg==} + dependencies: + debug: 4.3.4 + queue-microtask: 1.2.3 + streamx: 2.15.1 + uint8-util: 2.2.3 + ws: 8.14.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /@types/debug@4.1.8: resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} dependencies: @@ -686,6 +724,10 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true + /@types/node@20.6.0: + resolution: {integrity: sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==} + dev: true + /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} dev: true @@ -704,6 +746,12 @@ packages: csstype: 3.1.2 dev: true + /@types/ripemd160@2.0.0: + resolution: {integrity: sha512-LD6AO/+8cAa1ghXax9NG9iPDLPUEGB2WWPjd//04KYfXxTwHvlDEfL0NRjrM5z9XWBi6WbKw75Are0rDyn3PSA==} + dependencies: + '@types/node': 20.6.0 + dev: true + /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: true @@ -871,6 +919,11 @@ packages: hasBin: true dev: true + /addr-to-ip-port@2.0.0: + resolution: {integrity: sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==} + engines: {node: '>=12.20.0'} + dev: false + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -940,6 +993,56 @@ packages: resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} dev: false + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + + /bencode@4.0.0: + resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} + engines: {node: '>=12.20.0'} + dependencies: + uint8-util: 2.2.3 + dev: false + + /bittorrent-peerid@1.3.6: + resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==} + dev: false + + /bittorrent-tracker@10.0.12: + resolution: {integrity: sha512-EYQEwhOYkrRiiwkCFcM9pbzJInsAe7UVmUgevW133duwlZzjwf5ABwDE7pkkmNRS6iwN0b8LbI/94q16dYqiow==} + engines: {node: '>=12.20.0'} + hasBin: true + dependencies: + '@thaunknown/simple-peer': 9.12.1 + '@thaunknown/simple-websocket': 9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + bencode: 4.0.0 + bittorrent-peerid: 1.3.6 + chrome-dgram: 3.0.6 + clone: 2.1.2 + compact2string: 1.4.1 + debug: 4.3.4 + ip: 1.1.8 + lru: 3.1.0 + minimist: 1.2.8 + once: 1.4.0 + queue-microtask: 1.2.3 + random-iterate: 1.0.1 + run-parallel: 1.2.0 + run-series: 1.1.9 + simple-get: 4.0.1 + socks: 2.7.1 + string2compact: 2.0.1 + uint8-util: 2.2.3 + unordered-array-remove: 1.0.2 + ws: 8.14.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + optionalDependencies: + bufferutil: 4.0.7 + utf-8-validate: 5.0.10 + transitivePeerDependencies: + - supports-color + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -971,6 +1074,14 @@ packages: update-browserslist-db: 1.0.11(browserslist@4.21.8) dev: true + /bufferutil@4.0.7: + resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.1 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -997,6 +1108,18 @@ packages: supports-color: 7.2.0 dev: true + /chrome-dgram@3.0.6: + resolution: {integrity: sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA==} + dependencies: + inherits: 2.0.4 + run-series: 1.1.9 + dev: false + + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1025,6 +1148,12 @@ packages: delayed-stream: 1.0.0 dev: false + /compact2string@1.4.1: + resolution: {integrity: sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og==} + dependencies: + ipaddr.js: 2.1.0 + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1057,6 +1186,13 @@ packages: dependencies: ms: 2.1.2 + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -1114,6 +1250,10 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /err-code@3.0.1: + resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} + dev: false + /esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -1308,6 +1448,10 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1413,6 +1557,10 @@ packages: engines: {node: '>=6.9.0'} dev: true + /get-browser-rtc@1.1.0: + resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1495,6 +1643,15 @@ packages: engines: {node: '>=8'} dev: true + /hash-base@3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: false + /hls.js@1.4.5: resolution: {integrity: sha512-xb7IiSM9apU3tJWb5rdSStobXPNJJykHTwSy7JnLF5y/kLJXWjoR/fEpNBlwYxkKcDiiSfO9SQI8yFravZJxIg==} dev: false @@ -1526,7 +1683,19 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + + /ip@1.1.8: + resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + dev: false + + /ip@2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + dev: false + + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -1646,6 +1815,13 @@ packages: engines: {node: 14 || >=16.14} dev: true + /lru@3.1.0: + resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} + engines: {node: '>= 0.4.0'} + dependencies: + inherits: 2.0.4 + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1671,6 +1847,11 @@ packages: mime-db: 1.52.0 dev: false + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + /min-document@2.19.0: resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} dependencies: @@ -1690,6 +1871,10 @@ packages: brace-expansion: 2.0.1 dev: true + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + /minipass@6.0.2: resolution: {integrity: sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==} engines: {node: '>=16 || 14 >=14.17'} @@ -1721,6 +1906,11 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /node-gyp-build@4.6.1: + resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} + hasBin: true + dev: false + /node-releases@2.0.12: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} dev: true @@ -1729,7 +1919,6 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -1848,7 +2037,14 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true + + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + + /random-iterate@1.0.1: + resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==} + dev: false /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} @@ -1872,6 +2068,15 @@ packages: loose-envify: 1.4.0 dev: false + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false @@ -1901,6 +2106,13 @@ packages: glob: 10.2.7 dev: true + /ripemd160@2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + dev: false + /rollup@3.25.1: resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -1913,7 +2125,14 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 - dev: true + + /run-series@1.1.9: + resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} @@ -1958,16 +2177,48 @@ packages: engines: {node: '>=14'} dev: true + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + + /socks@2.7.1: + resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + dependencies: + ip: 2.0.0 + smart-buffer: 4.2.0 + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} dev: true + /streamx@2.15.1: + resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1986,6 +2237,20 @@ packages: strip-ansi: 7.1.0 dev: true + /string2compact@2.0.1: + resolution: {integrity: sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==} + engines: {node: '>=12.20.0'} + dependencies: + addr-to-ip-port: 2.0.0 + ipaddr.js: 2.1.0 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2067,6 +2332,16 @@ packages: hasBin: true dev: true + /uint8-util@2.2.3: + resolution: {integrity: sha512-FtRKosYvGfP6gcX15JSrG7BkvwdZTFtiXH05NMZagEwthY/XUFoSxENEdtky0MfhyVhffLGUt9M9gWEaSmj2aA==} + dependencies: + base64-arraybuffer: 1.0.2 + dev: false + + /unordered-array-remove@1.0.2: + resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==} + dev: false + /update-browserslist-db@1.0.11(browserslist@4.21.8): resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true @@ -2084,6 +2359,18 @@ packages: punycode: 2.3.0 dev: true + /utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.1 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + /vite@4.3.2: resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2149,7 +2436,22 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true + + /ws@8.14.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: + bufferutil: 4.0.7 + utf-8-validate: 5.0.10 + dev: false /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} From 81e85214841aed6bb5ef920ec70d373d88cb9a51 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 12 Sep 2023 14:05:15 +0300 Subject: [PATCH 021/127] Create type declaration for bittorrent tracker. --- .../src/declarations.d.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/p2p-media-loader-core/src/declarations.d.ts diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts new file mode 100644 index 00000000..91ae17ae --- /dev/null +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -0,0 +1,56 @@ +declare module "bittorrent-tracker" { + export default class Client { + constructor(options: { + infoHash: string | ArrayBuffer; + peerId: string | ArrayBuffer; + announce: string[]; + port: number; + rtcConfig?: RTCConfiguration; + getAnnounceOpts?: () => object; + }); + + on(event: E, handler: TrackerEventHandler): void; + + start(): void; + + complete(): void; + + update(data?: object): void; + + destroy(): void; + } + + export type TrackerEvent = "update" | "peer" | "warning" | "error"; + + export type TrackerEventHandler = E extends "update" + ? (data: object) => void + : E extends "peer" + ? (peer: PeerCandidate) => void + : E extends "warning" + ? (warning: unknown) => void + : E extends "error" + ? (error: unknown) => void + : never; + + type PeerEvent = "connect" | "data" | "close"; + + export type PeerCandidateEventHandler = + E extends "connect" + ? () => void + : E extends "data" + ? (data: ArrayBuffer) => void + : E extends "close" + ? () => void + : never; + + export type PeerCandidate = { + id: string; + initiator: boolean; + on( + event: E, + handler: PeerCandidateEventHandler + ): void; + send(data: string | ArrayBuffer | Blob): void; + destroy(): void; + }; +} From 5f899c7db1745e7c992c083c21f2a2ba9c78d97c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 12 Sep 2023 14:05:33 +0300 Subject: [PATCH 022/127] Create P2PLoader and Peer classes. --- .../src/internal-types.ts | 24 +++++++ .../p2p-media-loader-core/src/p2p-loader.ts | 64 +++++++++++++++++ .../p2p-media-loader-core/src/peer-utils.ts | 57 +++++++++++++++ packages/p2p-media-loader-core/src/peer.ts | 69 +++++++++++++++++++ .../p2p-media-loader-core/src/type-guards.ts | 25 +++++++ packages/p2p-media-loader-core/src/utils.ts | 7 ++ 6 files changed, 246 insertions(+) create mode 100644 packages/p2p-media-loader-core/src/p2p-loader.ts create mode 100644 packages/p2p-media-loader-core/src/peer-utils.ts create mode 100644 packages/p2p-media-loader-core/src/peer.ts create mode 100644 packages/p2p-media-loader-core/src/type-guards.ts diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 813c57a7..6cfcc282 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -22,3 +22,27 @@ export type LoadBufferRanges = { }; export type QueueItem = { segment: Segment; statuses: Set }; + +export enum PeerCommandType { + SegmentMap, + SegmentRequest, +} + +export type BasePeerCommand = { + c: T; +}; + +export type PeerSegmentCommand = + BasePeerCommand & { + i: string; + }; + +// {[streamId]: [segmentIds[]; segmentStatuses[]]} +export type JsonSegmentMap = { [key: string]: [string[], number[]] }; + +export type PeerSegmentMapCommand = + BasePeerCommand & { + m: JsonSegmentMap; + }; + +export type PeerCommand = PeerSegmentCommand | PeerSegmentMapCommand; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts new file mode 100644 index 00000000..6747aa35 --- /dev/null +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -0,0 +1,64 @@ +import TrackerClient, { TrackerEventHandler } from "bittorrent-tracker"; +import * as RIPEMD160 from "ripemd160"; +import { Peer } from "./peer"; +import * as PeerUtil from "./peer-utils"; + +export class P2PLoader { + private streamId?: string; + private streamHash?: string; + private peerHash?: string; + private trackerClient?: TrackerClient; + private readonly peers = new Map(); + + setStreamId(streamId: string) { + if (this.streamId === streamId) return; + + this.streamId = streamId; + const peerId = PeerUtil.generatePeerId(); + this.streamHash = getHash(streamId); + this.peerHash = getHash(peerId); + + this.trackerClient = new TrackerClient({ + infoHash: this.streamHash, + peerId: this.peerHash, + port: 6881, + announce: [ + "wss://tracker.novage.com.ua", + "wss://tracker.openwebtorrent.com", + ], + rtcConfig: { + iceServers: [ + { + urls: [ + "stun:stun.l.google.com:19302", + "stun:global.stun.twilio.com:3478", + ], + }, + ], + }, + }); + + this.trackerClient.on("update", this.onTrackerUpdate); + this.trackerClient.on("peer", this.onTrackerPeerConnect); + this.trackerClient.on("warning", this.onTrackerWarning); + this.trackerClient.on("error", this.onTrackerError); + + this.trackerClient.start(); + } + + private onTrackerUpdate: TrackerEventHandler<"update"> = (data) => {}; + private onTrackerPeerConnect: TrackerEventHandler<"peer"> = (candidate) => { + const peer = this.peers.get(candidate.id); + if (peer) { + peer.addCandidate(candidate); + return; + } + this.peers.set(candidate.id, new Peer(candidate)); + }; + private onTrackerWarning: TrackerEventHandler<"warning"> = (warning) => {}; + private onTrackerError: TrackerEventHandler<"error"> = (error) => {}; +} + +function getHash(data: string) { + return new RIPEMD160().update(data).digest("hex"); +} diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts new file mode 100644 index 00000000..3964f91b --- /dev/null +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -0,0 +1,57 @@ +import { JsonSegmentMap, PeerCommand } from "./internal-types"; +import * as TypeGuard from "./type-guards"; +import * as Util from "./utils"; +import { PeerSegmentStatus } from "./peer"; +import * as RIPEMD160 from "ripemd160"; + +export function generatePeerId(): string { + const PEER_ID_SYMBOLS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const PEER_ID_LENGTH = 20; + + let peerId = ""; + + for (let i = 0; i < PEER_ID_LENGTH - peerId.length; i++) { + peerId += PEER_ID_SYMBOLS.charAt( + Math.floor(Math.random() * PEER_ID_SYMBOLS.length) + ); + } + + return new RIPEMD160().update(peerId).digest("hex"); +} + +export function getPeerCommandFromArrayBuffer( + data: ArrayBuffer +): PeerCommand | undefined { + const bytes = new Uint8Array(data); + + // Serialized JSON string check by first, second and last characters: '{" .... }' + if ( + bytes[0] === 123 && + bytes[1] === 34 && + bytes[data.byteLength - 1] === 125 + ) { + try { + const decoded = new TextDecoder().decode(data); + const parsed = JSON.parse(decoded) as object; + if (TypeGuard.isPeerCommand(parsed)) return parsed; + } catch { + return undefined; + } + } +} + +export function getSegmentsFromPeerSegmentMapCommand( + map: JsonSegmentMap +): Map { + const segmentStatusMap = new Map(); + for (const [streamId, [segmentIds, statuses]] of Object.entries(map)) { + for (let i = 0; i < segmentIds.length; i++) { + const segmentId = segmentIds[i]; + const segmentStatus = statuses[i]; + const segmentFullId = Util.getSegmentFullExternalId(streamId, segmentId); + segmentStatusMap.set(segmentFullId, segmentStatus); + } + } + return segmentStatusMap; +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts new file mode 100644 index 00000000..f20f118f --- /dev/null +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -0,0 +1,69 @@ +import { PeerCandidate } from "bittorrent-tracker"; +import { PeerCommandType, PeerCommand } from "./internal-types"; +import * as PeerUtil from "./peer-utils"; + +export class Peer { + readonly id: string; + private readonly candidates = new Set(); + private connectedCandidate?: PeerCandidate; + private segments = new Map(); + + constructor(candidate: PeerCandidate) { + this.id = candidate.id; + this.addCandidate(candidate); + } + + addCandidate(candidate: PeerCandidate) { + candidate.on("connect", () => this.onCandidateConnect(candidate)); + candidate.on("close", () => this.onCandidateClose(candidate)); + candidate.on("data", () => this.onReceiveData.bind(this)); + this.candidates.add(candidate); + } + + private onCandidateConnect(candidate: PeerCandidate) { + if (this.connectedCandidate) { + candidate.destroy(); + return; + } + this.connectedCandidate = candidate; + + for (const candidate of this.candidates) { + if (candidate !== this.connectedCandidate) { + candidate.destroy(); + this.candidates.delete(candidate); + } + } + } + + private onCandidateClose(candidate: PeerCandidate) { + if (this.connectedCandidate !== candidate) { + this.candidates.delete(candidate); + return; + } + } + + private onReceiveData(data: ArrayBuffer) { + const command = PeerUtil.getPeerCommandFromArrayBuffer(data); + if (!command) return; + + this.handleCommand(command); + } + + private handleCommand(command: PeerCommand) { + switch (command.c) { + case PeerCommandType.SegmentMap: + this.segments = PeerUtil.getSegmentsFromPeerSegmentMapCommand( + command.m + ); + break; + + case PeerCommandType.SegmentRequest: + break; + } + } +} + +export enum PeerSegmentStatus { + Loaded, + LoadingByHttp, +} diff --git a/packages/p2p-media-loader-core/src/type-guards.ts b/packages/p2p-media-loader-core/src/type-guards.ts new file mode 100644 index 00000000..eddae249 --- /dev/null +++ b/packages/p2p-media-loader-core/src/type-guards.ts @@ -0,0 +1,25 @@ +import { + PeerSegmentCommand, + PeerCommand, + PeerCommandType, + PeerSegmentMapCommand, +} from "./internal-types"; + +export function isPeerSegmentCommand( + command: object +): command is PeerSegmentCommand { + return (command as PeerSegmentCommand).c === PeerCommandType.SegmentRequest; +} + +export function isPeerSegmentMapCommand( + command: object +): command is PeerSegmentMapCommand { + return (command as PeerSegmentMapCommand).c === PeerCommandType.SegmentMap; +} + +export function isPeerCommand(command: object): command is PeerCommand { + return ( + (command as PeerCommand).c !== undefined && + Object.values(PeerCommandType).includes((command as PeerCommand).c) + ); +} diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 66a56eff..6aa4c073 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -15,6 +15,13 @@ export function getStreamExternalId( return `${manifestResponseUrl}-${type}-${index}`; } +export function getSegmentFullExternalId( + externalStreamId: string, + externalSegmentId: string +) { + return `${externalStreamId}|${externalSegmentId}`; +} + export function getSegmentFromStreamsMap( streams: Map, segmentId: string From 9d7d6f3c145b2e99c0309a8d957f8ea511e7faea Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 12 Sep 2023 15:03:49 +0300 Subject: [PATCH 023/127] Move enums to separate file. --- packages/p2p-media-loader-core/src/enums.ts | 9 +++++++++ packages/p2p-media-loader-core/src/peer-utils.ts | 2 +- packages/p2p-media-loader-core/src/peer.ts | 8 ++------ 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/enums.ts diff --git a/packages/p2p-media-loader-core/src/enums.ts b/packages/p2p-media-loader-core/src/enums.ts new file mode 100644 index 00000000..42beee05 --- /dev/null +++ b/packages/p2p-media-loader-core/src/enums.ts @@ -0,0 +1,9 @@ +export enum PeerCommandType { + SegmentMap, + SegmentRequest, +} + +export enum PeerSegmentStatus { + Loaded, + LoadingByHttp, +} diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index 3964f91b..1cdec0ac 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -1,7 +1,7 @@ import { JsonSegmentMap, PeerCommand } from "./internal-types"; import * as TypeGuard from "./type-guards"; import * as Util from "./utils"; -import { PeerSegmentStatus } from "./peer"; +import { PeerSegmentStatus } from "./enums"; import * as RIPEMD160 from "ripemd160"; export function generatePeerId(): string { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index f20f118f..04e086cd 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -1,5 +1,6 @@ import { PeerCandidate } from "bittorrent-tracker"; -import { PeerCommandType, PeerCommand } from "./internal-types"; +import { PeerCommand } from "./internal-types"; +import { PeerSegmentStatus, PeerCommandType } from "./enums"; import * as PeerUtil from "./peer-utils"; export class Peer { @@ -62,8 +63,3 @@ export class Peer { } } } - -export enum PeerSegmentStatus { - Loaded, - LoadingByHttp, -} From 08f4c2388e72107310a3691410994f213d53d6f4 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 13 Sep 2023 12:47:48 +0300 Subject: [PATCH 024/127] Remove unused position core property. --- packages/p2p-media-loader-core/src/core.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 55593034..cd9589e6 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -21,7 +21,6 @@ export class Core { cachedSegmentExpiration: 120, cachedSegmentsCount: 50, }; - private position = 0; private readonly bandwidthApproximator = new BandwidthApproximator(); private readonly mainStreamLoader = new HybridLoader( this.settings, @@ -90,7 +89,6 @@ export class Core { destroy(): void { this.streams.clear(); - this.position = 0; this.mainStreamLoader.clear(); this.secondaryStreamLoader.clear(); this.manifestResponseUrl = undefined; From 23e9abcbf45e9d0d6a9e5de2f8a976bf6e82db69 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 13 Sep 2023 13:05:19 +0300 Subject: [PATCH 025/127] Create secondary hybrid loader only if necessary. --- packages/p2p-media-loader-core/src/core.ts | 28 +++++++++---------- .../src/hybrid-loader.ts | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index cd9589e6..d975984a 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -26,10 +26,7 @@ export class Core { this.settings, this.bandwidthApproximator ); - private readonly secondaryStreamLoader = new HybridLoader( - this.settings, - this.bandwidthApproximator - ); + private secondaryStreamLoader?: HybridLoader; setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; @@ -68,29 +65,32 @@ export class Core { loadSegment(segmentLocalId: string): Promise { const { segment, stream } = this.identifySegment(segmentLocalId); - const loader = - stream.type === "main" - ? this.mainStreamLoader - : this.secondaryStreamLoader; + let loader: HybridLoader; + if (stream.type === "main") { + loader = this.mainStreamLoader; + } else { + this.secondaryStreamLoader = + this.secondaryStreamLoader ?? + new HybridLoader(this.settings, this.bandwidthApproximator); + loader = this.secondaryStreamLoader; + } return loader.loadSegment(segment, stream); } abortSegmentLoading(segmentId: string): void { this.mainStreamLoader.abortSegment(segmentId); - this.secondaryStreamLoader.abortSegment(segmentId); + this.secondaryStreamLoader?.abortSegment(segmentId); } updatePlayback(position: number, rate: number): void { this.mainStreamLoader.updatePlayback(position, rate); - this.secondaryStreamLoader.updatePlayback(position, rate); - - // TODO: update playback position when the live stream is updated + this.secondaryStreamLoader?.updatePlayback(position, rate); } destroy(): void { this.streams.clear(); - this.mainStreamLoader.clear(); - this.secondaryStreamLoader.clear(); + this.mainStreamLoader.destroy(); + this.secondaryStreamLoader?.destroy(); this.manifestResponseUrl = undefined; } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7b10abf8..f56b759c 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -185,10 +185,10 @@ export class HybridLoader { return request; } - clear() { + destroy() { clearInterval(this.storageCleanUpIntervalId); this.storageCleanUpIntervalId = undefined; - void this.segmentStorage.clear(); + void this.segmentStorage.destroy(); this.httpLoader.abortAll(); for (const request of this.pluginRequests.values()) { request.onError("Aborted"); From fb5b0ea3ef3c7c23336f57b547fdf288e6a3f9db Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 13 Sep 2023 13:49:19 +0300 Subject: [PATCH 026/127] Add force parameter to process queue hybrid loader method. --- .../src/hybrid-loader.ts | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index f56b759c..379b2dc9 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -14,7 +14,7 @@ export class HybridLoader { private activeStream?: Readonly; private lastRequestedSegment?: Readonly; private playback?: Playback; - private segmentAvgLength?: number; + private lastQueueProcessingTimeStamp?: number; constructor( private readonly settings: Settings, @@ -45,10 +45,7 @@ export class HybridLoader { if (!this.playback) { this.playback = { position: segment.startTime, rate: 1 }; } - if (stream !== this.activeStream) { - this.segmentAvgLength = computeSegmentAvgLength(stream); - this.activeStream = stream; - } + if (stream !== this.activeStream) this.activeStream = stream; this.lastRequestedSegment = segment; this.processQueue(); @@ -63,10 +60,19 @@ export class HybridLoader { return request.responsePromise; } - private processQueue() { + private processQueue(force = true) { if (!this.activeStream || !this.lastRequestedSegment || !this.playback) { return; } + const now = performance.now(); + if ( + !force && + this.lastQueueProcessingTimeStamp !== undefined && + now - this.lastQueueProcessingTimeStamp >= 950 + ) { + return; + } + this.lastQueueProcessingTimeStamp = now; const { queue, queueSegmentIds } = Utils.generateQueue({ segment: this.lastRequestedSegment, @@ -151,19 +157,16 @@ export class HybridLoader { } } - updatePlayback(position: number, rate?: number) { + updatePlayback(position: number, rate: number) { if (!this.playback) return; - const isRateChanged = rate && this.playback.rate !== rate; - const isPositionSignificantlyChanged = - this.segmentAvgLength === undefined || - Math.abs(position - this.playback.position) / this.segmentAvgLength >= - 0.45; + const isRateChanged = this.playback.rate !== rate; + const isPositionChanged = this.playback.position !== position; - if (!isRateChanged && !isPositionSignificantlyChanged) return; + if (!isRateChanged && !isPositionChanged) return; - if (isPositionSignificantlyChanged) this.playback.position = position; + if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; - this.processQueue(); + this.processQueue(false); } private createPluginSegmentRequest(segment: Segment) { @@ -203,12 +206,3 @@ type Request = { onSuccess: (response: SegmentResponse) => void; onError: (reason?: unknown) => void; }; - -function computeSegmentAvgLength(stream: StreamWithSegments) { - if (!stream.segments.size) return; - let sum = 0; - for (const segment of stream.segments.values()) { - sum += segment.endTime - segment.startTime; - } - return sum / stream.segments.size; -} From d0da2ae306e1253426f390c21c5439f9d91093f9 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 13 Sep 2023 15:38:01 +0300 Subject: [PATCH 027/127] Add sendCommand method to Peer. --- .../p2p-media-loader-core/src/internal-types.ts | 6 +----- packages/p2p-media-loader-core/src/p2p-loader.ts | 7 ++----- packages/p2p-media-loader-core/src/peer.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 6cfcc282..90c17f01 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -1,4 +1,5 @@ import { Segment } from "./types"; +import { PeerCommandType } from "./enums"; export type Playback = { position: number; @@ -23,11 +24,6 @@ export type LoadBufferRanges = { export type QueueItem = { segment: Segment; statuses: Set }; -export enum PeerCommandType { - SegmentMap, - SegmentRequest, -} - export type BasePeerCommand = { c: T; }; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 6747aa35..b6abcaad 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -49,11 +49,8 @@ export class P2PLoader { private onTrackerUpdate: TrackerEventHandler<"update"> = (data) => {}; private onTrackerPeerConnect: TrackerEventHandler<"peer"> = (candidate) => { const peer = this.peers.get(candidate.id); - if (peer) { - peer.addCandidate(candidate); - return; - } - this.peers.set(candidate.id, new Peer(candidate)); + if (peer) peer.addCandidate(candidate); + else this.peers.set(candidate.id, new Peer(candidate)); }; private onTrackerWarning: TrackerEventHandler<"warning"> = (warning) => {}; private onTrackerError: TrackerEventHandler<"error"> = (error) => {}; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 04e086cd..a3c29a7b 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -6,7 +6,7 @@ import * as PeerUtil from "./peer-utils"; export class Peer { readonly id: string; private readonly candidates = new Set(); - private connectedCandidate?: PeerCandidate; + private connection?: PeerCandidate; private segments = new Map(); constructor(candidate: PeerCandidate) { @@ -22,14 +22,14 @@ export class Peer { } private onCandidateConnect(candidate: PeerCandidate) { - if (this.connectedCandidate) { + if (this.connection) { candidate.destroy(); return; } - this.connectedCandidate = candidate; + this.connection = candidate; for (const candidate of this.candidates) { - if (candidate !== this.connectedCandidate) { + if (candidate !== this.connection) { candidate.destroy(); this.candidates.delete(candidate); } @@ -37,7 +37,7 @@ export class Peer { } private onCandidateClose(candidate: PeerCandidate) { - if (this.connectedCandidate !== candidate) { + if (this.connection !== candidate) { this.candidates.delete(candidate); return; } @@ -62,4 +62,9 @@ export class Peer { break; } } + + private sendCommand(command: PeerCommand) { + if (!this.connection) return; + this.connection.send(JSON.stringify(command)); + } } From e294cad0dd816d93cdc1d2ad2666ef8be876a2c1 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 14 Sep 2023 00:01:45 +0300 Subject: [PATCH 028/127] Add request type. --- .../p2p-media-loader-core/src/http-loader.ts | 43 ++++++++++++++++++- .../src/hybrid-loader.ts | 42 ++++++++---------- packages/p2p-media-loader-core/src/request.ts | 23 ++++++++++ 3 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/request.ts diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 258f152c..d5344e19 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,7 +1,8 @@ import { FetchError } from "./errors"; import { Segment } from "./types"; +import { Request } from "./request"; -type Request = { +type Request1 = { promise: Promise; abortController: AbortController; }; @@ -74,3 +75,43 @@ export class HttpLoader { this.requests.clear(); } } + +export function loadSegmentHttp(segment: Segment): Request { + const { promise, abortController } = fetchSegment(segment); + const request: Request = { + type: "http", + promise, + segment, + abort: () => abortController.abort(), + }; + return request; +} + +function fetchSegment(segment: Segment) { + const headers = new Headers(); + const { url, byteRange } = segment; + + if (byteRange) { + const { start, end } = byteRange; + const byteRangeString = `bytes=${start}-${end}`; + headers.set("Range", byteRangeString); + } + const abortController = new AbortController(); + + const promise = fetch(url, { + headers, + signal: abortController.signal, + }).then((response) => { + if (!response.ok) { + throw new FetchError( + response.statusText ?? "Fetch, bad network response", + response.status, + response + ); + } + + return response.arrayBuffer(); + }); + + return { promise, abortController }; +} diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 379b2dc9..3f12c636 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,14 +1,15 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import { HttpLoader } from "./http-loader"; +import { HttpLoader, loadSegmentHttp } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; +import { RequestContainer } from "./request"; import * as Utils from "./utils"; export class HybridLoader { private readonly httpLoader = new HttpLoader(); - private readonly pluginRequests = new Map(); + private readonly requests = new RequestContainer(); private readonly segmentStorage: SegmentsMemoryStorage; private storageCleanUpIntervalId?: number; private activeStream?: Readonly; @@ -127,23 +128,24 @@ export class HybridLoader { } private async loadSegmentThroughHttp(segment: Segment) { + const request = loadSegmentHttp(segment); let data: ArrayBuffer | undefined; try { - data = await this.httpLoader.load(segment); + data = loadSegmentHttp(); } catch (err) { // TODO: handle abort } - if (!data) return; - this.bandwidthApproximator.addBytes(data.byteLength); - void this.segmentStorage.storeSegment(segment, data); - const request = this.pluginRequests.get(segment.localId); - if (request) { - request.onSuccess({ - bandwidth: this.bandwidthApproximator.getBandwidth(), - data, - }); - } - this.pluginRequests.delete(segment.localId); + // if (!data) return; + // this.bandwidthApproximator.addBytes(data.byteLength); + // void this.segmentStorage.storeSegment(segment, data); + // const request = this.pluginRequests.get(segment.localId); + // if (request) { + // request.onSuccess({ + // bandwidth: this.bandwidthApproximator.getBandwidth(), + // data, + // }); + // } + // this.pluginRequests.delete(segment.localId); } private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { @@ -170,13 +172,13 @@ export class HybridLoader { } private createPluginSegmentRequest(segment: Segment) { - let onSuccess: Request["onSuccess"]; - let onError: Request["onError"]; + let onSuccess: PlayerRequest["onSuccess"]; + let onError: PlayerRequest["onError"]; const responsePromise = new Promise((resolve, reject) => { onSuccess = resolve; onError = reject; }); - const request: Request = { + const request: PlayerRequest = { responsePromise, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion onSuccess: onSuccess!, @@ -200,9 +202,3 @@ export class HybridLoader { this.playback = undefined; } } - -type Request = { - responsePromise: Promise; - onSuccess: (response: SegmentResponse) => void; - onError: (reason?: unknown) => void; -}; diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts new file mode 100644 index 00000000..15b5c7cb --- /dev/null +++ b/packages/p2p-media-loader-core/src/request.ts @@ -0,0 +1,23 @@ +import { Segment, SegmentResponse } from "./types"; + +type PlayerRequest = { + responsePromise: Promise; + onSuccess: (response: SegmentResponse) => void; + onError: (reason?: unknown) => void; +}; + +export type Request = { + readonly type: "http" | "p2p"; + readonly segment: Segment; + playerRequest?: PlayerRequest; + readonly promise?: Promise; + readonly abort: () => void; +}; + +export class RequestContainer { + requests = new Map(); + + addRequest(request: Request) { + this.requests.set(request.segment.localId, request); + } +} From d67e4c82c66a70bd099cf548cf072a05a85681d6 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 14 Sep 2023 17:47:57 +0300 Subject: [PATCH 029/127] Create and apply RequestContainer class. --- .../p2p-media-loader-core/src/http-loader.ts | 82 +---------- .../src/hybrid-loader.ts | 113 +++++++--------- packages/p2p-media-loader-core/src/request.ts | 127 ++++++++++++++++-- 3 files changed, 166 insertions(+), 156 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index d5344e19..af860ea3 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,90 +1,14 @@ import { FetchError } from "./errors"; import { Segment } from "./types"; -import { Request } from "./request"; +import { HttpRequest } from "./request"; -type Request1 = { - promise: Promise; - abortController: AbortController; -}; - -export class HttpLoader { - private readonly requests = new Map(); - - async load(segment: Segment) { - const abortController = new AbortController(); - const promise = this.fetch(segment, abortController); - const requestContext: Request = { - abortController, - promise, - }; - this.requests.set(segment.localId, requestContext); - await promise; - this.requests.delete(segment.localId); - return promise; - } - - private async fetch(segment: Segment, abortController: AbortController) { - const headers = new Headers(); - const { url, byteRange } = segment; - - if (byteRange) { - const { start, end } = byteRange; - const byteRangeString = `bytes=${start}-${end}`; - headers.set("Range", byteRangeString); - } - const response = await fetch(url, { - headers, - signal: abortController.signal, - }); - if (!response.ok) { - throw new FetchError( - response.statusText ?? "Fetch, bad network response", - response.status, - response - ); - } - - return response.arrayBuffer(); - } - - isLoading(segmentId: string) { - return this.requests.has(segmentId); - } - - abort(segmentId: string) { - this.requests.get(segmentId)?.abortController.abort(); - this.requests.delete(segmentId); - } - - getLoadingsAmount() { - return this.requests.size; - } - - getLoadingSegmentIds() { - return this.requests.keys(); - } - - getRequest(segmentId: string) { - return this.requests.get(segmentId)?.promise; - } - - abortAll() { - for (const request of this.requests.values()) { - request.abortController.abort(); - } - this.requests.clear(); - } -} - -export function loadSegmentHttp(segment: Segment): Request { +export function loadSegmentHttp(segment: Segment): Readonly { const { promise, abortController } = fetchSegment(segment); - const request: Request = { + return { type: "http", promise, - segment, abort: () => abortController.abort(), }; - return request; } function fetchSegment(segment: Segment) { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 3f12c636..8d39332a 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,5 +1,5 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import { HttpLoader, loadSegmentHttp } from "./http-loader"; +import { loadSegmentHttp } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; @@ -8,7 +8,6 @@ import { RequestContainer } from "./request"; import * as Utils from "./utils"; export class HybridLoader { - private readonly httpLoader = new HttpLoader(); private readonly requests = new RequestContainer(); private readonly segmentStorage: SegmentsMemoryStorage; private storageCleanUpIntervalId?: number; @@ -33,7 +32,7 @@ export class HybridLoader { return Utils.isSegmentActual(segment, bufferRanges); }); - this.storageCleanUpIntervalId = setInterval( + this.storageCleanUpIntervalId = window.setInterval( () => this.segmentStorage.clear(), 1000 ); @@ -57,8 +56,7 @@ export class HybridLoader { bandwidth: this.bandwidthApproximator.getBandwidth(), }; } - const request = this.createPluginSegmentRequest(segment); - return request.responsePromise; + return this.createPluginSegmentRequest(segment); } private processQueue(force = true) { @@ -83,31 +81,20 @@ export class HybridLoader { isSegmentLoaded: (segmentId) => this.segmentStorage.has(segmentId), }); - const bufferRanges = Utils.getLoadBufferRanges( - this.playback, - this.settings + this.requests.abortNotRequestedByEngine((segmentId) => + queueSegmentIds.has(segmentId) ); - for (const segmentId of this.getLoadingSegmentIds()) { - const segment = this.activeStream.segments.get(segmentId); - if ( - !queueSegmentIds.has(segmentId) && - !this.pluginRequests.has(segmentId) && - !(segment && Utils.isSegmentActual(segment, bufferRanges)) - ) { - this.abortSegment(segmentId); - } - } const { simultaneousHttpDownloads } = this.settings; for (const { segment, statuses } of queue) { - if (this.httpLoader.isLoading(segment.localId)) continue; + if (this.requests.isHttpRequested(segment.localId)) continue; if (statuses.has("high-demand")) { - if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { + if (this.requests.countHttpRequests() < simultaneousHttpDownloads) { void this.loadSegmentThroughHttp(segment); continue; } this.abortLastHttpLoadingAfter(queue, segment.localId); - if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { + if (this.requests.countHttpRequests() < simultaneousHttpDownloads) { void this.loadSegmentThroughHttp(segment); } } @@ -115,44 +102,32 @@ export class HybridLoader { } } - getLoadingSegmentIds() { - return this.httpLoader.getLoadingSegmentIds(); - } - abortSegment(segmentId: string) { - this.httpLoader.abort(segmentId); - const request = this.pluginRequests.get(segmentId); - if (!request) return; - request.onError("Abort"); - this.pluginRequests.delete(segmentId); + this.requests.abort(segmentId); } private async loadSegmentThroughHttp(segment: Segment) { const request = loadSegmentHttp(segment); + this.requests.addHybridLoaderRequest(segment, request); let data: ArrayBuffer | undefined; try { - data = loadSegmentHttp(); + data = await request.promise; } catch (err) { // TODO: handle abort } - // if (!data) return; - // this.bandwidthApproximator.addBytes(data.byteLength); - // void this.segmentStorage.storeSegment(segment, data); - // const request = this.pluginRequests.get(segment.localId); - // if (request) { - // request.onSuccess({ - // bandwidth: this.bandwidthApproximator.getBandwidth(), - // data, - // }); - // } - // this.pluginRequests.delete(segment.localId); + if (!data) return; + this.bandwidthApproximator.addBytes(data.byteLength); + void this.segmentStorage.storeSegment(segment, data); + this.requests.resolveEngineRequest(segment.localId, { + data, + bandwidth: this.bandwidthApproximator.getBandwidth(), + }); } private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { - for (let i = queue.length - 1; i >= 0; i--) { - const { segment } = queue[i]; + for (const { segment } of arrayBackwards(queue)) { if (segment.localId === segmentId) break; - if (this.httpLoader.isLoading(segment.localId)) { + if (this.requests.isHttpRequested(segment.localId)) { this.abortSegment(segment.localId); break; } @@ -172,33 +147,39 @@ export class HybridLoader { } private createPluginSegmentRequest(segment: Segment) { - let onSuccess: PlayerRequest["onSuccess"]; - let onError: PlayerRequest["onError"]; - const responsePromise = new Promise((resolve, reject) => { - onSuccess = resolve; - onError = reject; - }); - const request: PlayerRequest = { - responsePromise, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onSuccess: onSuccess!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onError: onError!, - }; - - this.pluginRequests.set(segment.localId, request); - return request; + const request = getControlledPromise(); + this.requests.addPlayerRequest(segment, request); + return request.promise; } destroy() { clearInterval(this.storageCleanUpIntervalId); this.storageCleanUpIntervalId = undefined; void this.segmentStorage.destroy(); - this.httpLoader.abortAll(); - for (const request of this.pluginRequests.values()) { - request.onError("Aborted"); - } - this.pluginRequests.clear(); + this.requests.destroy(); this.playback = undefined; } } + +function getControlledPromise() { + let onSuccess: (value: T) => void; + let onError: (reason?: unknown) => void; + const promise = new Promise((resolve, reject) => { + onSuccess = resolve; + onError = reject; + }); + + return { + promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onSuccess: onSuccess!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onError: onError!, + }; +} + +function* arrayBackwards(arr: T[]) { + for (let i = arr.length - 1; i >= 0; i--) { + yield arr[i]; + } +} diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 15b5c7cb..ff7ef4fb 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,23 +1,128 @@ import { Segment, SegmentResponse } from "./types"; -type PlayerRequest = { - responsePromise: Promise; +type EngineRequest = { + promise: Promise; onSuccess: (response: SegmentResponse) => void; onError: (reason?: unknown) => void; }; -export type Request = { - readonly type: "http" | "p2p"; - readonly segment: Segment; - playerRequest?: PlayerRequest; - readonly promise?: Promise; - readonly abort: () => void; +type RequestBase = { + promise: Promise; + abort: () => void; }; +export type HttpRequest = RequestBase & { + type: "http"; +}; + +type P2PRequest = RequestBase & { + type: "p2p"; +}; + +type HybridLoaderRequest = HttpRequest | P2PRequest; + +type Request = { + segment: Readonly; + loaderRequest?: Readonly; + engineRequest?: Readonly; +}; + +function isHybridLoaderRequest( + request: HybridLoaderRequest | EngineRequest +): request is HybridLoaderRequest { + return !!(request as HybridLoaderRequest).type; +} + export class RequestContainer { - requests = new Map(); + private readonly requests = new Map(); + + addHybridLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { + const segmentId = segment.localId; + const existingRequest = this.requests.get(segmentId); + if (existingRequest) { + existingRequest.loaderRequest = loaderRequest; + } else { + this.requests.set(segmentId, { + segment, + loaderRequest, + }); + } + loaderRequest.promise.finally(() => this.requests.delete(segmentId)); + } + + addPlayerRequest(segment: Segment, engineRequest: EngineRequest) { + const segmentId = segment.localId; + const existingRequest = this.requests.get(segmentId); + if (existingRequest) { + existingRequest.engineRequest = engineRequest; + } else { + this.requests.set(segmentId, { + segment, + engineRequest, + }); + } + engineRequest.promise.finally(() => this.requests.delete(segmentId)); + } + + get(segmentId: string) { + return this.requests.get(segmentId); + } + + values() { + return this.requests.values(); + } + + *httpRequests(): Generator { + for (const request of this.requests.values()) { + if (request.loaderRequest?.type === "http") yield request; + } + } + + resolveEngineRequest(segmentId: string, response: SegmentResponse) { + this.requests.get(segmentId)?.engineRequest?.onSuccess(response); + } + + isRequestedByEngine(segmentId: string): boolean { + return !!this.requests.get(segmentId)?.engineRequest; + } + + isHttpRequested(segmentId: string): boolean { + return this.requests.get(segmentId)?.loaderRequest?.type === "http"; + } + + countHttpRequests(): number { + let count = 0; + for (const request of this.requests.values()) { + if (request.loaderRequest?.type === "http") count++; + } + + return count; + } + + abort(segmentId: string) { + const request = this.requests.get(segmentId); + if (!request) return; + + request.engineRequest?.onError(new Error("Aborted")); + request.loaderRequest?.abort(); + } + + abortNotRequestedByEngine(isLocked: (segmentId: string) => boolean) { + for (const { + loaderRequest, + engineRequest, + segment, + } of this.requests.values()) { + if (!engineRequest) continue; + if (!isLocked(segment.localId) && loaderRequest) loaderRequest.abort(); + } + } - addRequest(request: Request) { - this.requests.set(request.segment.localId, request); + destroy() { + for (const request of this.requests.values()) { + request.loaderRequest?.abort(); + request.engineRequest?.onError(); + } + this.requests.clear(); } } From 7b61d442672ebfafc7378dd1db7478b66141550f Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 15 Sep 2023 10:39:14 +0300 Subject: [PATCH 030/127] Rename settings time window properties. --- packages/p2p-media-loader-core/src/core.ts | 6 +++--- packages/p2p-media-loader-core/src/types.ts | 6 +++--- packages/p2p-media-loader-core/src/utils.ts | 17 ++++++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index d975984a..6b723b4a 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -15,9 +15,9 @@ export class Core { private readonly streams = new Map>(); private readonly settings: Settings = { simultaneousHttpDownloads: 3, - highDemandBufferLength: 25, - httpBufferLength: 60, - p2pBufferLength: 60, + highDemandTimeWindow: 25, + httpDownloadTimeWindow: 60, + p2pDownloadTimeWindow: 60, cachedSegmentExpiration: 120, cachedSegmentsCount: 50, }; diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 38aefe61..0a02fa59 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -40,9 +40,9 @@ export type SegmentResponse = { }; export type Settings = { - highDemandBufferLength: number; - httpBufferLength: number; - p2pBufferLength: number; + highDemandTimeWindow: number; + httpDownloadTimeWindow: number; + p2pDownloadTimeWindow: number; simultaneousHttpDownloads: number; cachedSegmentExpiration: number; cachedSegmentsCount: number; diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 66a56eff..b6a72b93 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -38,7 +38,7 @@ export function generateQueue({ isSegmentLoaded: (segmentId: string) => boolean; settings: Pick< Settings, - "highDemandBufferLength" | "httpBufferLength" | "p2pBufferLength" + "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" >; }) { const bufferRanges = getLoadBufferRanges(playback, settings); @@ -71,12 +71,15 @@ export function getLoadBufferRanges( playback: Readonly, settings: Pick< Settings, - "highDemandBufferLength" | "httpBufferLength" | "p2pBufferLength" + "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" > ): LoadBufferRanges { const { position, rate } = playback; - const { highDemandBufferLength, httpBufferLength, p2pBufferLength } = - settings; + const { + highDemandTimeWindow, + httpDownloadTimeWindow, + p2pDownloadTimeWindow, + } = settings; const getRange = (position: number, rate: number, bufferLength: number) => { return { @@ -85,9 +88,9 @@ export function getLoadBufferRanges( }; }; return { - highDemand: getRange(position, rate, highDemandBufferLength), - http: getRange(position, rate, httpBufferLength), - p2p: getRange(position, rate, p2pBufferLength), + highDemand: getRange(position, rate, highDemandTimeWindow), + http: getRange(position, rate, httpDownloadTimeWindow), + p2p: getRange(position, rate, p2pDownloadTimeWindow), }; } From 90f5d1d60e3eb44be55d38fa736b5fd73155258e Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 15 Sep 2023 14:15:02 +0300 Subject: [PATCH 031/127] Add AbortError. --- packages/p2p-media-loader-core/src/core.ts | 6 +-- packages/p2p-media-loader-core/src/errors.ts | 6 +++ .../p2p-media-loader-core/src/http-loader.ts | 27 ++++++---- .../src/hybrid-loader.ts | 54 ++++++++++--------- packages/p2p-media-loader-core/src/request.ts | 47 ++++++++++------ .../src/segments-storage.ts | 6 +-- .../src/fragment-loader.ts | 9 +++- 7 files changed, 96 insertions(+), 59 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 6b723b4a..ee2b158c 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -74,12 +74,12 @@ export class Core { new HybridLoader(this.settings, this.bandwidthApproximator); loader = this.secondaryStreamLoader; } - return loader.loadSegment(segment, stream); + return loader.loadSegmentByEngine(segment, stream); } abortSegmentLoading(segmentId: string): void { - this.mainStreamLoader.abortSegment(segmentId); - this.secondaryStreamLoader?.abortSegment(segmentId); + this.mainStreamLoader.abortSegmentByEngine(segmentId); + this.secondaryStreamLoader?.abortSegmentByEngine(segmentId); } updatePlayback(position: number, rate: number): void { diff --git a/packages/p2p-media-loader-core/src/errors.ts b/packages/p2p-media-loader-core/src/errors.ts index 25903a60..41af2e1e 100644 --- a/packages/p2p-media-loader-core/src/errors.ts +++ b/packages/p2p-media-loader-core/src/errors.ts @@ -8,3 +8,9 @@ export class FetchError extends Error { this.details = details; } } + +export class AbortError extends Error { + constructor(message = "AbortError") { + super(message); + } +} diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index af860ea3..b16aff8d 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,8 +1,10 @@ -import { FetchError } from "./errors"; +import { AbortError, FetchError } from "./errors"; import { Segment } from "./types"; import { HttpRequest } from "./request"; -export function loadSegmentHttp(segment: Segment): Readonly { +export function loadSegmentThroughHttp( + segment: Segment +): Readonly { const { promise, abortController } = fetchSegment(segment); return { type: "http", @@ -13,7 +15,7 @@ export function loadSegmentHttp(segment: Segment): Readonly { function fetchSegment(segment: Segment) { const headers = new Headers(); - const { url, byteRange } = segment; + const { url, byteRange, localId: segmentId } = segment; if (byteRange) { const { start, end } = byteRange; @@ -25,17 +27,22 @@ function fetchSegment(segment: Segment) { const promise = fetch(url, { headers, signal: abortController.signal, - }).then((response) => { - if (!response.ok) { + }) + .then((response) => { + if (response.ok) return response.arrayBuffer(); + throw new FetchError( - response.statusText ?? "Fetch, bad network response", + response.statusText ?? `Network response was not for ${segmentId}`, response.status, response ); - } - - return response.arrayBuffer(); - }); + }) + .catch((error) => { + if (error instanceof Error && error.name === "AbortError") { + throw new AbortError(`Segment fetch was aborted ${segmentId}`); + } + throw error; + }); return { promise, abortController }; } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 8d39332a..5e218657 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,11 +1,12 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import { loadSegmentHttp } from "./http-loader"; +import { loadSegmentThroughHttp } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestContainer } from "./request"; import * as Utils from "./utils"; +import { AbortError, FetchError } from "./errors"; export class HybridLoader { private readonly requests = new RequestContainer(); @@ -38,7 +39,7 @@ export class HybridLoader { ); } - async loadSegment( + async loadSegmentByEngine( segment: Readonly, stream: Readonly ): Promise { @@ -47,19 +48,23 @@ export class HybridLoader { } if (stream !== this.activeStream) this.activeStream = stream; this.lastRequestedSegment = segment; - this.processQueue(); + void this.processQueue(); - const storageData = await this.segmentStorage.getSegment(segment.localId); + const storageData = await this.segmentStorage.getSegmentData( + segment.localId + ); if (storageData) { return { data: storageData, bandwidth: this.bandwidthApproximator.getBandwidth(), }; } - return this.createPluginSegmentRequest(segment); + const request = getControlledPromise(); + this.requests.addEngineRequest(segment, request); + return request.promise; } - private processQueue(force = true) { + private async processQueue(force = true) { if (!this.activeStream || !this.lastRequestedSegment || !this.playback) { return; } @@ -73,15 +78,16 @@ export class HybridLoader { } this.lastQueueProcessingTimeStamp = now; + const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); const { queue, queueSegmentIds } = Utils.generateQueue({ segment: this.lastRequestedSegment, stream: this.activeStream, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segmentId) => this.segmentStorage.has(segmentId), + isSegmentLoaded: (segmentId) => storedSegmentIds.has(segmentId), }); - this.requests.abortNotRequestedByEngine((segmentId) => + this.requests.abortAllNotRequestedByEngine((segmentId) => queueSegmentIds.has(segmentId) ); @@ -102,18 +108,20 @@ export class HybridLoader { } } - abortSegment(segmentId: string) { - this.requests.abort(segmentId); + abortSegmentByEngine(segmentId: string) { + this.requests.abortEngineRequest(segmentId); } private async loadSegmentThroughHttp(segment: Segment) { - const request = loadSegmentHttp(segment); - this.requests.addHybridLoaderRequest(segment, request); let data: ArrayBuffer | undefined; try { - data = await request.promise; + const httpRequest = loadSegmentThroughHttp(segment); + this.requests.addLoaderRequest(segment, httpRequest); + data = await httpRequest.promise; } catch (err) { - // TODO: handle abort + if (err instanceof FetchError) { + // TODO: handle error + } } if (!data) return; this.bandwidthApproximator.addBytes(data.byteLength); @@ -125,10 +133,12 @@ export class HybridLoader { } private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { - for (const { segment } of arrayBackwards(queue)) { - if (segment.localId === segmentId) break; - if (this.requests.isHttpRequested(segment.localId)) { - this.abortSegment(segment.localId); + for (const { + segment: { localId: queueSegmentId }, + } of arrayBackwards(queue)) { + if (queueSegmentId === segmentId) break; + if (this.requests.isHttpRequested(queueSegmentId)) { + this.requests.abortLoaderRequest(queueSegmentId); break; } } @@ -143,13 +153,7 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; - this.processQueue(false); - } - - private createPluginSegmentRequest(segment: Segment) { - const request = getControlledPromise(); - this.requests.addPlayerRequest(segment, request); - return request.promise; + void this.processQueue(false); } destroy() { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index ff7ef4fb..dc90a42b 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,4 +1,5 @@ import { Segment, SegmentResponse } from "./types"; +import { AbortError } from "./errors"; type EngineRequest = { promise: Promise; @@ -27,16 +28,10 @@ type Request = { engineRequest?: Readonly; }; -function isHybridLoaderRequest( - request: HybridLoaderRequest | EngineRequest -): request is HybridLoaderRequest { - return !!(request as HybridLoaderRequest).type; -} - export class RequestContainer { private readonly requests = new Map(); - addHybridLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { + addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { const segmentId = segment.localId; const existingRequest = this.requests.get(segmentId); if (existingRequest) { @@ -47,21 +42,29 @@ export class RequestContainer { loaderRequest, }); } - loaderRequest.promise.finally(() => this.requests.delete(segmentId)); + loaderRequest.promise.finally(() => { + const request = this.requests.get(segmentId); + delete request?.loaderRequest; + if (request) this.clearRequest(request); + }); } - addPlayerRequest(segment: Segment, engineRequest: EngineRequest) { + addEngineRequest(segment: Segment, engineRequest: EngineRequest) { const segmentId = segment.localId; - const existingRequest = this.requests.get(segmentId); - if (existingRequest) { - existingRequest.engineRequest = engineRequest; + const requestItem = this.requests.get(segmentId); + if (requestItem) { + requestItem.engineRequest = engineRequest; } else { this.requests.set(segmentId, { segment, engineRequest, }); } - engineRequest.promise.finally(() => this.requests.delete(segmentId)); + engineRequest.promise.finally(() => { + const request = this.requests.get(segmentId); + delete request?.engineRequest; + if (request) this.clearRequest(request); + }); } get(segmentId: string) { @@ -99,15 +102,27 @@ export class RequestContainer { return count; } - abort(segmentId: string) { + abortEngineRequest(segmentId: string) { + const request = this.requests.get(segmentId); + if (!request) return; + + request.engineRequest?.onError(new AbortError()); + } + + abortLoaderRequest(segmentId: string) { const request = this.requests.get(segmentId); if (!request) return; - request.engineRequest?.onError(new Error("Aborted")); request.loaderRequest?.abort(); } - abortNotRequestedByEngine(isLocked: (segmentId: string) => boolean) { + private clearRequest(request: Request): void { + if (!request.engineRequest && !request.loaderRequest) { + this.requests.delete(request.segment.localId); + } + } + + abortAllNotRequestedByEngine(isLocked: (segmentId: string) => boolean) { for (const { loaderRequest, engineRequest, diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index c743d51c..d0d97a30 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -26,7 +26,7 @@ export class SegmentsMemoryStorage { }); } - async getSegment(segmentId: string): Promise { + async getSegmentData(segmentId: string): Promise { const cacheItem = this.cache.get(segmentId); if (cacheItem === undefined) return undefined; @@ -34,8 +34,8 @@ export class SegmentsMemoryStorage { return cacheItem.data; } - has(segmentId: string) { - return this.cache.has(segmentId); + async getStoredSegmentIds() { + return new Set([...this.cache.keys()]); } async clear(): Promise { diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index af8d2f38..da5cb1f2 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -8,7 +8,12 @@ import type { LoaderStats, } from "hls.js"; import * as Utils from "./utils"; -import { Core, FetchError, SegmentResponse } from "p2p-media-loader-core"; +import { + AbortError, + Core, + FetchError, + SegmentResponse, +} from "p2p-media-loader-core"; const DEFAULT_DOWNLOAD_LATENCY = 10; @@ -68,7 +73,7 @@ export class FragmentLoaderBase implements Loader { try { this.response = await this.core.loadSegment(this.segmentId); } catch (error) { - if (this.stats.aborted) return; + if (this.stats.aborted && error instanceof AbortError) return; return this.handleError(error); } if (!this.response) return; From 1cb735379c85eb81d9c525b4e6cefe5c1fc4348f Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 15 Sep 2023 17:59:47 +0300 Subject: [PATCH 032/127] Add streams map to hybrid loader. --- packages/p2p-media-loader-core/src/core.ts | 7 ++++- .../src/hybrid-loader.ts | 3 +- .../src/internal-types.ts | 4 +-- .../p2p-media-loader-core/src/peer-utils.ts | 11 ++++++++ packages/p2p-media-loader-core/src/peer.ts | 28 ++++++++----------- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index d975984a..d0491b1c 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -23,6 +23,7 @@ export class Core { }; private readonly bandwidthApproximator = new BandwidthApproximator(); private readonly mainStreamLoader = new HybridLoader( + this.streams, this.settings, this.bandwidthApproximator ); @@ -71,7 +72,11 @@ export class Core { } else { this.secondaryStreamLoader = this.secondaryStreamLoader ?? - new HybridLoader(this.settings, this.bandwidthApproximator); + new HybridLoader( + this.streams, + this.settings, + this.bandwidthApproximator + ); loader = this.secondaryStreamLoader; } return loader.loadSegment(segment, stream); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 379b2dc9..1e2687d2 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -17,6 +17,7 @@ export class HybridLoader { private lastQueueProcessingTimeStamp?: number; constructor( + private readonly segments: Map, private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator ) { @@ -32,7 +33,7 @@ export class HybridLoader { return Utils.isSegmentActual(segment, bufferRanges); }); - this.storageCleanUpIntervalId = setInterval( + this.storageCleanUpIntervalId = window.setInterval( () => this.segmentStorage.clear(), 1000 ); diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 90c17f01..0b23b779 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -28,7 +28,7 @@ export type BasePeerCommand = { c: T; }; -export type PeerSegmentCommand = +export type PeerSegmentRequestCommand = BasePeerCommand & { i: string; }; @@ -41,4 +41,4 @@ export type PeerSegmentMapCommand = m: JsonSegmentMap; }; -export type PeerCommand = PeerSegmentCommand | PeerSegmentMapCommand; +export type PeerCommand = PeerSegmentRequestCommand | PeerSegmentMapCommand; diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index 1cdec0ac..d9c9a7b9 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -3,6 +3,7 @@ import * as TypeGuard from "./type-guards"; import * as Util from "./utils"; import { PeerSegmentStatus } from "./enums"; import * as RIPEMD160 from "ripemd160"; +import { Segment } from "./types"; export function generatePeerId(): string { const PEER_ID_SYMBOLS = @@ -55,3 +56,13 @@ export function getSegmentsFromPeerSegmentMapCommand( } return segmentStatusMap; } + +export function getJsonSegmentsMapForCommand( + storedSegments: Map +): JsonSegmentMap { + const jsonMap: JsonSegmentMap = {}; + + for (const segment of storedSegments.values()) { + const { externalId } = segment; + } +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index a3c29a7b..f77347e7 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -1,6 +1,6 @@ import { PeerCandidate } from "bittorrent-tracker"; -import { PeerCommand } from "./internal-types"; -import { PeerSegmentStatus, PeerCommandType } from "./enums"; +import { PeerCommand, PeerSegmentRequestCommand } from "./internal-types"; +import { PeerCommandType, PeerSegmentStatus } from "./enums"; import * as PeerUtil from "./peer-utils"; export class Peer { @@ -22,24 +22,12 @@ export class Peer { } private onCandidateConnect(candidate: PeerCandidate) { - if (this.connection) { - candidate.destroy(); - return; - } this.connection = candidate; - - for (const candidate of this.candidates) { - if (candidate !== this.connection) { - candidate.destroy(); - this.candidates.delete(candidate); - } - } } private onCandidateClose(candidate: PeerCandidate) { - if (this.connection !== candidate) { - this.candidates.delete(candidate); - return; + if (this.connection === candidate) { + this.connection = undefined; } } @@ -67,4 +55,12 @@ export class Peer { if (!this.connection) return; this.connection.send(JSON.stringify(command)); } + + requestSegment(segmentExternalId: string) { + const command: PeerSegmentRequestCommand = { + c: PeerCommandType.SegmentRequest, + i: segmentExternalId, + }; + this.sendCommand(command); + } } From a18826ede867ee27699c174d50ddc3f07af2ee8d Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 18 Sep 2023 14:09:34 +0300 Subject: [PATCH 033/127] Add peer self segments map generation logic. --- packages/p2p-media-loader-core/src/core.ts | 11 ++--- .../src/hybrid-loader.ts | 42 +++++++++++++++++-- .../src/internal-types.ts | 2 +- .../p2p-media-loader-core/src/p2p-loader.ts | 30 ++++++++++--- .../p2p-media-loader-core/src/peer-utils.ts | 28 ++++++++++--- .../src/segments-storage.ts | 8 +++- packages/p2p-media-loader-core/src/utils.ts | 2 +- 7 files changed, 96 insertions(+), 27 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index d0491b1c..bf2d0284 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -22,8 +22,7 @@ export class Core { cachedSegmentsCount: 50, }; private readonly bandwidthApproximator = new BandwidthApproximator(); - private readonly mainStreamLoader = new HybridLoader( - this.streams, + private readonly mainStreamLoader: HybridLoader = new HybridLoader( this.settings, this.bandwidthApproximator ); @@ -31,6 +30,8 @@ export class Core { setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; + this.mainStreamLoader.setStreamManifestUrl(this.manifestResponseUrl); + this.secondaryStreamLoader?.setStreamManifestUrl(this.manifestResponseUrl); } hasSegment(segmentLocalId: string): boolean { @@ -72,11 +73,7 @@ export class Core { } else { this.secondaryStreamLoader = this.secondaryStreamLoader ?? - new HybridLoader( - this.streams, - this.settings, - this.bandwidthApproximator - ); + new HybridLoader(this.settings, this.bandwidthApproximator); loader = this.secondaryStreamLoader; } return loader.loadSegment(segment, stream); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 1e2687d2..7bcd702a 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,5 +1,6 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; import { HttpLoader } from "./http-loader"; +import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; @@ -7,7 +8,9 @@ import { Playback, QueueItem } from "./internal-types"; import * as Utils from "./utils"; export class HybridLoader { + private streamManifestUrl?: string; private readonly httpLoader = new HttpLoader(); + private p2pLoader?: P2PLoader; private readonly pluginRequests = new Map(); private readonly segmentStorage: SegmentsMemoryStorage; private storageCleanUpIntervalId?: number; @@ -17,7 +20,6 @@ export class HybridLoader { private lastQueueProcessingTimeStamp?: number; constructor( - private readonly segments: Map, private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator ) { @@ -39,6 +41,10 @@ export class HybridLoader { ); } + setStreamManifestUrl(url: string) { + this.streamManifestUrl = url; + } + async loadSegment( segment: Readonly, stream: Readonly @@ -46,7 +52,19 @@ export class HybridLoader { if (!this.playback) { this.playback = { position: segment.startTime, rate: 1 }; } - if (stream !== this.activeStream) this.activeStream = stream; + if (stream !== this.activeStream) { + this.activeStream = stream; + if (this.streamManifestUrl) { + const streamExternalId = Utils.getStreamExternalId( + stream, + this.streamManifestUrl + ); + this.p2pLoader = new P2PLoader( + streamExternalId, + this.getLoadedAndLoadingSegments.bind(this) + ); + } + } this.lastRequestedSegment = segment; this.processQueue(); @@ -61,7 +79,7 @@ export class HybridLoader { return request.responsePromise; } - private processQueue(force = true) { + private async processQueue(force = true) { if (!this.activeStream || !this.lastRequestedSegment || !this.playback) { return; } @@ -75,12 +93,13 @@ export class HybridLoader { } this.lastQueueProcessingTimeStamp = now; + const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); const { queue, queueSegmentIds } = Utils.generateQueue({ segment: this.lastRequestedSegment, stream: this.activeStream, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segmentId) => this.segmentStorage.has(segmentId), + isSegmentLoaded: (segmentId) => storedSegmentIds.has(segmentId), }); const bufferRanges = Utils.getLoadBufferRanges( @@ -189,6 +208,21 @@ export class HybridLoader { return request; } + private async getLoadedAndLoadingSegments() { + if (!this.streamManifestUrl || !this.activeStream) return; + const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); + const loaded: Segment[] = []; + + for (const id of storedSegmentIds) { + const segment = this.activeStream.segments.get(id); + if (!segment) continue; + + loaded.push(segment); + } + + return { loaded, httpLoading: [] }; + } + destroy() { clearInterval(this.storageCleanUpIntervalId); this.storageCleanUpIntervalId = undefined; diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 0b23b779..05ffb844 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -34,7 +34,7 @@ export type PeerSegmentRequestCommand = }; // {[streamId]: [segmentIds[]; segmentStatuses[]]} -export type JsonSegmentMap = { [key: string]: [string[], number[]] }; +export type JsonSegmentMap = { [key: string]: [number[], number[]] }; export type PeerSegmentMapCommand = BasePeerCommand & { diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index b6abcaad..e1864f57 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -2,20 +2,28 @@ import TrackerClient, { TrackerEventHandler } from "bittorrent-tracker"; import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./peer-utils"; +import { Segment } from "./types"; +import { JsonSegmentMap } from "./internal-types"; export class P2PLoader { - private streamId?: string; private streamHash?: string; private peerHash?: string; private trackerClient?: TrackerClient; private readonly peers = new Map(); + private segmentsMap?: JsonSegmentMap; - setStreamId(streamId: string) { - if (this.streamId === streamId) return; - - this.streamId = streamId; + constructor( + private readonly streamExternalId: string, + private readonly getLoadedSegments: () => Promise< + | { + loaded: Segment[]; + httpLoading: Segment[]; + } + | undefined + > + ) { const peerId = PeerUtil.generatePeerId(); - this.streamHash = getHash(streamId); + this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); this.trackerClient = new TrackerClient({ @@ -54,6 +62,16 @@ export class P2PLoader { }; private onTrackerWarning: TrackerEventHandler<"warning"> = (warning) => {}; private onTrackerError: TrackerEventHandler<"error"> = (error) => {}; + + private async updateSegmentMap() { + const { loaded = [], httpLoading = [] } = + (await this.getLoadedSegments()) ?? {}; + this.segmentsMap = PeerUtil.getJsonSegmentsMapForPeerCommand( + this.streamExternalId, + loaded, + httpLoading + ); + } } function getHash(data: string) { diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index d9c9a7b9..04676f88 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -50,19 +50,35 @@ export function getSegmentsFromPeerSegmentMapCommand( for (let i = 0; i < segmentIds.length; i++) { const segmentId = segmentIds[i]; const segmentStatus = statuses[i]; - const segmentFullId = Util.getSegmentFullExternalId(streamId, segmentId); + const segmentFullId = Util.getSegmentFullExternalId( + streamId, + segmentId.toString() + ); segmentStatusMap.set(segmentFullId, segmentStatus); } } return segmentStatusMap; } -export function getJsonSegmentsMapForCommand( - storedSegments: Map +export function getJsonSegmentsMapForPeerCommand( + streamExternalId: string, + storedSegments: Segment[], + loadingByHttpSegments: Segment[] ): JsonSegmentMap { - const jsonMap: JsonSegmentMap = {}; + const segmentIds: number[] = []; + const segmentStatuses: PeerSegmentStatus[] = []; - for (const segment of storedSegments.values()) { - const { externalId } = segment; + for (const segment of storedSegments) { + segmentIds.push(segment.externalId); + segmentStatuses.push(PeerSegmentStatus.Loaded); } + + for (const segment of loadingByHttpSegments) { + segmentIds.push(segment.externalId); + segmentStatuses.push(PeerSegmentStatus.LoadingByHttp); + } + + return { + [streamExternalId]: [segmentIds, segmentStatuses], + }; } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index c743d51c..1c2833a8 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -34,8 +34,12 @@ export class SegmentsMemoryStorage { return cacheItem.data; } - has(segmentId: string) { - return this.cache.has(segmentId); + async getStoredSegmentIds() { + const segmentIds = new Set(); + for (const segmentId of this.cache.keys()) { + segmentIds.add(segmentId); + } + return segmentIds; } async clear(): Promise { diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 6aa4c073..ea4f4d97 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -8,7 +8,7 @@ import { } from "./internal-types"; export function getStreamExternalId( - stream: Stream, + stream: Readonly, manifestResponseUrl: string ): string { const { type, index } = stream; From 70bff75b74229ad1b3899b548240776b287618ef Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 18 Sep 2023 16:22:42 +0300 Subject: [PATCH 034/127] Notify peers on segment loadings update. --- packages/p2p-media-loader-core/src/enums.ts | 2 +- .../src/hybrid-loader.ts | 19 +++---- .../src/internal-types.ts | 14 +++-- .../p2p-media-loader-core/src/p2p-loader.ts | 51 +++++++++++-------- .../p2p-media-loader-core/src/peer-utils.ts | 4 +- packages/p2p-media-loader-core/src/peer.ts | 29 ++++++++--- 6 files changed, 74 insertions(+), 45 deletions(-) diff --git a/packages/p2p-media-loader-core/src/enums.ts b/packages/p2p-media-loader-core/src/enums.ts index 42beee05..d260000e 100644 --- a/packages/p2p-media-loader-core/src/enums.ts +++ b/packages/p2p-media-loader-core/src/enums.ts @@ -1,5 +1,5 @@ export enum PeerCommandType { - SegmentMap, + SegmentsAnnouncement, SegmentRequest, } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7bcd702a..7233eae6 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -59,14 +59,12 @@ export class HybridLoader { stream, this.streamManifestUrl ); - this.p2pLoader = new P2PLoader( - streamExternalId, - this.getLoadedAndLoadingSegments.bind(this) - ); + this.p2pLoader = new P2PLoader(streamExternalId); + void this.updateSegmentsLoadingState(); } } this.lastRequestedSegment = segment; - this.processQueue(); + void this.processQueue(); const storageData = await this.segmentStorage.getSegment(segment.localId); if (storageData) { @@ -156,6 +154,7 @@ export class HybridLoader { if (!data) return; this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); + void this.updateSegmentsLoadingState(); const request = this.pluginRequests.get(segment.localId); if (request) { request.onSuccess({ @@ -186,7 +185,7 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; - this.processQueue(false); + void this.processQueue(false); } private createPluginSegmentRequest(segment: Segment) { @@ -208,8 +207,10 @@ export class HybridLoader { return request; } - private async getLoadedAndLoadingSegments() { - if (!this.streamManifestUrl || !this.activeStream) return; + private async updateSegmentsLoadingState() { + if (!this.streamManifestUrl || !this.activeStream || !this.p2pLoader) { + return; + } const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); const loaded: Segment[] = []; @@ -220,7 +221,7 @@ export class HybridLoader { loaded.push(segment); } - return { loaded, httpLoading: [] }; + void this.p2pLoader.updateSegmentsLoadingState(loaded, []); } destroy() { diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 05ffb844..752396cf 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -34,11 +34,15 @@ export type PeerSegmentRequestCommand = }; // {[streamId]: [segmentIds[]; segmentStatuses[]]} -export type JsonSegmentMap = { [key: string]: [number[], number[]] }; +export type JsonSegmentAnnouncementMap = { + [key: string]: [number[], number[]]; +}; -export type PeerSegmentMapCommand = - BasePeerCommand & { - m: JsonSegmentMap; +export type PeerSegmentAnnouncementCommand = + BasePeerCommand & { + m: JsonSegmentAnnouncementMap; }; -export type PeerCommand = PeerSegmentRequestCommand | PeerSegmentMapCommand; +export type PeerCommand = + | PeerSegmentRequestCommand + | PeerSegmentAnnouncementCommand; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index e1864f57..0771ba1a 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -3,25 +3,18 @@ import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./peer-utils"; import { Segment } from "./types"; -import { JsonSegmentMap } from "./internal-types"; +import { JsonSegmentAnnouncementMap } from "./internal-types"; export class P2PLoader { - private streamHash?: string; - private peerHash?: string; - private trackerClient?: TrackerClient; + private readonly streamExternalId: string; + private readonly streamHash: string; + private readonly peerHash: string; + private trackerClient: TrackerClient; private readonly peers = new Map(); - private segmentsMap?: JsonSegmentMap; + private announcementMap: JsonSegmentAnnouncementMap = {}; - constructor( - private readonly streamExternalId: string, - private readonly getLoadedSegments: () => Promise< - | { - loaded: Segment[]; - httpLoading: Segment[]; - } - | undefined - > - ) { + constructor(streamExternalId: string) { + this.streamExternalId = streamExternalId; const peerId = PeerUtil.generatePeerId(); this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); @@ -57,20 +50,34 @@ export class P2PLoader { private onTrackerUpdate: TrackerEventHandler<"update"> = (data) => {}; private onTrackerPeerConnect: TrackerEventHandler<"peer"> = (candidate) => { const peer = this.peers.get(candidate.id); - if (peer) peer.addCandidate(candidate); - else this.peers.set(candidate.id, new Peer(candidate)); + if (peer) { + peer.addCandidate(candidate); + } else { + const peer = new Peer(this.streamExternalId, candidate); + this.peers.set(candidate.id, peer); + } }; private onTrackerWarning: TrackerEventHandler<"warning"> = (warning) => {}; private onTrackerError: TrackerEventHandler<"error"> = (error) => {}; - private async updateSegmentMap() { - const { loaded = [], httpLoading = [] } = - (await this.getLoadedSegments()) ?? {}; - this.segmentsMap = PeerUtil.getJsonSegmentsMapForPeerCommand( + updateSegmentsLoadingState(loaded: Segment[], loading: Segment[]) { + this.announcementMap = PeerUtil.getJsonSegmentsAnnouncementMap( this.streamExternalId, loaded, - httpLoading + loading ); + this.broadcastSegmentAnnouncement(); + } + + sendSegmentsAnnouncementToPeer(peer: Peer) { + if (!peer?.isConnected) return; + peer.sendSegmentsAnnouncement(this.announcementMap); + } + + broadcastSegmentAnnouncement() { + for (const peer of this.peers.values()) { + this.sendSegmentsAnnouncementToPeer(peer); + } } } diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index 04676f88..58aef782 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -42,7 +42,7 @@ export function getPeerCommandFromArrayBuffer( } } -export function getSegmentsFromPeerSegmentMapCommand( +export function getSegmentsFromPeerAnnouncementMap( map: JsonSegmentMap ): Map { const segmentStatusMap = new Map(); @@ -60,7 +60,7 @@ export function getSegmentsFromPeerSegmentMapCommand( return segmentStatusMap; } -export function getJsonSegmentsMapForPeerCommand( +export function getJsonSegmentsAnnouncementMap( streamExternalId: string, storedSegments: Segment[], loadingByHttpSegments: Segment[] diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index f77347e7..a3090216 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -1,19 +1,30 @@ import { PeerCandidate } from "bittorrent-tracker"; -import { PeerCommand, PeerSegmentRequestCommand } from "./internal-types"; +import { + JsonSegmentAnnouncementMap, + PeerCommand, + PeerSegmentRequestCommand, + PeerSegmentAnnouncementCommand, +} from "./internal-types"; import { PeerCommandType, PeerSegmentStatus } from "./enums"; import * as PeerUtil from "./peer-utils"; export class Peer { readonly id: string; + private readonly streamExternalId: string; private readonly candidates = new Set(); private connection?: PeerCandidate; private segments = new Map(); - constructor(candidate: PeerCandidate) { + constructor(streamExternalId: string, candidate: PeerCandidate) { + this.streamExternalId = streamExternalId; this.id = candidate.id; this.addCandidate(candidate); } + get isConnected() { + return !!this.connection; + } + addCandidate(candidate: PeerCandidate) { candidate.on("connect", () => this.onCandidateConnect(candidate)); candidate.on("close", () => this.onCandidateClose(candidate)); @@ -40,10 +51,8 @@ export class Peer { private handleCommand(command: PeerCommand) { switch (command.c) { - case PeerCommandType.SegmentMap: - this.segments = PeerUtil.getSegmentsFromPeerSegmentMapCommand( - command.m - ); + case PeerCommandType.SegmentsAnnouncement: + this.segments = PeerUtil.getSegmentsFromPeerAnnouncementMap(command.m); break; case PeerCommandType.SegmentRequest: @@ -63,4 +72,12 @@ export class Peer { }; this.sendCommand(command); } + + sendSegmentsAnnouncement(map: JsonSegmentAnnouncementMap) { + const command: PeerSegmentAnnouncementCommand = { + c: PeerCommandType.SegmentsAnnouncement, + m: map, + }; + this.sendCommand(command); + } } From f61b852c72a3f5f84651203012626fe814fdebf0 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 18 Sep 2023 16:25:07 +0300 Subject: [PATCH 035/127] Fix types issues. --- .../p2p-media-loader-core/src/peer-utils.ts | 6 +++--- .../p2p-media-loader-core/src/type-guards.ts | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index 58aef782..dd51de21 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -1,4 +1,4 @@ -import { JsonSegmentMap, PeerCommand } from "./internal-types"; +import { JsonSegmentAnnouncementMap, PeerCommand } from "./internal-types"; import * as TypeGuard from "./type-guards"; import * as Util from "./utils"; import { PeerSegmentStatus } from "./enums"; @@ -43,7 +43,7 @@ export function getPeerCommandFromArrayBuffer( } export function getSegmentsFromPeerAnnouncementMap( - map: JsonSegmentMap + map: JsonSegmentAnnouncementMap ): Map { const segmentStatusMap = new Map(); for (const [streamId, [segmentIds, statuses]] of Object.entries(map)) { @@ -64,7 +64,7 @@ export function getJsonSegmentsAnnouncementMap( streamExternalId: string, storedSegments: Segment[], loadingByHttpSegments: Segment[] -): JsonSegmentMap { +): JsonSegmentAnnouncementMap { const segmentIds: number[] = []; const segmentStatuses: PeerSegmentStatus[] = []; diff --git a/packages/p2p-media-loader-core/src/type-guards.ts b/packages/p2p-media-loader-core/src/type-guards.ts index eddae249..64bfa16a 100644 --- a/packages/p2p-media-loader-core/src/type-guards.ts +++ b/packages/p2p-media-loader-core/src/type-guards.ts @@ -1,20 +1,25 @@ import { - PeerSegmentCommand, + PeerSegmentRequestCommand, PeerCommand, - PeerCommandType, - PeerSegmentMapCommand, + PeerSegmentAnnouncementCommand, } from "./internal-types"; +import { PeerCommandType } from "./enums"; export function isPeerSegmentCommand( command: object -): command is PeerSegmentCommand { - return (command as PeerSegmentCommand).c === PeerCommandType.SegmentRequest; +): command is PeerSegmentRequestCommand { + return ( + (command as PeerSegmentRequestCommand).c === PeerCommandType.SegmentRequest + ); } export function isPeerSegmentMapCommand( command: object -): command is PeerSegmentMapCommand { - return (command as PeerSegmentMapCommand).c === PeerCommandType.SegmentMap; +): command is PeerSegmentAnnouncementCommand { + return ( + (command as PeerSegmentAnnouncementCommand).c === + PeerCommandType.SegmentsAnnouncement + ); } export function isPeerCommand(command: object): command is PeerCommand { From 3305c1963d614688becf3f829290b1b890c789b0 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 18 Sep 2023 18:35:06 +0300 Subject: [PATCH 036/127] Add peer events. --- packages/p2p-media-loader-core/src/enums.ts | 2 + .../src/hybrid-loader.ts | 36 ++--- .../src/internal-types.ts | 22 ++- .../p2p-media-loader-core/src/p2p-loader.ts | 133 ++++++++++++------ packages/p2p-media-loader-core/src/peer.ts | 53 ++++++- .../src/segments-storage.ts | 10 ++ 6 files changed, 173 insertions(+), 83 deletions(-) diff --git a/packages/p2p-media-loader-core/src/enums.ts b/packages/p2p-media-loader-core/src/enums.ts index d260000e..62114ad3 100644 --- a/packages/p2p-media-loader-core/src/enums.ts +++ b/packages/p2p-media-loader-core/src/enums.ts @@ -1,6 +1,8 @@ export enum PeerCommandType { SegmentsAnnouncement, SegmentRequest, + SegmentData, + SegmentAbsent, } export enum PeerSegmentStatus { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7233eae6..c9ceca27 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -45,6 +45,15 @@ export class HybridLoader { this.streamManifestUrl = url; } + private createP2PLoader(stream: StreamWithSegments) { + if (!this.streamManifestUrl) return; + this.p2pLoader = new P2PLoader( + this.streamManifestUrl, + stream, + this.segmentStorage + ); + } + async loadSegment( segment: Readonly, stream: Readonly @@ -54,14 +63,7 @@ export class HybridLoader { } if (stream !== this.activeStream) { this.activeStream = stream; - if (this.streamManifestUrl) { - const streamExternalId = Utils.getStreamExternalId( - stream, - this.streamManifestUrl - ); - this.p2pLoader = new P2PLoader(streamExternalId); - void this.updateSegmentsLoadingState(); - } + this.createP2PLoader(stream); } this.lastRequestedSegment = segment; void this.processQueue(); @@ -154,7 +156,6 @@ export class HybridLoader { if (!data) return; this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); - void this.updateSegmentsLoadingState(); const request = this.pluginRequests.get(segment.localId); if (request) { request.onSuccess({ @@ -207,23 +208,6 @@ export class HybridLoader { return request; } - private async updateSegmentsLoadingState() { - if (!this.streamManifestUrl || !this.activeStream || !this.p2pLoader) { - return; - } - const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); - const loaded: Segment[] = []; - - for (const id of storedSegmentIds) { - const segment = this.activeStream.segments.get(id); - if (!segment) continue; - - loaded.push(segment); - } - - void this.p2pLoader.updateSegmentsLoadingState(loaded, []); - } - destroy() { clearInterval(this.storageCleanUpIntervalId); this.storageCleanUpIntervalId = undefined; diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 752396cf..e55126f9 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -28,21 +28,29 @@ export type BasePeerCommand = { c: T; }; -export type PeerSegmentRequestCommand = - BasePeerCommand & { - i: string; - }; - // {[streamId]: [segmentIds[]; segmentStatuses[]]} export type JsonSegmentAnnouncementMap = { [key: string]: [number[], number[]]; }; +export type PeerSegmentCommand = BasePeerCommand< + PeerCommandType.SegmentRequest | PeerCommandType.SegmentAbsent +> & { + i: string; +}; + export type PeerSegmentAnnouncementCommand = BasePeerCommand & { m: JsonSegmentAnnouncementMap; }; +export type PeerSendSegmentCommand = + BasePeerCommand & { + i: string; + s: number; + }; + export type PeerCommand = - | PeerSegmentRequestCommand - | PeerSegmentAnnouncementCommand; + | PeerSegmentCommand + | PeerSegmentAnnouncementCommand + | PeerSendSegmentCommand; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 0771ba1a..007a6fb6 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -1,82 +1,97 @@ -import TrackerClient, { TrackerEventHandler } from "bittorrent-tracker"; +import TrackerClient, { PeerCandidate } from "bittorrent-tracker"; import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./peer-utils"; -import { Segment } from "./types"; +import { Segment, StreamWithSegments } from "./types"; import { JsonSegmentAnnouncementMap } from "./internal-types"; +import { SegmentsMemoryStorage } from "./segments-storage"; +import * as Utils from "./utils"; export class P2PLoader { private readonly streamExternalId: string; private readonly streamHash: string; private readonly peerHash: string; - private trackerClient: TrackerClient; + private readonly trackerClient: TrackerClient; private readonly peers = new Map(); private announcementMap: JsonSegmentAnnouncementMap = {}; - constructor(streamExternalId: string) { - this.streamExternalId = streamExternalId; + constructor( + private streamManifestUrl: string, + private readonly stream: StreamWithSegments, + private readonly segmentStorage: SegmentsMemoryStorage + ) { const peerId = PeerUtil.generatePeerId(); + this.streamExternalId = Utils.getStreamExternalId( + this.stream, + this.streamManifestUrl + ); this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); - this.trackerClient = new TrackerClient({ - infoHash: this.streamHash, - peerId: this.peerHash, - port: 6881, - announce: [ - "wss://tracker.novage.com.ua", - "wss://tracker.openwebtorrent.com", - ], - rtcConfig: { - iceServers: [ - { - urls: [ - "stun:stun.l.google.com:19302", - "stun:global.stun.twilio.com:3478", - ], - }, - ], - }, + this.trackerClient = createTrackerClient({ + streamHash: this.streamHash, + peerHash: this.peerHash, }); + this.subscribeOnTrackerEvents(this.trackerClient); + this.segmentStorage.subscribeOnUpdate( + this.onSegmentStorageUpdate.bind(this) + ); + this.trackerClient.start(); + } - this.trackerClient.on("update", this.onTrackerUpdate); - this.trackerClient.on("peer", this.onTrackerPeerConnect); - this.trackerClient.on("warning", this.onTrackerWarning); - this.trackerClient.on("error", this.onTrackerError); + private subscribeOnTrackerEvents(trackerClient: TrackerClient) { + // TODO: tracker event handlers + trackerClient.on("update", () => {}); + trackerClient.on("peer", (candidate) => { + const peer = this.peers.get(candidate.id); + if (peer) peer.addCandidate(candidate); + else this.createPeer(candidate); + }); + trackerClient.on("warning", (warning) => {}); + trackerClient.on("error", (error) => {}); + } - this.trackerClient.start(); + private createPeer(candidate: PeerCandidate) { + const peer = new Peer(candidate, { + onPeerConnected: this.onPeerConnected.bind(this), + onSegmentRequested: this.onSegmentRequested.bind(this), + }); + this.peers.set(candidate.id, peer); } - private onTrackerUpdate: TrackerEventHandler<"update"> = (data) => {}; - private onTrackerPeerConnect: TrackerEventHandler<"peer"> = (candidate) => { - const peer = this.peers.get(candidate.id); - if (peer) { - peer.addCandidate(candidate); - } else { - const peer = new Peer(this.streamExternalId, candidate); - this.peers.set(candidate.id, peer); + private async onSegmentStorageUpdate() { + const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); + const loaded: Segment[] = []; + + for (const id of storedSegmentIds) { + const segment = this.stream.segments.get(id); + if (!segment) continue; + + loaded.push(segment); } - }; - private onTrackerWarning: TrackerEventHandler<"warning"> = (warning) => {}; - private onTrackerError: TrackerEventHandler<"error"> = (error) => {}; - updateSegmentsLoadingState(loaded: Segment[], loading: Segment[]) { this.announcementMap = PeerUtil.getJsonSegmentsAnnouncementMap( this.streamExternalId, loaded, - loading + [] ); this.broadcastSegmentAnnouncement(); } - sendSegmentsAnnouncementToPeer(peer: Peer) { - if (!peer?.isConnected) return; + private onPeerConnected(peer: Peer) { peer.sendSegmentsAnnouncement(this.announcementMap); } - broadcastSegmentAnnouncement() { + private async onSegmentRequested(peer: Peer, segmentExternalId: string) { + const segmentData = await this.segmentStorage.getSegment(segmentExternalId); + if (segmentData) peer.sendSegmentData(segmentExternalId, segmentData); + else peer.sendSegmentAbsent(segmentExternalId); + } + + private broadcastSegmentAnnouncement() { for (const peer of this.peers.values()) { - this.sendSegmentsAnnouncementToPeer(peer); + if (!peer.isConnected) continue; + peer.sendSegmentsAnnouncement(this.announcementMap); } } } @@ -84,3 +99,31 @@ export class P2PLoader { function getHash(data: string) { return new RIPEMD160().update(data).digest("hex"); } + +function createTrackerClient({ + streamHash, + peerHash, +}: { + streamHash: string; + peerHash: string; +}) { + return new TrackerClient({ + infoHash: streamHash, + peerId: peerHash, + port: 6881, + announce: [ + "wss://tracker.novage.com.ua", + "wss://tracker.openwebtorrent.com", + ], + rtcConfig: { + iceServers: [ + { + urls: [ + "stun:stun.l.google.com:19302", + "stun:global.stun.twilio.com:3478", + ], + }, + ], + }, + }); +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index a3090216..09e869f7 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -2,22 +2,30 @@ import { PeerCandidate } from "bittorrent-tracker"; import { JsonSegmentAnnouncementMap, PeerCommand, - PeerSegmentRequestCommand, PeerSegmentAnnouncementCommand, + PeerSegmentCommand, + PeerSendSegmentCommand, } from "./internal-types"; import { PeerCommandType, PeerSegmentStatus } from "./enums"; import * as PeerUtil from "./peer-utils"; +const webRtcMaxMessageSize: number = 64 * 1024 - 1; + +type PeerEventHandlers = { + onPeerConnected: (peer: Peer) => void; + onSegmentRequested: (peer: Peer, segmentId: string) => void; +}; + export class Peer { readonly id: string; - private readonly streamExternalId: string; private readonly candidates = new Set(); private connection?: PeerCandidate; + private readonly eventHandlers: PeerEventHandlers; private segments = new Map(); - constructor(streamExternalId: string, candidate: PeerCandidate) { - this.streamExternalId = streamExternalId; + constructor(candidate: PeerCandidate, eventHandlers: PeerEventHandlers) { this.id = candidate.id; + this.eventHandlers = eventHandlers; this.addCandidate(candidate); } @@ -34,6 +42,7 @@ export class Peer { private onCandidateConnect(candidate: PeerCandidate) { this.connection = candidate; + this.eventHandlers.onPeerConnected(this); } private onCandidateClose(candidate: PeerCandidate) { @@ -56,6 +65,7 @@ export class Peer { break; case PeerCommandType.SegmentRequest: + this.eventHandlers.onSegmentRequested(this, command.i); break; } } @@ -66,7 +76,7 @@ export class Peer { } requestSegment(segmentExternalId: string) { - const command: PeerSegmentRequestCommand = { + const command: PeerSegmentCommand = { c: PeerCommandType.SegmentRequest, i: segmentExternalId, }; @@ -80,4 +90,37 @@ export class Peer { }; this.sendCommand(command); } + + sendSegmentData(segmentExternalId: string, data: ArrayBuffer) { + if (!this.connection) return; + const command: PeerSendSegmentCommand = { + c: PeerCommandType.SegmentData, + i: segmentExternalId, + s: data.byteLength, + }; + + this.sendCommand(command); + + let bytesLeft = data.byteLength; + while (bytesLeft > 0) { + const bytesToSend = + bytesLeft >= webRtcMaxMessageSize ? webRtcMaxMessageSize : bytesLeft; + const buffer = Buffer.from( + data, + data.byteLength - bytesLeft, + bytesToSend + ); + + this.connection.send(buffer); + bytesLeft -= bytesToSend; + } + } + + sendSegmentAbsent(segmentExternalId: string) { + const command: PeerSegmentCommand = { + c: PeerCommandType.SegmentAbsent, + i: segmentExternalId, + }; + this.sendCommand(command); + } } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 1c2833a8..3ec10acc 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -6,6 +6,7 @@ export class SegmentsMemoryStorage { { segment: Segment; data: ArrayBuffer; lastAccessed: number } >(); private isSegmentLockedPredicate?: (segment: Segment) => boolean; + private onUpdateSubscriptions: (() => void)[] = []; constructor( private settings: { @@ -18,12 +19,17 @@ export class SegmentsMemoryStorage { this.isSegmentLockedPredicate = predicate; } + subscribeOnUpdate(callback: () => void) { + this.onUpdateSubscriptions.push(callback); + } + async storeSegment(segment: Segment, data: ArrayBuffer) { this.cache.set(segment.localId, { segment, data, lastAccessed: performance.now(), }); + this.onUpdateSubscriptions.forEach((c) => c()); } async getSegment(segmentId: string): Promise { @@ -78,10 +84,14 @@ export class SegmentsMemoryStorage { } segmentsToDelete.forEach((id) => this.cache.delete(id)); + if (segmentsToDelete.length) { + this.onUpdateSubscriptions.forEach((c) => c()); + } return segmentsToDelete.length > 0; } public async destroy() { this.cache.clear(); + this.onUpdateSubscriptions = []; } } From a2cee74b35f01214850ac243397ea7f6fff89f08 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 18 Sep 2023 20:11:30 +0300 Subject: [PATCH 037/127] Add downloadSegment method to p2p-loader. Use p2p request type in Peer class. --- .../src/hybrid-loader.ts | 21 +--------- .../p2p-media-loader-core/src/p2p-loader.ts | 26 ++++++++++++- packages/p2p-media-loader-core/src/peer.ts | 38 ++++++++++++++++++- packages/p2p-media-loader-core/src/request.ts | 2 +- packages/p2p-media-loader-core/src/utils.ts | 17 +++++++++ 5 files changed, 81 insertions(+), 23 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 90810f7d..3e1c9d72 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -7,7 +7,7 @@ import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestContainer } from "./request"; import * as Utils from "./utils"; -import { AbortError, FetchError } from "./errors"; +import { FetchError } from "./errors"; export class HybridLoader { private streamManifestUrl?: string; @@ -78,7 +78,7 @@ export class HybridLoader { bandwidth: this.bandwidthApproximator.getBandwidth(), }; } - const request = getControlledPromise(); + const request = Utils.getControlledPromise(); this.requests.addEngineRequest(segment, request); return request.promise; } @@ -184,23 +184,6 @@ export class HybridLoader { } } -function getControlledPromise() { - let onSuccess: (value: T) => void; - let onError: (reason?: unknown) => void; - const promise = new Promise((resolve, reject) => { - onSuccess = resolve; - onError = reject; - }); - - return { - promise, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onSuccess: onSuccess!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onError: onError!, - }; -} - function* arrayBackwards(arr: T[]) { for (let i = arr.length - 1; i >= 0; i--) { yield arr[i]; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 007a6fb6..e1ef6be5 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -6,6 +6,7 @@ import { Segment, StreamWithSegments } from "./types"; import { JsonSegmentAnnouncementMap } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils"; +import { PeerSegmentStatus } from "./enums"; export class P2PLoader { private readonly streamExternalId: string; @@ -51,6 +52,27 @@ export class P2PLoader { trackerClient.on("error", (error) => {}); } + async downloadSegment(segment: Segment): Promise { + const segmentExternalId = segment.externalId.toString(); + const peerWithSegment: Peer[] = []; + + for (const peer of this.peers.values()) { + if ( + !peer.downloadingSegment && + peer.getSegmentStatus(segmentExternalId) === PeerSegmentStatus.Loaded + ) { + peerWithSegment.push(peer); + } + } + + if (peerWithSegment.length === 0) return false; + + const peer = + peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; + const request = peer.downloadSegment(segment); + return request.promise; + } + private createPeer(candidate: PeerCandidate) { const peer = new Peer(candidate, { onPeerConnected: this.onPeerConnected.bind(this), @@ -83,7 +105,9 @@ export class P2PLoader { } private async onSegmentRequested(peer: Peer, segmentExternalId: string) { - const segmentData = await this.segmentStorage.getSegment(segmentExternalId); + const segmentData = await this.segmentStorage.getSegmentData( + segmentExternalId + ); if (segmentData) peer.sendSegmentData(segmentExternalId, segmentData); else peer.sendSegmentAbsent(segmentExternalId); } diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 09e869f7..aefa5567 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -8,6 +8,10 @@ import { } from "./internal-types"; import { PeerCommandType, PeerSegmentStatus } from "./enums"; import * as PeerUtil from "./peer-utils"; +import { P2PRequest } from "./request"; +import { Segment } from "./types"; +import * as Utils from "./utils"; +import { AbortError } from "./errors"; const webRtcMaxMessageSize: number = 64 * 1024 - 1; @@ -22,6 +26,11 @@ export class Peer { private connection?: PeerCandidate; private readonly eventHandlers: PeerEventHandlers; private segments = new Map(); + private request?: { + segment: Segment; + p2pRequest: P2PRequest; + onSuccess: (data: ArrayBuffer) => void; + }; constructor(candidate: PeerCandidate, eventHandlers: PeerEventHandlers) { this.id = candidate.id; @@ -33,6 +42,14 @@ export class Peer { return !!this.connection; } + get downloadingSegment(): Segment | undefined { + return this.request?.segment; + } + + getSegmentStatus(segmentExternalId: string): PeerSegmentStatus | undefined { + return this.segments.get(segmentExternalId); + } + addCandidate(candidate: PeerCandidate) { candidate.on("connect", () => this.onCandidateConnect(candidate)); candidate.on("close", () => this.onCandidateClose(candidate)); @@ -75,12 +92,29 @@ export class Peer { this.connection.send(JSON.stringify(command)); } - requestSegment(segmentExternalId: string) { + downloadSegment(segment: Segment) { + const { externalId } = segment; const command: PeerSegmentCommand = { c: PeerCommandType.SegmentRequest, - i: segmentExternalId, + i: externalId.toString(), + }; + const { promise, onSuccess, onError } = + Utils.getControlledPromise(); + this.request = { + segment, + p2pRequest: { + type: "p2p", + promise, + abort: () => { + onError(new AbortError()); + this.request = undefined; + }, + }, + onSuccess, }; this.sendCommand(command); + + return this.request.p2pRequest; } sendSegmentsAnnouncement(map: JsonSegmentAnnouncementMap) { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index dc90a42b..a582447f 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -16,7 +16,7 @@ export type HttpRequest = RequestBase & { type: "http"; }; -type P2PRequest = RequestBase & { +export type P2PRequest = RequestBase & { type: "p2p"; }; diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 84925033..210e5958 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -143,3 +143,20 @@ export function isSegmentActual( return isInRange(startTime) || isInRange(endTime); } + +export function getControlledPromise() { + let onSuccess: (value: T) => void; + let onError: (reason?: unknown) => void; + const promise = new Promise((resolve, reject) => { + onSuccess = resolve; + onError = reject; + }); + + return { + promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onSuccess: onSuccess!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onError: onError!, + }; +} From b93677ad4b1c9f9a0a66828ba40fb9f6fcc78d64 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 12:10:25 +0300 Subject: [PATCH 038/127] Use request container in p2p-loader. --- .../src/hybrid-loader.ts | 12 ++++- .../p2p-media-loader-core/src/p2p-loader.ts | 20 +++++++-- packages/p2p-media-loader-core/src/peer.ts | 44 ++++++++++++------- packages/p2p-media-loader-core/src/request.ts | 11 +++-- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 3e1c9d72..6cfece47 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -51,6 +51,7 @@ export class HybridLoader { this.p2pLoader = new P2PLoader( this.streamManifestUrl, stream, + this.requests, this.segmentStorage ); } @@ -142,7 +143,16 @@ export class HybridLoader { // TODO: handle error } } - if (!data) return; + if (data) this.handleSegmentLoaded(segment, data); + } + + private async loadThroughP2P(segment: Segment) { + if (!this.p2pLoader) return; + const data = await this.p2pLoader.downloadSegment(segment); + if (data) this.handleSegmentLoaded(segment, data); + } + + private handleSegmentLoaded(segment: Segment, data: ArrayBuffer) { this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); this.requests.resolveEngineRequest(segment.localId, { diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index e1ef6be5..3dec9832 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -7,6 +7,7 @@ import { JsonSegmentAnnouncementMap } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils"; import { PeerSegmentStatus } from "./enums"; +import { RequestContainer } from "./request"; export class P2PLoader { private readonly streamExternalId: string; @@ -19,6 +20,7 @@ export class P2PLoader { constructor( private streamManifestUrl: string, private readonly stream: StreamWithSegments, + private readonly requests: RequestContainer, private readonly segmentStorage: SegmentsMemoryStorage ) { const peerId = PeerUtil.generatePeerId(); @@ -52,7 +54,7 @@ export class P2PLoader { trackerClient.on("error", (error) => {}); } - async downloadSegment(segment: Segment): Promise { + async downloadSegment(segment: Segment): Promise { const segmentExternalId = segment.externalId.toString(); const peerWithSegment: Peer[] = []; @@ -65,11 +67,12 @@ export class P2PLoader { } } - if (peerWithSegment.length === 0) return false; + if (peerWithSegment.length === 0) return undefined; const peer = peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; - const request = peer.downloadSegment(segment); + const request = peer.requestSegment(segment); + this.requests.addLoaderRequest(segment, request); return request.promise; } @@ -84,6 +87,7 @@ export class P2PLoader { private async onSegmentStorageUpdate() { const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); const loaded: Segment[] = []; + const httpLoading: Segment[] = []; for (const id of storedSegmentIds) { const segment = this.stream.segments.get(id); @@ -92,10 +96,18 @@ export class P2PLoader { loaded.push(segment); } + for (const request of this.requests.values()) { + if (request.loaderRequest?.type !== "http") continue; + const segment = this.stream.segments.get(request.segment.localId); + if (!segment) continue; + + httpLoading.push(segment); + } + this.announcementMap = PeerUtil.getJsonSegmentsAnnouncementMap( this.streamExternalId, loaded, - [] + httpLoading ); this.broadcastSegmentAnnouncement(); } diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index aefa5567..ce920283 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -20,17 +20,21 @@ type PeerEventHandlers = { onSegmentRequested: (peer: Peer, segmentId: string) => void; }; +type PeerRequest = { + segment: Segment; + p2pRequest: P2PRequest; + onSuccess: (data: ArrayBuffer) => void; + bytesDownloaded: number; + pieces: ArrayBuffer[]; +}; + export class Peer { readonly id: string; private readonly candidates = new Set(); private connection?: PeerCandidate; private readonly eventHandlers: PeerEventHandlers; private segments = new Map(); - private request?: { - segment: Segment; - p2pRequest: P2PRequest; - onSuccess: (data: ArrayBuffer) => void; - }; + private request?: PeerRequest; constructor(candidate: PeerCandidate, eventHandlers: PeerEventHandlers) { this.id = candidate.id; @@ -38,6 +42,13 @@ export class Peer { this.addCandidate(candidate); } + addCandidate(candidate: PeerCandidate) { + candidate.on("connect", () => this.onCandidateConnect(candidate)); + candidate.on("close", () => this.onCandidateClose(candidate)); + candidate.on("data", () => this.onReceiveData.bind(this)); + this.candidates.add(candidate); + } + get isConnected() { return !!this.connection; } @@ -50,22 +61,13 @@ export class Peer { return this.segments.get(segmentExternalId); } - addCandidate(candidate: PeerCandidate) { - candidate.on("connect", () => this.onCandidateConnect(candidate)); - candidate.on("close", () => this.onCandidateClose(candidate)); - candidate.on("data", () => this.onReceiveData.bind(this)); - this.candidates.add(candidate); - } - private onCandidateConnect(candidate: PeerCandidate) { this.connection = candidate; this.eventHandlers.onPeerConnected(this); } private onCandidateClose(candidate: PeerCandidate) { - if (this.connection === candidate) { - this.connection = undefined; - } + if (this.connection === candidate) this.connection = undefined; } private onReceiveData(data: ArrayBuffer) { @@ -84,6 +86,9 @@ export class Peer { case PeerCommandType.SegmentRequest: this.eventHandlers.onSegmentRequested(this, command.i); break; + + case PeerCommandType.SegmentData: + break; } } @@ -92,7 +97,10 @@ export class Peer { this.connection.send(JSON.stringify(command)); } - downloadSegment(segment: Segment) { + requestSegment(segment: Segment) { + if (this.request) { + throw new Error("Segment already is downloading"); + } const { externalId } = segment; const command: PeerSegmentCommand = { c: PeerCommandType.SegmentRequest, @@ -102,6 +110,9 @@ export class Peer { Utils.getControlledPromise(); this.request = { segment, + onSuccess, + bytesDownloaded: 0, + pieces: [], p2pRequest: { type: "p2p", promise, @@ -110,7 +121,6 @@ export class Peer { this.request = undefined; }, }, - onSuccess, }; this.sendCommand(command); diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index a582447f..42082f1e 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -45,7 +45,7 @@ export class RequestContainer { loaderRequest.promise.finally(() => { const request = this.requests.get(segmentId); delete request?.loaderRequest; - if (request) this.clearRequest(request); + if (request) this.clearRequestItem(request); }); } @@ -63,7 +63,7 @@ export class RequestContainer { engineRequest.promise.finally(() => { const request = this.requests.get(segmentId); delete request?.engineRequest; - if (request) this.clearRequest(request); + if (request) this.clearRequestItem(request); }); } @@ -113,10 +113,13 @@ export class RequestContainer { const request = this.requests.get(segmentId); if (!request) return; - request.loaderRequest?.abort(); + if (request.loaderRequest) { + request.loaderRequest.abort(); + request.engineRequest?.onError(new AbortError()); + } } - private clearRequest(request: Request): void { + private clearRequestItem(request: Request): void { if (!request.engineRequest && !request.loaderRequest) { this.requests.delete(request.segment.localId); } From 098b10a429c7a4ae764d4b6dfe58a519160d9599 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 16:10:06 +0300 Subject: [PATCH 039/127] Add receiving segment chunks logic. --- packages/p2p-media-loader-core/src/enums.ts | 1 + .../src/internal-types.ts | 4 +- packages/p2p-media-loader-core/src/peer.ts | 162 ++++++++++++++---- packages/p2p-media-loader-core/src/utils.ts | 14 +- 4 files changed, 135 insertions(+), 46 deletions(-) diff --git a/packages/p2p-media-loader-core/src/enums.ts b/packages/p2p-media-loader-core/src/enums.ts index 62114ad3..059691b4 100644 --- a/packages/p2p-media-loader-core/src/enums.ts +++ b/packages/p2p-media-loader-core/src/enums.ts @@ -3,6 +3,7 @@ export enum PeerCommandType { SegmentRequest, SegmentData, SegmentAbsent, + CancelSegmentRequest, } export enum PeerSegmentStatus { diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index e55126f9..68b01deb 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -34,7 +34,9 @@ export type JsonSegmentAnnouncementMap = { }; export type PeerSegmentCommand = BasePeerCommand< - PeerCommandType.SegmentRequest | PeerCommandType.SegmentAbsent + | PeerCommandType.SegmentRequest + | PeerCommandType.SegmentAbsent + | PeerCommandType.CancelSegmentRequest > & { i: string; }; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index ce920283..e7684e3f 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -13,7 +13,9 @@ import { Segment } from "./types"; import * as Utils from "./utils"; import { AbortError } from "./errors"; +// TODO: add to settings const webRtcMaxMessageSize: number = 64 * 1024 - 1; +const p2pSegmentDownloadTimeout = 1000; type PeerEventHandlers = { onPeerConnected: (peer: Peer) => void; @@ -23,9 +25,12 @@ type PeerEventHandlers = { type PeerRequest = { segment: Segment; p2pRequest: P2PRequest; - onSuccess: (data: ArrayBuffer) => void; + resolve: (data: ArrayBuffer) => void; + reject: (reason: unknown) => void; bytesDownloaded: number; - pieces: ArrayBuffer[]; + chunks: ArrayBuffer[]; + segmentByteLength?: number; + responseTimeoutId: number; }; export class Peer { @@ -35,6 +40,7 @@ export class Peer { private readonly eventHandlers: PeerEventHandlers; private segments = new Map(); private request?: PeerRequest; + private isSendingData = false; constructor(candidate: PeerCandidate, eventHandlers: PeerEventHandlers) { this.id = candidate.id; @@ -43,8 +49,13 @@ export class Peer { } addCandidate(candidate: PeerCandidate) { - candidate.on("connect", () => this.onCandidateConnect(candidate)); - candidate.on("close", () => this.onCandidateClose(candidate)); + candidate.on("connect", () => { + this.connection = candidate; + this.eventHandlers.onPeerConnected(this); + }); + candidate.on("close", () => { + if (this.connection === candidate) this.connection = undefined; + }); candidate.on("data", () => this.onReceiveData.bind(this)); this.candidates.add(candidate); } @@ -61,23 +72,13 @@ export class Peer { return this.segments.get(segmentExternalId); } - private onCandidateConnect(candidate: PeerCandidate) { - this.connection = candidate; - this.eventHandlers.onPeerConnected(this); - } - - private onCandidateClose(candidate: PeerCandidate) { - if (this.connection === candidate) this.connection = undefined; - } - private onReceiveData(data: ArrayBuffer) { const command = PeerUtil.getPeerCommandFromArrayBuffer(data); - if (!command) return; - - this.handleCommand(command); - } + if (!command) { + this.receiveSegmentChuck(data); + return; + } - private handleCommand(command: PeerCommand) { switch (command.c) { case PeerCommandType.SegmentsAnnouncement: this.segments = PeerUtil.getSegmentsFromPeerAnnouncementMap(command.m); @@ -88,6 +89,20 @@ export class Peer { break; case PeerCommandType.SegmentData: + if (this.request?.segment.externalId.toString() === command.i) { + this.request.segmentByteLength = command.s; + } + break; + + case PeerCommandType.SegmentAbsent: + if (this.request?.segment.externalId.toString() === command.i) { + this.terminateSegmentRequest(); + this.segments.delete(command.i); + } + break; + + case PeerCommandType.CancelSegmentRequest: + this.stopSendSegmentData(); break; } } @@ -106,25 +121,46 @@ export class Peer { c: PeerCommandType.SegmentRequest, i: externalId.toString(), }; - const { promise, onSuccess, onError } = + this.sendCommand(command); + this.request = this.createPeerRequest(segment); + return this.request.p2pRequest; + } + + private createPeerRequest(segment: Segment): PeerRequest { + const { promise, resolve, reject } = Utils.getControlledPromise(); - this.request = { + return { segment, - onSuccess, + resolve, + reject, + responseTimeoutId: this.setResponseTimeout(), bytesDownloaded: 0, - pieces: [], + chunks: [], p2pRequest: { type: "p2p", promise, abort: () => { - onError(new AbortError()); + reject(new AbortError()); this.request = undefined; }, }, }; - this.sendCommand(command); + } - return this.request.p2pRequest; + private setResponseTimeout(): number { + return window.setTimeout(() => { + if (!this.request) return; + this.cancelSegmentRequest(); + }, p2pSegmentDownloadTimeout); + } + + private cancelSegmentRequest() { + if (!this.request) return; + this.sendCommand({ + c: PeerCommandType.CancelSegmentRequest, + i: this.request.segment.externalId.toString(), + }); + this.terminateSegmentRequest(); } sendSegmentsAnnouncement(map: JsonSegmentAnnouncementMap) { @@ -142,22 +178,19 @@ export class Peer { i: segmentExternalId, s: data.byteLength, }; - this.sendCommand(command); - let bytesLeft = data.byteLength; - while (bytesLeft > 0) { - const bytesToSend = - bytesLeft >= webRtcMaxMessageSize ? webRtcMaxMessageSize : bytesLeft; - const buffer = Buffer.from( - data, - data.byteLength - bytesLeft, - bytesToSend - ); - - this.connection.send(buffer); - bytesLeft -= bytesToSend; + this.isSendingData = true; + const sendChuck = async (data: ArrayBuffer) => this.connection?.send(data); + for (const chuck of getBufferChunks(data, webRtcMaxMessageSize)) { + if (!this.isSendingData) break; + void sendChuck(chuck); } + this.isSendingData = false; + } + + stopSendSegmentData() { + this.isSendingData = false; } sendSegmentAbsent(segmentExternalId: string) { @@ -167,4 +200,57 @@ export class Peer { }; this.sendCommand(command); } + + private receiveSegmentChuck(chuck: ArrayBuffer): void { + const { request } = this; + if (!request) return; + + request.bytesDownloaded += chuck.byteLength; + request.chunks.push(chuck); + + if (request.bytesDownloaded === request.segmentByteLength) { + const segmentData = joinChunks(request.chunks); + this.approveRequest(segmentData); + } else if (request.bytesDownloaded > request.segmentByteLength) { + this.cancelSegmentRequest(); + } + } + + private approveRequest(data: ArrayBuffer) { + if (!this.request) return; + clearTimeout(this.request.responseTimeoutId); + this.request.resolve(data); + this.request = undefined; + } + + private terminateSegmentRequest() { + if (!this.request) return; + clearTimeout(this.request.responseTimeoutId); + this.request = undefined; + } +} + +function* getBufferChunks( + data: ArrayBuffer, + maxChuckSize: number +): Generator { + let bytesLeft = data.byteLength; + while (bytesLeft > 0) { + const bytesToSend = bytesLeft >= maxChuckSize ? maxChuckSize : bytesLeft; + const buffer = Buffer.from(data, data.byteLength - bytesLeft, bytesToSend); + bytesLeft -= bytesToSend; + yield buffer; + } +} + +function joinChunks(chunks: ArrayBuffer[]): ArrayBuffer { + const bytesSum = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const buffer = new Uint8Array(bytesSum); + let offset = 0; + for (const chunk of chunks) { + buffer.set(new Uint8Array(chunk), offset); + offset += chunk.byteLength; + } + + return buffer; } diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 210e5958..a2712321 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -145,18 +145,18 @@ export function isSegmentActual( } export function getControlledPromise() { - let onSuccess: (value: T) => void; - let onError: (reason?: unknown) => void; - const promise = new Promise((resolve, reject) => { - onSuccess = resolve; - onError = reject; + let resolve: (value: T) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; }); return { promise, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onSuccess: onSuccess!, + resolve: resolve!, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onError: onError!, + reject: reject!, }; } From 7cf3524973c1c0061c24af83f578543a53caae10 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 17:11:05 +0300 Subject: [PATCH 040/127] Reject promise with errors in peer class. --- packages/p2p-media-loader-core/src/errors.ts | 20 +++++- packages/p2p-media-loader-core/src/peer.ts | 64 +++++++++++--------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/p2p-media-loader-core/src/errors.ts b/packages/p2p-media-loader-core/src/errors.ts index 41af2e1e..f585d330 100644 --- a/packages/p2p-media-loader-core/src/errors.ts +++ b/packages/p2p-media-loader-core/src/errors.ts @@ -9,8 +9,26 @@ export class FetchError extends Error { } } -export class AbortError extends Error { +export class RequestAbortError extends Error { constructor(message = "AbortError") { super(message); } } + +export class RequestTimeoutError extends Error { + constructor(message = "TimeoutError") { + super(message); + } +} + +export class ResponseBytesMismatchError extends Error { + constructor(message = "ResponseBytesMismatch") { + super(message); + } +} + +export class PeerSegmentAbsentError extends Error { + constructor(message = "PeerSegmentAbsent") { + super(message); + } +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index e7684e3f..5164c662 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -11,7 +11,12 @@ import * as PeerUtil from "./peer-utils"; import { P2PRequest } from "./request"; import { Segment } from "./types"; import * as Utils from "./utils"; -import { AbortError } from "./errors"; +import { + RequestAbortError, + RequestTimeoutError, + ResponseBytesMismatchError, + PeerSegmentAbsentError, +} from "./errors"; // TODO: add to settings const webRtcMaxMessageSize: number = 64 * 1024 - 1; @@ -26,7 +31,7 @@ type PeerRequest = { segment: Segment; p2pRequest: P2PRequest; resolve: (data: ArrayBuffer) => void; - reject: (reason: unknown) => void; + reject: (reason?: unknown) => void; bytesDownloaded: number; chunks: ArrayBuffer[]; segmentByteLength?: number; @@ -96,7 +101,7 @@ export class Peer { case PeerCommandType.SegmentAbsent: if (this.request?.segment.externalId.toString() === command.i) { - this.terminateSegmentRequest(); + this.cancelSegmentRequest(new PeerSegmentAbsentError()); this.segments.delete(command.i); } break; @@ -133,34 +138,40 @@ export class Peer { segment, resolve, reject, - responseTimeoutId: this.setResponseTimeout(), + responseTimeoutId: this.setRequestTimeout(), bytesDownloaded: 0, chunks: [], p2pRequest: { type: "p2p", promise, - abort: () => { - reject(new AbortError()); - this.request = undefined; - }, + abort: () => this.cancelSegmentRequest(new RequestAbortError()), }, }; } - private setResponseTimeout(): number { - return window.setTimeout(() => { - if (!this.request) return; - this.cancelSegmentRequest(); - }, p2pSegmentDownloadTimeout); + private setRequestTimeout(): number { + return window.setTimeout( + () => this.cancelSegmentRequest(new RequestTimeoutError()), + p2pSegmentDownloadTimeout + ); } - private cancelSegmentRequest() { + private cancelSegmentRequest( + reason: + | RequestAbortError + | RequestTimeoutError + | PeerSegmentAbsentError + | ResponseBytesMismatchError + ) { if (!this.request) return; - this.sendCommand({ - c: PeerCommandType.CancelSegmentRequest, - i: this.request.segment.externalId.toString(), - }); - this.terminateSegmentRequest(); + if (!(reason instanceof PeerSegmentAbsentError)) { + this.sendCommand({ + c: PeerCommandType.CancelSegmentRequest, + i: this.request.segment.externalId.toString(), + }); + } + this.request.reject(reason); + this.clearRequest(); } sendSegmentsAnnouncement(map: JsonSegmentAnnouncementMap) { @@ -211,21 +222,18 @@ export class Peer { if (request.bytesDownloaded === request.segmentByteLength) { const segmentData = joinChunks(request.chunks); this.approveRequest(segmentData); - } else if (request.bytesDownloaded > request.segmentByteLength) { - this.cancelSegmentRequest(); + } else if (request.bytesDownloaded > (request.segmentByteLength ?? 0)) { + this.cancelSegmentRequest(new ResponseBytesMismatchError()); } } private approveRequest(data: ArrayBuffer) { - if (!this.request) return; - clearTimeout(this.request.responseTimeoutId); - this.request.resolve(data); - this.request = undefined; + this.request?.resolve(data); + this.clearRequest(); } - private terminateSegmentRequest() { - if (!this.request) return; - clearTimeout(this.request.responseTimeoutId); + private clearRequest() { + clearTimeout(this.request?.responseTimeoutId); this.request = undefined; } } From bb22472c3095d92e5bf449c73e8238860c5c1f59 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 17:46:36 +0300 Subject: [PATCH 041/127] Rewrite fetch segment with async await. --- .../p2p-media-loader-core/src/http-loader.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index b16aff8d..439b5da9 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,4 +1,4 @@ -import { AbortError, FetchError } from "./errors"; +import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; import { HttpRequest } from "./request"; @@ -24,25 +24,26 @@ function fetchSegment(segment: Segment) { } const abortController = new AbortController(); - const promise = fetch(url, { - headers, - signal: abortController.signal, - }) - .then((response) => { - if (response.ok) return response.arrayBuffer(); + const loadSegmentData = async () => { + try { + const response = await fetch(url, { + headers, + signal: abortController.signal, + }); + if (response.ok) return response.arrayBuffer(); throw new FetchError( response.statusText ?? `Network response was not for ${segmentId}`, response.status, response ); - }) - .catch((error) => { + } catch (error) { if (error instanceof Error && error.name === "AbortError") { - throw new AbortError(`Segment fetch was aborted ${segmentId}`); + throw new RequestAbortError(`Segment fetch was aborted ${segmentId}`); } throw error; - }); + } + }; - return { promise, abortController }; + return { promise: loadSegmentData(), abortController }; } From 486058faf420f10ff2eca652547ae29bd78cb480 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 18:02:57 +0300 Subject: [PATCH 042/127] Use object with booleans instead of set for segment queue statuses. --- .../src/hybrid-loader.ts | 2 +- .../src/internal-types.ts | 13 +++---- packages/p2p-media-loader-core/src/request.ts | 6 ++-- packages/p2p-media-loader-core/src/utils.ts | 34 ++++++++----------- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 6cfece47..fd241f9b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -114,7 +114,7 @@ export class HybridLoader { const { simultaneousHttpDownloads } = this.settings; for (const { segment, statuses } of queue) { if (this.requests.isHttpRequested(segment.localId)) continue; - if (statuses.has("high-demand")) { + if (statuses.isHighDemand) { if (this.requests.countHttpRequests() < simultaneousHttpDownloads) { void this.loadSegmentThroughHttp(segment); continue; diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 68b01deb..0e69c6a2 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -6,11 +6,6 @@ export type Playback = { rate: number; }; -export type SegmentLoadStatus = - | "high-demand" - | "http-downloadable" - | "p2p-downloadable"; - export type NumberRange = { from: number; to: number; @@ -22,7 +17,13 @@ export type LoadBufferRanges = { p2p: NumberRange; }; -export type QueueItem = { segment: Segment; statuses: Set }; +export type QueueStatuses = { + isHighDemand: boolean; + isHttpDownloadable: boolean; + isP2PDownloadable: boolean; +}; + +export type QueueItem = { segment: Segment; statuses: QueueStatuses }; export type BasePeerCommand = { c: T; diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 42082f1e..d19f8f85 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,5 +1,5 @@ import { Segment, SegmentResponse } from "./types"; -import { AbortError } from "./errors"; +import { RequestAbortError } from "./errors"; type EngineRequest = { promise: Promise; @@ -106,7 +106,7 @@ export class RequestContainer { const request = this.requests.get(segmentId); if (!request) return; - request.engineRequest?.onError(new AbortError()); + request.engineRequest?.onError(new RequestAbortError()); } abortLoaderRequest(segmentId: string) { @@ -115,7 +115,7 @@ export class RequestContainer { if (request.loaderRequest) { request.loaderRequest.abort(); - request.engineRequest?.onError(new AbortError()); + request.engineRequest?.onError(new RequestAbortError()); } } diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index a2712321..4cc59206 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -1,6 +1,6 @@ import { Segment, Settings, Stream, StreamWithSegments } from "./index"; import { - SegmentLoadStatus, + QueueStatuses, Playback, LoadBufferRanges, QueueItem, @@ -47,7 +47,7 @@ export function generateQueue({ Settings, "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" >; -}) { +}): { queue: QueueItem[]; queueSegmentIds: Set } { const bufferRanges = getLoadBufferRanges(playback, settings); const { localId: requestedSegmentId } = segment; @@ -57,7 +57,7 @@ export function generateQueue({ const nextSegment = stream.segments.getNextTo(segment.localId)?.[1]; const isNextSegmentHighDemand = !!( nextSegment && - getSegmentLoadStatuses(nextSegment, bufferRanges)?.has("high-demand") + getSegmentLoadStatuses(nextSegment, bufferRanges).isHighDemand ); let i = 0; @@ -67,7 +67,8 @@ export function generateQueue({ if (isSegmentLoaded(segment.localId)) continue; queueSegmentIds.add(segment.localId); - queue.push({ segment, statuses: statuses ?? new Set(["high-demand"]) }); + statuses.isHighDemand = true; + queue.push({ segment, statuses }); i++; } @@ -104,27 +105,22 @@ export function getLoadBufferRanges( export function getSegmentLoadStatuses( segment: Readonly, loadBufferRanges: LoadBufferRanges -): Set | undefined { +): QueueStatuses { const { highDemand, http, p2p } = loadBufferRanges; const { startTime, endTime } = segment; - const statuses = new Set(); const isValueInRange = (value: number, range: NumberRange) => value >= range.from && value < range.to; - if ( - isValueInRange(startTime, highDemand) || - isValueInRange(endTime, highDemand) - ) { - statuses.add("high-demand"); - } - if (isValueInRange(startTime, http) || isValueInRange(endTime, http)) { - statuses.add("http-downloadable"); - } - if (isValueInRange(startTime, p2p) || isValueInRange(endTime, p2p)) { - statuses.add("p2p-downloadable"); - } - if (statuses.size) return statuses; + return { + isHighDemand: + isValueInRange(startTime, highDemand) || + isValueInRange(endTime, highDemand), + isHttpDownloadable: + isValueInRange(startTime, http) || isValueInRange(endTime, http), + isP2PDownloadable: + isValueInRange(startTime, p2p) || isValueInRange(endTime, p2p), + }; } export function isSegmentActual( From 8ef7daecf82aea5f67ffea2a57cd72190aa1fc45 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 18:06:13 +0300 Subject: [PATCH 043/127] Rename fetch segment function. --- packages/p2p-media-loader-core/src/http-loader.ts | 10 ++++------ packages/p2p-media-loader-core/src/hybrid-loader.ts | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 439b5da9..e6522539 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -2,10 +2,8 @@ import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; import { HttpRequest } from "./request"; -export function loadSegmentThroughHttp( - segment: Segment -): Readonly { - const { promise, abortController } = fetchSegment(segment); +export function getHttpSegmentRequest(segment: Segment): Readonly { + const { promise, abortController } = fetchSegmentData(segment); return { type: "http", promise, @@ -13,7 +11,7 @@ export function loadSegmentThroughHttp( }; } -function fetchSegment(segment: Segment) { +function fetchSegmentData(segment: Segment) { const headers = new Headers(); const { url, byteRange, localId: segmentId } = segment; @@ -26,7 +24,7 @@ function fetchSegment(segment: Segment) { const loadSegmentData = async () => { try { - const response = await fetch(url, { + const response = await window.fetch(url, { headers, signal: abortController.signal, }); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index fd241f9b..6d66d20b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,5 +1,5 @@ import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import { loadSegmentThroughHttp } from "./http-loader"; +import { getHttpSegmentRequest } from "./http-loader"; import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; @@ -135,7 +135,7 @@ export class HybridLoader { private async loadSegmentThroughHttp(segment: Segment) { let data: ArrayBuffer | undefined; try { - const httpRequest = loadSegmentThroughHttp(segment); + const httpRequest = getHttpSegmentRequest(segment); this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; } catch (err) { From 920db7d3b92854ab1a80026b167375689c6dbbd8 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 19 Sep 2023 18:09:33 +0300 Subject: [PATCH 044/127] Rename hybrid loader public method. --- packages/p2p-media-loader-core/src/core.ts | 6 +++--- packages/p2p-media-loader-core/src/hybrid-loader.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index de2ccd6b..8b6932da 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -76,12 +76,12 @@ export class Core { new HybridLoader(this.settings, this.bandwidthApproximator); loader = this.secondaryStreamLoader; } - return loader.loadSegmentByEngine(segment, stream); + return loader.loadSegment(segment, stream); } abortSegmentLoading(segmentId: string): void { - this.mainStreamLoader.abortSegmentByEngine(segmentId); - this.secondaryStreamLoader?.abortSegmentByEngine(segmentId); + this.mainStreamLoader.abortSegment(segmentId); + this.secondaryStreamLoader?.abortSegment(segmentId); } updatePlayback(position: number, rate: number): void { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 6d66d20b..137ef4ed 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -56,7 +56,8 @@ export class HybridLoader { ); } - async loadSegmentByEngine( + // api method for engines + async loadSegment( segment: Readonly, stream: Readonly ): Promise { @@ -128,7 +129,8 @@ export class HybridLoader { } } - abortSegmentByEngine(segmentId: string) { + // api method for engines + abortSegment(segmentId: string) { this.requests.abortEngineRequest(segmentId); } From 7783df10cb9492bb42b9622c4294b605f7ea6b9c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 20 Sep 2023 11:18:02 +0300 Subject: [PATCH 045/127] Use engine callbacks instead of promise creation in core. --- packages/p2p-media-loader-core/src/core.ts | 13 ++--- .../src/hybrid-loader.ts | 17 +++--- packages/p2p-media-loader-core/src/request.ts | 56 ++++++++++--------- packages/p2p-media-loader-core/src/types.ts | 2 + .../src/fragment-loader.ts | 45 ++++++++++++++- .../src/loading-handler.ts | 44 +++++++++++++-- 6 files changed, 126 insertions(+), 51 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 8b6932da..dc2cab94 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,14 +1,9 @@ import { HybridLoader } from "./hybrid-loader"; -import { - Stream, - StreamWithSegments, - Segment, - SegmentResponse, - Settings, -} from "./types"; +import { Stream, StreamWithSegments, Segment, Settings } from "./types"; import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; +import { EngineCallbacks } from "./request"; export class Core { private manifestResponseUrl?: string; @@ -64,7 +59,7 @@ export class Core { removeSegmentIds?.forEach((id) => stream.segments.delete(id)); } - loadSegment(segmentLocalId: string): Promise { + loadSegment(segmentLocalId: string, callbacks: EngineCallbacks) { const { segment, stream } = this.identifySegment(segmentLocalId); let loader: HybridLoader; @@ -76,7 +71,7 @@ export class Core { new HybridLoader(this.settings, this.bandwidthApproximator); loader = this.secondaryStreamLoader; } - return loader.loadSegment(segment, stream); + void loader.loadSegment(segment, stream, callbacks); } abortSegmentLoading(segmentId: string): void { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 137ef4ed..c57f22f8 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,11 +1,11 @@ -import { Segment, SegmentResponse, StreamWithSegments } from "./index"; +import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; -import { RequestContainer } from "./request"; +import { RequestContainer, EngineCallbacks } from "./request"; import * as Utils from "./utils"; import { FetchError } from "./errors"; @@ -59,8 +59,9 @@ export class HybridLoader { // api method for engines async loadSegment( segment: Readonly, - stream: Readonly - ): Promise { + stream: Readonly, + callbacks: EngineCallbacks + ) { if (!this.playback) { this.playback = { position: segment.startTime, rate: 1 }; } @@ -75,14 +76,12 @@ export class HybridLoader { segment.localId ); if (storageData) { - return { + callbacks.onSuccess({ data: storageData, bandwidth: this.bandwidthApproximator.getBandwidth(), - }; + }); } - const request = Utils.getControlledPromise(); - this.requests.addEngineRequest(segment, request); - return request.promise; + this.requests.addEngineCallbacks(segment, callbacks); } private async processQueue(force = true) { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index d19f8f85..75415ea8 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,8 +1,7 @@ import { Segment, SegmentResponse } from "./types"; import { RequestAbortError } from "./errors"; -type EngineRequest = { - promise: Promise; +export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; onError: (reason?: unknown) => void; }; @@ -25,7 +24,7 @@ type HybridLoaderRequest = HttpRequest | P2PRequest; type Request = { segment: Readonly; loaderRequest?: Readonly; - engineRequest?: Readonly; + engineCallbacks?: Readonly; }; export class RequestContainer { @@ -42,29 +41,26 @@ export class RequestContainer { loaderRequest, }); } - loaderRequest.promise.finally(() => { - const request = this.requests.get(segmentId); - delete request?.loaderRequest; - if (request) this.clearRequestItem(request); - }); + loaderRequest.promise.finally(() => + this.clearRequestItem(segmentId, "loader") + ); } - addEngineRequest(segment: Segment, engineRequest: EngineRequest) { + addEngineCallbacks(segment: Segment, engineCallbacks: EngineCallbacks) { const segmentId = segment.localId; const requestItem = this.requests.get(segmentId); if (requestItem) { - requestItem.engineRequest = engineRequest; + requestItem.engineCallbacks = engineCallbacks; } else { + engineCallbacks.onSuccess = (response) => { + this.clearRequestItem(segmentId, "engine"); + return response; + }; this.requests.set(segmentId, { segment, - engineRequest, + engineCallbacks, }); } - engineRequest.promise.finally(() => { - const request = this.requests.get(segmentId); - delete request?.engineRequest; - if (request) this.clearRequestItem(request); - }); } get(segmentId: string) { @@ -82,11 +78,11 @@ export class RequestContainer { } resolveEngineRequest(segmentId: string, response: SegmentResponse) { - this.requests.get(segmentId)?.engineRequest?.onSuccess(response); + this.requests.get(segmentId)?.engineCallbacks?.onSuccess(response); } isRequestedByEngine(segmentId: string): boolean { - return !!this.requests.get(segmentId)?.engineRequest; + return !!this.requests.get(segmentId)?.engineCallbacks; } isHttpRequested(segmentId: string): boolean { @@ -106,7 +102,7 @@ export class RequestContainer { const request = this.requests.get(segmentId); if (!request) return; - request.engineRequest?.onError(new RequestAbortError()); + request.engineCallbacks?.onError(new RequestAbortError()); } abortLoaderRequest(segmentId: string) { @@ -115,23 +111,31 @@ export class RequestContainer { if (request.loaderRequest) { request.loaderRequest.abort(); - request.engineRequest?.onError(new RequestAbortError()); + request.engineCallbacks?.onError(new RequestAbortError()); } } - private clearRequestItem(request: Request): void { - if (!request.engineRequest && !request.loaderRequest) { - this.requests.delete(request.segment.localId); + private clearRequestItem( + requestItemId: string, + type: "loader" | "engine" + ): void { + const requestItem = this.requests.get(requestItemId); + if (!requestItem) return; + + if (type === "engine") delete requestItem.engineCallbacks; + if (type === "loader") delete requestItem.loaderRequest; + if (!requestItem.engineCallbacks && !requestItem.loaderRequest) { + this.requests.delete(requestItem.segment.localId); } } abortAllNotRequestedByEngine(isLocked: (segmentId: string) => boolean) { for (const { loaderRequest, - engineRequest, + engineCallbacks, segment, } of this.requests.values()) { - if (!engineRequest) continue; + if (!engineCallbacks) continue; if (!isLocked(segment.localId) && loaderRequest) loaderRequest.abort(); } } @@ -139,7 +143,7 @@ export class RequestContainer { destroy() { for (const request of this.requests.values()) { request.loaderRequest?.abort(); - request.engineRequest?.onError(); + request.engineCallbacks?.onError(); } this.requests.clear(); } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 0a02fa59..e8dd2f9c 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -1,5 +1,7 @@ import { LinkedMap } from "./linked-map"; +export type { EngineCallbacks } from "./request"; + export type StreamType = "main" | "secondary"; export type ByteRange = { start: number; end: number }; diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index da5cb1f2..6df6b5ec 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -9,10 +9,11 @@ import type { } from "hls.js"; import * as Utils from "./utils"; import { - AbortError, + RequestAbortError, Core, FetchError, SegmentResponse, + EngineCallbacks, } from "p2p-media-loader-core"; const DEFAULT_DOWNLOAD_LATENCY = 10; @@ -71,9 +72,11 @@ export class FragmentLoaderBase implements Loader { } try { - this.response = await this.core.loadSegment(this.segmentId); + const { request, callbacks } = getSegmentRequest(); + this.core.loadSegment(this.segmentId, callbacks); + this.response = await request; } catch (error) { - if (this.stats.aborted && error instanceof AbortError) return; + if (this.stats.aborted && error instanceof RequestAbortError) return; return this.handleError(error); } if (!this.response) return; @@ -150,3 +153,39 @@ function getLoadingStat({ return { start, first, end: loadingEndTime }; } + +function getControlledPromise() { + let resolve: (value: T) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolve: resolve!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + reject: reject!, + }; +} + +function getSegmentRequest(): { + callbacks: EngineCallbacks; + request: Promise; +} { + const { + promise: request, + resolve: onSuccess, + reject: onError, + } = getControlledPromise(); + + return { + request, + callbacks: { + onSuccess, + onError, + }, + }; +} diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index 8b2ef37c..27bfaa07 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -2,7 +2,7 @@ import * as Utils from "./stream-utils"; import { SegmentManager } from "./segment-manager"; import { StreamInfo } from "./types"; import { Shaka, Stream } from "./types"; -import { Core } from "p2p-media-loader-core"; +import { Core, EngineCallbacks, SegmentResponse } from "p2p-media-loader-core"; interface LoadingHandlerInterface { handleLoading: shaka.extern.SchemePlugin; @@ -83,9 +83,9 @@ export class LoadingHandler implements LoadingHandlerInterface { if (!this.core.hasSegment(segmentId)) return this.defaultLoad(); const loadSegment = async (): Promise => { - const response = await this.core.loadSegment(segmentId); - - const { data, bandwidth } = response; + const { request, callbacks } = getSegmentRequest(); + this.core.loadSegment(segmentId, callbacks); + const { data, bandwidth } = await request; return { data, headers: {}, @@ -113,3 +113,39 @@ function getLoadingDurationBasedOnBandwidth( const bits = bytesLoaded * 8; return Math.round(bits / bandwidth) * 1000; } + +function getControlledPromise() { + let resolve: (value: T) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolve: resolve!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + reject: reject!, + }; +} + +function getSegmentRequest(): { + callbacks: EngineCallbacks; + request: Promise; +} { + const { + promise: request, + resolve: onSuccess, + reject: onError, + } = getControlledPromise(); + + return { + request, + callbacks: { + onSuccess, + onError, + }, + }; +} From 4ad6e5e0ef783b2fd9047c90a204cadc119f01dd Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 20 Sep 2023 13:11:11 +0300 Subject: [PATCH 046/127] Remove unnecessary getControlledPromise function from engines. --- .../src/fragment-loader.ts | 34 ++++++------------- .../src/loading-handler.ts | 34 ++++++------------- 2 files changed, 20 insertions(+), 48 deletions(-) diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index 6df6b5ec..cd624183 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -154,38 +154,24 @@ function getLoadingStat({ return { start, first, end: loadingEndTime }; } -function getControlledPromise() { - let resolve: (value: T) => void; - let reject: (reason?: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - return { - promise, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - resolve: resolve!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - reject: reject!, - }; -} - function getSegmentRequest(): { callbacks: EngineCallbacks; request: Promise; } { - const { - promise: request, - resolve: onSuccess, - reject: onError, - } = getControlledPromise(); + let onSuccess: (value: SegmentResponse) => void; + let onError: (reason?: unknown) => void; + const request = new Promise((resolve, reject) => { + onSuccess = resolve; + onError = reject; + }); return { request, callbacks: { - onSuccess, - onError, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onSuccess: onSuccess!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onError: onError!, }, }; } diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index 27bfaa07..ae822412 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -114,38 +114,24 @@ function getLoadingDurationBasedOnBandwidth( return Math.round(bits / bandwidth) * 1000; } -function getControlledPromise() { - let resolve: (value: T) => void; - let reject: (reason?: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - return { - promise, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - resolve: resolve!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - reject: reject!, - }; -} - function getSegmentRequest(): { callbacks: EngineCallbacks; request: Promise; } { - const { - promise: request, - resolve: onSuccess, - reject: onError, - } = getControlledPromise(); + let onSuccess: (value: SegmentResponse) => void; + let onError: (reason?: unknown) => void; + const request = new Promise((resolve, reject) => { + onSuccess = resolve; + onError = reject; + }); return { request, callbacks: { - onSuccess, - onError, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onSuccess: onSuccess!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onError: onError!, }, }; } From 2bb356e46f4c89c964630a5523341534128a5e48 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 20 Sep 2023 15:56:32 +0300 Subject: [PATCH 047/127] Create hybrid loaders when necessary data is already set. --- packages/p2p-media-loader-core/src/core.ts | 46 +++++++++++-------- .../src/hybrid-loader.ts | 31 +++++-------- packages/p2p-media-loader-core/src/request.ts | 16 +++++-- .../src/segments-storage.ts | 37 +++++++++------ packages/p2p-media-loader-core/src/types.ts | 2 +- packages/p2p-media-loader-core/src/utils.ts | 4 +- 6 files changed, 76 insertions(+), 60 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index dc2cab94..2c8c2fe8 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -17,16 +17,11 @@ export class Core { cachedSegmentsCount: 50, }; private readonly bandwidthApproximator = new BandwidthApproximator(); - private readonly mainStreamLoader: HybridLoader = new HybridLoader( - this.settings, - this.bandwidthApproximator - ); + private mainStreamLoader?: HybridLoader; private secondaryStreamLoader?: HybridLoader; setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; - this.mainStreamLoader.setStreamManifestUrl(this.manifestResponseUrl); - this.secondaryStreamLoader?.setStreamManifestUrl(this.manifestResponseUrl); } hasSegment(segmentLocalId: string): boolean { @@ -61,32 +56,23 @@ export class Core { loadSegment(segmentLocalId: string, callbacks: EngineCallbacks) { const { segment, stream } = this.identifySegment(segmentLocalId); - - let loader: HybridLoader; - if (stream.type === "main") { - loader = this.mainStreamLoader; - } else { - this.secondaryStreamLoader = - this.secondaryStreamLoader ?? - new HybridLoader(this.settings, this.bandwidthApproximator); - loader = this.secondaryStreamLoader; - } + const loader = this.getStreamHybridLoader(segment, stream); void loader.loadSegment(segment, stream, callbacks); } abortSegmentLoading(segmentId: string): void { - this.mainStreamLoader.abortSegment(segmentId); + this.mainStreamLoader?.abortSegment(segmentId); this.secondaryStreamLoader?.abortSegment(segmentId); } updatePlayback(position: number, rate: number): void { - this.mainStreamLoader.updatePlayback(position, rate); + this.mainStreamLoader?.updatePlayback(position, rate); this.secondaryStreamLoader?.updatePlayback(position, rate); } destroy(): void { this.streams.clear(); - this.mainStreamLoader.destroy(); + this.mainStreamLoader?.destroy(); this.secondaryStreamLoader?.destroy(); this.manifestResponseUrl = undefined; } @@ -104,4 +90,26 @@ export class Core { return { segment, stream }; } + + private getStreamHybridLoader(segment: Segment, stream: StreamWithSegments) { + if (!this.manifestResponseUrl) { + throw new Error("Manifest response url is not defined"); + } + const streamTypeLoaderKeyMap = { + main: "mainStreamLoader", + secondary: "secondaryStreamLoader", + } as const; + const { type } = stream; + const loaderKey = streamTypeLoaderKeyMap[type]; + + return (this[loaderKey] = + this[loaderKey] ?? + new HybridLoader( + this.manifestResponseUrl, + segment, + stream, + this.settings, + this.bandwidthApproximator + )); + } } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index c57f22f8..7a9dc4a6 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -10,23 +10,28 @@ import * as Utils from "./utils"; import { FetchError } from "./errors"; export class HybridLoader { - private streamManifestUrl?: string; private readonly requests = new RequestContainer(); private p2pLoader?: P2PLoader; private readonly segmentStorage: SegmentsMemoryStorage; private storageCleanUpIntervalId?: number; - private activeStream?: Readonly; - private lastRequestedSegment?: Readonly; - private playback?: Playback; + private activeStream: Readonly; + private lastRequestedSegment: Readonly; + private readonly playback: Playback; private lastQueueProcessingTimeStamp?: number; constructor( + private streamManifestUrl: string, + requestedSegment: Segment, + requestedStream: Readonly, private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator ) { + this.lastRequestedSegment = requestedSegment; + this.activeStream = requestedStream; + this.playback = { position: requestedSegment.startTime, rate: 1 }; this.segmentStorage = new SegmentsMemoryStorage(this.settings); this.segmentStorage.setIsSegmentLockedPredicate((segment) => { - if (!this.playback || !this.activeStream?.segments.has(segment.localId)) { + if (!this.activeStream.segments.has(segment.localId)) { return false; } const bufferRanges = Utils.getLoadBufferRanges( @@ -42,12 +47,7 @@ export class HybridLoader { ); } - setStreamManifestUrl(url: string) { - this.streamManifestUrl = url; - } - private createP2PLoader(stream: StreamWithSegments) { - if (!this.streamManifestUrl) return; this.p2pLoader = new P2PLoader( this.streamManifestUrl, stream, @@ -62,9 +62,6 @@ export class HybridLoader { stream: Readonly, callbacks: EngineCallbacks ) { - if (!this.playback) { - this.playback = { position: segment.startTime, rate: 1 }; - } if (stream !== this.activeStream) { this.activeStream = stream; this.createP2PLoader(stream); @@ -85,9 +82,6 @@ export class HybridLoader { } private async processQueue(force = true) { - if (!this.activeStream || !this.lastRequestedSegment || !this.playback) { - return; - } const now = performance.now(); if ( !force && @@ -98,13 +92,12 @@ export class HybridLoader { } this.lastQueueProcessingTimeStamp = now; - const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); const { queue, queueSegmentIds } = Utils.generateQueue({ segment: this.lastRequestedSegment, stream: this.activeStream, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segmentId) => storedSegmentIds.has(segmentId), + isSegmentLoaded: (segmentId) => this.segmentStorage.hasSegment(segmentId), }); this.requests.abortAllNotRequestedByEngine((segmentId) => @@ -175,7 +168,6 @@ export class HybridLoader { } updatePlayback(position: number, rate: number) { - if (!this.playback) return; const isRateChanged = this.playback.rate !== rate; const isPositionChanged = this.playback.position !== position; @@ -191,7 +183,6 @@ export class HybridLoader { this.storageCleanUpIntervalId = undefined; void this.segmentStorage.destroy(); this.requests.destroy(); - this.playback = undefined; } } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 75415ea8..d2ca3492 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -27,11 +27,15 @@ type Request = { engineCallbacks?: Readonly; }; +function getRequestItemId(segment: Segment) { + return segment.localId; +} + export class RequestContainer { private readonly requests = new Map(); addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { - const segmentId = segment.localId; + const segmentId = getRequestItemId(segment); const existingRequest = this.requests.get(segmentId); if (existingRequest) { existingRequest.loaderRequest = loaderRequest; @@ -41,13 +45,13 @@ export class RequestContainer { loaderRequest, }); } - loaderRequest.promise.finally(() => + loaderRequest.promise.then(() => this.clearRequestItem(segmentId, "loader") ); } addEngineCallbacks(segment: Segment, engineCallbacks: EngineCallbacks) { - const segmentId = segment.localId; + const segmentId = getRequestItemId(segment); const requestItem = this.requests.get(segmentId); if (requestItem) { requestItem.engineCallbacks = engineCallbacks; @@ -125,7 +129,8 @@ export class RequestContainer { if (type === "engine") delete requestItem.engineCallbacks; if (type === "loader") delete requestItem.loaderRequest; if (!requestItem.engineCallbacks && !requestItem.loaderRequest) { - this.requests.delete(requestItem.segment.localId); + const segmentId = getRequestItemId(requestItem.segment); + this.requests.delete(segmentId); } } @@ -136,7 +141,8 @@ export class RequestContainer { segment, } of this.requests.values()) { if (!engineCallbacks) continue; - if (!isLocked(segment.localId) && loaderRequest) loaderRequest.abort(); + const segmentId = getRequestItemId(segment); + if (!isLocked(segmentId) && loaderRequest) loaderRequest.abort(); } } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 48bb27a5..8dec3af3 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -5,6 +5,7 @@ export class SegmentsMemoryStorage { string, { segment: Segment; data: ArrayBuffer; lastAccessed: number } >(); + private readonly cachedSegmentIds = new Set(); private isSegmentLockedPredicate?: (segment: Segment) => boolean; private onUpdateSubscriptions: (() => void)[] = []; @@ -15,6 +16,10 @@ export class SegmentsMemoryStorage { } ) {} + async initialize(masterManifestUrl: string) { + // empty + } + setIsSegmentLockedPredicate(predicate: (segment: Segment) => boolean) { this.isSegmentLockedPredicate = predicate; } @@ -24,28 +29,28 @@ export class SegmentsMemoryStorage { } async storeSegment(segment: Segment, data: ArrayBuffer) { - this.cache.set(segment.localId, { + const id = segment.externalId; + this.cache.set(id, { segment, data, lastAccessed: performance.now(), }); + this.cachedSegmentIds.add(id); this.onUpdateSubscriptions.forEach((c) => c()); } - async getSegmentData(segmentId: string): Promise { - const cacheItem = this.cache.get(segmentId); + async getSegmentData( + segmentExternalId: string + ): Promise { + const cacheItem = this.cache.get(segmentExternalId); if (cacheItem === undefined) return undefined; cacheItem.lastAccessed = performance.now(); return cacheItem.data; } - async getStoredSegmentIds() { - const segmentIds = new Set(); - for (const segmentId of this.cache.keys()) { - segmentIds.add(segmentId); - } - return segmentIds; + hasSegment(segmentExternalId: string): boolean { + return this.cachedSegmentIds.has(segmentExternalId); } async clear(): Promise { @@ -58,10 +63,13 @@ export class SegmentsMemoryStorage { // Delete old segments const now = performance.now(); - for (const [segmentId, { lastAccessed, segment }] of this.cache.entries()) { + for (const [ + segmentExternalId, + { lastAccessed, segment }, + ] of this.cache.entries()) { if (now - lastAccessed > this.settings.cachedSegmentExpiration) { if (!this.isSegmentLockedPredicate?.(segment)) { - segmentsToDelete.push(segmentId); + segmentsToDelete.push(segmentExternalId); } } else { remainingSegments.push({ segment, lastAccessed }); @@ -76,14 +84,17 @@ export class SegmentsMemoryStorage { for (const cachedSegment of remainingSegments) { if (!this.isSegmentLockedPredicate?.(cachedSegment.segment)) { - segmentsToDelete.push(cachedSegment.segment.localId); + segmentsToDelete.push(cachedSegment.segment.externalId); countOverhead--; if (countOverhead === 0) break; } } } - segmentsToDelete.forEach((id) => this.cache.delete(id)); + segmentsToDelete.forEach((id) => { + this.cache.delete(id); + this.cachedSegmentIds.delete(id); + }); if (segmentsToDelete.length) { this.onUpdateSubscriptions.forEach((c) => c()); } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index e8dd2f9c..f9cd65f0 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -8,7 +8,7 @@ export type ByteRange = { start: number; end: number }; export type Segment = { readonly localId: string; - readonly externalId: number; + readonly externalId: string; readonly url: string; readonly byteRange?: ByteRange; readonly startTime: number; diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index 4cc59206..d5851e4a 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -42,7 +42,7 @@ export function generateQueue({ stream: Readonly; segment: Readonly; playback: Readonly; - isSegmentLoaded: (segmentId: string) => boolean; + isSegmentLoaded: (segmentExternalId: string) => boolean; settings: Pick< Settings, "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" @@ -64,7 +64,7 @@ export function generateQueue({ for (const segment of stream.segments.values(requestedSegmentId)) { const statuses = getSegmentLoadStatuses(segment, bufferRanges); if (!statuses && !(i === 0 && isNextSegmentHighDemand)) break; - if (isSegmentLoaded(segment.localId)) continue; + if (isSegmentLoaded(segment.externalId)) continue; queueSegmentIds.add(segment.localId); statuses.isHighDemand = true; From 695deca91e958bf30998223dcd05fa5dea63e538 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 20 Sep 2023 17:18:13 +0300 Subject: [PATCH 048/127] Add progress to http loading. --- .../p2p-media-loader-core/src/http-loader.ts | 54 +++++++++++++++++-- packages/p2p-media-loader-core/src/request.ts | 8 +++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index e6522539..d6592f28 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,12 +1,15 @@ import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; -import { HttpRequest } from "./request"; +import { HttpRequest, LoadProgress } from "./request"; export function getHttpSegmentRequest(segment: Segment): Readonly { - const { promise, abortController } = fetchSegmentData(segment); + const { promise, abortController, progress, startTimestamp } = + fetchSegmentData(segment); return { type: "http", promise, + progress, + startTimestamp, abort: () => abortController.abort(), }; } @@ -22,6 +25,7 @@ function fetchSegmentData(segment: Segment) { } const abortController = new AbortController(); + let progress: LoadProgress | undefined; const loadSegmentData = async () => { try { const response = await window.fetch(url, { @@ -29,7 +33,10 @@ function fetchSegmentData(segment: Segment) { signal: abortController.signal, }); - if (response.ok) return response.arrayBuffer(); + if (response.ok) { + progress = monitorFetchProgress(response); + return response.arrayBuffer(); + } throw new FetchError( response.statusText ?? `Network response was not for ${segmentId}`, response.status, @@ -43,5 +50,44 @@ function fetchSegmentData(segment: Segment) { } }; - return { promise: loadSegmentData(), abortController }; + return { + promise: loadSegmentData(), + abortController, + progress, + startTimestamp: performance.now(), + }; +} + +function monitorFetchProgress( + response: Response +): Readonly | undefined { + const totalLengthString = response.headers.get("Content-Length"); + if (totalLengthString === null || !response.body) return; + + const totalLength = +totalLengthString; + const progress: LoadProgress = { + percent: 0, + loadedBytes: 0, + totalLength, + }; + const reader = response.body.getReader(); + + const monitor = async () => { + for await (const chunk of readStream(reader)) { + progress.loadedBytes += chunk.length; + progress.percent = (progress.loadedBytes / totalLength) * 100; + } + }; + void monitor(); + return progress; +} + +async function* readStream( + reader: ReadableStreamDefaultReader +): AsyncGenerator { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + yield value; + } } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index d2ca3492..49929c27 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -6,9 +6,17 @@ export type EngineCallbacks = { onError: (reason?: unknown) => void; }; +export type LoadProgress = { + percent: number; + loadedBytes: number; + totalLength: number; +}; + type RequestBase = { promise: Promise; abort: () => void; + progress?: Readonly; + startTimestamp: number; }; export type HttpRequest = RequestBase & { From ddf2265075ec34da959b489c4cc5b919ebb3aeac Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 20 Sep 2023 17:49:06 +0300 Subject: [PATCH 049/127] Add progress functionality to Peer class. --- .../p2p-media-loader-core/src/p2p-loader.ts | 2 +- packages/p2p-media-loader-core/src/peer.ts | 21 ++++++++++++------- packages/p2p-media-loader-core/src/request.ts | 4 ++-- .../src/segments-storage.ts | 4 ++++ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 3dec9832..28ddbc1a 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -85,7 +85,7 @@ export class P2PLoader { } private async onSegmentStorageUpdate() { - const storedSegmentIds = await this.segmentStorage.getStoredSegmentIds(); + const { storedSegmentIds } = this.segmentStorage; const loaded: Segment[] = []; const httpLoading: Segment[] = []; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 5164c662..9aa0ea31 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -32,9 +32,7 @@ type PeerRequest = { p2pRequest: P2PRequest; resolve: (data: ArrayBuffer) => void; reject: (reason?: unknown) => void; - bytesDownloaded: number; chunks: ArrayBuffer[]; - segmentByteLength?: number; responseTimeoutId: number; }; @@ -95,7 +93,11 @@ export class Peer { case PeerCommandType.SegmentData: if (this.request?.segment.externalId.toString() === command.i) { - this.request.segmentByteLength = command.s; + this.request.p2pRequest.progress = { + percent: 0, + loadedBytes: 0, + totalBytes: command.s, + }; } break; @@ -139,10 +141,10 @@ export class Peer { resolve, reject, responseTimeoutId: this.setRequestTimeout(), - bytesDownloaded: 0, chunks: [], p2pRequest: { type: "p2p", + startTimestamp: performance.now(), promise, abort: () => this.cancelSegmentRequest(new RequestAbortError()), }, @@ -213,16 +215,19 @@ export class Peer { } private receiveSegmentChuck(chuck: ArrayBuffer): void { + // TODO: check can be chunk received before peer command answer const { request } = this; - if (!request) return; + const progress = request?.p2pRequest?.progress; + if (!request || !progress) return; - request.bytesDownloaded += chuck.byteLength; + progress.loadedBytes += chuck.byteLength; + progress.percent = (progress.loadedBytes / progress.loadedBytes) * 100; request.chunks.push(chuck); - if (request.bytesDownloaded === request.segmentByteLength) { + if (progress.loadedBytes === progress.totalBytes) { const segmentData = joinChunks(request.chunks); this.approveRequest(segmentData); - } else if (request.bytesDownloaded > (request.segmentByteLength ?? 0)) { + } else if (progress.loadedBytes > progress.totalBytes) { this.cancelSegmentRequest(new ResponseBytesMismatchError()); } } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 49929c27..9ed306e3 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -9,13 +9,13 @@ export type EngineCallbacks = { export type LoadProgress = { percent: number; loadedBytes: number; - totalLength: number; + totalBytes: number; }; type RequestBase = { promise: Promise; abort: () => void; - progress?: Readonly; + progress?: LoadProgress; startTimestamp: number; }; diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 8dec3af3..10d3b590 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -53,6 +53,10 @@ export class SegmentsMemoryStorage { return this.cachedSegmentIds.has(segmentExternalId); } + get storedSegmentIds(): ReadonlySet { + return this.cachedSegmentIds; + } + async clear(): Promise { const segmentsToDelete: string[] = []; const remainingSegments: { From 7bd22672b5beab63a6ade6ea482152c340bb78f7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 20 Sep 2023 17:59:04 +0300 Subject: [PATCH 050/127] Fix type issues. --- .../p2p-media-loader-core/src/http-loader.ts | 10 ++++---- .../src/internal-types.ts | 2 +- .../p2p-media-loader-core/src/p2p-loader.ts | 3 +++ .../p2p-media-loader-core/src/peer-utils.ts | 8 +++---- .../p2p-media-loader-core/src/type-guards.ts | 23 +------------------ .../src/segment-mananger.ts | 2 +- 6 files changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index d6592f28..44a56d53 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -61,21 +61,21 @@ function fetchSegmentData(segment: Segment) { function monitorFetchProgress( response: Response ): Readonly | undefined { - const totalLengthString = response.headers.get("Content-Length"); - if (totalLengthString === null || !response.body) return; + const totalBytesString = response.headers.get("Content-Length"); + if (totalBytesString === null || !response.body) return; - const totalLength = +totalLengthString; + const totalBytes = +totalBytesString; const progress: LoadProgress = { percent: 0, loadedBytes: 0, - totalLength, + totalBytes, }; const reader = response.body.getReader(); const monitor = async () => { for await (const chunk of readStream(reader)) { progress.loadedBytes += chunk.length; - progress.percent = (progress.loadedBytes / totalLength) * 100; + progress.percent = (progress.loadedBytes / totalBytes) * 100; } }; void monitor(); diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 0e69c6a2..f6acf623 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -31,7 +31,7 @@ export type BasePeerCommand = { // {[streamId]: [segmentIds[]; segmentStatuses[]]} export type JsonSegmentAnnouncementMap = { - [key: string]: [number[], number[]]; + [key: string]: [string[], number[]]; }; export type PeerSegmentCommand = BasePeerCommand< diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 28ddbc1a..8416b12d 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -44,13 +44,16 @@ export class P2PLoader { private subscribeOnTrackerEvents(trackerClient: TrackerClient) { // TODO: tracker event handlers + // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", () => {}); trackerClient.on("peer", (candidate) => { const peer = this.peers.get(candidate.id); if (peer) peer.addCandidate(candidate); else this.createPeer(candidate); }); + // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("warning", (warning) => {}); + // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("error", (error) => {}); } diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index dd51de21..4ca233f4 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -65,20 +65,20 @@ export function getJsonSegmentsAnnouncementMap( storedSegments: Segment[], loadingByHttpSegments: Segment[] ): JsonSegmentAnnouncementMap { - const segmentIds: number[] = []; + const segmentExternalIds: string[] = []; const segmentStatuses: PeerSegmentStatus[] = []; for (const segment of storedSegments) { - segmentIds.push(segment.externalId); + segmentExternalIds.push(segment.externalId); segmentStatuses.push(PeerSegmentStatus.Loaded); } for (const segment of loadingByHttpSegments) { - segmentIds.push(segment.externalId); + segmentExternalIds.push(segment.externalId); segmentStatuses.push(PeerSegmentStatus.LoadingByHttp); } return { - [streamExternalId]: [segmentIds, segmentStatuses], + [streamExternalId]: [segmentExternalIds, segmentStatuses], }; } diff --git a/packages/p2p-media-loader-core/src/type-guards.ts b/packages/p2p-media-loader-core/src/type-guards.ts index 64bfa16a..f02ef077 100644 --- a/packages/p2p-media-loader-core/src/type-guards.ts +++ b/packages/p2p-media-loader-core/src/type-guards.ts @@ -1,27 +1,6 @@ -import { - PeerSegmentRequestCommand, - PeerCommand, - PeerSegmentAnnouncementCommand, -} from "./internal-types"; +import { PeerCommand } from "./internal-types"; import { PeerCommandType } from "./enums"; -export function isPeerSegmentCommand( - command: object -): command is PeerSegmentRequestCommand { - return ( - (command as PeerSegmentRequestCommand).c === PeerCommandType.SegmentRequest - ); -} - -export function isPeerSegmentMapCommand( - command: object -): command is PeerSegmentAnnouncementCommand { - return ( - (command as PeerSegmentAnnouncementCommand).c === - PeerCommandType.SegmentsAnnouncement - ); -} - export function isPeerCommand(command: object): command is PeerCommand { return ( (command as PeerCommand).c !== undefined && diff --git a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts index 944ca1f9..71572845 100644 --- a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts +++ b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts @@ -66,7 +66,7 @@ export class SegmentManager { newSegments.push({ localId: segmentLocalId, url: responseUrl, - externalId: live ? sn : index, + externalId: live ? sn.toString() : index.toString(), byteRange, startTime, endTime, From 6fb3fbed669c25a902255794e4887bd27a386df7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 21 Sep 2023 14:11:44 +0300 Subject: [PATCH 051/127] Use only callbacks instead of promise creation in hls engine. --- .../src/fragment-loader.ts | 68 +++++++------------ 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index cd624183..497dcca4 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -71,30 +71,30 @@ export class FragmentLoaderBase implements Loader { return; } - try { - const { request, callbacks } = getSegmentRequest(); - this.core.loadSegment(this.segmentId, callbacks); - this.response = await request; - } catch (error) { + const onSuccess = (response: SegmentResponse) => { + this.response = response; + const loadedBytes = this.response.data.byteLength; + stats.loading = getLoadingStat({ + targetBitrate: this.response.bandwidth, + loadingEndTime: performance.now(), + loadedBytes, + }); + stats.total = stats.loaded = loadedBytes; + + callbacks.onSuccess( + { data: this.response.data, url: context.url }, + this.stats, + context, + this.response + ); + }; + + const onError = (error: unknown) => { if (this.stats.aborted && error instanceof RequestAbortError) return; - return this.handleError(error); - } - if (!this.response) return; - const loadedBytes = this.response.data.byteLength; - - stats.loading = getLoadingStat({ - targetBitrate: this.response.bandwidth, - loadingEndTime: performance.now(), - loadedBytes, - }); - stats.total = stats.loaded = loadedBytes; - - callbacks.onSuccess( - { data: this.response.data, url: context.url }, - this.stats, - context, - this.response - ); + this.handleError(error); + }; + + this.core.loadSegment(this.segmentId, { onSuccess, onError }); } private handleError(thrownError: unknown) { @@ -153,25 +153,3 @@ function getLoadingStat({ return { start, first, end: loadingEndTime }; } - -function getSegmentRequest(): { - callbacks: EngineCallbacks; - request: Promise; -} { - let onSuccess: (value: SegmentResponse) => void; - let onError: (reason?: unknown) => void; - const request = new Promise((resolve, reject) => { - onSuccess = resolve; - onError = reject; - }); - - return { - request, - callbacks: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onSuccess: onSuccess!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onError: onError!, - }, - }; -} From ffc4424577fcb3c9be4efc478acd6e1119b6e603 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 21 Sep 2023 15:18:24 +0300 Subject: [PATCH 052/127] Add segment storage initialization. --- packages/p2p-media-loader-core/src/core.ts | 32 +++++++++++++------ .../src/hybrid-loader.ts | 12 ++++--- .../src/segments-storage.ts | 24 ++++++++++---- .../src/fragment-loader.ts | 2 +- .../src/loading-handler.ts | 2 +- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 2c8c2fe8..6c5c4e6b 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -4,6 +4,7 @@ import * as Utils from "./utils"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { EngineCallbacks } from "./request"; +import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { private manifestResponseUrl?: string; @@ -17,6 +18,7 @@ export class Core { cachedSegmentsCount: 50, }; private readonly bandwidthApproximator = new BandwidthApproximator(); + private readonly segmentStorage = new SegmentsMemoryStorage(this.settings); private mainStreamLoader?: HybridLoader; private secondaryStreamLoader?: HybridLoader; @@ -54,7 +56,13 @@ export class Core { removeSegmentIds?.forEach((id) => stream.segments.delete(id)); } - loadSegment(segmentLocalId: string, callbacks: EngineCallbacks) { + async loadSegment(segmentLocalId: string, callbacks: EngineCallbacks) { + if (!this.manifestResponseUrl) { + throw new Error("Manifest response url is not defined"); + } + if (!this.segmentStorage.isInitialized) { + await this.segmentStorage.initialize(this.manifestResponseUrl); + } const { segment, stream } = this.identifySegment(segmentLocalId); const loader = this.getStreamHybridLoader(segment, stream); void loader.loadSegment(segment, stream, callbacks); @@ -95,6 +103,19 @@ export class Core { if (!this.manifestResponseUrl) { throw new Error("Manifest response url is not defined"); } + const createNewHybridLoader = (manifestResponseUrl: string) => { + if (!this.segmentStorage.isInitialized) { + throw new Error("Segment storage is not initialized"); + } + return new HybridLoader( + manifestResponseUrl, + segment, + stream, + this.settings, + this.bandwidthApproximator, + this.segmentStorage + ); + }; const streamTypeLoaderKeyMap = { main: "mainStreamLoader", secondary: "secondaryStreamLoader", @@ -103,13 +124,6 @@ export class Core { const loaderKey = streamTypeLoaderKeyMap[type]; return (this[loaderKey] = - this[loaderKey] ?? - new HybridLoader( - this.manifestResponseUrl, - segment, - stream, - this.settings, - this.bandwidthApproximator - )); + this[loaderKey] ?? createNewHybridLoader(this.manifestResponseUrl)); } } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7a9dc4a6..2453ccea 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -12,7 +12,6 @@ import { FetchError } from "./errors"; export class HybridLoader { private readonly requests = new RequestContainer(); private p2pLoader?: P2PLoader; - private readonly segmentStorage: SegmentsMemoryStorage; private storageCleanUpIntervalId?: number; private activeStream: Readonly; private lastRequestedSegment: Readonly; @@ -24,13 +23,17 @@ export class HybridLoader { requestedSegment: Segment, requestedStream: Readonly, private readonly settings: Settings, - private readonly bandwidthApproximator: BandwidthApproximator + private readonly bandwidthApproximator: BandwidthApproximator, + private readonly segmentStorage: SegmentsMemoryStorage ) { this.lastRequestedSegment = requestedSegment; this.activeStream = requestedStream; this.playback = { position: requestedSegment.startTime, rate: 1 }; - this.segmentStorage = new SegmentsMemoryStorage(this.settings); - this.segmentStorage.setIsSegmentLockedPredicate((segment) => { + + if (!this.segmentStorage.isInitialized) { + throw new Error("Segment storage is not initialized."); + } + this.segmentStorage.addIsSegmentLockedPredicate((segment) => { if (!this.activeStream.segments.has(segment.localId)) { return false; } @@ -41,6 +44,7 @@ export class HybridLoader { return Utils.isSegmentActual(segment, bufferRanges); }); + // TODO: move cleanup somewhere this.storageCleanUpIntervalId = window.setInterval( () => this.segmentStorage.clear(), 1000 diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 10d3b590..92363f5a 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -6,8 +6,11 @@ export class SegmentsMemoryStorage { { segment: Segment; data: ArrayBuffer; lastAccessed: number } >(); private readonly cachedSegmentIds = new Set(); - private isSegmentLockedPredicate?: (segment: Segment) => boolean; + private readonly isSegmentLockedPredicates: (( + segment: Segment + ) => boolean)[] = []; private onUpdateSubscriptions: (() => void)[] = []; + private _isInitialized = false; constructor( private settings: { @@ -17,11 +20,19 @@ export class SegmentsMemoryStorage { ) {} async initialize(masterManifestUrl: string) { - // empty + this._isInitialized = true; } - setIsSegmentLockedPredicate(predicate: (segment: Segment) => boolean) { - this.isSegmentLockedPredicate = predicate; + get isInitialized(): boolean { + return this._isInitialized; + } + + addIsSegmentLockedPredicate(predicate: (segment: Segment) => boolean) { + this.isSegmentLockedPredicates.push(predicate); + } + + private isSegmentLocked(segment: Segment) { + return this.isSegmentLockedPredicates.some((p) => p(segment)); } subscribeOnUpdate(callback: () => void) { @@ -72,7 +83,7 @@ export class SegmentsMemoryStorage { { lastAccessed, segment }, ] of this.cache.entries()) { if (now - lastAccessed > this.settings.cachedSegmentExpiration) { - if (!this.isSegmentLockedPredicate?.(segment)) { + if (!this.isSegmentLocked(segment)) { segmentsToDelete.push(segmentExternalId); } } else { @@ -87,7 +98,7 @@ export class SegmentsMemoryStorage { remainingSegments.sort((a, b) => a.lastAccessed - b.lastAccessed); for (const cachedSegment of remainingSegments) { - if (!this.isSegmentLockedPredicate?.(cachedSegment.segment)) { + if (!this.isSegmentLocked(cachedSegment.segment)) { segmentsToDelete.push(cachedSegment.segment.externalId); countOverhead--; if (countOverhead === 0) break; @@ -108,5 +119,6 @@ export class SegmentsMemoryStorage { public async destroy() { this.cache.clear(); this.onUpdateSubscriptions = []; + this._isInitialized = false; } } diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index 497dcca4..433389d4 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -94,7 +94,7 @@ export class FragmentLoaderBase implements Loader { this.handleError(error); }; - this.core.loadSegment(this.segmentId, { onSuccess, onError }); + void this.core.loadSegment(this.segmentId, { onSuccess, onError }); } private handleError(thrownError: unknown) { diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index ae822412..d7daa49c 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -84,7 +84,7 @@ export class LoadingHandler implements LoadingHandlerInterface { const loadSegment = async (): Promise => { const { request, callbacks } = getSegmentRequest(); - this.core.loadSegment(segmentId, callbacks); + await this.core.loadSegment(segmentId, callbacks); const { data, bandwidth } = await request; return { data, From 513b361bfb5767f1d2fc03c86f8f195ba4440a06 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 21 Sep 2023 17:20:27 +0300 Subject: [PATCH 053/127] Add peer settings options. --- packages/p2p-media-loader-core/src/core.ts | 2 ++ .../src/hybrid-loader.ts | 9 ++++--- .../p2p-media-loader-core/src/p2p-loader.ts | 17 ++++++++----- packages/p2p-media-loader-core/src/peer.ts | 25 ++++++++++++------- packages/p2p-media-loader-core/src/request.ts | 6 +++-- packages/p2p-media-loader-core/src/types.ts | 2 ++ 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 6c5c4e6b..707d0d72 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -16,6 +16,8 @@ export class Core { p2pDownloadTimeWindow: 60, cachedSegmentExpiration: 120, cachedSegmentsCount: 50, + webRtcMaxMessageSize: 64 * 1024 - 1, + p2pSegmentDownloadTimeout: 1000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); private readonly segmentStorage = new SegmentsMemoryStorage(this.settings); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 2453ccea..777b04c5 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -85,7 +85,7 @@ export class HybridLoader { this.requests.addEngineCallbacks(segment, callbacks); } - private async processQueue(force = true) { + private processQueue(force = true) { const now = performance.now(); if ( !force && @@ -141,22 +141,23 @@ export class HybridLoader { // TODO: handle error } } - if (data) this.handleSegmentLoaded(segment, data); + if (data) this.onSegmentLoaded(segment, data); } private async loadThroughP2P(segment: Segment) { if (!this.p2pLoader) return; const data = await this.p2pLoader.downloadSegment(segment); - if (data) this.handleSegmentLoaded(segment, data); + if (data) this.onSegmentLoaded(segment, data); } - private handleSegmentLoaded(segment: Segment, data: ArrayBuffer) { + private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); this.requests.resolveEngineRequest(segment.localId, { data, bandwidth: this.bandwidthApproximator.getBandwidth(), }); + this.processQueue(); } private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 8416b12d..a7cf05ec 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -2,7 +2,7 @@ import TrackerClient, { PeerCandidate } from "bittorrent-tracker"; import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./peer-utils"; -import { Segment, StreamWithSegments } from "./types"; +import { Segment, Settings, StreamWithSegments } from "./types"; import { JsonSegmentAnnouncementMap } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils"; @@ -21,7 +21,8 @@ export class P2PLoader { private streamManifestUrl: string, private readonly stream: StreamWithSegments, private readonly requests: RequestContainer, - private readonly segmentStorage: SegmentsMemoryStorage + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly settings: Settings ) { const peerId = PeerUtil.generatePeerId(); this.streamExternalId = Utils.getStreamExternalId( @@ -80,10 +81,14 @@ export class P2PLoader { } private createPeer(candidate: PeerCandidate) { - const peer = new Peer(candidate, { - onPeerConnected: this.onPeerConnected.bind(this), - onSegmentRequested: this.onSegmentRequested.bind(this), - }); + const peer = new Peer( + candidate, + { + onPeerConnected: this.onPeerConnected.bind(this), + onSegmentRequested: this.onSegmentRequested.bind(this), + }, + this.settings + ); this.peers.set(candidate.id, peer); } diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 9aa0ea31..1410dc6b 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -9,7 +9,7 @@ import { import { PeerCommandType, PeerSegmentStatus } from "./enums"; import * as PeerUtil from "./peer-utils"; import { P2PRequest } from "./request"; -import { Segment } from "./types"; +import { Segment, Settings } from "./types"; import * as Utils from "./utils"; import { RequestAbortError, @@ -18,10 +18,6 @@ import { PeerSegmentAbsentError, } from "./errors"; -// TODO: add to settings -const webRtcMaxMessageSize: number = 64 * 1024 - 1; -const p2pSegmentDownloadTimeout = 1000; - type PeerEventHandlers = { onPeerConnected: (peer: Peer) => void; onSegmentRequested: (peer: Peer, segmentId: string) => void; @@ -36,16 +32,24 @@ type PeerRequest = { responseTimeoutId: number; }; +type PeerSettings = Pick< + Settings, + "p2pSegmentDownloadTimeout" | "webRtcMaxMessageSize" +>; + export class Peer { readonly id: string; private readonly candidates = new Set(); private connection?: PeerCandidate; - private readonly eventHandlers: PeerEventHandlers; private segments = new Map(); private request?: PeerRequest; private isSendingData = false; - constructor(candidate: PeerCandidate, eventHandlers: PeerEventHandlers) { + constructor( + candidate: PeerCandidate, + private readonly eventHandlers: PeerEventHandlers, + private readonly settings: PeerSettings + ) { this.id = candidate.id; this.eventHandlers = eventHandlers; this.addCandidate(candidate); @@ -154,7 +158,7 @@ export class Peer { private setRequestTimeout(): number { return window.setTimeout( () => this.cancelSegmentRequest(new RequestTimeoutError()), - p2pSegmentDownloadTimeout + this.settings.p2pSegmentDownloadTimeout ); } @@ -195,7 +199,10 @@ export class Peer { this.isSendingData = true; const sendChuck = async (data: ArrayBuffer) => this.connection?.send(data); - for (const chuck of getBufferChunks(data, webRtcMaxMessageSize)) { + for (const chuck of getBufferChunks( + data, + this.settings.webRtcMaxMessageSize + )) { if (!this.isSendingData) break; void sendChuck(chuck); } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 9ed306e3..3e8f6af5 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -142,7 +142,7 @@ export class RequestContainer { } } - abortAllNotRequestedByEngine(isLocked: (segmentId: string) => boolean) { + abortAllNotRequestedByEngine(isLocked?: (segmentId: string) => boolean) { for (const { loaderRequest, engineCallbacks, @@ -150,7 +150,9 @@ export class RequestContainer { } of this.requests.values()) { if (!engineCallbacks) continue; const segmentId = getRequestItemId(segment); - if (!isLocked(segmentId) && loaderRequest) loaderRequest.abort(); + if ((!isLocked || !isLocked(segmentId)) && loaderRequest) { + loaderRequest.abort(); + } } } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index f9cd65f0..6537d739 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -48,4 +48,6 @@ export type Settings = { simultaneousHttpDownloads: number; cachedSegmentExpiration: number; cachedSegmentsCount: number; + webRtcMaxMessageSize: number; + p2pSegmentDownloadTimeout: number; }; From 6e6e108846cf8624f7542b979aab87489bd61406 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 21 Sep 2023 17:27:02 +0300 Subject: [PATCH 054/127] Move storage cleanup logic into storage class. --- .../src/hybrid-loader.ts | 9 ++------- .../src/segments-storage.ts | 19 +++++++++++-------- packages/p2p-media-loader-core/src/types.ts | 1 + 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 777b04c5..583f26a8 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -43,12 +43,6 @@ export class HybridLoader { ); return Utils.isSegmentActual(segment, bufferRanges); }); - - // TODO: move cleanup somewhere - this.storageCleanUpIntervalId = window.setInterval( - () => this.segmentStorage.clear(), - 1000 - ); } private createP2PLoader(stream: StreamWithSegments) { @@ -56,7 +50,8 @@ export class HybridLoader { this.streamManifestUrl, stream, this.requests, - this.segmentStorage + this.segmentStorage, + this.settings ); } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 92363f5a..0f401b27 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -1,4 +1,9 @@ -import { Segment } from "./types"; +import { Segment, Settings } from "./types"; + +type StorageSettings = Pick< + Settings, + "cachedSegmentExpiration" | "cachedSegmentsCount" | "storageCleanupInterval" +>; export class SegmentsMemoryStorage { private cache = new Map< @@ -11,16 +16,13 @@ export class SegmentsMemoryStorage { ) => boolean)[] = []; private onUpdateSubscriptions: (() => void)[] = []; private _isInitialized = false; + private cleanupIntervalId?: number; - constructor( - private settings: { - cachedSegmentExpiration: number; - cachedSegmentsCount: number; - } - ) {} + constructor(private settings: StorageSettings) {} async initialize(masterManifestUrl: string) { this._isInitialized = true; + this.cleanupIntervalId = window.setInterval(() => this.clear(), 1000); } get isInitialized(): boolean { @@ -68,7 +70,7 @@ export class SegmentsMemoryStorage { return this.cachedSegmentIds; } - async clear(): Promise { + private async clear(): Promise { const segmentsToDelete: string[] = []; const remainingSegments: { lastAccessed: number; @@ -120,5 +122,6 @@ export class SegmentsMemoryStorage { this.cache.clear(); this.onUpdateSubscriptions = []; this._isInitialized = false; + clearInterval(this.cleanupIntervalId); } } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 6537d739..59f27cab 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -50,4 +50,5 @@ export type Settings = { cachedSegmentsCount: number; webRtcMaxMessageSize: number; p2pSegmentDownloadTimeout: number; + storageCleanupInterval: number; }; From 541d78b25b3d7e692174cc6a4a93cd6545de8ad9 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 22 Sep 2023 11:26:21 +0300 Subject: [PATCH 055/127] Add p2p loaders container. --- packages/p2p-media-loader-core/src/core.ts | 1 + .../src/hybrid-loader.ts | 57 +++++++++++++++---- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 707d0d72..9510ab56 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -18,6 +18,7 @@ export class Core { cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, p2pSegmentDownloadTimeout: 1000, + storageCleanupInterval: 1000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); private readonly segmentStorage = new SegmentsMemoryStorage(this.settings); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 583f26a8..bfd91b53 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -11,7 +11,7 @@ import { FetchError } from "./errors"; export class HybridLoader { private readonly requests = new RequestContainer(); - private p2pLoader?: P2PLoader; + private readonly p2pLoaders: P2PLoadersContainer; private storageCleanUpIntervalId?: number; private activeStream: Readonly; private lastRequestedSegment: Readonly; @@ -43,12 +43,9 @@ export class HybridLoader { ); return Utils.isSegmentActual(segment, bufferRanges); }); - } - - private createP2PLoader(stream: StreamWithSegments) { - this.p2pLoader = new P2PLoader( + this.p2pLoaders = new P2PLoadersContainer( this.streamManifestUrl, - stream, + requestedStream, this.requests, this.segmentStorage, this.settings @@ -61,9 +58,9 @@ export class HybridLoader { stream: Readonly, callbacks: EngineCallbacks ) { - if (stream !== this.activeStream) { + if (this.activeStream !== stream) { this.activeStream = stream; - this.createP2PLoader(stream); + this.p2pLoaders.changeActiveLoader(stream); } this.lastRequestedSegment = segment; void this.processQueue(); @@ -140,8 +137,8 @@ export class HybridLoader { } private async loadThroughP2P(segment: Segment) { - if (!this.p2pLoader) return; - const data = await this.p2pLoader.downloadSegment(segment); + const p2pLoader = this.p2pLoaders.activeLoader; + const data = await p2pLoader.downloadSegment(segment); if (data) this.onSegmentLoaded(segment, data); } @@ -191,3 +188,43 @@ function* arrayBackwards(arr: T[]) { yield arr[i]; } } + +class P2PLoadersContainer { + private readonly loaders = new Map(); + private _activeLoader: P2PLoader; + + constructor( + private readonly streamManifestUrl: string, + stream: StreamWithSegments, + private readonly requests: RequestContainer, + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly settings: Settings + ) { + this._activeLoader = this.createLoader(stream); + } + + createLoader(stream: StreamWithSegments) { + if (this.loaders.has(stream.localId)) { + throw new Error("Loader for this stream already exists"); + } + this._activeLoader = new P2PLoader( + this.streamManifestUrl, + stream, + this.requests, + this.segmentStorage, + this.settings + ); + this.loaders.set(stream.localId, this._activeLoader); + return this._activeLoader; + } + + changeActiveLoader(stream: StreamWithSegments) { + const existingLoader = this.loaders.get(stream.localId); + if (existingLoader) this._activeLoader = existingLoader; + else this.createLoader(stream); + } + + get activeLoader() { + return this._activeLoader; + } +} From 789323011739c2fba60a064603c9eda1fae6222c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 22 Sep 2023 11:49:05 +0300 Subject: [PATCH 056/127] Add p2p loaders destroy logic. --- .../src/hybrid-loader.ts | 7 ++ .../p2p-media-loader-core/src/p2p-loader.ts | 8 ++ packages/p2p-media-loader-core/src/peer.ts | 93 ++++++++++--------- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index bfd91b53..7bb3b584 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -180,6 +180,7 @@ export class HybridLoader { this.storageCleanUpIntervalId = undefined; void this.segmentStorage.destroy(); this.requests.destroy(); + this.p2pLoaders.destroy(); } } @@ -227,4 +228,10 @@ class P2PLoadersContainer { get activeLoader() { return this._activeLoader; } + + destroy() { + for (const loader of this.loaders.values()) { + loader.destroy(); + } + } } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index a7cf05ec..f1d60307 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -138,6 +138,14 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(this.announcementMap); } } + + destroy() { + for (const peer of this.peers.values()) { + peer.destroy(); + } + this.peers.clear(); + this.trackerClient.destroy(); + } } function getHash(data: string) { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 1410dc6b..dead9740 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -137,49 +137,6 @@ export class Peer { return this.request.p2pRequest; } - private createPeerRequest(segment: Segment): PeerRequest { - const { promise, resolve, reject } = - Utils.getControlledPromise(); - return { - segment, - resolve, - reject, - responseTimeoutId: this.setRequestTimeout(), - chunks: [], - p2pRequest: { - type: "p2p", - startTimestamp: performance.now(), - promise, - abort: () => this.cancelSegmentRequest(new RequestAbortError()), - }, - }; - } - - private setRequestTimeout(): number { - return window.setTimeout( - () => this.cancelSegmentRequest(new RequestTimeoutError()), - this.settings.p2pSegmentDownloadTimeout - ); - } - - private cancelSegmentRequest( - reason: - | RequestAbortError - | RequestTimeoutError - | PeerSegmentAbsentError - | ResponseBytesMismatchError - ) { - if (!this.request) return; - if (!(reason instanceof PeerSegmentAbsentError)) { - this.sendCommand({ - c: PeerCommandType.CancelSegmentRequest, - i: this.request.segment.externalId.toString(), - }); - } - this.request.reject(reason); - this.clearRequest(); - } - sendSegmentsAnnouncement(map: JsonSegmentAnnouncementMap) { const command: PeerSegmentAnnouncementCommand = { c: PeerCommandType.SegmentsAnnouncement, @@ -221,6 +178,24 @@ export class Peer { this.sendCommand(command); } + private createPeerRequest(segment: Segment): PeerRequest { + const { promise, resolve, reject } = + Utils.getControlledPromise(); + return { + segment, + resolve, + reject, + responseTimeoutId: this.setRequestTimeout(), + chunks: [], + p2pRequest: { + type: "p2p", + startTimestamp: performance.now(), + promise, + abort: () => this.cancelSegmentRequest(new RequestAbortError()), + }, + }; + } + private receiveSegmentChuck(chuck: ArrayBuffer): void { // TODO: check can be chunk received before peer command answer const { request } = this; @@ -244,10 +219,42 @@ export class Peer { this.clearRequest(); } + private cancelSegmentRequest( + reason: + | RequestAbortError + | RequestTimeoutError + | PeerSegmentAbsentError + | ResponseBytesMismatchError + ) { + if (!this.request) return; + if (!(reason instanceof PeerSegmentAbsentError)) { + this.sendCommand({ + c: PeerCommandType.CancelSegmentRequest, + i: this.request.segment.externalId.toString(), + }); + } + this.request.reject(reason); + this.clearRequest(); + } + + private setRequestTimeout(): number { + return window.setTimeout( + () => this.cancelSegmentRequest(new RequestTimeoutError()), + this.settings.p2pSegmentDownloadTimeout + ); + } + private clearRequest() { clearTimeout(this.request?.responseTimeoutId); this.request = undefined; } + + destroy() { + // TODO: error for peer destroyed + this.cancelSegmentRequest(); + this.connection?.destroy(); + this.candidates.clear(); + } } function* getBufferChunks( From 83d067f56ad988fca392e52c92d7f9a0b6f7e32f Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 22 Sep 2023 15:48:17 +0300 Subject: [PATCH 057/127] Remove unnecessary toString on string variable. --- packages/p2p-media-loader-core/src/p2p-loader.ts | 2 +- packages/p2p-media-loader-core/src/peer-utils.ts | 5 +---- packages/p2p-media-loader-core/src/peer.ts | 8 ++++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index f1d60307..9307ef48 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -59,7 +59,7 @@ export class P2PLoader { } async downloadSegment(segment: Segment): Promise { - const segmentExternalId = segment.externalId.toString(); + const segmentExternalId = segment.externalId; const peerWithSegment: Peer[] = []; for (const peer of this.peers.values()) { diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index 4ca233f4..e6daa5c3 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -50,10 +50,7 @@ export function getSegmentsFromPeerAnnouncementMap( for (let i = 0; i < segmentIds.length; i++) { const segmentId = segmentIds[i]; const segmentStatus = statuses[i]; - const segmentFullId = Util.getSegmentFullExternalId( - streamId, - segmentId.toString() - ); + const segmentFullId = Util.getSegmentFullExternalId(streamId, segmentId); segmentStatusMap.set(segmentFullId, segmentStatus); } } diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index dead9740..39155b18 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -96,7 +96,7 @@ export class Peer { break; case PeerCommandType.SegmentData: - if (this.request?.segment.externalId.toString() === command.i) { + if (this.request?.segment.externalId === command.i) { this.request.p2pRequest.progress = { percent: 0, loadedBytes: 0, @@ -106,7 +106,7 @@ export class Peer { break; case PeerCommandType.SegmentAbsent: - if (this.request?.segment.externalId.toString() === command.i) { + if (this.request?.segment.externalId === command.i) { this.cancelSegmentRequest(new PeerSegmentAbsentError()); this.segments.delete(command.i); } @@ -130,7 +130,7 @@ export class Peer { const { externalId } = segment; const command: PeerSegmentCommand = { c: PeerCommandType.SegmentRequest, - i: externalId.toString(), + i: externalId, }; this.sendCommand(command); this.request = this.createPeerRequest(segment); @@ -230,7 +230,7 @@ export class Peer { if (!(reason instanceof PeerSegmentAbsentError)) { this.sendCommand({ c: PeerCommandType.CancelSegmentRequest, - i: this.request.segment.externalId.toString(), + i: this.request.segment.externalId, }); } this.request.reject(reason); From c6419ae8cf6e2b4d06ac1389d7302da37bd2a3b1 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 25 Sep 2023 12:17:00 +0300 Subject: [PATCH 058/127] Don't use response.arrayBuffer() if getting data from stream. --- .../p2p-media-loader-core/src/http-loader.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 44a56d53..5cb947de 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -34,8 +34,10 @@ function fetchSegmentData(segment: Segment) { }); if (response.ok) { - progress = monitorFetchProgress(response); - return response.arrayBuffer(); + const result = getDataPromiseAndMonitorProgress(response); + if (!result) return response.arrayBuffer(); + progress = result.progress; + return result.dataPromise; } throw new FetchError( response.statusText ?? `Network response was not for ${segmentId}`, @@ -58,9 +60,12 @@ function fetchSegmentData(segment: Segment) { }; } -function monitorFetchProgress( - response: Response -): Readonly | undefined { +function getDataPromiseAndMonitorProgress(response: Response): + | { + progress: LoadProgress; + dataPromise: Promise; + } + | undefined { const totalBytesString = response.headers.get("Content-Length"); if (totalBytesString === null || !response.body) return; @@ -72,14 +77,26 @@ function monitorFetchProgress( }; const reader = response.body.getReader(); - const monitor = async () => { + const getDataPromise = async () => { + const chunks: Uint8Array[] = []; for await (const chunk of readStream(reader)) { + chunks.push(chunk); progress.loadedBytes += chunk.length; progress.percent = (progress.loadedBytes / totalBytes) * 100; } + + const resultBuffer = new ArrayBuffer(progress.loadedBytes); + const view = new Uint8Array(resultBuffer); + + let offset = 0; + for (const chunk of chunks) { + view.set(chunk, offset); + offset += chunk.length; + } + + return resultBuffer; }; - void monitor(); - return progress; + return { progress, dataPromise: getDataPromise() }; } async function* readStream( From b443b03fe375b4baa0be2d99bc6747a8b24de2e9 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 25 Sep 2023 16:02:05 +0300 Subject: [PATCH 059/127] Change peer announcement type and corresponding logic. --- .../p2p-media-loader-core/src/http-loader.ts | 16 +++--- .../src/internal-types.ts | 9 ++-- .../p2p-media-loader-core/src/p2p-loader.ts | 35 ++++++------- .../p2p-media-loader-core/src/peer-utils.ts | 50 ++++++++----------- packages/p2p-media-loader-core/src/peer.ts | 9 ++-- packages/p2p-media-loader-core/src/request.ts | 38 ++++++++++---- 6 files changed, 79 insertions(+), 78 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 5cb947de..3f91d23e 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -35,7 +35,6 @@ function fetchSegmentData(segment: Segment) { if (response.ok) { const result = getDataPromiseAndMonitorProgress(response); - if (!result) return response.arrayBuffer(); progress = result.progress; return result.dataPromise; } @@ -60,14 +59,14 @@ function fetchSegmentData(segment: Segment) { }; } -function getDataPromiseAndMonitorProgress(response: Response): - | { - progress: LoadProgress; - dataPromise: Promise; - } - | undefined { +function getDataPromiseAndMonitorProgress(response: Response): { + progress?: LoadProgress; + dataPromise: Promise; +} { const totalBytesString = response.headers.get("Content-Length"); - if (totalBytesString === null || !response.body) return; + if (totalBytesString === null || !response.body) { + return { dataPromise: response.arrayBuffer() }; + } const totalBytes = +totalBytesString; const progress: LoadProgress = { @@ -83,6 +82,7 @@ function getDataPromiseAndMonitorProgress(response: Response): chunks.push(chunk); progress.loadedBytes += chunk.length; progress.percent = (progress.loadedBytes / totalBytes) * 100; + progress.lastLoadedChunkTimestamp = performance.now(); } const resultBuffer = new ArrayBuffer(progress.loadedBytes); diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index f6acf623..51b413ea 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -29,9 +29,10 @@ export type BasePeerCommand = { c: T; }; -// {[streamId]: [segmentIds[]; segmentStatuses[]]} -export type JsonSegmentAnnouncementMap = { - [key: string]: [string[], number[]]; +// {i: segmentExternalId[]; s: segment status separator position in ids array} +export type JsonSegmentAnnouncement = { + i: string[]; + s: number; }; export type PeerSegmentCommand = BasePeerCommand< @@ -44,7 +45,7 @@ export type PeerSegmentCommand = BasePeerCommand< export type PeerSegmentAnnouncementCommand = BasePeerCommand & { - m: JsonSegmentAnnouncementMap; + a: JsonSegmentAnnouncement; }; export type PeerSendSegmentCommand = diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 9307ef48..15b3d667 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -3,7 +3,7 @@ import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./peer-utils"; import { Segment, Settings, StreamWithSegments } from "./types"; -import { JsonSegmentAnnouncementMap } from "./internal-types"; +import { JsonSegmentAnnouncement } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils"; import { PeerSegmentStatus } from "./enums"; @@ -15,7 +15,7 @@ export class P2PLoader { private readonly peerHash: string; private readonly trackerClient: TrackerClient; private readonly peers = new Map(); - private announcementMap: JsonSegmentAnnouncementMap = {}; + private announcement: JsonSegmentAnnouncement = { i: [], s: 0 }; constructor( private streamManifestUrl: string, @@ -37,9 +37,11 @@ export class P2PLoader { peerHash: this.peerHash, }); this.subscribeOnTrackerEvents(this.trackerClient); - this.segmentStorage.subscribeOnUpdate( - this.onSegmentStorageUpdate.bind(this) - ); + this.segmentStorage.subscribeOnUpdate(() => { + this.updateSegmentAnnouncement(); + this.broadcastSegmentAnnouncement(); + }); + this.updateSegmentAnnouncement(); this.trackerClient.start(); } @@ -92,36 +94,27 @@ export class P2PLoader { this.peers.set(candidate.id, peer); } - private async onSegmentStorageUpdate() { + private updateSegmentAnnouncement() { const { storedSegmentIds } = this.segmentStorage; - const loaded: Segment[] = []; - const httpLoading: Segment[] = []; - - for (const id of storedSegmentIds) { - const segment = this.stream.segments.get(id); - if (!segment) continue; - - loaded.push(segment); - } + const loaded: string[] = [...storedSegmentIds]; + const httpLoading: string[] = []; for (const request of this.requests.values()) { if (request.loaderRequest?.type !== "http") continue; const segment = this.stream.segments.get(request.segment.localId); if (!segment) continue; - httpLoading.push(segment); + httpLoading.push(segment.externalId); } - this.announcementMap = PeerUtil.getJsonSegmentsAnnouncementMap( - this.streamExternalId, + this.announcement = PeerUtil.getJsonSegmentsAnnouncement( loaded, httpLoading ); - this.broadcastSegmentAnnouncement(); } private onPeerConnected(peer: Peer) { - peer.sendSegmentsAnnouncement(this.announcementMap); + peer.sendSegmentsAnnouncement(this.announcement); } private async onSegmentRequested(peer: Peer, segmentExternalId: string) { @@ -135,7 +128,7 @@ export class P2PLoader { private broadcastSegmentAnnouncement() { for (const peer of this.peers.values()) { if (!peer.isConnected) continue; - peer.sendSegmentsAnnouncement(this.announcementMap); + peer.sendSegmentsAnnouncement(this.announcement); } } diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/peer-utils.ts index e6daa5c3..cd5374fa 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/peer-utils.ts @@ -1,9 +1,7 @@ -import { JsonSegmentAnnouncementMap, PeerCommand } from "./internal-types"; +import { JsonSegmentAnnouncement, PeerCommand } from "./internal-types"; import * as TypeGuard from "./type-guards"; -import * as Util from "./utils"; import { PeerSegmentStatus } from "./enums"; import * as RIPEMD160 from "ripemd160"; -import { Segment } from "./types"; export function generatePeerId(): string { const PEER_ID_SYMBOLS = @@ -42,40 +40,32 @@ export function getPeerCommandFromArrayBuffer( } } -export function getSegmentsFromPeerAnnouncementMap( - map: JsonSegmentAnnouncementMap +export function getSegmentsFromPeerAnnouncement( + announcement: JsonSegmentAnnouncement ): Map { const segmentStatusMap = new Map(); - for (const [streamId, [segmentIds, statuses]] of Object.entries(map)) { - for (let i = 0; i < segmentIds.length; i++) { - const segmentId = segmentIds[i]; - const segmentStatus = statuses[i]; - const segmentFullId = Util.getSegmentFullExternalId(streamId, segmentId); - segmentStatusMap.set(segmentFullId, segmentStatus); + const separator = announcement.s; + for (const [index, segmentExternalId] of announcement.i.entries()) { + if (index < separator) { + segmentStatusMap.set(segmentExternalId, PeerSegmentStatus.Loaded); + } else { + segmentStatusMap.set(segmentExternalId, PeerSegmentStatus.LoadingByHttp); } } return segmentStatusMap; } -export function getJsonSegmentsAnnouncementMap( - streamExternalId: string, - storedSegments: Segment[], - loadingByHttpSegments: Segment[] -): JsonSegmentAnnouncementMap { - const segmentExternalIds: string[] = []; - const segmentStatuses: PeerSegmentStatus[] = []; - - for (const segment of storedSegments) { - segmentExternalIds.push(segment.externalId); - segmentStatuses.push(PeerSegmentStatus.Loaded); - } - - for (const segment of loadingByHttpSegments) { - segmentExternalIds.push(segment.externalId); - segmentStatuses.push(PeerSegmentStatus.LoadingByHttp); - } - +export function getJsonSegmentsAnnouncement( + storedSegmentExternalIds: string[], + loadingByHttpSegmentExternalIds: string[] +): JsonSegmentAnnouncement { + const segmentIds = [ + ...storedSegmentExternalIds, + ...loadingByHttpSegmentExternalIds, + ]; + const segmentStatusSeparator = storedSegmentExternalIds.length; return { - [streamExternalId]: [segmentExternalIds, segmentStatuses], + i: segmentIds, + s: segmentStatusSeparator, }; } diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 39155b18..e3b9b9d4 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -1,6 +1,6 @@ import { PeerCandidate } from "bittorrent-tracker"; import { - JsonSegmentAnnouncementMap, + JsonSegmentAnnouncement, PeerCommand, PeerSegmentAnnouncementCommand, PeerSegmentCommand, @@ -88,7 +88,7 @@ export class Peer { switch (command.c) { case PeerCommandType.SegmentsAnnouncement: - this.segments = PeerUtil.getSegmentsFromPeerAnnouncementMap(command.m); + this.segments = PeerUtil.getSegmentsFromPeerAnnouncement(command.a); break; case PeerCommandType.SegmentRequest: @@ -137,10 +137,10 @@ export class Peer { return this.request.p2pRequest; } - sendSegmentsAnnouncement(map: JsonSegmentAnnouncementMap) { + sendSegmentsAnnouncement(announcement: JsonSegmentAnnouncement) { const command: PeerSegmentAnnouncementCommand = { c: PeerCommandType.SegmentsAnnouncement, - m: map, + a: announcement, }; this.sendCommand(command); } @@ -204,6 +204,7 @@ export class Peer { progress.loadedBytes += chuck.byteLength; progress.percent = (progress.loadedBytes / progress.loadedBytes) * 100; + progress.lastLoadedChunkTimestamp = performance.now(); request.chunks.push(chuck); if (progress.loadedBytes === progress.totalBytes) { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 3e8f6af5..fcdcea1e 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -10,6 +10,7 @@ export type LoadProgress = { percent: number; loadedBytes: number; totalBytes: number; + lastLoadedChunkTimestamp?: number; }; type RequestBase = { @@ -27,7 +28,7 @@ export type P2PRequest = RequestBase & { type: "p2p"; }; -type HybridLoaderRequest = HttpRequest | P2PRequest; +export type HybridLoaderRequest = HttpRequest | P2PRequest; type Request = { segment: Readonly; @@ -41,6 +42,26 @@ function getRequestItemId(segment: Segment) { export class RequestContainer { private readonly requests = new Map(); + private _httpRequestsCount = 0; + private _p2pRequestsCount = 0; + + get httpRequestsCount() { + return this._httpRequestsCount; + } + + get p2pRequestsCount() { + return this._p2pRequestsCount; + } + + private increaseRequestCounters(requestType: "http" | "p2p") { + if (requestType === "http") this._httpRequestsCount++; + else this._p2pRequestsCount++; + } + + private decreaseRequestCounters(requestType: "http" | "p2p") { + if (requestType === "http") this._httpRequestsCount--; + else this._p2pRequestsCount--; + } addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { const segmentId = getRequestItemId(segment); @@ -53,6 +74,7 @@ export class RequestContainer { loaderRequest, }); } + this.increaseRequestCounters(loaderRequest.type); loaderRequest.promise.then(() => this.clearRequestItem(segmentId, "loader") ); @@ -101,15 +123,6 @@ export class RequestContainer { return this.requests.get(segmentId)?.loaderRequest?.type === "http"; } - countHttpRequests(): number { - let count = 0; - for (const request of this.requests.values()) { - if (request.loaderRequest?.type === "http") count++; - } - - return count; - } - abortEngineRequest(segmentId: string) { const request = this.requests.get(segmentId); if (!request) return; @@ -135,7 +148,10 @@ export class RequestContainer { if (!requestItem) return; if (type === "engine") delete requestItem.engineCallbacks; - if (type === "loader") delete requestItem.loaderRequest; + if (type === "loader" && requestItem.loaderRequest) { + this.decreaseRequestCounters(requestItem.loaderRequest.type); + delete requestItem.loaderRequest; + } if (!requestItem.engineCallbacks && !requestItem.loaderRequest) { const segmentId = getRequestItemId(requestItem.segment); this.requests.delete(segmentId); From 0c8af4f149429bae3716c41ec0b0205fff7f57c2 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 25 Sep 2023 17:56:09 +0300 Subject: [PATCH 060/127] Use single PeerRequestError with different types instead of lot of Error classes. --- .../src/declarations.d.ts | 4 +- packages/p2p-media-loader-core/src/errors.ts | 26 +++++------ .../p2p-media-loader-core/src/p2p-loader.ts | 24 +++++------ packages/p2p-media-loader-core/src/peer.ts | 43 ++++++++----------- 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index 91ae17ae..da331a27 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -32,7 +32,7 @@ declare module "bittorrent-tracker" { ? (error: unknown) => void : never; - type PeerEvent = "connect" | "data" | "close"; + type PeerEvent = "connect" | "data" | "close" | "error"; export type PeerCandidateEventHandler = E extends "connect" @@ -41,6 +41,8 @@ declare module "bittorrent-tracker" { ? (data: ArrayBuffer) => void : E extends "close" ? () => void + : E extends "error" + ? (error?: unknown) => void : never; export type PeerCandidate = { diff --git a/packages/p2p-media-loader-core/src/errors.ts b/packages/p2p-media-loader-core/src/errors.ts index f585d330..c052ebec 100644 --- a/packages/p2p-media-loader-core/src/errors.ts +++ b/packages/p2p-media-loader-core/src/errors.ts @@ -15,20 +15,16 @@ export class RequestAbortError extends Error { } } -export class RequestTimeoutError extends Error { - constructor(message = "TimeoutError") { - super(message); - } -} - -export class ResponseBytesMismatchError extends Error { - constructor(message = "ResponseBytesMismatch") { - super(message); - } -} - -export class PeerSegmentAbsentError extends Error { - constructor(message = "PeerSegmentAbsent") { - super(message); +export class PeerRequestError extends Error { + constructor( + readonly type: + | "abort" + | "request-timeout" + | "response-bytes-mismatch" + | "segment-absent" + | "peer-closed" + | "destroy" + ) { + super(); } } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 15b3d667..34bf308a 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -60,6 +60,18 @@ export class P2PLoader { trackerClient.on("error", (error) => {}); } + private createPeer(candidate: PeerCandidate) { + const peer = new Peer( + candidate, + { + onPeerConnected: this.onPeerConnected.bind(this), + onSegmentRequested: this.onSegmentRequested.bind(this), + }, + this.settings + ); + this.peers.set(candidate.id, peer); + } + async downloadSegment(segment: Segment): Promise { const segmentExternalId = segment.externalId; const peerWithSegment: Peer[] = []; @@ -82,18 +94,6 @@ export class P2PLoader { return request.promise; } - private createPeer(candidate: PeerCandidate) { - const peer = new Peer( - candidate, - { - onPeerConnected: this.onPeerConnected.bind(this), - onSegmentRequested: this.onSegmentRequested.bind(this), - }, - this.settings - ); - this.peers.set(candidate.id, peer); - } - private updateSegmentAnnouncement() { const { storedSegmentIds } = this.segmentStorage; const loaded: string[] = [...storedSegmentIds]; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index e3b9b9d4..b67103b5 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -11,12 +11,7 @@ import * as PeerUtil from "./peer-utils"; import { P2PRequest } from "./request"; import { Segment, Settings } from "./types"; import * as Utils from "./utils"; -import { - RequestAbortError, - RequestTimeoutError, - ResponseBytesMismatchError, - PeerSegmentAbsentError, -} from "./errors"; +import { PeerRequestError } from "./errors"; type PeerEventHandlers = { onPeerConnected: (peer: Peer) => void; @@ -27,7 +22,7 @@ type PeerRequest = { segment: Segment; p2pRequest: P2PRequest; resolve: (data: ArrayBuffer) => void; - reject: (reason?: unknown) => void; + reject: (error: PeerRequestError) => void; chunks: ArrayBuffer[]; responseTimeoutId: number; }; @@ -60,10 +55,15 @@ export class Peer { this.connection = candidate; this.eventHandlers.onPeerConnected(this); }); + candidate.on("data", () => this.onReceiveData.bind(this)); candidate.on("close", () => { - if (this.connection === candidate) this.connection = undefined; + if (this.connection === candidate) { + this.connection = undefined; + this.cancelSegmentRequest("peer-closed"); + } }); - candidate.on("data", () => this.onReceiveData.bind(this)); + // eslint-disable-next-line @typescript-eslint/no-empty-function + candidate.on("error", () => {}); this.candidates.add(candidate); } @@ -107,7 +107,7 @@ export class Peer { case PeerCommandType.SegmentAbsent: if (this.request?.segment.externalId === command.i) { - this.cancelSegmentRequest(new PeerSegmentAbsentError()); + this.cancelSegmentRequest("segment-absent"); this.segments.delete(command.i); } break; @@ -189,9 +189,10 @@ export class Peer { chunks: [], p2pRequest: { type: "p2p", + startTimestamp: performance.now(), promise, - abort: () => this.cancelSegmentRequest(new RequestAbortError()), + abort: () => this.cancelSegmentRequest("abort"), }, }; } @@ -211,7 +212,7 @@ export class Peer { const segmentData = joinChunks(request.chunks); this.approveRequest(segmentData); } else if (progress.loadedBytes > progress.totalBytes) { - this.cancelSegmentRequest(new ResponseBytesMismatchError()); + this.cancelSegmentRequest("response-bytes-mismatch"); } } @@ -220,27 +221,22 @@ export class Peer { this.clearRequest(); } - private cancelSegmentRequest( - reason: - | RequestAbortError - | RequestTimeoutError - | PeerSegmentAbsentError - | ResponseBytesMismatchError - ) { + private cancelSegmentRequest(type: PeerRequestError["type"]) { + const error = new PeerRequestError(type); if (!this.request) return; - if (!(reason instanceof PeerSegmentAbsentError)) { + if (!["segment-absent", "peer-closed"].includes(type)) { this.sendCommand({ c: PeerCommandType.CancelSegmentRequest, i: this.request.segment.externalId, }); } - this.request.reject(reason); + this.request.reject(error); this.clearRequest(); } private setRequestTimeout(): number { return window.setTimeout( - () => this.cancelSegmentRequest(new RequestTimeoutError()), + () => this.cancelSegmentRequest("request-timeout"), this.settings.p2pSegmentDownloadTimeout ); } @@ -251,8 +247,7 @@ export class Peer { } destroy() { - // TODO: error for peer destroyed - this.cancelSegmentRequest(); + this.cancelSegmentRequest("destroy"); this.connection?.destroy(); this.candidates.clear(); } From 1938c55826217d5983afbc29d9cc0fd1e20b9299 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 25 Sep 2023 17:56:31 +0300 Subject: [PATCH 061/127] Change peer announcement type and corresponding logic. --- packages/p2p-media-loader-core/src/core.ts | 1 + .../src/hybrid-loader.ts | 56 ++++++++++++++++--- packages/p2p-media-loader-core/src/types.ts | 1 + 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 9510ab56..bf0d3e6e 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -11,6 +11,7 @@ export class Core { private readonly streams = new Map>(); private readonly settings: Settings = { simultaneousHttpDownloads: 3, + simultaneousP2PDownloads: 3, highDemandTimeWindow: 25, httpDownloadTimeWindow: 60, p2pDownloadTimeWindow: 60, diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7bb3b584..aae67ab7 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -100,17 +100,40 @@ export class HybridLoader { queueSegmentIds.has(segmentId) ); - const { simultaneousHttpDownloads } = this.settings; + const { simultaneousHttpDownloads, simultaneousP2PDownloads } = + this.settings; for (const { segment, statuses } of queue) { - if (this.requests.isHttpRequested(segment.localId)) continue; + // const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); if (statuses.isHighDemand) { - if (this.requests.countHttpRequests() < simultaneousHttpDownloads) { - void this.loadSegmentThroughHttp(segment); + if (this.requests.isHttpRequested(segment.localId)) continue; + // const request = this.requests.get(segment.localId); + // if (request?.loaderRequest?.type === "p2p") { + // const remainingDownloadTime = getPredictedRemainingDownloadTime( + // request.loaderRequest + // ); + // if ( + // remainingDownloadTime === undefined || + // remainingDownloadTime > timeToPlayback + // ) { + // request.loaderRequest.abort(); + // } else { + // continue; + // } + // } + if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(segment); continue; } + this.abortLastHttpLoadingAfter(queue, segment.localId); - if (this.requests.countHttpRequests() < simultaneousHttpDownloads) { - void this.loadSegmentThroughHttp(segment); + if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(segment); + continue; + } + } + if (statuses.isP2PDownloadable) { + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(segment); } } break; @@ -122,7 +145,7 @@ export class HybridLoader { this.requests.abortEngineRequest(segmentId); } - private async loadSegmentThroughHttp(segment: Segment) { + private async loadThroughHttp(segment: Segment) { let data: ArrayBuffer | undefined; try { const httpRequest = getHttpSegmentRequest(segment); @@ -225,6 +248,8 @@ class P2PLoadersContainer { else this.createLoader(stream); } + // TODO: add stale loaders destroying + get activeLoader() { return this._activeLoader; } @@ -235,3 +260,20 @@ class P2PLoadersContainer { } } } + +// function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { +// return Math.max(segment.startTime - playback.position, 0) / playback.rate; +// } +// +// function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { +// const { startTimestamp, progress } = request; +// if (!progress || progress.percent === 0) return undefined; +// const now = performance.now(); +// const bandwidth = +// progress.percent / (progress.lastLoadedChunkTimestamp - startTimestamp); +// const remainingDownloadPercent = 100 - progress.percent; +// const predictedRemainingTimeFromLastDownload = +// remainingDownloadPercent / bandwidth; +// const timeFromLastDownload = now - progress.lastLoadedChunkTimestamp; +// return predictedRemainingTimeFromLastDownload - timeFromLastDownload; +// } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 59f27cab..3c6e6634 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -46,6 +46,7 @@ export type Settings = { httpDownloadTimeWindow: number; p2pDownloadTimeWindow: number; simultaneousHttpDownloads: number; + simultaneousP2PDownloads: number; cachedSegmentExpiration: number; cachedSegmentsCount: number; webRtcMaxMessageSize: number; From f966ca464b5ac5c58800dda3b69b94586b251d57 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 26 Sep 2023 11:31:11 +0300 Subject: [PATCH 062/127] Remove memory request container counters. --- .../src/hybrid-loader.ts | 22 ++++++++++++ packages/p2p-media-loader-core/src/request.ts | 34 ++++++++++--------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index aae67ab7..e6a41a05 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -130,6 +130,16 @@ export class HybridLoader { void this.loadThroughHttp(segment); continue; } + + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(segment); + } + + this.abortLastP2PLoadingAfter(queue, segment.localId); + if (this.requests.p2pRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(segment); + continue; + } } if (statuses.isP2PDownloadable) { if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { @@ -187,6 +197,18 @@ export class HybridLoader { } } + private abortLastP2PLoadingAfter(queue: QueueItem[], segmentId: string) { + for (const { + segment: { localId: queueSegmentId }, + } of arrayBackwards(queue)) { + if (queueSegmentId === segmentId) break; + if (this.requests.isP2PRequested(queueSegmentId)) { + this.requests.abortLoaderRequest(queueSegmentId); + break; + } + } + } + updatePlayback(position: number, rate: number) { const isRateChanged = this.playback.rate !== rate; const isPositionChanged = this.playback.position !== position; diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index fcdcea1e..dccf42b1 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -42,25 +42,19 @@ function getRequestItemId(segment: Segment) { export class RequestContainer { private readonly requests = new Map(); - private _httpRequestsCount = 0; - private _p2pRequestsCount = 0; get httpRequestsCount() { - return this._httpRequestsCount; + let count = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const request of this.httpRequests()) count++; + return count; } get p2pRequestsCount() { - return this._p2pRequestsCount; - } - - private increaseRequestCounters(requestType: "http" | "p2p") { - if (requestType === "http") this._httpRequestsCount++; - else this._p2pRequestsCount++; - } - - private decreaseRequestCounters(requestType: "http" | "p2p") { - if (requestType === "http") this._httpRequestsCount--; - else this._p2pRequestsCount--; + let count = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const request of this.p2pRequests()) count++; + return count; } addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { @@ -74,7 +68,6 @@ export class RequestContainer { loaderRequest, }); } - this.increaseRequestCounters(loaderRequest.type); loaderRequest.promise.then(() => this.clearRequestItem(segmentId, "loader") ); @@ -111,6 +104,12 @@ export class RequestContainer { } } + *p2pRequests(): Generator { + for (const request of this.requests.values()) { + if (request.loaderRequest?.type === "p2p") yield request; + } + } + resolveEngineRequest(segmentId: string, response: SegmentResponse) { this.requests.get(segmentId)?.engineCallbacks?.onSuccess(response); } @@ -123,6 +122,10 @@ export class RequestContainer { return this.requests.get(segmentId)?.loaderRequest?.type === "http"; } + isP2PRequested(segmentId: string): boolean { + return this.requests.get(segmentId)?.loaderRequest?.type === "p2p"; + } + abortEngineRequest(segmentId: string) { const request = this.requests.get(segmentId); if (!request) return; @@ -149,7 +152,6 @@ export class RequestContainer { if (type === "engine") delete requestItem.engineCallbacks; if (type === "loader" && requestItem.loaderRequest) { - this.decreaseRequestCounters(requestItem.loaderRequest.type); delete requestItem.loaderRequest; } if (!requestItem.engineCallbacks && !requestItem.loaderRequest) { From a0864cfd21074a51e23a1bb76c30c198ee9f66e4 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 26 Sep 2023 13:07:44 +0300 Subject: [PATCH 063/127] Remove unnecessary segmentsToDelete from memory segment storage. --- .../p2p-media-loader-core/src/segments-storage.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 0f401b27..d36c2c7a 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -10,7 +10,6 @@ export class SegmentsMemoryStorage { string, { segment: Segment; data: ArrayBuffer; lastAccessed: number } >(); - private readonly cachedSegmentIds = new Set(); private readonly isSegmentLockedPredicates: (( segment: Segment ) => boolean)[] = []; @@ -48,7 +47,6 @@ export class SegmentsMemoryStorage { data, lastAccessed: performance.now(), }); - this.cachedSegmentIds.add(id); this.onUpdateSubscriptions.forEach((c) => c()); } @@ -63,11 +61,11 @@ export class SegmentsMemoryStorage { } hasSegment(segmentExternalId: string): boolean { - return this.cachedSegmentIds.has(segmentExternalId); + return this.cache.has(segmentExternalId); } - get storedSegmentIds(): ReadonlySet { - return this.cachedSegmentIds; + get storedSegmentIds() { + return this.cache.keys(); } private async clear(): Promise { @@ -108,10 +106,7 @@ export class SegmentsMemoryStorage { } } - segmentsToDelete.forEach((id) => { - this.cache.delete(id); - this.cachedSegmentIds.delete(id); - }); + segmentsToDelete.forEach((id) => this.cache.delete(id)); if (segmentsToDelete.length) { this.onUpdateSubscriptions.forEach((c) => c()); } From 093f87b75886fda338ab9cc600d1fb109f27a982 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 27 Sep 2023 11:41:42 +0300 Subject: [PATCH 064/127] Fix issue with typo in "chunk" --- packages/p2p-media-loader-core/src/peer.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index b67103b5..63dbf97c 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -82,7 +82,7 @@ export class Peer { private onReceiveData(data: ArrayBuffer) { const command = PeerUtil.getPeerCommandFromArrayBuffer(data); if (!command) { - this.receiveSegmentChuck(data); + this.receiveSegmentChunk(data); return; } @@ -155,13 +155,13 @@ export class Peer { this.sendCommand(command); this.isSendingData = true; - const sendChuck = async (data: ArrayBuffer) => this.connection?.send(data); - for (const chuck of getBufferChunks( + const sendChunk = async (data: ArrayBuffer) => this.connection?.send(data); + for (const chunk of getBufferChunks( data, this.settings.webRtcMaxMessageSize )) { if (!this.isSendingData) break; - void sendChuck(chuck); + void sendChunk(chunk); } this.isSendingData = false; } @@ -197,16 +197,16 @@ export class Peer { }; } - private receiveSegmentChuck(chuck: ArrayBuffer): void { + private receiveSegmentChunk(chunk: ArrayBuffer): void { // TODO: check can be chunk received before peer command answer const { request } = this; const progress = request?.p2pRequest?.progress; if (!request || !progress) return; - progress.loadedBytes += chuck.byteLength; + progress.loadedBytes += chunk.byteLength; progress.percent = (progress.loadedBytes / progress.loadedBytes) * 100; progress.lastLoadedChunkTimestamp = performance.now(); - request.chunks.push(chuck); + request.chunks.push(chunk); if (progress.loadedBytes === progress.totalBytes) { const segmentData = joinChunks(request.chunks); @@ -255,11 +255,11 @@ export class Peer { function* getBufferChunks( data: ArrayBuffer, - maxChuckSize: number + maxChunkSize: number ): Generator { let bytesLeft = data.byteLength; while (bytesLeft > 0) { - const bytesToSend = bytesLeft >= maxChuckSize ? maxChuckSize : bytesLeft; + const bytesToSend = bytesLeft >= maxChunkSize ? maxChunkSize : bytesLeft; const buffer = Buffer.from(data, data.byteLength - bytesLeft, bytesToSend); bytesLeft -= bytesToSend; yield buffer; From 7363354130a0915efff3e78c887c50bdf454b75c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 27 Sep 2023 21:26:46 +0300 Subject: [PATCH 065/127] Add ability to subscribe to segments store update. --- packages/p2p-media-loader-core/src/core.ts | 14 +- .../src/hybrid-loader.ts | 29 +-- .../p2p-media-loader-core/src/p2p-loader.ts | 23 ++- .../src/segments-storage.ts | 176 +++++++++++++----- packages/p2p-media-loader-core/src/utils.ts | 8 +- 5 files changed, 169 insertions(+), 81 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index bf0d3e6e..bf9e2d53 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -19,10 +19,10 @@ export class Core { cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, p2pSegmentDownloadTimeout: 1000, - storageCleanupInterval: 1000, + storageCleanupInterval: 5000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); - private readonly segmentStorage = new SegmentsMemoryStorage(this.settings); + private segmentStorage?: SegmentsMemoryStorage; private mainStreamLoader?: HybridLoader; private secondaryStreamLoader?: HybridLoader; @@ -64,8 +64,12 @@ export class Core { if (!this.manifestResponseUrl) { throw new Error("Manifest response url is not defined"); } - if (!this.segmentStorage.isInitialized) { - await this.segmentStorage.initialize(this.manifestResponseUrl); + if (!this.segmentStorage) { + this.segmentStorage = new SegmentsMemoryStorage( + this.manifestResponseUrl, + this.settings + ); + await this.segmentStorage.initialize(); } const { segment, stream } = this.identifySegment(segmentLocalId); const loader = this.getStreamHybridLoader(segment, stream); @@ -108,7 +112,7 @@ export class Core { throw new Error("Manifest response url is not defined"); } const createNewHybridLoader = (manifestResponseUrl: string) => { - if (!this.segmentStorage.isInitialized) { + if (!this.segmentStorage?.isInitialized) { throw new Error("Segment storage is not initialized"); } return new HybridLoader( diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index aae67ab7..abe2561f 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,4 +1,4 @@ -import { Segment, StreamWithSegments } from "./index"; +import { Segment, Stream, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; @@ -66,7 +66,8 @@ export class HybridLoader { void this.processQueue(); const storageData = await this.segmentStorage.getSegmentData( - segment.localId + stream, + segment ); if (storageData) { callbacks.onSuccess({ @@ -88,12 +89,14 @@ export class HybridLoader { } this.lastQueueProcessingTimeStamp = now; + const stream = this.activeStream; const { queue, queueSegmentIds } = Utils.generateQueue({ segment: this.lastRequestedSegment, - stream: this.activeStream, + stream, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segmentId) => this.segmentStorage.hasSegment(segmentId), + isSegmentLoaded: (segment) => + this.segmentStorage.hasSegment(segment, stream), }); this.requests.abortAllNotRequestedByEngine((segmentId) => @@ -121,19 +124,19 @@ export class HybridLoader { // } // } if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); + void this.loadThroughHttp(stream, segment); continue; } this.abortLastHttpLoadingAfter(queue, segment.localId); if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); + void this.loadThroughHttp(stream, segment); continue; } } if (statuses.isP2PDownloadable) { if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); + void this.loadThroughP2P(stream, segment); } } break; @@ -145,7 +148,7 @@ export class HybridLoader { this.requests.abortEngineRequest(segmentId); } - private async loadThroughHttp(segment: Segment) { + private async loadThroughHttp(stream: Stream, segment: Segment) { let data: ArrayBuffer | undefined; try { const httpRequest = getHttpSegmentRequest(segment); @@ -156,18 +159,18 @@ export class HybridLoader { // TODO: handle error } } - if (data) this.onSegmentLoaded(segment, data); + if (data) this.onSegmentLoaded(stream, segment, data); } - private async loadThroughP2P(segment: Segment) { + private async loadThroughP2P(stream: Stream, segment: Segment) { const p2pLoader = this.p2pLoaders.activeLoader; const data = await p2pLoader.downloadSegment(segment); - if (data) this.onSegmentLoaded(segment, data); + if (data) this.onSegmentLoaded(stream, segment, data); } - private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { + private onSegmentLoaded(stream: Stream, segment: Segment, data: ArrayBuffer) { this.bandwidthApproximator.addBytes(data.byteLength); - void this.segmentStorage.storeSegment(segment, data); + void this.segmentStorage.storeSegment(stream, segment, data); this.requests.resolveEngineRequest(segment.localId, { data, bandwidth: this.bandwidthApproximator.getBandwidth(), diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 34bf308a..56fa89e1 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -26,8 +26,8 @@ export class P2PLoader { ) { const peerId = PeerUtil.generatePeerId(); this.streamExternalId = Utils.getStreamExternalId( - this.stream, - this.streamManifestUrl + this.streamManifestUrl, + this.stream ); this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); @@ -37,10 +37,7 @@ export class P2PLoader { peerHash: this.peerHash, }); this.subscribeOnTrackerEvents(this.trackerClient); - this.segmentStorage.subscribeOnUpdate(() => { - this.updateSegmentAnnouncement(); - this.broadcastSegmentAnnouncement(); - }); + this.segmentStorage.subscribeOnUpdate(this.stream, this.onStorageUpdate); this.updateSegmentAnnouncement(); this.trackerClient.start(); } @@ -95,8 +92,8 @@ export class P2PLoader { } private updateSegmentAnnouncement() { - const { storedSegmentIds } = this.segmentStorage; - const loaded: string[] = [...storedSegmentIds]; + const loaded: string[] = + this.segmentStorage.getStoredSegmentExternalIdsOfStream(this.stream); const httpLoading: string[] = []; for (const request of this.requests.values()) { @@ -117,8 +114,14 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(this.announcement); } + private onStorageUpdate = () => { + this.updateSegmentAnnouncement(); + this.broadcastSegmentAnnouncement(); + }; + private async onSegmentRequested(peer: Peer, segmentExternalId: string) { const segmentData = await this.segmentStorage.getSegmentData( + this.stream, segmentExternalId ); if (segmentData) peer.sendSegmentData(segmentExternalId, segmentData); @@ -133,6 +136,10 @@ export class P2PLoader { } destroy() { + this.segmentStorage.unsubscribeFromUpdate( + this.stream, + this.onStorageUpdate + ); for (const peer of this.peers.values()) { peer.destroy(); } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 0f401b27..1cc4101b 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -1,28 +1,75 @@ -import { Segment, Settings } from "./types"; +import { Segment, Settings, Stream } from "./types"; type StorageSettings = Pick< Settings, "cachedSegmentExpiration" | "cachedSegmentsCount" | "storageCleanupInterval" >; +function getStreamShortExternalId(stream: Readonly) { + const { type, index } = stream; + return `${type}-${index}`; +} + +function getStorageItemId(stream: Stream, segment: Segment | string) { + const segmentExternalId = + typeof segment === "string" ? segment : segment.externalId; + const streamExternalId = getStreamShortExternalId(stream); + return `${streamExternalId}|${segmentExternalId}`; +} + +class Subscriptions void> { + private readonly list: Set; + + constructor(handlers: T | T[]) { + this.list = new Set(Array.isArray(handlers) ? handlers : [handlers]); + } + + add(handler: T) { + this.list.add(handler); + } + + remove(handler: T) { + this.list.delete(handler); + } + + fire(...args: Parameters) { + for (const handler of this.list) { + handler(...args); + } + } + + get isEmpty() { + return this.list.size === 0; + } +} + +type StorageItem = { + streamId: string; + segment: Segment; + data: ArrayBuffer; + lastAccessed: number; +}; + export class SegmentsMemoryStorage { - private cache = new Map< - string, - { segment: Segment; data: ArrayBuffer; lastAccessed: number } - >(); - private readonly cachedSegmentIds = new Set(); + private cache = new Map(); + private _isInitialized = false; + private cleanupIntervalId?: number; private readonly isSegmentLockedPredicates: (( segment: Segment ) => boolean)[] = []; - private onUpdateSubscriptions: (() => void)[] = []; - private _isInitialized = false; - private cleanupIntervalId?: number; + private onUpdateSubscriptions = new Map void>>(); - constructor(private settings: StorageSettings) {} + constructor( + private readonly masterManifestUrl: string, + private readonly settings: StorageSettings + ) {} - async initialize(masterManifestUrl: string) { + async initialize() { this._isInitialized = true; - this.cleanupIntervalId = window.setInterval(() => this.clear(), 1000); + this.cleanupIntervalId = window.setInterval( + () => this.clear(), + this.settings.storageCleanupInterval + ); } get isInitialized(): boolean { @@ -33,94 +80,121 @@ export class SegmentsMemoryStorage { this.isSegmentLockedPredicates.push(predicate); } - private isSegmentLocked(segment: Segment) { + private isSegmentLocked(segment: Segment): boolean { return this.isSegmentLockedPredicates.some((p) => p(segment)); } - subscribeOnUpdate(callback: () => void) { - this.onUpdateSubscriptions.push(callback); - } - - async storeSegment(segment: Segment, data: ArrayBuffer) { - const id = segment.externalId; + async storeSegment(stream: Stream, segment: Segment, data: ArrayBuffer) { + const id = getStorageItemId(stream, segment); + const streamId = getStreamShortExternalId(stream); this.cache.set(id, { + streamId, segment, data, lastAccessed: performance.now(), }); - this.cachedSegmentIds.add(id); - this.onUpdateSubscriptions.forEach((c) => c()); + this.fireOnUpdateSubscriptions(streamId); } async getSegmentData( - segmentExternalId: string + stream: Stream, + segment: Segment | string ): Promise { - const cacheItem = this.cache.get(segmentExternalId); + const itemId = getStorageItemId(stream, segment); + const cacheItem = this.cache.get(itemId); if (cacheItem === undefined) return undefined; cacheItem.lastAccessed = performance.now(); return cacheItem.data; } - hasSegment(segmentExternalId: string): boolean { - return this.cachedSegmentIds.has(segmentExternalId); + hasSegment(segment: Segment, stream: Stream): boolean { + const id = getStorageItemId(stream, segment); + return this.cache.has(id); } - get storedSegmentIds(): ReadonlySet { - return this.cachedSegmentIds; + getStoredSegmentExternalIdsOfStream(stream: Stream) { + const streamId = getStreamShortExternalId(stream); + const externalIds: string[] = []; + for (const { streamId: itemStreamId, segment } of this.cache.values()) { + if (itemStreamId === streamId) externalIds.push(segment.externalId); + } + return externalIds; } private async clear(): Promise { - const segmentsToDelete: string[] = []; - const remainingSegments: { - lastAccessed: number; - segment: Segment; - }[] = []; + const itemsToDelete: string[] = []; + const remainingItems: [string, StorageItem][] = []; + const streamIdsOfChangedItems = new Set(); // Delete old segments const now = performance.now(); - for (const [ - segmentExternalId, - { lastAccessed, segment }, - ] of this.cache.entries()) { + for (const entry of this.cache.entries()) { + const [itemId, item] = entry; + const { lastAccessed, segment, streamId } = item; if (now - lastAccessed > this.settings.cachedSegmentExpiration) { if (!this.isSegmentLocked(segment)) { - segmentsToDelete.push(segmentExternalId); + itemsToDelete.push(itemId); + streamIdsOfChangedItems.add(streamId); } } else { - remainingSegments.push({ segment, lastAccessed }); + remainingItems.push(entry); } } // Delete segments over cached count let countOverhead = - remainingSegments.length - this.settings.cachedSegmentsCount; + remainingItems.length - this.settings.cachedSegmentsCount; if (countOverhead > 0) { - remainingSegments.sort((a, b) => a.lastAccessed - b.lastAccessed); + remainingItems.sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed); - for (const cachedSegment of remainingSegments) { - if (!this.isSegmentLocked(cachedSegment.segment)) { - segmentsToDelete.push(cachedSegment.segment.externalId); + for (const [itemId, { segment, streamId }] of remainingItems) { + if (!this.isSegmentLocked(segment)) { + itemsToDelete.push(itemId); + streamIdsOfChangedItems.add(streamId); countOverhead--; if (countOverhead === 0) break; } } } - segmentsToDelete.forEach((id) => { - this.cache.delete(id); - this.cachedSegmentIds.delete(id); - }); - if (segmentsToDelete.length) { - this.onUpdateSubscriptions.forEach((c) => c()); + if (itemsToDelete.length) { + itemsToDelete.forEach((id) => this.cache.delete(id)); + for (const streamId of streamIdsOfChangedItems) { + this.fireOnUpdateSubscriptions(streamId); + } } - return segmentsToDelete.length > 0; + + return itemsToDelete.length > 0; + } + + subscribeOnUpdate(stream: Stream, handler: () => void) { + const streamId = getStreamShortExternalId(stream); + const handlers = this.onUpdateSubscriptions.get(streamId); + if (!handlers) { + this.onUpdateSubscriptions.set(streamId, new Subscriptions(handler)); + } else { + handlers.add(handler); + } + } + + unsubscribeFromUpdate(stream: Stream, handler: () => void) { + const streamId = getStreamShortExternalId(stream); + const handlers = this.onUpdateSubscriptions.get(streamId); + if (handlers) { + handlers.remove(handler); + if (handlers.isEmpty) this.onUpdateSubscriptions.delete(streamId); + } + } + + private fireOnUpdateSubscriptions(streamId: string) { + this.onUpdateSubscriptions.get(streamId)?.fire(); } public async destroy() { this.cache.clear(); - this.onUpdateSubscriptions = []; + this.onUpdateSubscriptions.clear(); this._isInitialized = false; clearInterval(this.cleanupIntervalId); } diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts index d5851e4a..89fe84e3 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils.ts @@ -8,8 +8,8 @@ import { } from "./internal-types"; export function getStreamExternalId( - stream: Readonly, - manifestResponseUrl: string + manifestResponseUrl: string, + stream: Readonly ): string { const { type, index } = stream; return `${manifestResponseUrl}-${type}-${index}`; @@ -42,7 +42,7 @@ export function generateQueue({ stream: Readonly; segment: Readonly; playback: Readonly; - isSegmentLoaded: (segmentExternalId: string) => boolean; + isSegmentLoaded: (segment: Segment) => boolean; settings: Pick< Settings, "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" @@ -64,7 +64,7 @@ export function generateQueue({ for (const segment of stream.segments.values(requestedSegmentId)) { const statuses = getSegmentLoadStatuses(segment, bufferRanges); if (!statuses && !(i === 0 && isNextSegmentHighDemand)) break; - if (isSegmentLoaded(segment.externalId)) continue; + if (isSegmentLoaded(segment)) continue; queueSegmentIds.add(segment.localId); statuses.isHighDemand = true; From ff29655392eb395f543bc6a3366f5eb245eef0d8 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 28 Sep 2023 12:17:14 +0300 Subject: [PATCH 066/127] Add stale p2p loader destroy timeout. --- packages/p2p-media-loader-core/src/core.ts | 1 + .../src/hybrid-loader.ts | 47 ++++++++++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index bf9e2d53..3306943f 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -20,6 +20,7 @@ export class Core { webRtcMaxMessageSize: 64 * 1024 - 1, p2pSegmentDownloadTimeout: 1000, storageCleanupInterval: 5000, + p2pLoaderDestroyTimeout: 30000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); private segmentStorage?: SegmentsMemoryStorage; diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 2cc8f0ec..60ad6001 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -238,9 +238,15 @@ function* arrayBackwards(arr: T[]) { } } +type P2PLoaderContainerItem = { + streamId: string; + loader: P2PLoader; + destroyTimeoutId?: number; +}; + class P2PLoadersContainer { - private readonly loaders = new Map(); - private _activeLoader: P2PLoader; + private readonly loaders = new Map(); + private _activeLoaderItem: P2PLoaderContainerItem; constructor( private readonly streamManifestUrl: string, @@ -249,40 +255,55 @@ class P2PLoadersContainer { private readonly segmentStorage: SegmentsMemoryStorage, private readonly settings: Settings ) { - this._activeLoader = this.createLoader(stream); + this._activeLoaderItem = this.createLoaderItem(stream); } - createLoader(stream: StreamWithSegments) { + createLoaderItem(stream: StreamWithSegments) { if (this.loaders.has(stream.localId)) { throw new Error("Loader for this stream already exists"); } - this._activeLoader = new P2PLoader( + const loader = new P2PLoader( this.streamManifestUrl, stream, this.requests, this.segmentStorage, this.settings ); - this.loaders.set(stream.localId, this._activeLoader); - return this._activeLoader; + const item = { loader, streamId: stream.localId }; + this.loaders.set(stream.localId, item); + this._activeLoaderItem = item; + return item; } changeActiveLoader(stream: StreamWithSegments) { - const existingLoader = this.loaders.get(stream.localId); - if (existingLoader) this._activeLoader = existingLoader; - else this.createLoader(stream); + const loaderItem = this.loaders.get(stream.localId); + const prevActive = this._activeLoaderItem; + if (loaderItem) { + this._activeLoaderItem = loaderItem; + clearTimeout(loaderItem.destroyTimeoutId); + } else { + this.createLoaderItem(stream); + } + this.setLoaderDestroyTimeout(prevActive); } - // TODO: add stale loaders destroying + private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { + item.destroyTimeoutId = window.setTimeout(() => { + item.loader.destroy(); + this.loaders.delete(item.streamId); + }, this.settings.p2pLoaderDestroyTimeout); + } get activeLoader() { - return this._activeLoader; + return this._activeLoaderItem.loader; } destroy() { - for (const loader of this.loaders.values()) { + for (const { loader, destroyTimeoutId } of this.loaders.values()) { loader.destroy(); + clearTimeout(destroyTimeoutId); } + this.loaders.clear(); } } From fe58ea06adb440594d8844b6176a5038062c58c3 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 28 Sep 2023 12:17:45 +0300 Subject: [PATCH 067/127] Add request container http requests subscriptions. --- .../p2p-media-loader-core/src/p2p-loader.ts | 15 ++++++++--- packages/p2p-media-loader-core/src/request.ts | 13 ++++++++++ .../src/segments-storage.ts | 26 ++++++++++++------- packages/p2p-media-loader-core/src/types.ts | 1 + 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 56fa89e1..ab850df5 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -37,7 +37,13 @@ export class P2PLoader { peerHash: this.peerHash, }); this.subscribeOnTrackerEvents(this.trackerClient); - this.segmentStorage.subscribeOnUpdate(this.stream, this.onStorageUpdate); + this.segmentStorage.subscribeOnUpdate( + this.stream, + this.updateAndBroadcastAnnouncement + ); + this.requests.subscribeOnHttpRequestsUpdate( + this.updateAndBroadcastAnnouncement + ); this.updateSegmentAnnouncement(); this.trackerClient.start(); } @@ -114,7 +120,7 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(this.announcement); } - private onStorageUpdate = () => { + private updateAndBroadcastAnnouncement = () => { this.updateSegmentAnnouncement(); this.broadcastSegmentAnnouncement(); }; @@ -138,7 +144,10 @@ export class P2PLoader { destroy() { this.segmentStorage.unsubscribeFromUpdate( this.stream, - this.onStorageUpdate + this.updateAndBroadcastAnnouncement + ); + this.requests.unsubscribeFromHttpRequestsUpdate( + this.updateAndBroadcastAnnouncement ); for (const peer of this.peers.values()) { peer.destroy(); diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index dccf42b1..ab83f231 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,5 +1,6 @@ import { Segment, SegmentResponse } from "./types"; import { RequestAbortError } from "./errors"; +import { Subscriptions } from "./segments-storage"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -42,6 +43,7 @@ function getRequestItemId(segment: Segment) { export class RequestContainer { private readonly requests = new Map(); + private readonly onHttpRequestsHandlers = new Subscriptions(); get httpRequestsCount() { let count = 0; @@ -68,6 +70,9 @@ export class RequestContainer { loaderRequest, }); } + if (loaderRequest.type === "http") { + this.onHttpRequestsHandlers.fire(); + } loaderRequest.promise.then(() => this.clearRequestItem(segmentId, "loader") ); @@ -174,6 +179,14 @@ export class RequestContainer { } } + subscribeOnHttpRequestsUpdate(handler: () => void) { + this.onHttpRequestsHandlers.add(handler); + } + + unsubscribeFromHttpRequestsUpdate(handler: () => void) { + this.onHttpRequestsHandlers.remove(handler); + } + destroy() { for (const request of this.requests.values()) { request.loaderRequest?.abort(); diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 1cc4101b..6fc3a87e 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -17,11 +17,17 @@ function getStorageItemId(stream: Stream, segment: Segment | string) { return `${streamExternalId}|${segmentExternalId}`; } -class Subscriptions void> { +export class Subscriptions< + T extends (...args: unknown[]) => void = () => void +> { private readonly list: Set; - constructor(handlers: T | T[]) { - this.list = new Set(Array.isArray(handlers) ? handlers : [handlers]); + constructor(handlers?: T | T[]) { + if (handlers) { + this.list = new Set(Array.isArray(handlers) ? handlers : [handlers]); + } else { + this.list = new Set(); + } } add(handler: T) { @@ -57,7 +63,7 @@ export class SegmentsMemoryStorage { private readonly isSegmentLockedPredicates: (( segment: Segment ) => boolean)[] = []; - private onUpdateSubscriptions = new Map void>>(); + private onUpdateHandlers = new Map void>>(); constructor( private readonly masterManifestUrl: string, @@ -171,9 +177,9 @@ export class SegmentsMemoryStorage { subscribeOnUpdate(stream: Stream, handler: () => void) { const streamId = getStreamShortExternalId(stream); - const handlers = this.onUpdateSubscriptions.get(streamId); + const handlers = this.onUpdateHandlers.get(streamId); if (!handlers) { - this.onUpdateSubscriptions.set(streamId, new Subscriptions(handler)); + this.onUpdateHandlers.set(streamId, new Subscriptions(handler)); } else { handlers.add(handler); } @@ -181,20 +187,20 @@ export class SegmentsMemoryStorage { unsubscribeFromUpdate(stream: Stream, handler: () => void) { const streamId = getStreamShortExternalId(stream); - const handlers = this.onUpdateSubscriptions.get(streamId); + const handlers = this.onUpdateHandlers.get(streamId); if (handlers) { handlers.remove(handler); - if (handlers.isEmpty) this.onUpdateSubscriptions.delete(streamId); + if (handlers.isEmpty) this.onUpdateHandlers.delete(streamId); } } private fireOnUpdateSubscriptions(streamId: string) { - this.onUpdateSubscriptions.get(streamId)?.fire(); + this.onUpdateHandlers.get(streamId)?.fire(); } public async destroy() { this.cache.clear(); - this.onUpdateSubscriptions.clear(); + this.onUpdateHandlers.clear(); this._isInitialized = false; clearInterval(this.cleanupIntervalId); } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 3c6e6634..763f6a35 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -52,4 +52,5 @@ export type Settings = { webRtcMaxMessageSize: number; p2pSegmentDownloadTimeout: number; storageCleanupInterval: number; + p2pLoaderDestroyTimeout: number; }; From 978d95d6528f1fc42cfe94a75f2c4acede97fd0f Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 28 Sep 2023 14:23:11 +0300 Subject: [PATCH 068/127] Add vite-plugin-node-polyfills to demo. --- p2p-media-loader-demo/package.json | 3 +- p2p-media-loader-demo/vite.config.ts | 3 +- packages/p2p-media-loader-core/package.json | 2 +- pnpm-lock.yaml | 828 ++++++++++++++++++-- 4 files changed, 753 insertions(+), 83 deletions(-) diff --git a/p2p-media-loader-demo/package.json b/p2p-media-loader-demo/package.json index c54ef890..94839827 100644 --- a/p2p-media-loader-demo/package.json +++ b/p2p-media-loader-demo/package.json @@ -27,6 +27,7 @@ "@types/react-dom": "^18.2.4", "@vitejs/plugin-react": "^4.0.0", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.1" + "eslint-plugin-react-refresh": "^0.4.1", + "vite-plugin-node-polyfills": "^0.14.1" } } diff --git a/p2p-media-loader-demo/vite.config.ts b/p2p-media-loader-demo/vite.config.ts index 081c8d9f..d00bebd0 100644 --- a/p2p-media-loader-demo/vite.config.ts +++ b/p2p-media-loader-demo/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; export default defineConfig({ - plugins: [react()], + plugins: [nodePolyfills(), react()], }); diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index b42d0390..5857eeb4 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -28,7 +28,7 @@ "type-check": "npx tsc --noEmit" }, "dependencies": { - "bittorrent-tracker": "^10.0.12", + "bittorrent-tracker": "^9.19.0", "ripemd160": "^2.0.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54c6c991..fceced56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,12 +81,15 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.1 version: 0.4.1(eslint@8.39.0) + vite-plugin-node-polyfills: + specifier: ^0.14.1 + version: 0.14.1(vite@4.3.2) packages/p2p-media-loader-core: dependencies: bittorrent-tracker: - specifier: ^10.0.12 - version: 10.0.12 + specifier: ^9.19.0 + version: 9.19.0 ripemd160: specifier: ^2.0.2 version: 2.0.2 @@ -679,32 +682,33 @@ packages: dev: true optional: true - /@thaunknown/simple-peer@9.12.1: - resolution: {integrity: sha512-IS5BXvXx7cvBAzaxqotJf4s4rJCPk5JABLK6Gbnn7oAmWVcH4hYABabBBrvvJtv/xyUqR4v/H3LalnGRJJfEog==} + /@rollup/plugin-inject@5.0.3: + resolution: {integrity: sha512-411QlbL+z2yXpRWFXSmw/teQRMkXcAAC8aYTemc15gwJRpvEVDQwoe+N/HTFD8RFG8+88Bme9DK2V9CVm7hJdA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - debug: 4.3.4 - err-code: 3.0.1 - get-browser-rtc: 1.1.0 - queue-microtask: 1.2.3 - streamx: 2.15.1 - uint8-util: 2.2.3 - transitivePeerDependencies: - - supports-color - dev: false + '@rollup/pluginutils': 5.0.4 + estree-walker: 2.0.2 + magic-string: 0.27.0 + dev: true - /@thaunknown/simple-websocket@9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): - resolution: {integrity: sha512-vzQloFWRodRZqZhpxMpBljFtISesY8TihA8T5uKwCYdj2I1ImMhE/gAeTCPsCGOtxJfGKu3hw/is6MXauWLjOg==} + /@rollup/pluginutils@5.0.4: + resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - debug: 4.3.4 - queue-microtask: 1.2.3 - streamx: 2.15.1 - uint8-util: 2.2.3 - ws: 8.14.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false + '@types/estree': 1.0.2 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true /@types/debug@4.1.8: resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} @@ -716,6 +720,10 @@ packages: resolution: {integrity: sha512-bkTVZkK3Vi7N7eX2FUBnqKhCjTaeRLkhvY8H6zolatbSTtjPPdxyUzhE3C29sIBYRRq1kQHSduFgCHKg5VF3Jw==} dev: true + /@types/estree@1.0.2: + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + dev: true + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true @@ -919,9 +927,8 @@ packages: hasBin: true dev: true - /addr-to-ip-port@2.0.0: - resolution: {integrity: sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==} - engines: {node: '>=12.20.0'} + /addr-to-ip-port@1.5.4: + resolution: {integrity: sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg==} dev: false /ajv@6.12.6: @@ -971,10 +978,34 @@ packages: engines: {node: '>=8'} dev: true + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: true + + /assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + dependencies: + call-bind: 1.0.2 + is-nan: 1.3.2 + object-is: 1.1.5 + object.assign: 4.1.4 + util: 0.12.5 + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + /axios@1.2.3(debug@4.3.4): resolution: {integrity: sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==} dependencies: @@ -993,31 +1024,25 @@ packages: resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} dev: false - /base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - dev: false + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - /bencode@4.0.0: - resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} - engines: {node: '>=12.20.0'} - dependencies: - uint8-util: 2.2.3 + /bencode@2.0.3: + resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==} dev: false /bittorrent-peerid@1.3.6: resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==} dev: false - /bittorrent-tracker@10.0.12: - resolution: {integrity: sha512-EYQEwhOYkrRiiwkCFcM9pbzJInsAe7UVmUgevW133duwlZzjwf5ABwDE7pkkmNRS6iwN0b8LbI/94q16dYqiow==} - engines: {node: '>=12.20.0'} + /bittorrent-tracker@9.19.0: + resolution: {integrity: sha512-09d0aD2b+MC+zWvWajkUAKkYMynYW4tMbTKiRSthKtJZbafzEoNQSUHyND24SoCe3ZOb2fKfa6fu2INAESL9wA==} + engines: {node: '>=12'} hasBin: true dependencies: - '@thaunknown/simple-peer': 9.12.1 - '@thaunknown/simple-websocket': 9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) - bencode: 4.0.0 + bencode: 2.0.3 bittorrent-peerid: 1.3.6 + bn.js: 5.2.1 chrome-dgram: 3.0.6 clone: 2.1.2 compact2string: 1.4.1 @@ -1028,14 +1053,16 @@ packages: once: 1.4.0 queue-microtask: 1.2.3 random-iterate: 1.0.1 + randombytes: 2.1.0 run-parallel: 1.2.0 run-series: 1.1.9 simple-get: 4.0.1 + simple-peer: 9.11.1 + simple-websocket: 9.1.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) socks: 2.7.1 - string2compact: 2.0.1 - uint8-util: 2.2.3 + string2compact: 1.3.2 unordered-array-remove: 1.0.2 - ws: 8.14.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@5.0.10) optionalDependencies: bufferutil: 4.0.7 utf-8-validate: 5.0.10 @@ -1043,6 +1070,13 @@ packages: - supports-color dev: false + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: true + + /bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1063,6 +1097,71 @@ packages: fill-range: 7.0.1 dev: true + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: true + + /browser-resolve@2.0.0: + resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} + dependencies: + resolve: 1.22.6 + dev: true + + /browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + dev: true + + /browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + dependencies: + cipher-base: 1.0.4 + des.js: 1.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-rsa@4.1.0: + resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} + dependencies: + bn.js: 5.2.1 + randombytes: 2.1.0 + dev: true + + /browserify-sign@4.2.1: + resolution: {integrity: sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==} + dependencies: + bn.js: 5.2.1 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.5.4 + inherits: 2.0.4 + parse-asn1: 5.1.6 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: true + + /browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + dependencies: + pako: 1.0.11 + dev: true + /browserslist@4.21.8: resolution: {integrity: sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1074,6 +1173,23 @@ packages: update-browserslist-db: 1.0.11(browserslist@4.21.8) dev: true + /buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: true + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + /bufferutil@4.0.7: resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} engines: {node: '>=6.14.2'} @@ -1082,6 +1198,17 @@ packages: node-gyp-build: 4.6.1 dev: false + /builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1115,6 +1242,13 @@ packages: run-series: 1.1.9 dev: false + /cipher-base@1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /clone@2.1.2: resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} engines: {node: '>=0.8'} @@ -1158,10 +1292,50 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + dev: true + + /constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + dev: true + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true + /create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + dependencies: + bn.js: 4.12.0 + elliptic: 6.5.4 + dev: true + + /create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + dev: true + + /create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1171,6 +1345,22 @@ packages: which: 2.0.2 dev: true + /crypto-browserify@3.12.0: + resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.1 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + inherits: 2.0.4 + pbkdf2: 3.1.2 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + dev: true + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: true @@ -1197,11 +1387,44 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} dev: false + /des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + dependencies: + bn.js: 4.12.0 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1220,6 +1443,11 @@ packages: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} dev: false + /domain-browser@4.22.0: + resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==} + engines: {node: '>=10'} + dev: true + /dplayer@1.27.1(debug@4.3.4): resolution: {integrity: sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==} dependencies: @@ -1238,6 +1466,18 @@ packages: resolution: {integrity: sha512-FytjTbGwz///F+ToZ5XSeXbbSaXalsVRXsz2mHityI5gfxft7ieW3HqFLkU5V1aIrY42aflICqbmFoDxW10etg==} dev: true + /elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + /eme-encryption-scheme-polyfill@2.1.1: resolution: {integrity: sha512-njD17wcUrbqCj0ArpLu5zWXtaiupHb/2fIUQGdInf83GlI+Q6mmqaPGLdrke4savKAu15J/z1Tg/ivDgl14g0g==} dev: false @@ -1435,11 +1675,27 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1448,10 +1704,6 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true - /fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - dev: false - /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1523,6 +1775,12 @@ packages: debug: 4.3.4 dev: false + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -1552,6 +1810,10 @@ packages: dev: true optional: true + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1561,6 +1823,15 @@ packages: resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==} dev: false + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1629,6 +1900,12 @@ packages: slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true @@ -1643,6 +1920,36 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + /hash-base@3.1.0: resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} engines: {node: '>=4'} @@ -1650,12 +1957,33 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 safe-buffer: 5.2.1 - dev: false + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true /hls.js@1.4.5: resolution: {integrity: sha512-xb7IiSM9apU3tJWb5rdSStobXPNJJykHTwSy7JnLF5y/kLJXWjoR/fEpNBlwYxkKcDiiSfO9SQI8yFravZJxIg==} dev: false + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + + /https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -1697,6 +2025,25 @@ packages: engines: {node: '>= 10'} dev: false + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1707,6 +2054,13 @@ packages: engines: {node: '>=8'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1714,6 +2068,14 @@ packages: is-extglob: 2.1.1 dev: true + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1724,10 +2086,22 @@ packages: engines: {node: '>=8'} dev: true + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /isomorphic-timers-promises@1.0.1: + resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} + engines: {node: '>=10'} + dev: true + /jackspeak@2.2.1: resolution: {integrity: sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==} engines: {node: '>=14'} @@ -1822,6 +2196,21 @@ packages: inherits: 2.0.4 dev: false + /magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1835,6 +2224,14 @@ packages: picomatch: 2.3.1 dev: true + /miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + dev: true + /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1858,6 +2255,14 @@ packages: dom-walk: 0.1.2 dev: false + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: true + + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -1915,6 +2320,66 @@ packages: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} dev: true + /node-stdlib-browser@1.2.0: + resolution: {integrity: sha512-VSjFxUhRhkyed8AtLwSCkMrJRfQ3e2lGtG3sP6FEgaLKBBbxM/dLfjRe1+iLhjvyLFW3tBQ8+c0pcOtXGbAZJg==} + engines: {node: '>=10'} + dependencies: + assert: 2.1.0 + browser-resolve: 2.0.0 + browserify-zlib: 0.2.0 + buffer: 5.7.1 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + create-require: 1.1.1 + crypto-browserify: 3.12.0 + domain-browser: 4.22.0 + events: 3.3.0 + https-browserify: 1.0.0 + isomorphic-timers-promises: 1.0.1 + os-browserify: 0.3.0 + path-browserify: 1.0.1 + pkg-dir: 5.0.0 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 3.6.2 + stream-browserify: 3.0.0 + stream-http: 3.2.0 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.1 + url: 0.11.3 + util: 0.12.5 + vm-browserify: 1.1.2 + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -1932,6 +2397,10 @@ packages: word-wrap: 1.2.3 dev: true + /os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1946,6 +2415,10 @@ packages: p-limit: 3.1.0 dev: true + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: true + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1953,6 +2426,20 @@ packages: callsites: 3.1.0 dev: true + /parse-asn1@5.1.6: + resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} + dependencies: + asn1.js: 5.4.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.2 + safe-buffer: 5.2.1 + dev: true + + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1968,6 +2455,10 @@ packages: engines: {node: '>=8'} dev: true + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + /path-scurry@1.9.2: resolution: {integrity: sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==} engines: {node: '>=16 || 14 >=14.17'} @@ -1981,6 +2472,17 @@ packages: engines: {node: '>=8'} dev: true + /pbkdf2@3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -1990,6 +2492,13 @@ packages: engines: {node: '>=8.6'} dev: true + /pkg-dir@5.0.0: + resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} + engines: {node: '>=10'} + dependencies: + find-up: 5.0.0 + dev: true + /postcss@8.4.24: resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==} engines: {node: ^10 || ^12 || >=14} @@ -2020,7 +2529,6 @@ packages: /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: false /promise-polyfill@8.3.0: resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} @@ -2030,22 +2538,57 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + dependencies: + bn.js: 4.12.0 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + parse-asn1: 5.1.6 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: true + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + + /querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - /queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - dev: false - /random-iterate@1.0.1: resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==} dev: false + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + + /randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -2075,7 +2618,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: false /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -2086,6 +2628,15 @@ packages: engines: {node: '>=4'} dev: true + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2111,7 +2662,6 @@ packages: dependencies: hash-base: 3.1.0 inherits: 2.0.4 - dev: false /rollup@3.25.1: resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} @@ -2132,7 +2682,10 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} @@ -2153,6 +2706,18 @@ packages: lru-cache: 6.0.0 dev: true + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: true + + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /shaka-player@4.3.7: resolution: {integrity: sha512-3aeRb/AdVnGJKI1i23pD+b2e4eXljjJflxW+RLeWrSGHNRt/givvV3DyGIzpNQ9icUDatUyOtSkx8AdTXZWYXQ==} engines: {node: '>=14'} @@ -2172,6 +2737,14 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + /signal-exit@4.0.2: resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} engines: {node: '>=14'} @@ -2189,6 +2762,34 @@ packages: simple-concat: 1.0.1 dev: false + /simple-peer@9.11.1: + resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==} + dependencies: + buffer: 6.0.3 + debug: 4.3.4 + err-code: 3.0.1 + get-browser-rtc: 1.1.0 + queue-microtask: 1.2.3 + randombytes: 2.1.0 + readable-stream: 3.6.2 + transitivePeerDependencies: + - supports-color + dev: false + + /simple-websocket@9.1.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==} + dependencies: + debug: 4.3.4 + queue-microtask: 1.2.3 + randombytes: 2.1.0 + readable-stream: 3.6.2 + ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2212,12 +2813,21 @@ packages: engines: {node: '>=0.10.0'} dev: true - /streamx@2.15.1: - resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} + /stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} dependencies: - fast-fifo: 1.3.2 - queue-tick: 1.0.1 - dev: false + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /stream-http@3.2.0: + resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + xtend: 4.0.2 + dev: true /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -2237,11 +2847,10 @@ packages: strip-ansi: 7.1.0 dev: true - /string2compact@2.0.1: - resolution: {integrity: sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==} - engines: {node: '>=12.20.0'} + /string2compact@1.3.2: + resolution: {integrity: sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw==} dependencies: - addr-to-ip-port: 2.0.0 + addr-to-ip-port: 1.5.4 ipaddr.js: 2.1.0 dev: false @@ -2249,7 +2858,6 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -2284,10 +2892,22 @@ packages: has-flag: 4.0.0 dev: true + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + dependencies: + setimmediate: 1.0.5 + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -2314,6 +2934,10 @@ packages: typescript: 5.0.2 dev: true + /tty-browserify@0.0.1: + resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2332,12 +2956,6 @@ packages: hasBin: true dev: true - /uint8-util@2.2.3: - resolution: {integrity: sha512-FtRKosYvGfP6gcX15JSrG7BkvwdZTFtiXH05NMZagEwthY/XUFoSxENEdtky0MfhyVhffLGUt9M9gWEaSmj2aA==} - dependencies: - base64-arraybuffer: 1.0.2 - dev: false - /unordered-array-remove@1.0.2: resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==} dev: false @@ -2359,6 +2977,13 @@ packages: punycode: 2.3.0 dev: true + /url@0.11.3: + resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} + dependencies: + punycode: 1.4.1 + qs: 6.11.2 + dev: true + /utf-8-validate@5.0.10: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} @@ -2369,7 +2994,30 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.11 + dev: true + + /vite-plugin-node-polyfills@0.14.1(vite@4.3.2): + resolution: {integrity: sha512-S5ofYUkXea/d94AHzDwiTA7Pv/yEwzagnjgVEuBZdy7E72GBfK17qpljAlyK3CD+CRcDzAwwl/4bEjKdvZmTGQ==} + peerDependencies: + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 + dependencies: + '@rollup/plugin-inject': 5.0.3 + buffer-polyfill: /buffer@6.0.3 + node-stdlib-browser: 1.2.0 + process: 0.11.10 + vite: 4.3.2 + transitivePeerDependencies: + - rollup + dev: true /vite@4.3.2: resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==} @@ -2403,6 +3051,21 @@ packages: fsevents: 2.3.2 dev: true + /vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2437,12 +3100,12 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /ws@8.14.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): - resolution: {integrity: sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==} - engines: {node: '>=10.0.0'} + /ws@7.5.9(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' + utf-8-validate: ^5.0.2 peerDependenciesMeta: bufferutil: optional: true @@ -2453,6 +3116,11 @@ packages: utf-8-validate: 5.0.10 dev: false + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true From 9671b0c10410c9790ea28239e032765fedbaad7c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 28 Sep 2023 14:25:48 +0300 Subject: [PATCH 069/127] Move utils to separate folder. --- packages/p2p-media-loader-core/src/core.ts | 2 +- .../src/hybrid-loader.ts | 8 +-- .../src/internal-types.ts | 4 +- .../p2p-media-loader-core/src/p2p-loader.ts | 4 +- packages/p2p-media-loader-core/src/peer.ts | 4 +- .../src/{ => utils}/peer-utils.ts | 6 +- .../src/{utils.ts => utils/queue-utils.ts} | 66 +++++-------------- .../p2p-media-loader-core/src/utils/utils.ts | 43 ++++++++++++ 8 files changed, 73 insertions(+), 64 deletions(-) rename packages/p2p-media-loader-core/src/{ => utils}/peer-utils.ts (92%) rename packages/p2p-media-loader-core/src/{utils.ts => utils/queue-utils.ts} (69%) create mode 100644 packages/p2p-media-loader-core/src/utils/utils.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 3306943f..c42e9bc5 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,6 +1,6 @@ import { HybridLoader } from "./hybrid-loader"; import { Stream, StreamWithSegments, Segment, Settings } from "./types"; -import * as Utils from "./utils"; +import * as Utils from "./utils/utils"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { EngineCallbacks } from "./request"; diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 60ad6001..b6f1be2f 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -6,7 +6,7 @@ import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestContainer, EngineCallbacks } from "./request"; -import * as Utils from "./utils"; +import * as QueueUtils from "./utils/queue-utils"; import { FetchError } from "./errors"; export class HybridLoader { @@ -37,11 +37,11 @@ export class HybridLoader { if (!this.activeStream.segments.has(segment.localId)) { return false; } - const bufferRanges = Utils.getLoadBufferRanges( + const bufferRanges = QueueUtils.getLoadBufferRanges( this.playback, this.settings ); - return Utils.isSegmentActual(segment, bufferRanges); + return QueueUtils.isSegmentActual(segment, bufferRanges); }); this.p2pLoaders = new P2PLoadersContainer( this.streamManifestUrl, @@ -90,7 +90,7 @@ export class HybridLoader { this.lastQueueProcessingTimeStamp = now; const stream = this.activeStream; - const { queue, queueSegmentIds } = Utils.generateQueue({ + const { queue, queueSegmentIds } = QueueUtils.generateQueue({ segment: this.lastRequestedSegment, stream, playback: this.playback, diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 51b413ea..8d93d8c2 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -17,13 +17,13 @@ export type LoadBufferRanges = { p2p: NumberRange; }; -export type QueueStatuses = { +export type QueueItemStatuses = { isHighDemand: boolean; isHttpDownloadable: boolean; isP2PDownloadable: boolean; }; -export type QueueItem = { segment: Segment; statuses: QueueStatuses }; +export type QueueItem = { segment: Segment; statuses: QueueItemStatuses }; export type BasePeerCommand = { c: T; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index ab850df5..692ddbd2 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -1,11 +1,11 @@ import TrackerClient, { PeerCandidate } from "bittorrent-tracker"; import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; -import * as PeerUtil from "./peer-utils"; +import * as PeerUtil from "./utils/peer-utils"; import { Segment, Settings, StreamWithSegments } from "./types"; import { JsonSegmentAnnouncement } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; -import * as Utils from "./utils"; +import * as Utils from "./utils/utils"; import { PeerSegmentStatus } from "./enums"; import { RequestContainer } from "./request"; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 63dbf97c..f4c9b502 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -7,10 +7,10 @@ import { PeerSendSegmentCommand, } from "./internal-types"; import { PeerCommandType, PeerSegmentStatus } from "./enums"; -import * as PeerUtil from "./peer-utils"; +import * as PeerUtil from "./utils/peer-utils"; import { P2PRequest } from "./request"; import { Segment, Settings } from "./types"; -import * as Utils from "./utils"; +import * as Utils from "./utils/utils"; import { PeerRequestError } from "./errors"; type PeerEventHandlers = { diff --git a/packages/p2p-media-loader-core/src/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer-utils.ts similarity index 92% rename from packages/p2p-media-loader-core/src/peer-utils.ts rename to packages/p2p-media-loader-core/src/utils/peer-utils.ts index cd5374fa..11a3a92a 100644 --- a/packages/p2p-media-loader-core/src/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/peer-utils.ts @@ -1,6 +1,6 @@ -import { JsonSegmentAnnouncement, PeerCommand } from "./internal-types"; -import * as TypeGuard from "./type-guards"; -import { PeerSegmentStatus } from "./enums"; +import { JsonSegmentAnnouncement, PeerCommand } from "../internal-types"; +import * as TypeGuard from "../type-guards"; +import { PeerSegmentStatus } from "../enums"; import * as RIPEMD160 from "ripemd160"; export function generatePeerId(): string { diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils/queue-utils.ts similarity index 69% rename from packages/p2p-media-loader-core/src/utils.ts rename to packages/p2p-media-loader-core/src/utils/queue-utils.ts index 89fe84e3..a861f495 100644 --- a/packages/p2p-media-loader-core/src/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/queue-utils.ts @@ -1,36 +1,11 @@ -import { Segment, Settings, Stream, StreamWithSegments } from "./index"; +import { Segment, Settings, StreamWithSegments } from "../types"; import { - QueueStatuses, - Playback, LoadBufferRanges, - QueueItem, NumberRange, -} from "./internal-types"; - -export function getStreamExternalId( - manifestResponseUrl: string, - stream: Readonly -): string { - const { type, index } = stream; - return `${manifestResponseUrl}-${type}-${index}`; -} - -export function getSegmentFullExternalId( - externalStreamId: string, - externalSegmentId: string -) { - return `${externalStreamId}|${externalSegmentId}`; -} - -export function getSegmentFromStreamsMap( - streams: Map, - segmentId: string -): { segment: Segment; stream: StreamWithSegments } | undefined { - for (const stream of streams.values()) { - const segment = stream.segments.get(segmentId); - if (segment) return { segment, stream }; - } -} + Playback, + QueueItem, + QueueItemStatuses, +} from "../internal-types"; export function generateQueue({ segment, @@ -63,11 +38,14 @@ export function generateQueue({ let i = 0; for (const segment of stream.segments.values(requestedSegmentId)) { const statuses = getSegmentLoadStatuses(segment, bufferRanges); - if (!statuses && !(i === 0 && isNextSegmentHighDemand)) break; + const isNotActual = isNotActualStatuses(statuses); + if (isNotActual && !(i === 0 && isNextSegmentHighDemand)) { + break; + } if (isSegmentLoaded(segment)) continue; queueSegmentIds.add(segment.localId); - statuses.isHighDemand = true; + if (isNotActual) statuses.isHighDemand = true; queue.push({ segment, statuses }); i++; } @@ -105,7 +83,7 @@ export function getLoadBufferRanges( export function getSegmentLoadStatuses( segment: Readonly, loadBufferRanges: LoadBufferRanges -): QueueStatuses { +): QueueItemStatuses { const { highDemand, http, p2p } = loadBufferRanges; const { startTime, endTime } = segment; @@ -123,6 +101,11 @@ export function getSegmentLoadStatuses( }; } +function isNotActualStatuses(statuses: QueueItemStatuses) { + const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; + return !isHighDemand && !isHttpDownloadable && !isP2PDownloadable; +} + export function isSegmentActual( segment: Readonly, bufferRanges: LoadBufferRanges @@ -139,20 +122,3 @@ export function isSegmentActual( return isInRange(startTime) || isInRange(endTime); } - -export function getControlledPromise() { - let resolve: (value: T) => void; - let reject: (reason?: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - return { - promise, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - resolve: resolve!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - reject: reject!, - }; -} diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts new file mode 100644 index 00000000..f9987466 --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -0,0 +1,43 @@ +import { Segment, Stream, StreamWithSegments } from "../index"; + +export function getStreamExternalId( + manifestResponseUrl: string, + stream: Readonly +): string { + const { type, index } = stream; + return `${manifestResponseUrl}-${type}-${index}`; +} + +export function getSegmentFullExternalId( + externalStreamId: string, + externalSegmentId: string +) { + return `${externalStreamId}|${externalSegmentId}`; +} + +export function getSegmentFromStreamsMap( + streams: Map, + segmentId: string +): { segment: Segment; stream: StreamWithSegments } | undefined { + for (const stream of streams.values()) { + const segment = stream.segments.get(segmentId); + if (segment) return { segment, stream }; + } +} + +export function getControlledPromise() { + let resolve: (value: T) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolve: resolve!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + reject: reject!, + }; +} From 083d59368d424f6f1b169546055654a9af3f16d1 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 28 Sep 2023 18:21:36 +0300 Subject: [PATCH 070/127] Fix issues with abort looping, not handling requests in container. --- packages/p2p-media-loader-core/src/core.ts | 2 +- .../src/hybrid-loader.ts | 33 ++++++------- packages/p2p-media-loader-core/src/request.ts | 47 ++++++++++--------- .../src/fragment-loader.ts | 5 +- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index c42e9bc5..ac0b66d2 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -12,7 +12,7 @@ export class Core { private readonly settings: Settings = { simultaneousHttpDownloads: 3, simultaneousP2PDownloads: 3, - highDemandTimeWindow: 25, + highDemandTimeWindow: 30, httpDownloadTimeWindow: 60, p2pDownloadTimeWindow: 60, cachedSegmentExpiration: 120, diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index b6f1be2f..8cdf569b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -34,9 +34,7 @@ export class HybridLoader { throw new Error("Segment storage is not initialized."); } this.segmentStorage.addIsSegmentLockedPredicate((segment) => { - if (!this.activeStream.segments.has(segment.localId)) { - return false; - } + if (!this.activeStream.segments.has(segment.localId)) return false; const bufferRanges = QueueUtils.getLoadBufferRanges( this.playback, this.settings @@ -63,19 +61,18 @@ export class HybridLoader { this.p2pLoaders.changeActiveLoader(stream); } this.lastRequestedSegment = segment; + this.requests.addEngineCallbacks(segment, callbacks); void this.processQueue(); - const storageData = await this.segmentStorage.getSegmentData( - stream, - segment - ); - if (storageData) { - callbacks.onSuccess({ - data: storageData, - bandwidth: this.bandwidthApproximator.getBandwidth(), - }); + if (this.segmentStorage.hasSegment(segment, stream)) { + const data = await this.segmentStorage.getSegmentData(stream, segment); + if (data) { + this.requests.resolveEngineRequest(segment, { + data, + bandwidth: this.bandwidthApproximator.getBandwidth(), + }); + } } - this.requests.addEngineCallbacks(segment, callbacks); } private processQueue(force = true) { @@ -83,7 +80,7 @@ export class HybridLoader { if ( !force && this.lastQueueProcessingTimeStamp !== undefined && - now - this.lastQueueProcessingTimeStamp >= 950 + now - this.lastQueueProcessingTimeStamp <= 950 ) { return; } @@ -99,8 +96,8 @@ export class HybridLoader { this.segmentStorage.hasSegment(segment, stream), }); - this.requests.abortAllNotRequestedByEngine((segmentId) => - queueSegmentIds.has(segmentId) + this.requests.abortAllNotRequestedByEngine((segment) => + queueSegmentIds.has(segment.localId) ); const { simultaneousHttpDownloads, simultaneousP2PDownloads } = @@ -164,12 +161,12 @@ export class HybridLoader { const httpRequest = getHttpSegmentRequest(segment); this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; + if (data) this.onSegmentLoaded(stream, segment, data); } catch (err) { if (err instanceof FetchError) { // TODO: handle error } } - if (data) this.onSegmentLoaded(stream, segment, data); } private async loadThroughP2P(stream: Stream, segment: Segment) { @@ -181,7 +178,7 @@ export class HybridLoader { private onSegmentLoaded(stream: Stream, segment: Segment, data: ArrayBuffer) { this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(stream, segment, data); - this.requests.resolveEngineRequest(segment.localId, { + this.requests.resolveEngineRequest(segment, { data, bandwidth: this.bandwidthApproximator.getBandwidth(), }); diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index ab83f231..3ec64b48 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,6 +1,7 @@ import { Segment, SegmentResponse } from "./types"; import { RequestAbortError } from "./errors"; import { Subscriptions } from "./segments-storage"; +import Debug from "debug"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -44,6 +45,11 @@ function getRequestItemId(segment: Segment) { export class RequestContainer { private readonly requests = new Map(); private readonly onHttpRequestsHandlers = new Subscriptions(); + private readonly debug = Debug("core:request-container"); + + get totalCount() { + return this.requests.size; + } get httpRequestsCount() { let count = 0; @@ -73,6 +79,7 @@ export class RequestContainer { if (loaderRequest.type === "http") { this.onHttpRequestsHandlers.fire(); } + this.debug(`add loader request: ${loaderRequest.type}`); loaderRequest.promise.then(() => this.clearRequestItem(segmentId, "loader") ); @@ -81,22 +88,22 @@ export class RequestContainer { addEngineCallbacks(segment: Segment, engineCallbacks: EngineCallbacks) { const segmentId = getRequestItemId(segment); const requestItem = this.requests.get(segmentId); + + const { onSuccess } = engineCallbacks; + engineCallbacks.onSuccess = (response) => { + this.clearRequestItem(segmentId, "engine"); + return onSuccess(response); + }; + if (requestItem) { requestItem.engineCallbacks = engineCallbacks; } else { - engineCallbacks.onSuccess = (response) => { - this.clearRequestItem(segmentId, "engine"); - return response; - }; this.requests.set(segmentId, { segment, engineCallbacks, }); } - } - - get(segmentId: string) { - return this.requests.get(segmentId); + this.debug(`add engine request`); } values() { @@ -115,8 +122,9 @@ export class RequestContainer { } } - resolveEngineRequest(segmentId: string, response: SegmentResponse) { - this.requests.get(segmentId)?.engineCallbacks?.onSuccess(response); + resolveEngineRequest(segment: Segment, response: SegmentResponse) { + const id = getRequestItemId(segment); + this.requests.get(id)?.engineCallbacks?.onSuccess(response); } isRequestedByEngine(segmentId: string): boolean { @@ -136,16 +144,11 @@ export class RequestContainer { if (!request) return; request.engineCallbacks?.onError(new RequestAbortError()); + request.loaderRequest?.abort(); } abortLoaderRequest(segmentId: string) { - const request = this.requests.get(segmentId); - if (!request) return; - - if (request.loaderRequest) { - request.loaderRequest.abort(); - request.engineCallbacks?.onError(new RequestAbortError()); - } + this.requests.get(segmentId)?.loaderRequest?.abort(); } private clearRequestItem( @@ -165,17 +168,15 @@ export class RequestContainer { } } - abortAllNotRequestedByEngine(isLocked?: (segmentId: string) => boolean) { + abortAllNotRequestedByEngine(isLocked?: (segment: Segment) => boolean) { + const isSegmentLocked = isLocked ? isLocked : () => false; for (const { loaderRequest, engineCallbacks, segment, } of this.requests.values()) { - if (!engineCallbacks) continue; - const segmentId = getRequestItemId(segment); - if ((!isLocked || !isLocked(segmentId)) && loaderRequest) { - loaderRequest.abort(); - } + if (engineCallbacks || !loaderRequest) continue; + if (!isSegmentLocked(segment)) loaderRequest.abort(); } } diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index 433389d4..52f03dac 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -13,7 +13,6 @@ import { Core, FetchError, SegmentResponse, - EngineCallbacks, } from "p2p-media-loader-core"; const DEFAULT_DOWNLOAD_LATENCY = 10; @@ -112,8 +111,8 @@ export class FragmentLoaderBase implements Loader { private abortInternal() { if (!this.response && this.segmentId) { - this.core.abortSegmentLoading(this.segmentId); this.stats.aborted = true; + this.core.abortSegmentLoading(this.segmentId); } } @@ -130,7 +129,7 @@ export class FragmentLoaderBase implements Loader { if (this.defaultLoader) { this.defaultLoader.destroy(); } else { - this.abortInternal(); + if (!this.stats.aborted) this.abortInternal(); this.callbacks = null; this.config = null; } From 420ffcde115084bdf2b89ba4fc5337d4551d6eed Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 28 Sep 2023 18:36:08 +0300 Subject: [PATCH 071/127] Force queue process if playback position was significantly changed. --- .../src/hybrid-loader.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 8cdf569b..12fa7291 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -17,6 +17,7 @@ export class HybridLoader { private lastRequestedSegment: Readonly; private readonly playback: Playback; private lastQueueProcessingTimeStamp?: number; + private readonly segmentAvgDuration: number; constructor( private streamManifestUrl: string, @@ -29,6 +30,7 @@ export class HybridLoader { this.lastRequestedSegment = requestedSegment; this.activeStream = requestedStream; this.playback = { position: requestedSegment.startTime, rate: 1 }; + this.segmentAvgDuration = getSegmentAvgDuration(requestedStream); if (!this.segmentStorage.isInitialized) { throw new Error("Segment storage is not initialized."); @@ -215,9 +217,13 @@ export class HybridLoader { if (!isRateChanged && !isPositionChanged) return; + const isPositionSignificantlyChanged = + Math.abs(position - this.playback.position) / this.segmentAvgDuration > + 0.5; + if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; - void this.processQueue(false); + void this.processQueue(isPositionSignificantlyChanged); } destroy() { @@ -320,3 +326,15 @@ class P2PLoadersContainer { // const timeFromLastDownload = now - progress.lastLoadedChunkTimestamp; // return predictedRemainingTimeFromLastDownload - timeFromLastDownload; // } + +function getSegmentAvgDuration(stream: StreamWithSegments) { + const { segments } = stream; + let sumDuration = 0; + const size = segments.size; + for (const segment of segments.values()) { + const duration = segment.endTime - segment.startTime; + sumDuration += duration; + } + + return sumDuration / size; +} From 7622bdf121e591e8c04089960d52d0fe07c3ca3c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 29 Sep 2023 13:24:07 +0300 Subject: [PATCH 072/127] Add stream property to segment type. --- packages/p2p-media-loader-core/src/core.ts | 41 ++++++++++------ .../src/hybrid-loader.ts | 49 ++++++++----------- .../p2p-media-loader-core/src/p2p-loader.ts | 4 +- .../src/segments-storage.ts | 38 +++++++------- packages/p2p-media-loader-core/src/types.ts | 13 +++-- .../src/utils/queue-utils.ts | 6 +-- .../p2p-media-loader-core/src/utils/utils.ts | 13 ++++- .../src/segment-mananger.ts | 4 +- .../src/segment-manager.ts | 12 ++--- .../src/stream-utils.ts | 6 +-- 10 files changed, 99 insertions(+), 87 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index ac0b66d2..32dd4dcd 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -1,5 +1,11 @@ import { HybridLoader } from "./hybrid-loader"; -import { Stream, StreamWithSegments, Segment, Settings } from "./types"; +import { + Stream, + StreamWithSegments, + Segment, + Settings, + SegmentBase, +} from "./types"; import * as Utils from "./utils/utils"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; @@ -32,8 +38,10 @@ export class Core { } hasSegment(segmentLocalId: string): boolean { - const { segment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentLocalId) ?? {}; + const segment = Utils.getSegmentFromStreamsMap( + this.streams, + segmentLocalId + ); return !!segment; } @@ -51,13 +59,16 @@ export class Core { updateStream( streamLocalId: string, - addSegments?: Segment[], + addSegments?: SegmentBase[], removeSegmentIds?: string[] ): void { const stream = this.streams.get(streamLocalId); if (!stream) return; - addSegments?.forEach((s) => stream.segments.addToEnd(s.localId, s)); + addSegments?.forEach((s) => { + const segment = { ...s, stream }; + stream.segments.addToEnd(segment.localId, segment); + }); removeSegmentIds?.forEach((id) => stream.segments.delete(id)); } @@ -72,9 +83,9 @@ export class Core { ); await this.segmentStorage.initialize(); } - const { segment, stream } = this.identifySegment(segmentLocalId); - const loader = this.getStreamHybridLoader(segment, stream); - void loader.loadSegment(segment, stream, callbacks); + const segment = this.identifySegment(segmentLocalId); + const loader = this.getStreamHybridLoader(segment); + void loader.loadSegment(segment, callbacks); } abortSegmentLoading(segmentId: string): void { @@ -94,21 +105,20 @@ export class Core { this.manifestResponseUrl = undefined; } - private identifySegment(segmentId: string) { + private identifySegment(segmentId: string): Segment { if (!this.manifestResponseUrl) { throw new Error("Manifest response url is undefined"); } - const { stream, segment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - if (!segment || !stream) { + const segment = Utils.getSegmentFromStreamsMap(this.streams, segmentId); + if (!segment) { throw new Error(`Not found segment with id: ${segmentId}`); } - return { segment, stream }; + return segment; } - private getStreamHybridLoader(segment: Segment, stream: StreamWithSegments) { + private getStreamHybridLoader(segment: Segment) { if (!this.manifestResponseUrl) { throw new Error("Manifest response url is not defined"); } @@ -119,7 +129,6 @@ export class Core { return new HybridLoader( manifestResponseUrl, segment, - stream, this.settings, this.bandwidthApproximator, this.segmentStorage @@ -129,7 +138,7 @@ export class Core { main: "mainStreamLoader", secondary: "secondaryStreamLoader", } as const; - const { type } = stream; + const { type } = segment.stream; const loaderKey = streamTypeLoaderKeyMap[type]; return (this[loaderKey] = diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 12fa7291..e0fde58b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,4 +1,4 @@ -import { Segment, Stream, StreamWithSegments } from "./index"; +import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; @@ -13,7 +13,6 @@ export class HybridLoader { private readonly requests = new RequestContainer(); private readonly p2pLoaders: P2PLoadersContainer; private storageCleanUpIntervalId?: number; - private activeStream: Readonly; private lastRequestedSegment: Readonly; private readonly playback: Playback; private lastQueueProcessingTimeStamp?: number; @@ -22,21 +21,20 @@ export class HybridLoader { constructor( private streamManifestUrl: string, requestedSegment: Segment, - requestedStream: Readonly, private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator, private readonly segmentStorage: SegmentsMemoryStorage ) { this.lastRequestedSegment = requestedSegment; - this.activeStream = requestedStream; + const activeStream = requestedSegment.stream; this.playback = { position: requestedSegment.startTime, rate: 1 }; - this.segmentAvgDuration = getSegmentAvgDuration(requestedStream); + this.segmentAvgDuration = getSegmentAvgDuration(activeStream); if (!this.segmentStorage.isInitialized) { throw new Error("Segment storage is not initialized."); } this.segmentStorage.addIsSegmentLockedPredicate((segment) => { - if (!this.activeStream.segments.has(segment.localId)) return false; + if (segment.stream !== activeStream) return false; const bufferRanges = QueueUtils.getLoadBufferRanges( this.playback, this.settings @@ -45,7 +43,7 @@ export class HybridLoader { }); this.p2pLoaders = new P2PLoadersContainer( this.streamManifestUrl, - requestedStream, + requestedSegment.stream, this.requests, this.segmentStorage, this.settings @@ -53,21 +51,17 @@ export class HybridLoader { } // api method for engines - async loadSegment( - segment: Readonly, - stream: Readonly, - callbacks: EngineCallbacks - ) { - if (this.activeStream !== stream) { - this.activeStream = stream; + async loadSegment(segment: Readonly, callbacks: EngineCallbacks) { + const { stream } = segment; + if (stream !== this.lastRequestedSegment.stream) { this.p2pLoaders.changeActiveLoader(stream); } this.lastRequestedSegment = segment; this.requests.addEngineCallbacks(segment, callbacks); void this.processQueue(); - if (this.segmentStorage.hasSegment(segment, stream)) { - const data = await this.segmentStorage.getSegmentData(stream, segment); + if (this.segmentStorage.hasSegment(segment)) { + const data = await this.segmentStorage.getSegmentData(segment); if (data) { this.requests.resolveEngineRequest(segment, { data, @@ -88,14 +82,11 @@ export class HybridLoader { } this.lastQueueProcessingTimeStamp = now; - const stream = this.activeStream; const { queue, queueSegmentIds } = QueueUtils.generateQueue({ segment: this.lastRequestedSegment, - stream, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segment) => - this.segmentStorage.hasSegment(segment, stream), + isSegmentLoaded: (segment) => this.segmentStorage.hasSegment(segment), }); this.requests.abortAllNotRequestedByEngine((segment) => @@ -123,13 +114,13 @@ export class HybridLoader { // } // } if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(stream, segment); + void this.loadThroughHttp(segment); continue; } this.abortLastHttpLoadingAfter(queue, segment.localId); if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(stream, segment); + void this.loadThroughHttp(segment); continue; } @@ -145,7 +136,7 @@ export class HybridLoader { } if (statuses.isP2PDownloadable) { if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(stream, segment); + void this.loadThroughP2P(segment); } } break; @@ -157,13 +148,13 @@ export class HybridLoader { this.requests.abortEngineRequest(segmentId); } - private async loadThroughHttp(stream: Stream, segment: Segment) { + private async loadThroughHttp(segment: Segment) { let data: ArrayBuffer | undefined; try { const httpRequest = getHttpSegmentRequest(segment); this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; - if (data) this.onSegmentLoaded(stream, segment, data); + if (data) this.onSegmentLoaded(segment, data); } catch (err) { if (err instanceof FetchError) { // TODO: handle error @@ -171,15 +162,15 @@ export class HybridLoader { } } - private async loadThroughP2P(stream: Stream, segment: Segment) { + private async loadThroughP2P(segment: Segment) { const p2pLoader = this.p2pLoaders.activeLoader; const data = await p2pLoader.downloadSegment(segment); - if (data) this.onSegmentLoaded(stream, segment, data); + if (data) this.onSegmentLoaded(segment, data); } - private onSegmentLoaded(stream: Stream, segment: Segment, data: ArrayBuffer) { + private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { this.bandwidthApproximator.addBytes(data.byteLength); - void this.segmentStorage.storeSegment(stream, segment, data); + void this.segmentStorage.storeSegment(segment, data); this.requests.resolveEngineRequest(segment, { data, bandwidth: this.bandwidthApproximator.getBandwidth(), diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 692ddbd2..50ae7834 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -126,10 +126,12 @@ export class P2PLoader { }; private async onSegmentRequested(peer: Peer, segmentExternalId: string) { - const segmentData = await this.segmentStorage.getSegmentData( + const segment = Utils.getSegmentFromStreamByExternalId( this.stream, segmentExternalId ); + const segmentData = + segment && (await this.segmentStorage.getSegmentData(segment)); if (segmentData) peer.sendSegmentData(segmentExternalId, segmentData); else peer.sendSegmentAbsent(segmentExternalId); } diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 6fc3a87e..64f9d2f4 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -10,11 +10,9 @@ function getStreamShortExternalId(stream: Readonly) { return `${type}-${index}`; } -function getStorageItemId(stream: Stream, segment: Segment | string) { - const segmentExternalId = - typeof segment === "string" ? segment : segment.externalId; - const streamExternalId = getStreamShortExternalId(stream); - return `${streamExternalId}|${segmentExternalId}`; +function getStorageItemId(segment: Segment) { + const streamExternalId = getStreamShortExternalId(segment.stream); + return `${streamExternalId}|${segment.externalId}`; } export class Subscriptions< @@ -50,7 +48,6 @@ export class Subscriptions< } type StorageItem = { - streamId: string; segment: Segment; data: ArrayBuffer; lastAccessed: number; @@ -63,7 +60,7 @@ export class SegmentsMemoryStorage { private readonly isSegmentLockedPredicates: (( segment: Segment ) => boolean)[] = []; - private onUpdateHandlers = new Map void>>(); + private onUpdateHandlers = new Map(); constructor( private readonly masterManifestUrl: string, @@ -90,11 +87,10 @@ export class SegmentsMemoryStorage { return this.isSegmentLockedPredicates.some((p) => p(segment)); } - async storeSegment(stream: Stream, segment: Segment, data: ArrayBuffer) { - const id = getStorageItemId(stream, segment); - const streamId = getStreamShortExternalId(stream); + async storeSegment(segment: Segment, data: ArrayBuffer) { + const id = getStorageItemId(segment); + const streamId = getStreamShortExternalId(segment.stream); this.cache.set(id, { - streamId, segment, data, lastAccessed: performance.now(), @@ -102,11 +98,8 @@ export class SegmentsMemoryStorage { this.fireOnUpdateSubscriptions(streamId); } - async getSegmentData( - stream: Stream, - segment: Segment | string - ): Promise { - const itemId = getStorageItemId(stream, segment); + async getSegmentData(segment: Segment): Promise { + const itemId = getStorageItemId(segment); const cacheItem = this.cache.get(itemId); if (cacheItem === undefined) return undefined; @@ -114,15 +107,16 @@ export class SegmentsMemoryStorage { return cacheItem.data; } - hasSegment(segment: Segment, stream: Stream): boolean { - const id = getStorageItemId(stream, segment); + hasSegment(segment: Segment): boolean { + const id = getStorageItemId(segment); return this.cache.has(id); } getStoredSegmentExternalIdsOfStream(stream: Stream) { const streamId = getStreamShortExternalId(stream); const externalIds: string[] = []; - for (const { streamId: itemStreamId, segment } of this.cache.values()) { + for (const { segment } of this.cache.values()) { + const itemStreamId = getStreamShortExternalId(segment.stream); if (itemStreamId === streamId) externalIds.push(segment.externalId); } return externalIds; @@ -138,9 +132,10 @@ export class SegmentsMemoryStorage { for (const entry of this.cache.entries()) { const [itemId, item] = entry; - const { lastAccessed, segment, streamId } = item; + const { lastAccessed, segment } = item; if (now - lastAccessed > this.settings.cachedSegmentExpiration) { if (!this.isSegmentLocked(segment)) { + const streamId = getStreamShortExternalId(segment.stream); itemsToDelete.push(itemId); streamIdsOfChangedItems.add(streamId); } @@ -155,8 +150,9 @@ export class SegmentsMemoryStorage { if (countOverhead > 0) { remainingItems.sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed); - for (const [itemId, { segment, streamId }] of remainingItems) { + for (const [itemId, { segment }] of remainingItems) { if (!this.isSegmentLocked(segment)) { + const streamId = getStreamShortExternalId(segment.stream); itemsToDelete.push(itemId); streamIdsOfChangedItems.add(streamId); countOverhead--; diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 763f6a35..22791da2 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -6,7 +6,7 @@ export type StreamType = "main" | "secondary"; export type ByteRange = { start: number; end: number }; -export type Segment = { +export type SegmentBase = { readonly localId: string; readonly externalId: string; readonly url: string; @@ -15,6 +15,10 @@ export type Segment = { readonly endTime: number; }; +export type Segment = SegmentBase & { + readonly stream: StreamWithSegments; +}; + export type Stream = { readonly localId: string; readonly type: StreamType; @@ -28,13 +32,16 @@ export type ReadonlyLinkedMap = Pick< export type StreamWithSegments< TStream extends Stream = Stream, - TMap extends ReadonlyLinkedMap = LinkedMap + TMap extends ReadonlyLinkedMap = LinkedMap< + string, + Segment + > > = TStream & { readonly segments: TMap; }; export type StreamWithReadonlySegments = - StreamWithSegments>; + StreamWithSegments>; export type SegmentResponse = { data: ArrayBuffer; diff --git a/packages/p2p-media-loader-core/src/utils/queue-utils.ts b/packages/p2p-media-loader-core/src/utils/queue-utils.ts index a861f495..720b9381 100644 --- a/packages/p2p-media-loader-core/src/utils/queue-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/queue-utils.ts @@ -1,4 +1,4 @@ -import { Segment, Settings, StreamWithSegments } from "../types"; +import { Segment, Settings } from "../types"; import { LoadBufferRanges, NumberRange, @@ -9,12 +9,10 @@ import { export function generateQueue({ segment, - stream, playback, settings, isSegmentLoaded, }: { - stream: Readonly; segment: Readonly; playback: Readonly; isSegmentLoaded: (segment: Segment) => boolean; @@ -24,7 +22,7 @@ export function generateQueue({ >; }): { queue: QueueItem[]; queueSegmentIds: Set } { const bufferRanges = getLoadBufferRanges(playback, settings); - const { localId: requestedSegmentId } = segment; + const { localId: requestedSegmentId, stream } = segment; const queue: QueueItem[] = []; const queueSegmentIds = new Set(); diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index f9987466..9570efc3 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -18,10 +18,19 @@ export function getSegmentFullExternalId( export function getSegmentFromStreamsMap( streams: Map, segmentId: string -): { segment: Segment; stream: StreamWithSegments } | undefined { +): Segment | undefined { for (const stream of streams.values()) { const segment = stream.segments.get(segmentId); - if (segment) return { segment, stream }; + if (segment) return segment; + } +} + +export function getSegmentFromStreamByExternalId( + stream: StreamWithSegments, + segmentExternalId: string +): Segment | undefined { + for (const segment of stream.segments.values()) { + if (segment.externalId === segmentExternalId) return segment; } } diff --git a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts index 71572845..345e5846 100644 --- a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts +++ b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts @@ -4,7 +4,7 @@ import type { LevelUpdatedData, AudioTrackLoadedData, } from "hls.js"; -import { Core, Segment } from "p2p-media-loader-core"; +import { Core, SegmentBase } from "p2p-media-loader-core"; export class SegmentManager { core: Core; @@ -43,7 +43,7 @@ export class SegmentManager { if (!playlist) return; const segmentToRemoveIds = new Set(playlist.segments.keys()); - const newSegments: Segment[] = []; + const newSegments: SegmentBase[] = []; fragments.forEach((fragment, index) => { const { url: responseUrl, diff --git a/packages/p2p-media-loader-shaka/src/segment-manager.ts b/packages/p2p-media-loader-shaka/src/segment-manager.ts index b99258d8..5d502a8c 100644 --- a/packages/p2p-media-loader-shaka/src/segment-manager.ts +++ b/packages/p2p-media-loader-shaka/src/segment-manager.ts @@ -3,7 +3,7 @@ import { HookedStream, StreamInfo, Stream } from "./types"; import { Core, StreamWithReadonlySegments, - Segment, + SegmentBase, StreamType, } from "p2p-media-loader-core"; @@ -61,9 +61,9 @@ export class SegmentManager { segmentReferences: shaka.media.SegmentReference[] ) { const staleSegmentsIds = new Set(managerStream.segments.keys()); - const newSegments: Segment[] = []; + const newSegments: SegmentBase[] = []; for (const reference of segmentReferences) { - const externalId = reference.getStartTime(); + const externalId = reference.getStartTime().toString(); const segmentLocalId = Utils.getSegmentLocalIdFromReference(reference); if (!managerStream.segments.has(segmentLocalId)) { @@ -89,7 +89,7 @@ export class SegmentManager { const segments = [...managerStream.segments.values()]; const lastMediaSequence = Utils.getStreamLastMediaSequence(managerStream); - const newSegments: Segment[] = []; + const newSegments: SegmentBase[] = []; if (segments.length === 0) { const firstReferenceMediaSequence = lastMediaSequence === undefined @@ -98,7 +98,7 @@ export class SegmentManager { segmentReferences.forEach((reference, index) => { const segment = Utils.createSegment({ segmentReference: reference, - externalId: firstReferenceMediaSequence + index, + externalId: (firstReferenceMediaSequence + index).toString(), }); newSegments.push(segment); }); @@ -116,7 +116,7 @@ export class SegmentManager { const segment = Utils.createSegment({ localId, segmentReference: reference, - externalId: index, + externalId: index.toString(), }); newSegments.push(segment); index--; diff --git a/packages/p2p-media-loader-shaka/src/stream-utils.ts b/packages/p2p-media-loader-shaka/src/stream-utils.ts index cc5a2897..efd69db3 100644 --- a/packages/p2p-media-loader-shaka/src/stream-utils.ts +++ b/packages/p2p-media-loader-shaka/src/stream-utils.ts @@ -1,6 +1,6 @@ import { HookedStream, Stream } from "./types"; import { - Segment, + SegmentBase, StreamWithReadonlySegments, ByteRange, } from "p2p-media-loader-core"; @@ -11,9 +11,9 @@ export function createSegment({ localId, }: { segmentReference: shaka.media.SegmentReference; - externalId: number; + externalId: string; localId?: string; -}): Segment { +}): SegmentBase { const { byteRange, url, startTime, endTime } = getSegmentInfoFromReference(segmentReference); return { From 44ed56b8d39627b110573ac26294eb3e73a1979b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Sun, 1 Oct 2023 23:05:22 +0300 Subject: [PATCH 073/127] Fix issue with fetch result data wrong promise error handling. --- packages/p2p-media-loader-core/src/core.ts | 6 ++ .../p2p-media-loader-core/src/http-loader.ts | 14 ++++- .../src/hybrid-loader.ts | 59 ++++++++++++------- packages/p2p-media-loader-core/src/request.ts | 14 +++-- .../src/utils/queue-utils.ts | 10 ++-- .../src/fragment-loader.ts | 5 +- 6 files changed, 73 insertions(+), 35 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 32dd4dcd..ee7dd013 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -84,6 +84,12 @@ export class Core { await this.segmentStorage.initialize(); } const segment = this.identifySegment(segmentLocalId); + console.log( + "segment: ", + `${segment.stream.index}-${segment.externalId}`, + segment.startTime, + segment.endTime + ); const loader = this.getStreamHybridLoader(segment); void loader.loadSegment(segment, callbacks); } diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 3f91d23e..f0a5327a 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -36,7 +36,10 @@ function fetchSegmentData(segment: Segment) { if (response.ok) { const result = getDataPromiseAndMonitorProgress(response); progress = result.progress; - return result.dataPromise; + // Don't return dataPromise immediately + // should await it for catch correct working + const resultData = await result.dataPromise; + return resultData; } throw new FetchError( response.statusText ?? `Network response was not for ${segmentId}`, @@ -44,7 +47,7 @@ function fetchSegmentData(segment: Segment) { response ); } catch (error) { - if (error instanceof Error && error.name === "AbortError") { + if (isAbortFetchError(error)) { throw new RequestAbortError(`Segment fetch was aborted ${segmentId}`); } throw error; @@ -108,3 +111,10 @@ async function* readStream( yield value; } } + +function isAbortFetchError(error: unknown) { + return ( + typeof error === "object" && + (error as { name?: string }).name === "AbortError" + ); +} diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index e0fde58b..d22d117b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -58,7 +58,7 @@ export class HybridLoader { } this.lastRequestedSegment = segment; this.requests.addEngineCallbacks(segment, callbacks); - void this.processQueue(); + this.processQueue(); if (this.segmentStorage.hasSegment(segment)) { const data = await this.segmentStorage.getSegmentData(segment); @@ -83,7 +83,7 @@ export class HybridLoader { this.lastQueueProcessingTimeStamp = now; const { queue, queueSegmentIds } = QueueUtils.generateQueue({ - segment: this.lastRequestedSegment, + lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, settings: this.settings, isSegmentLoaded: (segment) => this.segmentStorage.hasSegment(segment), @@ -118,29 +118,40 @@ export class HybridLoader { continue; } - this.abortLastHttpLoadingAfter(queue, segment.localId); - if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); - continue; - } - - if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); - } - - this.abortLastP2PLoadingAfter(queue, segment.localId); - if (this.requests.p2pRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); - continue; - } - } - if (statuses.isP2PDownloadable) { - if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); - } + // this.abortLastHttpLoadingAfter(queue, segment.localId); + // if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + // void this.loadThroughHttp(segment); + // continue; + // } + // + // if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + // void this.loadThroughP2P(segment); + // } + // + // this.abortLastP2PLoadingAfter(queue, segment.localId); + // if (this.requests.p2pRequestsCount < simultaneousHttpDownloads) { + // void this.loadThroughHttp(segment); + // continue; + // } } + // if (statuses.isP2PDownloadable) { + // if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + // void this.loadThroughP2P(segment); + // } + // } break; } + + console.log( + [...this.requests.values()].map((req) => { + const { loaderRequest, engineCallbacks, segment } = req; + const { stream } = segment; + + return `${stream.index}-${segment.externalId}-l${ + loaderRequest ? 1 : 0 + }-e${engineCallbacks ? 1 : 0}`; + }) + ); } // api method for engines @@ -214,6 +225,10 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; + + if (isPositionSignificantlyChanged) { + console.log("\nposition: ", position); + } void this.processQueue(isPositionSignificantlyChanged); } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 3ec64b48..5f2f4b1a 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -76,13 +76,15 @@ export class RequestContainer { loaderRequest, }); } - if (loaderRequest.type === "http") { - this.onHttpRequestsHandlers.fire(); - } this.debug(`add loader request: ${loaderRequest.type}`); - loaderRequest.promise.then(() => - this.clearRequestItem(segmentId, "loader") - ); + + const clearRequestItem = () => this.clearRequestItem(segmentId, "loader"); + loaderRequest.promise + .then(() => clearRequestItem()) + .catch((err) => { + if (err instanceof RequestAbortError) clearRequestItem(); + }); + if (loaderRequest.type === "http") this.onHttpRequestsHandlers.fire(); } addEngineCallbacks(segment: Segment, engineCallbacks: EngineCallbacks) { diff --git a/packages/p2p-media-loader-core/src/utils/queue-utils.ts b/packages/p2p-media-loader-core/src/utils/queue-utils.ts index 720b9381..5fae11c2 100644 --- a/packages/p2p-media-loader-core/src/utils/queue-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/queue-utils.ts @@ -8,12 +8,12 @@ import { } from "../internal-types"; export function generateQueue({ - segment, + lastRequestedSegment, playback, settings, isSegmentLoaded, }: { - segment: Readonly; + lastRequestedSegment: Readonly; playback: Readonly; isSegmentLoaded: (segment: Segment) => boolean; settings: Pick< @@ -22,12 +22,14 @@ export function generateQueue({ >; }): { queue: QueueItem[]; queueSegmentIds: Set } { const bufferRanges = getLoadBufferRanges(playback, settings); - const { localId: requestedSegmentId, stream } = segment; + const { localId: requestedSegmentId, stream } = lastRequestedSegment; const queue: QueueItem[] = []; const queueSegmentIds = new Set(); - const nextSegment = stream.segments.getNextTo(segment.localId)?.[1]; + const nextSegment = stream.segments.getNextTo( + lastRequestedSegment.localId + )?.[1]; const isNextSegmentHighDemand = !!( nextSegment && getSegmentLoadStatuses(nextSegment, bufferRanges).isHighDemand diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index 52f03dac..97f9b241 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -89,7 +89,10 @@ export class FragmentLoaderBase implements Loader { }; const onError = (error: unknown) => { - if (this.stats.aborted && error instanceof RequestAbortError) return; + if (error instanceof RequestAbortError) { + if (this.stats.aborted) return; + this.callbacks?.onAbort?.(this.stats, this.context, {}); + } this.handleError(error); }; From 2b59ee264017502f4900b45a6ac707740787ccf1 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 2 Oct 2023 13:16:43 +0300 Subject: [PATCH 074/127] Fix issue with not clearing aborted engine request. --- p2p-media-loader-demo/src/App.tsx | 1 + packages/p2p-media-loader-core/src/core.ts | 7 +---- .../src/hybrid-loader.ts | 29 +++++++++---------- packages/p2p-media-loader-core/src/request.ts | 19 +++++++++++- .../src/fragment-loader.ts | 5 +--- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index da462e5d..a8cce14a 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -159,6 +159,7 @@ function App() { }, }, }); + player.play(); setPlayerToWindow(player); }; diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index ee7dd013..5f010994 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -84,17 +84,12 @@ export class Core { await this.segmentStorage.initialize(); } const segment = this.identifySegment(segmentLocalId); - console.log( - "segment: ", - `${segment.stream.index}-${segment.externalId}`, - segment.startTime, - segment.endTime - ); const loader = this.getStreamHybridLoader(segment); void loader.loadSegment(segment, callbacks); } abortSegmentLoading(segmentId: string): void { + const segment = this.identifySegment(segmentId); this.mainStreamLoader?.abortSegment(segmentId); this.secondaryStreamLoader?.abortSegment(segmentId); } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index d22d117b..8cbfa0d6 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,4 +1,4 @@ -import { Segment, StreamWithSegments } from "./index"; +import { RequestAbortError, Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; @@ -95,6 +95,18 @@ export class HybridLoader { const { simultaneousHttpDownloads, simultaneousP2PDownloads } = this.settings; + + for (const request of this.requests.engineRequests()) { + const { segment, loaderRequest } = request; + if ( + !queueSegmentIds.has(segment.localId) && + !loaderRequest && + segment.startTime < this.lastRequestedSegment.startTime + ) { + request.engineCallbacks.onError(new RequestAbortError()); + } + } + for (const { segment, statuses } of queue) { // const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); if (statuses.isHighDemand) { @@ -141,17 +153,6 @@ export class HybridLoader { // } break; } - - console.log( - [...this.requests.values()].map((req) => { - const { loaderRequest, engineCallbacks, segment } = req; - const { stream } = segment; - - return `${stream.index}-${segment.externalId}-l${ - loaderRequest ? 1 : 0 - }-e${engineCallbacks ? 1 : 0}`; - }) - ); } // api method for engines @@ -225,10 +226,6 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; - - if (isPositionSignificantlyChanged) { - console.log("\nposition: ", position); - } void this.processQueue(isPositionSignificantlyChanged); } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 5f2f4b1a..4e0dc3a5 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -3,6 +3,8 @@ import { RequestAbortError } from "./errors"; import { Subscriptions } from "./segments-storage"; import Debug from "debug"; +type SetRequired = Omit & Required>; + export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; onError: (reason?: unknown) => void; @@ -38,6 +40,8 @@ type Request = { engineCallbacks?: Readonly; }; +type RequestWithEngineCallbacks = SetRequired; + function getRequestItemId(segment: Segment) { return segment.localId; } @@ -91,12 +95,19 @@ export class RequestContainer { const segmentId = getRequestItemId(segment); const requestItem = this.requests.get(segmentId); - const { onSuccess } = engineCallbacks; + const { onSuccess, onError } = engineCallbacks; engineCallbacks.onSuccess = (response) => { this.clearRequestItem(segmentId, "engine"); return onSuccess(response); }; + engineCallbacks.onError = (error) => { + if (error instanceof RequestAbortError) { + this.clearRequestItem(segmentId, "engine"); + } + return onError(error); + }; + if (requestItem) { requestItem.engineCallbacks = engineCallbacks; } else { @@ -124,6 +135,12 @@ export class RequestContainer { } } + *engineRequests(): Generator { + for (const request of this.requests.values()) { + if (request.engineCallbacks) yield request as RequestWithEngineCallbacks; + } + } + resolveEngineRequest(segment: Segment, response: SegmentResponse) { const id = getRequestItemId(segment); this.requests.get(id)?.engineCallbacks?.onSuccess(response); diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index 97f9b241..8ed41e65 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -89,10 +89,7 @@ export class FragmentLoaderBase implements Loader { }; const onError = (error: unknown) => { - if (error instanceof RequestAbortError) { - if (this.stats.aborted) return; - this.callbacks?.onAbort?.(this.stats, this.context, {}); - } + if (error instanceof RequestAbortError && this.stats.aborted) return; this.handleError(error); }; From 15cfdda7c6288fa1f88e134d3690cac38b93f543 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 2 Oct 2023 14:59:50 +0300 Subject: [PATCH 075/127] Add random http loading. --- .../src/hybrid-loader.ts | 105 ++++++++++-------- packages/p2p-media-loader-core/src/request.ts | 20 +++- .../src/utils/queue-utils.ts | 6 +- 3 files changed, 78 insertions(+), 53 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 8cbfa0d6..95b6fd2e 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -17,6 +17,7 @@ export class HybridLoader { private readonly playback: Playback; private lastQueueProcessingTimeStamp?: number; private readonly segmentAvgDuration: number; + private readonly randomHttpDownloadInterval: number; constructor( private streamManifestUrl: string, @@ -48,6 +49,11 @@ export class HybridLoader { this.segmentStorage, this.settings ); + + this.randomHttpDownloadInterval = window.setInterval( + () => this.loadRandomThroughHttp(), + 1000 + ); } // api method for engines @@ -86,7 +92,7 @@ export class HybridLoader { lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segment) => this.segmentStorage.hasSegment(segment), + skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); this.requests.abortAllNotRequestedByEngine((segment) => @@ -96,17 +102,6 @@ export class HybridLoader { const { simultaneousHttpDownloads, simultaneousP2PDownloads } = this.settings; - for (const request of this.requests.engineRequests()) { - const { segment, loaderRequest } = request; - if ( - !queueSegmentIds.has(segment.localId) && - !loaderRequest && - segment.startTime < this.lastRequestedSegment.startTime - ) { - request.engineCallbacks.onError(new RequestAbortError()); - } - } - for (const { segment, statuses } of queue) { // const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); if (statuses.isHighDemand) { @@ -130,27 +125,27 @@ export class HybridLoader { continue; } - // this.abortLastHttpLoadingAfter(queue, segment.localId); - // if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - // void this.loadThroughHttp(segment); - // continue; - // } - // - // if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - // void this.loadThroughP2P(segment); - // } - // - // this.abortLastP2PLoadingAfter(queue, segment.localId); - // if (this.requests.p2pRequestsCount < simultaneousHttpDownloads) { - // void this.loadThroughHttp(segment); - // continue; - // } + this.abortLastHttpLoadingAfter(queue, segment.localId); + if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(segment); + continue; + } + + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(segment); + } + + this.abortLastP2PLoadingAfter(queue, segment.localId); + if (this.requests.p2pRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(segment); + continue; + } + } + if (statuses.isP2PDownloadable) { + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(segment); + } } - // if (statuses.isP2PDownloadable) { - // if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - // void this.loadThroughP2P(segment); - // } - // } break; } } @@ -180,6 +175,25 @@ export class HybridLoader { if (data) this.onSegmentLoaded(segment, data); } + private loadRandomThroughHttp() { + const { simultaneousHttpDownloads } = this.settings; + if (this.requests.httpRequestsCount >= simultaneousHttpDownloads) return; + const { queue } = QueueUtils.generateQueue({ + lastRequestedSegment: this.lastRequestedSegment, + playback: this.playback, + settings: this.settings, + skipSegment: (segment, statuses) => + !statuses.isHttpDownloadable || + this.segmentStorage.hasSegment(segment) || + this.requests.isHybridLoaderRequested(segment), + }); + if (!queue.length) return; + + const { segment } = queue[Math.floor(Math.random() * queue.length)]; + // console.log("load random: ", getSegmentStringId(segment)); + void this.loadThroughHttp(segment); + } + private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); @@ -191,24 +205,20 @@ export class HybridLoader { } private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { - for (const { - segment: { localId: queueSegmentId }, - } of arrayBackwards(queue)) { - if (queueSegmentId === segmentId) break; - if (this.requests.isHttpRequested(queueSegmentId)) { - this.requests.abortLoaderRequest(queueSegmentId); + for (const { segment } of arrayBackwards(queue)) { + if (segment.localId === segmentId) break; + if (this.requests.isHttpRequested(segment)) { + this.requests.abortLoaderRequest(segment); break; } } } private abortLastP2PLoadingAfter(queue: QueueItem[], segmentId: string) { - for (const { - segment: { localId: queueSegmentId }, - } of arrayBackwards(queue)) { - if (queueSegmentId === segmentId) break; - if (this.requests.isP2PRequested(queueSegmentId)) { - this.requests.abortLoaderRequest(queueSegmentId); + for (const { segment } of arrayBackwards(queue)) { + if (segment.localId === segmentId) break; + if (this.requests.isP2PRequested(segment)) { + this.requests.abortLoaderRequest(segment); break; } } @@ -231,6 +241,7 @@ export class HybridLoader { destroy() { clearInterval(this.storageCleanUpIntervalId); + clearInterval(this.randomHttpDownloadInterval); this.storageCleanUpIntervalId = undefined; void this.segmentStorage.destroy(); this.requests.destroy(); @@ -341,3 +352,9 @@ function getSegmentAvgDuration(stream: StreamWithSegments) { return sumDuration / size; } + +function getSegmentStringId(segment: Segment) { + const { index } = segment.stream; + const { externalId } = segment; + return `${index}-${externalId}`; +} diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 4e0dc3a5..6365ad96 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -150,12 +150,19 @@ export class RequestContainer { return !!this.requests.get(segmentId)?.engineCallbacks; } - isHttpRequested(segmentId: string): boolean { - return this.requests.get(segmentId)?.loaderRequest?.type === "http"; + isHttpRequested(segment: Segment): boolean { + const id = getRequestItemId(segment); + return this.requests.get(id)?.loaderRequest?.type === "http"; + } + + isP2PRequested(segment: Segment): boolean { + const id = getRequestItemId(segment); + return this.requests.get(id)?.loaderRequest?.type === "p2p"; } - isP2PRequested(segmentId: string): boolean { - return this.requests.get(segmentId)?.loaderRequest?.type === "p2p"; + isHybridLoaderRequested(segment: Segment): boolean { + const id = getRequestItemId(segment); + return !!this.requests.get(id)?.loaderRequest; } abortEngineRequest(segmentId: string) { @@ -166,8 +173,9 @@ export class RequestContainer { request.loaderRequest?.abort(); } - abortLoaderRequest(segmentId: string) { - this.requests.get(segmentId)?.loaderRequest?.abort(); + abortLoaderRequest(segment: Segment) { + const id = getRequestItemId(segment); + this.requests.get(id)?.loaderRequest?.abort(); } private clearRequestItem( diff --git a/packages/p2p-media-loader-core/src/utils/queue-utils.ts b/packages/p2p-media-loader-core/src/utils/queue-utils.ts index 5fae11c2..2d166fc7 100644 --- a/packages/p2p-media-loader-core/src/utils/queue-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/queue-utils.ts @@ -11,11 +11,11 @@ export function generateQueue({ lastRequestedSegment, playback, settings, - isSegmentLoaded, + skipSegment, }: { lastRequestedSegment: Readonly; playback: Readonly; - isSegmentLoaded: (segment: Segment) => boolean; + skipSegment: (segment: Segment, statuses: QueueItemStatuses) => boolean; settings: Pick< Settings, "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" @@ -42,7 +42,7 @@ export function generateQueue({ if (isNotActual && !(i === 0 && isNextSegmentHighDemand)) { break; } - if (isSegmentLoaded(segment)) continue; + if (skipSegment(segment, statuses)) continue; queueSegmentIds.add(segment.localId); if (isNotActual) statuses.isHighDemand = true; From 6a607a3be1a5a8e6e83d7d5ec8de8df3e7e7055f Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 2 Oct 2023 19:20:59 +0300 Subject: [PATCH 076/127] Change JsonSegmentAnnouncement type. --- .../src/internal-types.ts | 4 +-- .../p2p-media-loader-core/src/p2p-loader.ts | 5 ++-- packages/p2p-media-loader-core/src/peer.ts | 2 +- .../src/utils/peer-utils.ts | 25 +++++++++++-------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index 8d93d8c2..b841cca9 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -31,8 +31,8 @@ export type BasePeerCommand = { // {i: segmentExternalId[]; s: segment status separator position in ids array} export type JsonSegmentAnnouncement = { - i: string[]; - s: number; + i: string; + s?: number; }; export type PeerSegmentCommand = BasePeerCommand< diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 50ae7834..ef0378f8 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -15,7 +15,7 @@ export class P2PLoader { private readonly peerHash: string; private readonly trackerClient: TrackerClient; private readonly peers = new Map(); - private announcement: JsonSegmentAnnouncement = { i: [], s: 0 }; + private announcement: JsonSegmentAnnouncement = { i: "" }; constructor( private streamManifestUrl: string, @@ -102,8 +102,7 @@ export class P2PLoader { this.segmentStorage.getStoredSegmentExternalIdsOfStream(this.stream); const httpLoading: string[] = []; - for (const request of this.requests.values()) { - if (request.loaderRequest?.type !== "http") continue; + for (const request of this.requests.httpRequests()) { const segment = this.stream.segments.get(request.segment.localId); if (!segment) continue; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index f4c9b502..ce92490c 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -55,7 +55,7 @@ export class Peer { this.connection = candidate; this.eventHandlers.onPeerConnected(this); }); - candidate.on("data", () => this.onReceiveData.bind(this)); + candidate.on("data", this.onReceiveData.bind(this)); candidate.on("close", () => { if (this.connection === candidate) { this.connection = undefined; diff --git a/packages/p2p-media-loader-core/src/utils/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer-utils.ts index 11a3a92a..f570661b 100644 --- a/packages/p2p-media-loader-core/src/utils/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/peer-utils.ts @@ -45,7 +45,11 @@ export function getSegmentsFromPeerAnnouncement( ): Map { const segmentStatusMap = new Map(); const separator = announcement.s; - for (const [index, segmentExternalId] of announcement.i.entries()) { + const ids = announcement.i.split("|"); + if (!separator) { + return new Map(ids.map((id) => [id, PeerSegmentStatus.Loaded])); + } + for (const [index, segmentExternalId] of ids.entries()) { if (index < separator) { segmentStatusMap.set(segmentExternalId, PeerSegmentStatus.Loaded); } else { @@ -59,13 +63,14 @@ export function getJsonSegmentsAnnouncement( storedSegmentExternalIds: string[], loadingByHttpSegmentExternalIds: string[] ): JsonSegmentAnnouncement { - const segmentIds = [ - ...storedSegmentExternalIds, - ...loadingByHttpSegmentExternalIds, - ]; - const segmentStatusSeparator = storedSegmentExternalIds.length; - return { - i: segmentIds, - s: segmentStatusSeparator, - }; + let segmentsListing = storedSegmentExternalIds.join("|"); + if (loadingByHttpSegmentExternalIds.length) { + if (segmentsListing) segmentsListing += "|"; + segmentsListing += loadingByHttpSegmentExternalIds.join("|"); + } + const announcement: JsonSegmentAnnouncement = { i: segmentsListing }; + if (loadingByHttpSegmentExternalIds.length) { + announcement.s = storedSegmentExternalIds.length; + } + return announcement; } From 7b6330ae7a669b63adedfa18c05d5c1ea3b944ee Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 2 Oct 2023 22:04:16 +0300 Subject: [PATCH 077/127] Add loading through p2p to process queue. --- packages/p2p-media-loader-core/src/core.ts | 10 +-- .../src/declarations.d.ts | 1 + .../src/hybrid-loader.ts | 63 ++++++++++++++++--- .../p2p-media-loader-core/src/p2p-loader.ts | 13 +++- packages/p2p-media-loader-core/src/peer.ts | 10 +-- packages/p2p-media-loader-core/src/request.ts | 4 +- 6 files changed, 83 insertions(+), 18 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 5f010994..eb7c6513 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -16,17 +16,17 @@ export class Core { private manifestResponseUrl?: string; private readonly streams = new Map>(); private readonly settings: Settings = { - simultaneousHttpDownloads: 3, + simultaneousHttpDownloads: 2, simultaneousP2PDownloads: 3, highDemandTimeWindow: 30, httpDownloadTimeWindow: 60, p2pDownloadTimeWindow: 60, - cachedSegmentExpiration: 120, + cachedSegmentExpiration: 120 * 1000, cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, - p2pSegmentDownloadTimeout: 1000, + p2pSegmentDownloadTimeout: 5000, storageCleanupInterval: 5000, - p2pLoaderDestroyTimeout: 30000, + p2pLoaderDestroyTimeout: 30 * 1000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); private segmentStorage?: SegmentsMemoryStorage; @@ -70,6 +70,8 @@ export class Core { stream.segments.addToEnd(segment.localId, segment); }); removeSegmentIds?.forEach((id) => stream.segments.delete(id)); + this.mainStreamLoader?.updateStream(stream); + this.secondaryStreamLoader?.updateStream(stream); } async loadSegment(segmentLocalId: string, callbacks: EngineCallbacks) { diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index da331a27..0d198026 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -53,6 +53,7 @@ declare module "bittorrent-tracker" { handler: PeerCandidateEventHandler ): void; send(data: string | ArrayBuffer | Blob): void; + write(data: string | ArrayBuffer | Blob): void; destroy(): void; }; } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 95b6fd2e..01679860 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,4 +1,4 @@ -import { RequestAbortError, Segment, StreamWithSegments } from "./index"; +import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; @@ -58,6 +58,7 @@ export class HybridLoader { // api method for engines async loadSegment(segment: Readonly, callbacks: EngineCallbacks) { + console.log("REQUESTED: ", getSegmentStringId(segment)); const { stream } = segment; if (stream !== this.lastRequestedSegment.stream) { this.p2pLoaders.changeActiveLoader(stream); @@ -105,7 +106,7 @@ export class HybridLoader { for (const { segment, statuses } of queue) { // const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); if (statuses.isHighDemand) { - if (this.requests.isHttpRequested(segment.localId)) continue; + if (this.requests.isHttpRequested(segment)) continue; // const request = this.requests.get(segment.localId); // if (request?.loaderRequest?.type === "p2p") { // const remainingDownloadTime = getPredictedRemainingDownloadTime( @@ -131,23 +132,45 @@ export class HybridLoader { continue; } + if (this.requests.isP2PRequested(segment)) continue; + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { void this.loadThroughP2P(segment); + continue; } this.abortLastP2PLoadingAfter(queue, segment.localId); - if (this.requests.p2pRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(segment); continue; } + break; } if (statuses.isP2PDownloadable) { + if (this.requests.isP2PRequested(segment)) continue; + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(segment); + continue; + } + + this.abortLastP2PLoadingAfter(queue, segment.localId); if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { void this.loadThroughP2P(segment); + continue; } } break; } + + // console.log( + // [...this.requests.values()].map((req) => { + // const { loaderRequest, engineCallbacks, segment } = req; + // + // return `${getSegmentStringId(segment)}-l${loaderRequest ? 1 : 0}-e${ + // engineCallbacks ? 1 : 0 + // }`; + // }) + // ); } // api method for engines @@ -158,9 +181,12 @@ export class HybridLoader { private async loadThroughHttp(segment: Segment) { let data: ArrayBuffer | undefined; try { + const idStr = getSegmentStringId(segment); + console.log(`http requested: ${idStr}`); const httpRequest = getHttpSegmentRequest(segment); this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; + console.log(`=> http loaded: ${idStr}`); if (data) this.onSegmentLoaded(segment, data); } catch (err) { if (err instanceof FetchError) { @@ -171,13 +197,24 @@ export class HybridLoader { private async loadThroughP2P(segment: Segment) { const p2pLoader = this.p2pLoaders.activeLoader; - const data = await p2pLoader.downloadSegment(segment); - if (data) this.onSegmentLoaded(segment, data); + const idStr = getSegmentStringId(segment); + try { + const data = await p2pLoader.downloadSegment(segment); + if (data) { + console.log(`=> p2p loaded: ${idStr}, ${data?.byteLength}`); + this.onSegmentLoaded(segment, data); + } + } catch (error) { + console.log(""); + console.log(JSON.stringify(error)); + console.log(""); + } } private loadRandomThroughHttp() { const { simultaneousHttpDownloads } = this.settings; if (this.requests.httpRequestsCount >= simultaneousHttpDownloads) return; + const p2pLoader = this.p2pLoaders.activeLoader; const { queue } = QueueUtils.generateQueue({ lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, @@ -185,7 +222,8 @@ export class HybridLoader { skipSegment: (segment, statuses) => !statuses.isHttpDownloadable || this.segmentStorage.hasSegment(segment) || - this.requests.isHybridLoaderRequested(segment), + this.requests.isHybridLoaderRequested(segment) || + p2pLoader.isLoadingOrLoadedBySomeone(segment), }); if (!queue.length) return; @@ -209,6 +247,7 @@ export class HybridLoader { if (segment.localId === segmentId) break; if (this.requests.isHttpRequested(segment)) { this.requests.abortLoaderRequest(segment); + console.log("aborted http: ", getSegmentStringId(segment)); break; } } @@ -219,6 +258,7 @@ export class HybridLoader { if (segment.localId === segmentId) break; if (this.requests.isP2PRequested(segment)) { this.requests.abortLoaderRequest(segment); + console.log("aborted p2p: ", getSegmentStringId(segment)); break; } } @@ -236,9 +276,18 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; + + if (isPositionSignificantlyChanged) { + console.log("\nposition: ", position); + } void this.processQueue(isPositionSignificantlyChanged); } + updateStream(stream: StreamWithSegments) { + if (stream !== this.lastRequestedSegment.stream) return; + this.processQueue(); + } + destroy() { clearInterval(this.storageCleanUpIntervalId); clearInterval(this.randomHttpDownloadInterval); diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index ef0378f8..f256411e 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -29,6 +29,7 @@ export class P2PLoader { this.streamManifestUrl, this.stream ); + console.log("\ncreate tracker client", this.streamExternalId); this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); @@ -76,13 +77,12 @@ export class P2PLoader { } async downloadSegment(segment: Segment): Promise { - const segmentExternalId = segment.externalId; const peerWithSegment: Peer[] = []; for (const peer of this.peers.values()) { if ( !peer.downloadingSegment && - peer.getSegmentStatus(segmentExternalId) === PeerSegmentStatus.Loaded + peer.getSegmentStatus(segment) === PeerSegmentStatus.Loaded ) { peerWithSegment.push(peer); } @@ -93,10 +93,19 @@ export class P2PLoader { const peer = peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; const request = peer.requestSegment(segment); + const idStr = `${segment.stream.index}-${segment.externalId}`; + console.log(`=> p2p requested: ${idStr}`); this.requests.addLoaderRequest(segment, request); return request.promise; } + isLoadingOrLoadedBySomeone(segment: Segment): boolean { + for (const peer of this.peers.values()) { + if (peer.getSegmentStatus(segment)) return true; + } + return false; + } + private updateSegmentAnnouncement() { const loaded: string[] = this.segmentStorage.getStoredSegmentExternalIdsOfStream(this.stream); diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index ce92490c..b042a0ca 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -52,6 +52,7 @@ export class Peer { addCandidate(candidate: PeerCandidate) { candidate.on("connect", () => { + console.log("\nconnected with peer", this.connection === candidate); this.connection = candidate; this.eventHandlers.onPeerConnected(this); }); @@ -75,8 +76,9 @@ export class Peer { return this.request?.segment; } - getSegmentStatus(segmentExternalId: string): PeerSegmentStatus | undefined { - return this.segments.get(segmentExternalId); + getSegmentStatus(segment: Segment): PeerSegmentStatus | undefined { + const { externalId } = segment; + return this.segments.get(externalId); } private onReceiveData(data: ArrayBuffer) { @@ -120,7 +122,7 @@ export class Peer { private sendCommand(command: PeerCommand) { if (!this.connection) return; - this.connection.send(JSON.stringify(command)); + this.connection.write(JSON.stringify(command)); } requestSegment(segment: Segment) { @@ -155,7 +157,7 @@ export class Peer { this.sendCommand(command); this.isSendingData = true; - const sendChunk = async (data: ArrayBuffer) => this.connection?.send(data); + const sendChunk = async (data: ArrayBuffer) => this.connection?.write(data); for (const chunk of getBufferChunks( data, this.settings.webRtcMaxMessageSize diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 6365ad96..7ba8caab 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,5 +1,5 @@ import { Segment, SegmentResponse } from "./types"; -import { RequestAbortError } from "./errors"; +import { RequestAbortError, FetchError } from "./errors"; import { Subscriptions } from "./segments-storage"; import Debug from "debug"; @@ -87,6 +87,8 @@ export class RequestContainer { .then(() => clearRequestItem()) .catch((err) => { if (err instanceof RequestAbortError) clearRequestItem(); + if (err instanceof FetchError) { + } }); if (loaderRequest.type === "http") this.onHttpRequestsHandlers.fire(); } From 759982078f42b8f1b97750bedcbafa477cf98951 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 3 Oct 2023 18:27:52 +0300 Subject: [PATCH 078/127] Change load method parameter to queue item. --- p2p-media-loader-demo/src/App.tsx | 5 +- .../src/hybrid-loader.ts | 52 ++++++++++--------- .../p2p-media-loader-core/src/p2p-loader.ts | 8 ++- packages/p2p-media-loader-core/src/peer.ts | 4 +- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index a8cce14a..a0c79b9a 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -150,6 +150,7 @@ function App() { customHls: (video: HTMLVideoElement) => { const hls = new Hls({ ...engine.getConfig(), + liveSyncDurationCount: 7, }); engine.initHlsJsEvents(hls); hls.loadSource(video.src); @@ -181,10 +182,6 @@ function App() { }; const createNewPlayer = () => { - if (!localStorage.videoUrl) { - localStorage.streamUrl = streamUrl.live2; - setUrl(streamUrl.live2); - } switch (playerType) { case "hls-dplayer": initHlsDplayer(url); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 01679860..76a1d4ac 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -4,7 +4,7 @@ import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; -import { Playback, QueueItem } from "./internal-types"; +import { Playback, QueueItem, QueueItemStatuses } from "./internal-types"; import { RequestContainer, EngineCallbacks } from "./request"; import * as QueueUtils from "./utils/queue-utils"; import { FetchError } from "./errors"; @@ -103,7 +103,8 @@ export class HybridLoader { const { simultaneousHttpDownloads, simultaneousP2PDownloads } = this.settings; - for (const { segment, statuses } of queue) { + for (const item of queue) { + const { statuses, segment } = item; // const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); if (statuses.isHighDemand) { if (this.requests.isHttpRequested(segment)) continue; @@ -122,41 +123,39 @@ export class HybridLoader { // } // } if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); + void this.loadThroughHttp(item); continue; } - this.abortLastHttpLoadingAfter(queue, segment.localId); + this.abortLastHttpLoadingAfter(queue, segment); if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(segment); + void this.loadThroughHttp(item); continue; } if (this.requests.isP2PRequested(segment)) continue; if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); + void this.loadThroughP2P(item); continue; } - this.abortLastP2PLoadingAfter(queue, segment.localId); + this.abortLastP2PLoadingAfter(queue, segment); if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); - continue; + void this.loadThroughP2P(item); } break; } if (statuses.isP2PDownloadable) { if (this.requests.isP2PRequested(segment)) continue; if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); + void this.loadThroughP2P(item); continue; } - this.abortLastP2PLoadingAfter(queue, segment.localId); + this.abortLastP2PLoadingAfter(queue, segment); if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(segment); - continue; + void this.loadThroughP2P(item); } } break; @@ -178,11 +177,12 @@ export class HybridLoader { this.requests.abortEngineRequest(segmentId); } - private async loadThroughHttp(segment: Segment) { + private async loadThroughHttp(item: QueueItem) { + const { segment, statuses } = item; let data: ArrayBuffer | undefined; try { const idStr = getSegmentStringId(segment); - console.log(`http requested: ${idStr}`); + console.log(`http requested: ${idStr} - ${getStatusesString(statuses)}`); const httpRequest = getHttpSegmentRequest(segment); this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; @@ -195,7 +195,8 @@ export class HybridLoader { } } - private async loadThroughP2P(segment: Segment) { + private async loadThroughP2P(item: QueueItem) { + const { segment, statuses } = item; const p2pLoader = this.p2pLoaders.activeLoader; const idStr = getSegmentStringId(segment); try { @@ -227,9 +228,10 @@ export class HybridLoader { }); if (!queue.length) return; - const { segment } = queue[Math.floor(Math.random() * queue.length)]; + const item = queue[Math.floor(Math.random() * queue.length)]; + console.log("HTTP RANDOM"); // console.log("load random: ", getSegmentStringId(segment)); - void this.loadThroughHttp(segment); + void this.loadThroughHttp(item); } private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { @@ -242,9 +244,9 @@ export class HybridLoader { this.processQueue(); } - private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { - for (const { segment } of arrayBackwards(queue)) { - if (segment.localId === segmentId) break; + private abortLastHttpLoadingAfter(queue: QueueItem[], segment: Segment) { + for (const { segment: itemSegment } of arrayBackwards(queue)) { + if (itemSegment.localId === segment.localId) break; if (this.requests.isHttpRequested(segment)) { this.requests.abortLoaderRequest(segment); console.log("aborted http: ", getSegmentStringId(segment)); @@ -253,9 +255,9 @@ export class HybridLoader { } } - private abortLastP2PLoadingAfter(queue: QueueItem[], segmentId: string) { - for (const { segment } of arrayBackwards(queue)) { - if (segment.localId === segmentId) break; + private abortLastP2PLoadingAfter(queue: QueueItem[], segment: Segment) { + for (const { segment: itemSegment } of arrayBackwards(queue)) { + if (itemSegment.localId === segment.localId) break; if (this.requests.isP2PRequested(segment)) { this.requests.abortLoaderRequest(segment); console.log("aborted p2p: ", getSegmentStringId(segment)); @@ -285,6 +287,7 @@ export class HybridLoader { updateStream(stream: StreamWithSegments) { if (stream !== this.lastRequestedSegment.stream) return; + console.log("STREAM UPDATED"); this.processQueue(); } @@ -357,6 +360,7 @@ class P2PLoadersContainer { item.destroyTimeoutId = window.setTimeout(() => { item.loader.destroy(); this.loaders.delete(item.streamId); + console.log("loader destroyed"); }, this.settings.p2pLoaderDestroyTimeout); } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index f256411e..3ac50cc2 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -29,9 +29,10 @@ export class P2PLoader { this.streamManifestUrl, this.stream ); - console.log("\ncreate tracker client", this.streamExternalId); this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); + console.log("\ncreate tracker client", this.streamExternalId); + console.log("PEER ID: " + this.peerHash); this.trackerClient = createTrackerClient({ streamHash: this.streamHash, @@ -54,6 +55,7 @@ export class P2PLoader { // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", () => {}); trackerClient.on("peer", (candidate) => { + console.log("Peer found: ", candidate); const peer = this.peers.get(candidate.id); if (peer) peer.addCandidate(candidate); else this.createPeer(candidate); @@ -61,7 +63,9 @@ export class P2PLoader { // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("warning", (warning) => {}); // eslint-disable-next-line @typescript-eslint/no-empty-function - trackerClient.on("error", (error) => {}); + trackerClient.on("error", (error) => { + console.log("TRACKER ERROR", error); + }); } private createPeer(candidate: PeerCandidate) { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index b042a0ca..572b8816 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -64,7 +64,9 @@ export class Peer { } }); // eslint-disable-next-line @typescript-eslint/no-empty-function - candidate.on("error", () => {}); + candidate.on("error", (error) => { + console.log("PEER ERROR:", error); + }); this.candidates.add(candidate); } From 3cc30fec064e205411b75b40dfdfc93d86cb4a58 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 5 Oct 2023 12:01:46 +0300 Subject: [PATCH 079/127] Add loggers to hybrid loader, p2p-manager. --- packages/p2p-media-loader-core/src/core.ts | 5 +- .../src/hybrid-loader.ts | 139 ++++++------------ .../p2p-media-loader-core/src/p2p-loader.ts | 30 +++- .../src/p2p-loaders-container.ts | 86 +++++++++++ packages/p2p-media-loader-core/src/peer.ts | 8 +- packages/p2p-media-loader-core/src/request.ts | 5 +- .../p2p-media-loader-core/src/utils/logger.ts | 31 ++++ .../p2p-media-loader-core/src/utils/utils.ts | 1 + 8 files changed, 195 insertions(+), 110 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/p2p-loaders-container.ts create mode 100644 packages/p2p-media-loader-core/src/utils/logger.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index eb7c6513..d5ebdcd5 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -92,8 +92,9 @@ export class Core { abortSegmentLoading(segmentId: string): void { const segment = this.identifySegment(segmentId); - this.mainStreamLoader?.abortSegment(segmentId); - this.secondaryStreamLoader?.abortSegment(segmentId); + const streamType = segment.stream.type; + if (streamType === "main") this.mainStreamLoader?.abortSegment(segment); + else this.secondaryStreamLoader?.abortSegment(segment); } updatePlayback(position: number, rate: number): void { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 76a1d4ac..740fbedb 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,13 +1,15 @@ import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; -import { P2PLoader } from "./p2p-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; -import { Playback, QueueItem, QueueItemStatuses } from "./internal-types"; +import { Playback, QueueItem } from "./internal-types"; import { RequestContainer, EngineCallbacks } from "./request"; import * as QueueUtils from "./utils/queue-utils"; +import * as LoggerUtils from "./utils/logger"; import { FetchError } from "./errors"; +import { P2PLoadersContainer } from "./p2p-loaders-container"; +import debug from "debug"; export class HybridLoader { private readonly requests = new RequestContainer(); @@ -18,6 +20,7 @@ export class HybridLoader { private lastQueueProcessingTimeStamp?: number; private readonly segmentAvgDuration: number; private readonly randomHttpDownloadInterval: number; + private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; constructor( private streamManifestUrl: string, @@ -50,6 +53,11 @@ export class HybridLoader { this.settings ); + this.logger = { + loader: debug(`core:${activeStream.type}-hybrid-loader`), + engine: debug(`core:${activeStream.type}-hybrid-loader-engine`), + }; + this.randomHttpDownloadInterval = window.setInterval( () => this.loadRandomThroughHttp(), 1000 @@ -58,9 +66,12 @@ export class HybridLoader { // api method for engines async loadSegment(segment: Readonly, callbacks: EngineCallbacks) { - console.log("REQUESTED: ", getSegmentStringId(segment)); + this.logger.engine(`requests: ${LoggerUtils.getSegmentString(segment)}`); const { stream } = segment; if (stream !== this.lastRequestedSegment.stream) { + this.logger.loader( + `STREAM CHANGED ${LoggerUtils.getStreamString(stream)}` + ); this.p2pLoaders.changeActiveLoader(stream); } this.lastRequestedSegment = segment; @@ -173,20 +184,26 @@ export class HybridLoader { } // api method for engines - abortSegment(segmentId: string) { - this.requests.abortEngineRequest(segmentId); + abortSegment(segment: Segment) { + this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); + this.requests.abortEngineRequest(segment); } - private async loadThroughHttp(item: QueueItem) { - const { segment, statuses } = item; + private async loadThroughHttp(item: QueueItem, isRandom = false) { + const { segment } = item; let data: ArrayBuffer | undefined; try { - const idStr = getSegmentStringId(segment); - console.log(`http requested: ${idStr} - ${getStatusesString(statuses)}`); const httpRequest = getHttpSegmentRequest(segment); + + const loadType = isRandom ? " random" : ""; + this.logger.loader( + `http${loadType} request: ${LoggerUtils.getQueueItemString(item)}` + ); + this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; - console.log(`=> http loaded: ${idStr}`); + + this.logger.loader(`http responses: ${segment.externalId}`); if (data) this.onSegmentLoaded(segment, data); } catch (err) { if (err instanceof FetchError) { @@ -198,11 +215,12 @@ export class HybridLoader { private async loadThroughP2P(item: QueueItem) { const { segment, statuses } = item; const p2pLoader = this.p2pLoaders.activeLoader; - const idStr = getSegmentStringId(segment); try { + const segmentString = LoggerUtils.getSegmentString(segment); + const statusesStr = LoggerUtils.getStatusesString(statuses); const data = await p2pLoader.downloadSegment(segment); if (data) { - console.log(`=> p2p loaded: ${idStr}, ${data?.byteLength}`); + this.logger.loader(`p2p loaded: ${segmentString} | ${statusesStr}`); this.onSegmentLoaded(segment, data); } } catch (error) { @@ -229,9 +247,7 @@ export class HybridLoader { if (!queue.length) return; const item = queue[Math.floor(Math.random() * queue.length)]; - console.log("HTTP RANDOM"); - // console.log("load random: ", getSegmentStringId(segment)); - void this.loadThroughHttp(item); + void this.loadThroughHttp(item, true); } private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { @@ -249,7 +265,10 @@ export class HybridLoader { if (itemSegment.localId === segment.localId) break; if (this.requests.isHttpRequested(segment)) { this.requests.abortLoaderRequest(segment); - console.log("aborted http: ", getSegmentStringId(segment)); + this.logger.loader( + "http aborted: ", + LoggerUtils.getSegmentString(segment) + ); break; } } @@ -260,7 +279,10 @@ export class HybridLoader { if (itemSegment.localId === segment.localId) break; if (this.requests.isP2PRequested(segment)) { this.requests.abortLoaderRequest(segment); - console.log("aborted p2p: ", getSegmentStringId(segment)); + this.logger.loader( + "p2p aborted: ", + LoggerUtils.getSegmentString(segment) + ); break; } } @@ -278,16 +300,15 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged) this.playback.rate = rate; - if (isPositionSignificantlyChanged) { - console.log("\nposition: ", position); + this.logger.engine("position significantly changed"); } void this.processQueue(isPositionSignificantlyChanged); } updateStream(stream: StreamWithSegments) { if (stream !== this.lastRequestedSegment.stream) return; - console.log("STREAM UPDATED"); + this.logger.engine(`update stream: ${LoggerUtils.getStreamString(stream)}`); this.processQueue(); } @@ -298,6 +319,8 @@ export class HybridLoader { void this.segmentStorage.destroy(); this.requests.destroy(); this.p2pLoaders.destroy(); + this.logger.loader.destroy(); + this.logger.engine.destroy(); } } @@ -307,76 +330,6 @@ function* arrayBackwards(arr: T[]) { } } -type P2PLoaderContainerItem = { - streamId: string; - loader: P2PLoader; - destroyTimeoutId?: number; -}; - -class P2PLoadersContainer { - private readonly loaders = new Map(); - private _activeLoaderItem: P2PLoaderContainerItem; - - constructor( - private readonly streamManifestUrl: string, - stream: StreamWithSegments, - private readonly requests: RequestContainer, - private readonly segmentStorage: SegmentsMemoryStorage, - private readonly settings: Settings - ) { - this._activeLoaderItem = this.createLoaderItem(stream); - } - - createLoaderItem(stream: StreamWithSegments) { - if (this.loaders.has(stream.localId)) { - throw new Error("Loader for this stream already exists"); - } - const loader = new P2PLoader( - this.streamManifestUrl, - stream, - this.requests, - this.segmentStorage, - this.settings - ); - const item = { loader, streamId: stream.localId }; - this.loaders.set(stream.localId, item); - this._activeLoaderItem = item; - return item; - } - - changeActiveLoader(stream: StreamWithSegments) { - const loaderItem = this.loaders.get(stream.localId); - const prevActive = this._activeLoaderItem; - if (loaderItem) { - this._activeLoaderItem = loaderItem; - clearTimeout(loaderItem.destroyTimeoutId); - } else { - this.createLoaderItem(stream); - } - this.setLoaderDestroyTimeout(prevActive); - } - - private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { - item.destroyTimeoutId = window.setTimeout(() => { - item.loader.destroy(); - this.loaders.delete(item.streamId); - console.log("loader destroyed"); - }, this.settings.p2pLoaderDestroyTimeout); - } - - get activeLoader() { - return this._activeLoaderItem.loader; - } - - destroy() { - for (const { loader, destroyTimeoutId } of this.loaders.values()) { - loader.destroy(); - clearTimeout(destroyTimeoutId); - } - this.loaders.clear(); - } -} - // function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { // return Math.max(segment.startTime - playback.position, 0) / playback.rate; // } @@ -405,9 +358,3 @@ function getSegmentAvgDuration(stream: StreamWithSegments) { return sumDuration / size; } - -function getSegmentStringId(segment: Segment) { - const { index } = segment.stream; - const { externalId } = segment; - return `${index}-${externalId}`; -} diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 3ac50cc2..3ebdcbd2 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -6,8 +6,10 @@ import { Segment, Settings, StreamWithSegments } from "./types"; import { JsonSegmentAnnouncement } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils/utils"; +import * as LoggerUtils from "./utils/logger"; import { PeerSegmentStatus } from "./enums"; import { RequestContainer } from "./request"; +import debug from "debug"; export class P2PLoader { private readonly streamExternalId: string; @@ -16,6 +18,7 @@ export class P2PLoader { private readonly trackerClient: TrackerClient; private readonly peers = new Map(); private announcement: JsonSegmentAnnouncement = { i: "" }; + private readonly logger = debug("core:p2p-manager"); constructor( private streamManifestUrl: string, @@ -31,13 +34,14 @@ export class P2PLoader { ); this.streamHash = getHash(this.streamExternalId); this.peerHash = getHash(peerId); - console.log("\ncreate tracker client", this.streamExternalId); - console.log("PEER ID: " + this.peerHash); this.trackerClient = createTrackerClient({ streamHash: this.streamHash, peerHash: this.peerHash, }); + this.logger( + `create tracker client: ${LoggerUtils.getStreamString(stream)}` + ); this.subscribeOnTrackerEvents(this.trackerClient); this.segmentStorage.subscribeOnUpdate( this.stream, @@ -55,21 +59,29 @@ export class P2PLoader { // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", () => {}); trackerClient.on("peer", (candidate) => { - console.log("Peer found: ", candidate); const peer = this.peers.get(candidate.id); if (peer) peer.addCandidate(candidate); else this.createPeer(candidate); }); // eslint-disable-next-line @typescript-eslint/no-empty-function - trackerClient.on("warning", (warning) => {}); + trackerClient.on("warning", (warning) => { + this.logger( + `tracker warning (${LoggerUtils.getStreamString( + this.stream + )}: ${warning})` + ); + }); // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("error", (error) => { - console.log("TRACKER ERROR", error); + this.logger( + `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` + ); }); } private createPeer(candidate: PeerCandidate) { const peer = new Peer( + `${LoggerUtils.getStreamString(this.stream)}-${this.peers.size + 1}`, candidate, { onPeerConnected: this.onPeerConnected.bind(this), @@ -78,6 +90,7 @@ export class P2PLoader { this.settings ); this.peers.set(candidate.id, peer); + this.logger(`create new peer: ${peer.localId}`); } async downloadSegment(segment: Segment): Promise { @@ -97,8 +110,7 @@ export class P2PLoader { const peer = peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; const request = peer.requestSegment(segment); - const idStr = `${segment.stream.index}-${segment.externalId}`; - console.log(`=> p2p requested: ${idStr}`); + this.logger(`p2p request ${segment.externalId}`); this.requests.addLoaderRequest(segment, request); return request.promise; } @@ -129,6 +141,7 @@ export class P2PLoader { } private onPeerConnected(peer: Peer) { + this.logger(`connected with peer: ${peer.localId}`); peer.sendSegmentsAnnouncement(this.announcement); } @@ -156,6 +169,9 @@ export class P2PLoader { } destroy() { + this.logger( + `destroy tracker client: ${LoggerUtils.getStreamString(this.stream)}` + ); this.segmentStorage.unsubscribeFromUpdate( this.stream, this.updateAndBroadcastAnnouncement diff --git a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts b/packages/p2p-media-loader-core/src/p2p-loaders-container.ts new file mode 100644 index 00000000..bccf8a37 --- /dev/null +++ b/packages/p2p-media-loader-core/src/p2p-loaders-container.ts @@ -0,0 +1,86 @@ +import { P2PLoader } from "./p2p-loader"; +import debug from "debug"; +import { Settings, StreamWithSegments } from "./index"; +import { RequestContainer } from "./request"; +import { SegmentsMemoryStorage } from "./segments-storage"; +import * as LoggerUtils from "./utils/logger"; + +type P2PLoaderContainerItem = { + streamId: string; + loader: P2PLoader; + destroyTimeoutId?: number; + loggerInfo: string; +}; + +export class P2PLoadersContainer { + private readonly loaders = new Map(); + private _activeLoaderItem!: P2PLoaderContainerItem; + private readonly logger = debug("core:p2p-loaders-container"); + + constructor( + private readonly streamManifestUrl: string, + stream: StreamWithSegments, + private readonly requests: RequestContainer, + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly settings: Settings + ) { + this.changeActiveLoader(stream); + } + + private createLoader(stream: StreamWithSegments): P2PLoaderContainerItem { + if (this.loaders.has(stream.localId)) { + throw new Error("Loader for this stream already exists"); + } + const loader = new P2PLoader( + this.streamManifestUrl, + stream, + this.requests, + this.segmentStorage, + this.settings + ); + const loggerInfo = LoggerUtils.getStreamString(stream); + this.logger(`created new loader: ${loggerInfo}`); + return { + loader, + streamId: stream.localId, + loggerInfo: LoggerUtils.getStreamString(stream), + }; + } + + changeActiveLoader(stream: StreamWithSegments) { + const loaderItem = this.loaders.get(stream.localId); + const prevActive = this._activeLoaderItem; + if (loaderItem) { + this._activeLoaderItem = loaderItem; + clearTimeout(loaderItem.destroyTimeoutId); + } else { + const loader = this.createLoader(stream); + this.loaders.set(stream.localId, loader); + this._activeLoaderItem = loader; + } + this.logger( + `change active p2p loader: ${LoggerUtils.getStreamString(stream)}` + ); + if (prevActive) this.setLoaderDestroyTimeout(prevActive); + } + + private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { + item.destroyTimeoutId = window.setTimeout(() => { + item.loader.destroy(); + this.loaders.delete(item.streamId); + this.logger(`destroy p2p loader: `, item.loggerInfo); + }, this.settings.p2pLoaderDestroyTimeout); + } + + get activeLoader() { + return this._activeLoaderItem.loader; + } + + destroy() { + for (const { loader, destroyTimeoutId } of this.loaders.values()) { + loader.destroy(); + clearTimeout(destroyTimeoutId); + } + this.loaders.clear(); + } +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 572b8816..aa81eb09 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -41,6 +41,7 @@ export class Peer { private isSendingData = false; constructor( + readonly localId: string, candidate: PeerCandidate, private readonly eventHandlers: PeerEventHandlers, private readonly settings: PeerSettings @@ -52,9 +53,10 @@ export class Peer { addCandidate(candidate: PeerCandidate) { candidate.on("connect", () => { - console.log("\nconnected with peer", this.connection === candidate); - this.connection = candidate; - this.eventHandlers.onPeerConnected(this); + if (candidate !== this.connection) { + this.connection = candidate; + this.eventHandlers.onPeerConnected(this); + } }); candidate.on("data", this.onReceiveData.bind(this)); candidate.on("close", () => { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 7ba8caab..fa38fd65 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -167,8 +167,9 @@ export class RequestContainer { return !!this.requests.get(id)?.loaderRequest; } - abortEngineRequest(segmentId: string) { - const request = this.requests.get(segmentId); + abortEngineRequest(segment: Segment) { + const id = getRequestItemId(segment); + const request = this.requests.get(id); if (!request) return; request.engineCallbacks?.onError(new RequestAbortError()); diff --git a/packages/p2p-media-loader-core/src/utils/logger.ts b/packages/p2p-media-loader-core/src/utils/logger.ts new file mode 100644 index 00000000..9f802f2b --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/logger.ts @@ -0,0 +1,31 @@ +import { Segment, Stream } from "../types"; +import { QueueItem, QueueItemStatuses } from "../internal-types"; + +export function getStreamString(stream: Stream) { + return `${stream.type}-${stream.index}`; +} + +export function getSegmentString(segment: Segment) { + const { externalId } = segment; + return `(${getStreamString(segment.stream)} | ${externalId})`; +} + +export function getSegmentFullString(segment: Segment) { + const { externalId } = segment; + return `(${getStreamString(segment.stream)} | ${externalId})`; +} + +export function getStatusesString(statuses: QueueItemStatuses): string { + const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; + if (isHighDemand) return "high-demand"; + if (isHttpDownloadable && isP2PDownloadable) return "http-p2p-window"; + if (isHttpDownloadable) return "http-window"; + if (isP2PDownloadable) return "p2p-window"; + return "-"; +} + +export function getQueueItemString(item: QueueItem) { + const { segment, statuses } = item; + const statusString = getStatusesString(statuses); + return `${segment.externalId} ${statusString}`; +} diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index 9570efc3..de8b963e 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -1,4 +1,5 @@ import { Segment, Stream, StreamWithSegments } from "../index"; +import { QueueItem, QueueItemStatuses } from "../internal-types"; export function getStreamExternalId( manifestResponseUrl: string, From e564a76ac4dd1ff51ff1ff9793fefa2f5ed36d6b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 5 Oct 2023 16:47:24 +0300 Subject: [PATCH 080/127] Add load probability to http random load. --- .../src/hybrid-loader.ts | 21 +++++++++++++------ .../p2p-media-loader-core/src/p2p-loader.ts | 19 +++++++++++++---- .../p2p-media-loader-core/src/utils/utils.ts | 1 - .../src/segment-mananger.ts | 1 + 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 740fbedb..7edb9bdb 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -59,8 +59,8 @@ export class HybridLoader { }; this.randomHttpDownloadInterval = window.setInterval( - () => this.loadRandomThroughHttp(), - 1000 + this.loadRandomThroughHttp.bind(this), + 1500 ); } @@ -195,10 +195,11 @@ export class HybridLoader { try { const httpRequest = getHttpSegmentRequest(segment); - const loadType = isRandom ? " random" : ""; - this.logger.loader( - `http${loadType} request: ${LoggerUtils.getQueueItemString(item)}` - ); + if (!isRandom) { + this.logger.loader( + `http request: ${LoggerUtils.getQueueItemString(item)}` + ); + } this.requests.addLoaderRequest(segment, httpRequest); data = await httpRequest.promise; @@ -245,9 +246,17 @@ export class HybridLoader { p2pLoader.isLoadingOrLoadedBySomeone(segment), }); if (!queue.length) return; + const peersAmount = p2pLoader.connectedPeersAmount + 1; + const probability = Math.min(queue.length / peersAmount, 1); + const shouldLoad = Math.random() < probability; + if (!shouldLoad) return; const item = queue[Math.floor(Math.random() * queue.length)]; void this.loadThroughHttp(item, true); + + this.logger.loader( + `http random request: ${LoggerUtils.getQueueItemString(item)}` + ); } private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 3ebdcbd2..c510fa16 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -57,7 +57,7 @@ export class P2PLoader { private subscribeOnTrackerEvents(trackerClient: TrackerClient) { // TODO: tracker event handlers // eslint-disable-next-line @typescript-eslint/no-empty-function - trackerClient.on("update", () => {}); + trackerClient.on("update", (data) => {}); trackerClient.on("peer", (candidate) => { const peer = this.peers.get(candidate.id); if (peer) peer.addCandidate(candidate); @@ -80,8 +80,12 @@ export class P2PLoader { } private createPeer(candidate: PeerCandidate) { + const peerLocalId = `${LoggerUtils.getStreamString(this.stream)}-${ + this.peers.size + 1 + }`; + this.logger(`create new peer: ${peerLocalId}`); const peer = new Peer( - `${LoggerUtils.getStreamString(this.stream)}-${this.peers.size + 1}`, + peerLocalId, candidate, { onPeerConnected: this.onPeerConnected.bind(this), @@ -90,7 +94,6 @@ export class P2PLoader { this.settings ); this.peers.set(candidate.id, peer); - this.logger(`create new peer: ${peer.localId}`); } async downloadSegment(segment: Segment): Promise { @@ -122,6 +125,14 @@ export class P2PLoader { return false; } + get connectedPeersAmount() { + let count = 0; + for (const peer of this.peers.values()) { + if (peer.isConnected) count++; + } + return count; + } + private updateSegmentAnnouncement() { const loaded: string[] = this.segmentStorage.getStoredSegmentExternalIdsOfStream(this.stream); @@ -203,7 +214,7 @@ function createTrackerClient({ peerId: peerHash, port: 6881, announce: [ - "wss://tracker.novage.com.ua", + // "wss://tracker.novage.com.ua", "wss://tracker.openwebtorrent.com", ], rtcConfig: { diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index de8b963e..9570efc3 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -1,5 +1,4 @@ import { Segment, Stream, StreamWithSegments } from "../index"; -import { QueueItem, QueueItemStatuses } from "../internal-types"; export function getStreamExternalId( manifestResponseUrl: string, diff --git a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts index 345e5846..f2732a17 100644 --- a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts +++ b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts @@ -73,6 +73,7 @@ export class SegmentManager { }); }); + if (!newSegments.length && !segmentToRemoveIds.size) return; this.core.updateStream(url, newSegments, [...segmentToRemoveIds]); } } From 67886a93182f99c18a153eee6f7a2a29e35856b1 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 5 Oct 2023 18:57:37 +0300 Subject: [PATCH 081/127] Show stat in Demo. Destroy p2p manager if there is no uploadable data. --- p2p-media-loader-demo/src/App.tsx | 156 ++++++++++++------ packages/p2p-media-loader-core/src/core.ts | 8 +- .../src/hybrid-loader.ts | 29 +++- .../src/p2p-loaders-container.ts | 28 +++- packages/p2p-media-loader-core/src/request.ts | 2 - packages/p2p-media-loader-core/src/types.ts | 4 + packages/p2p-media-loader-hlsjs/src/engine.ts | 6 +- 7 files changed, 163 insertions(+), 70 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index a0c79b9a..c8a7b201 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -49,12 +49,35 @@ function App() { localStorage.player ); const [url, setUrl] = useState(localStorage.streamUrl); - const shakaEngine = useRef(new ShakaEngine(shakaLib)); - const hlsEngine = useRef(new HlsJsEngine()); const shakaInstance = useRef(); const hlsInstance = useRef(); const containerRef = useRef(null); const videoRef = useRef(null); + const shakaEngine = useRef(new ShakaEngine(shakaLib)); + const statIntervalId = useRef(); + + const [httpLoaded, setHttpLoaded] = useState(0); + const [p2pLoaded, setP2PLoaded] = useState(0); + const [globHttpLoaded, setGlobHttpLoaded] = useState(0); + const [globP2PLoaded, setGlobP2PLoaded] = useState(0); + + const hlsEngine = useRef( + new HlsJsEngine({ + onDataLoaded: (byteLength, type) => { + if (type === "http") { + setHttpLoaded((prev) => prev + byteLength / Math.pow(1024, 2)); + } else if (type === "p2p") { + setP2PLoaded((prev) => prev + byteLength / Math.pow(1024, 2)); + } + const add = (prop: "httpLoaded" | "p2pLoaded") => { + const value = +localStorage[prop]; + localStorage[prop] = value + byteLength; + }; + if (type === "http") add("httpLoaded"); + else if (type === "p2p") add("p2pLoaded"); + }, + }) + ); useEffect(() => { if ( @@ -182,6 +205,8 @@ function App() { }; const createNewPlayer = () => { + localStorage.httpLoaded = 0; + localStorage.p2pLoaded = 0; switch (playerType) { case "hls-dplayer": initHlsDplayer(url); @@ -196,6 +221,14 @@ function App() { initShakaPlayer(url); break; } + + statIntervalId.current = window.setInterval(() => { + const httpLoaded = +localStorage.httpLoaded; + const p2pLoaded = +localStorage.p2pLoaded; + if (!httpLoaded && !p2pLoaded) return; + setGlobHttpLoaded(httpLoaded / Math.pow(1024, 2)); + setGlobP2PLoaded(p2pLoaded / Math.pow(1024, 2)); + }, 2000); }; const loadStreamWithExistingInstance = () => { @@ -212,54 +245,85 @@ function App() { }; return ( -
-
-

This is Demo

-
- - - - +
+
+
+

This is Demo

+
+ + + + +
+
+
+
+
+ {!!playerType && ["hlsjs", "shaka-player"].includes(playerType) && ( +
-
-
-
- {!!playerType && ["hlsjs", "shaka-player"].includes(playerType) && ( -
); } +function getPercent(a: number, b: number) { + if (a === 0 && b === 0) return "0"; + if (b === 0) return "100"; + return ((a / (a + b)) * 100).toFixed(2); +} + export default App; diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index d5ebdcd5..ab9a4f5f 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -5,6 +5,7 @@ import { Segment, Settings, SegmentBase, + CoreEventHandlers, } from "./types"; import * as Utils from "./utils/utils"; import { LinkedMap } from "./linked-map"; @@ -18,7 +19,7 @@ export class Core { private readonly settings: Settings = { simultaneousHttpDownloads: 2, simultaneousP2PDownloads: 3, - highDemandTimeWindow: 30, + highDemandTimeWindow: 10, httpDownloadTimeWindow: 60, p2pDownloadTimeWindow: 60, cachedSegmentExpiration: 120 * 1000, @@ -33,6 +34,8 @@ export class Core { private mainStreamLoader?: HybridLoader; private secondaryStreamLoader?: HybridLoader; + constructor(private readonly eventHandlers?: CoreEventHandlers) {} + setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; } @@ -135,7 +138,8 @@ export class Core { segment, this.settings, this.bandwidthApproximator, - this.segmentStorage + this.segmentStorage, + this.eventHandlers ); }; const streamTypeLoaderKeyMap = { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7edb9bdb..d5f40be8 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,7 +1,7 @@ import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; -import { Settings } from "./types"; +import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestContainer, EngineCallbacks } from "./request"; @@ -27,7 +27,8 @@ export class HybridLoader { requestedSegment: Segment, private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator, - private readonly segmentStorage: SegmentsMemoryStorage + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly eventHandlers?: Pick ) { this.lastRequestedSegment = requestedSegment; const activeStream = requestedSegment.stream; @@ -205,7 +206,7 @@ export class HybridLoader { data = await httpRequest.promise; this.logger.loader(`http responses: ${segment.externalId}`); - if (data) this.onSegmentLoaded(segment, data); + if (data) this.onSegmentLoaded(segment, data, "http"); } catch (err) { if (err instanceof FetchError) { // TODO: handle error @@ -222,7 +223,7 @@ export class HybridLoader { const data = await p2pLoader.downloadSegment(segment); if (data) { this.logger.loader(`p2p loaded: ${segmentString} | ${statusesStr}`); - this.onSegmentLoaded(segment, data); + this.onSegmentLoaded(segment, data, "p2p"); } } catch (error) { console.log(""); @@ -233,8 +234,14 @@ export class HybridLoader { private loadRandomThroughHttp() { const { simultaneousHttpDownloads } = this.settings; - if (this.requests.httpRequestsCount >= simultaneousHttpDownloads) return; const p2pLoader = this.p2pLoaders.activeLoader; + const connectedPeersAmount = p2pLoader.connectedPeersAmount; + if ( + this.requests.httpRequestsCount >= simultaneousHttpDownloads || + !connectedPeersAmount + ) { + return; + } const { queue } = QueueUtils.generateQueue({ lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, @@ -246,8 +253,8 @@ export class HybridLoader { p2pLoader.isLoadingOrLoadedBySomeone(segment), }); if (!queue.length) return; - const peersAmount = p2pLoader.connectedPeersAmount + 1; - const probability = Math.min(queue.length / peersAmount, 1); + const peersAmount = connectedPeersAmount + 1; + const probability = Math.min(queue.length / peersAmount, 1) / 2; const shouldLoad = Math.random() < probability; if (!shouldLoad) return; @@ -259,13 +266,19 @@ export class HybridLoader { ); } - private onSegmentLoaded(segment: Segment, data: ArrayBuffer) { + private onSegmentLoaded( + segment: Segment, + data: ArrayBuffer, + type: "http" | "p2p" + ) { + const byteLength = data.byteLength; this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); this.requests.resolveEngineRequest(segment, { data, bandwidth: this.bandwidthApproximator.getBandwidth(), }); + this.eventHandlers?.onDataLoaded?.(byteLength, type); this.processQueue(); } diff --git a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts b/packages/p2p-media-loader-core/src/p2p-loaders-container.ts index bccf8a37..ddb68656 100644 --- a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts +++ b/packages/p2p-media-loader-core/src/p2p-loaders-container.ts @@ -1,12 +1,12 @@ import { P2PLoader } from "./p2p-loader"; import debug from "debug"; -import { Settings, StreamWithSegments } from "./index"; +import { Settings, Stream, StreamWithSegments } from "./index"; import { RequestContainer } from "./request"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as LoggerUtils from "./utils/logger"; type P2PLoaderContainerItem = { - streamId: string; + stream: Stream; loader: P2PLoader; destroyTimeoutId?: number; loggerInfo: string; @@ -42,7 +42,7 @@ export class P2PLoadersContainer { this.logger(`created new loader: ${loggerInfo}`); return { loader, - streamId: stream.localId, + stream, loggerInfo: LoggerUtils.getStreamString(stream), }; } @@ -61,15 +61,25 @@ export class P2PLoadersContainer { this.logger( `change active p2p loader: ${LoggerUtils.getStreamString(stream)}` ); - if (prevActive) this.setLoaderDestroyTimeout(prevActive); + + if (!prevActive) return; + + const ids = this.segmentStorage.getStoredSegmentExternalIdsOfStream(stream); + if (!ids.length) this.destroyAndRemoveLoader(prevActive); + else this.setLoaderDestroyTimeout(prevActive); } private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { - item.destroyTimeoutId = window.setTimeout(() => { - item.loader.destroy(); - this.loaders.delete(item.streamId); - this.logger(`destroy p2p loader: `, item.loggerInfo); - }, this.settings.p2pLoaderDestroyTimeout); + item.destroyTimeoutId = window.setTimeout( + () => this.destroyAndRemoveLoader(item), + this.settings.p2pLoaderDestroyTimeout + ); + } + + private destroyAndRemoveLoader(item: P2PLoaderContainerItem) { + item.loader.destroy(); + this.loaders.delete(item.stream.localId); + this.logger(`destroy p2p loader: `, item.loggerInfo); } get activeLoader() { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index fa38fd65..e75eaca5 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -87,8 +87,6 @@ export class RequestContainer { .then(() => clearRequestItem()) .catch((err) => { if (err instanceof RequestAbortError) clearRequestItem(); - if (err instanceof FetchError) { - } }); if (loaderRequest.type === "http") this.onHttpRequestsHandlers.fire(); } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.ts index 22791da2..a50c7327 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.ts @@ -61,3 +61,7 @@ export type Settings = { storageCleanupInterval: number; p2pLoaderDestroyTimeout: number; }; + +export type CoreEventHandlers = { + onDataLoaded?: (byteLength: number, type: "http" | "p2p") => void; +}; diff --git a/packages/p2p-media-loader-hlsjs/src/engine.ts b/packages/p2p-media-loader-hlsjs/src/engine.ts index 4f620135..c419b3e8 100644 --- a/packages/p2p-media-loader-hlsjs/src/engine.ts +++ b/packages/p2p-media-loader-hlsjs/src/engine.ts @@ -2,7 +2,7 @@ import type Hls from "hls.js"; import type { HlsConfig, Events } from "hls.js"; import { FragmentLoaderBase } from "./fragment-loader"; import { SegmentManager } from "./segment-mananger"; -import { Core } from "p2p-media-loader-core"; +import { Core, CoreEventHandlers } from "p2p-media-loader-core"; import Debug from "debug"; export class Engine { @@ -10,8 +10,8 @@ export class Engine { private readonly segmentManager: SegmentManager; private debugDestroying = Debug("hls:destroying"); - constructor() { - this.core = new Core(); + constructor(eventHandlers?: CoreEventHandlers) { + this.core = new Core(eventHandlers); this.segmentManager = new SegmentManager(this.core); } From 7f11315da35c68a1925cc43c9d4c3f4cbbafa0f6 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 6 Oct 2023 13:36:33 +0300 Subject: [PATCH 082/127] Refactor code related to stat components. --- p2p-media-loader-demo/src/App.tsx | 84 +++++++++++++++++-------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index c8a7b201..7fbb50e2 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -64,14 +64,11 @@ function App() { const hlsEngine = useRef( new HlsJsEngine({ onDataLoaded: (byteLength, type) => { - if (type === "http") { - setHttpLoaded((prev) => prev + byteLength / Math.pow(1024, 2)); - } else if (type === "p2p") { - setP2PLoaded((prev) => prev + byteLength / Math.pow(1024, 2)); - } + const MBytes = getMBFromBytes(byteLength); + if (type === "http") setHttpLoaded((prev) => prev + MBytes); + else if (type === "p2p") setP2PLoaded((prev) => prev + MBytes); const add = (prop: "httpLoaded" | "p2pLoaded") => { - const value = +localStorage[prop]; - localStorage[prop] = value + byteLength; + localStorage[prop] = +localStorage[prop] + MBytes; }; if (type === "http") add("httpLoaded"); else if (type === "p2p") add("p2pLoaded"); @@ -223,11 +220,8 @@ function App() { } statIntervalId.current = window.setInterval(() => { - const httpLoaded = +localStorage.httpLoaded; - const p2pLoaded = +localStorage.p2pLoaded; - if (!httpLoaded && !p2pLoaded) return; - setGlobHttpLoaded(httpLoaded / Math.pow(1024, 2)); - setGlobP2PLoaded(p2pLoaded / Math.pow(1024, 2)); + setGlobHttpLoaded(+localStorage.httpLoaded); + setGlobP2PLoaded(+localStorage.p2pLoaded); }, 2000); }; @@ -245,8 +239,8 @@ function App() { }; return ( -
-
+
+

This is Demo

@@ -292,29 +286,37 @@ function App() { {!!playerType && ["hlsjs", "shaka-player"].includes(playerType) && (
+
+ ); +} -
-

Global stat

-
- Http global loaded: {globHttpLoaded.toFixed(2)} MB;{" "} - {getPercent(globHttpLoaded, globP2PLoaded)}% -
-
- P2P loaded: {globP2PLoaded.toFixed(2)} MB;{" "} - {getPercent(globP2PLoaded, globHttpLoaded)}% -
-
+export default App; + +function LoadStat({ + http, + p2p, + title, +}: { + http: number; + p2p: number; + title: string; +}) { + const sum = http + p2p; + return ( +
+

{title}

+
+ Http loaded: {http.toFixed(2)} MB; {getPercent(http, sum)}% +
+
+ P2P loaded: {p2p.toFixed(2)} MB; {getPercent(p2p, sum)}%
); @@ -323,7 +325,13 @@ function App() { function getPercent(a: number, b: number) { if (a === 0 && b === 0) return "0"; if (b === 0) return "100"; - return ((a / (a + b)) * 100).toFixed(2); + return ((a / b) * 100).toFixed(2); } -export default App; +function round(value: number, digitsAfterComma = 2) { + return Math.round(value * Math.pow(10, digitsAfterComma)) / 100; +} + +function getMBFromBytes(bytes: number) { + return round(bytes / Math.pow(1024, 2)); +} From 52d88d0f15db65a67e9d75d40e82aa994cbecd3c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Fri, 6 Oct 2023 18:13:53 +0300 Subject: [PATCH 083/127] Add loggers select to demo. --- p2p-media-loader-demo/src/App.tsx | 137 +++++++++++++++--- .../src/hybrid-loader.ts | 4 +- 2 files changed, 119 insertions(+), 22 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index 7fbb50e2..001e6a1a 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -1,10 +1,11 @@ -import { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { Engine as HlsJsEngine } from "p2p-media-loader-hlsjs"; import { Engine as ShakaEngine } from "p2p-media-loader-shaka"; import Hls from "hls.js"; import DPlayer from "dplayer"; import shakaLib from "shaka-player"; import muxjs from "mux.js"; +import debug from "debug"; window.muxjs = muxjs; @@ -54,24 +55,33 @@ function App() { const containerRef = useRef(null); const videoRef = useRef(null); const shakaEngine = useRef(new ShakaEngine(shakaLib)); - const statIntervalId = useRef(); const [httpLoaded, setHttpLoaded] = useState(0); const [p2pLoaded, setP2PLoaded] = useState(0); - const [globHttpLoaded, setGlobHttpLoaded] = useState(0); - const [globP2PLoaded, setGlobP2PLoaded] = useState(0); + const [httpLoadedGlob, setHttpLoadedGlob] = useLocalStorageItem( + "httpLoaded", + 0, + (v) => v.toString(), + (v) => (v !== null ? +v : 0) + ); + const [p2pLoadedGlob, setP2PLoadedGlob] = useLocalStorageItem( + "p2pLoaded", + 0, + (v) => v.toString(), + (v) => (v !== null ? +v : 0) + ); const hlsEngine = useRef( new HlsJsEngine({ onDataLoaded: (byteLength, type) => { const MBytes = getMBFromBytes(byteLength); - if (type === "http") setHttpLoaded((prev) => prev + MBytes); - else if (type === "p2p") setP2PLoaded((prev) => prev + MBytes); - const add = (prop: "httpLoaded" | "p2pLoaded") => { - localStorage[prop] = +localStorage[prop] + MBytes; - }; - if (type === "http") add("httpLoaded"); - else if (type === "p2p") add("p2pLoaded"); + if (type === "http") { + setHttpLoaded((prev) => round(prev + MBytes)); + setHttpLoadedGlob((prev) => round(prev + MBytes)); + } else if (type === "p2p") { + setP2PLoaded((prev) => round(prev + MBytes)); + setP2PLoadedGlob((prev) => round(prev + MBytes)); + } }, }) ); @@ -202,8 +212,8 @@ function App() { }; const createNewPlayer = () => { - localStorage.httpLoaded = 0; - localStorage.p2pLoaded = 0; + setHttpLoadedGlob(0); + setP2PLoadedGlob(0); switch (playerType) { case "hls-dplayer": initHlsDplayer(url); @@ -218,11 +228,6 @@ function App() { initShakaPlayer(url); break; } - - statIntervalId.current = window.setInterval(() => { - setGlobHttpLoaded(+localStorage.httpLoaded); - setGlobP2PLoaded(+localStorage.p2pLoaded); - }, 2000); }; const loadStreamWithExistingInstance = () => { @@ -286,12 +291,15 @@ function App() { {!!playerType && ["hlsjs", "shaka-player"].includes(playerType) && (
+
+
); @@ -322,6 +330,47 @@ function LoadStat({ ); } +function LoggersSelect() { + const [activeLoggers, setActiveLoggers] = useLocalStorageItem( + "debug", + [], + (list) => { + setTimeout(() => debug.enable(localStorage.debug), 0); + if (list.length === 0) return null; + return list.join(","); + }, + (storageItem) => { + setTimeout(() => debug.enable(localStorage.debug), 0); + if (!storageItem) return []; + return storageItem.split(","); + } + ); + + const onChange = (event: React.ChangeEvent) => { + setActiveLoggers( + Array.from(event.target.selectedOptions, (option) => option.value) + ); + }; + + return ( +
+

Loggers:

+ +
+ ); +} + function getPercent(a: number, b: number) { if (a === 0 && b === 0) return "0"; if (b === 0) return "100"; @@ -335,3 +384,51 @@ function round(value: number, digitsAfterComma = 2) { function getMBFromBytes(bytes: number) { return round(bytes / Math.pow(1024, 2)); } + +function useLocalStorageItem( + prop: string, + initValue: T, + valueToStorageItem: (value: T) => string | null, + storageItemToValue: (storageItem: string | null) => T +): [T, React.Dispatch>] { + const [value, setValue] = useState( + storageItemToValue(localStorage[prop]) ?? initValue + ); + const setValueExternal = useCallback((value: T | ((prev: T) => T)) => { + setValue(value); + if (typeof value === "function") { + const prev = storageItemToValue(localStorage.getItem(prop)); + const next = (value as (prev: T) => T)(prev); + const result = valueToStorageItem(next); + if (result !== null) localStorage.setItem(prop, result); + else localStorage.removeItem(prop); + } else { + const result = valueToStorageItem(value); + if (result !== null) localStorage.setItem(prop, result); + else localStorage.removeItem(prop); + } + }, []); + const eventHandler = useCallback((event: StorageEvent) => { + if (event.key !== prop) return; + const value = event.newValue; + setValue(storageItemToValue(value)); + }, []); + + useEffect(() => { + window.addEventListener("storage", eventHandler); + return () => { + window.removeEventListener("storage", eventHandler); + }; + }, []); + + return [value, setValueExternal]; +} + +const loggers = [ + "core:hybrid-loader-main", + "core:hybrid-loader-main-engine", + "core:hybrid-loader-secondary", + "core:hybrid-loader-secondary-engine", + "core:p2p-manager", + "core:p2p-loaders-container", +] as const; diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index d5f40be8..16d24880 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -55,8 +55,8 @@ export class HybridLoader { ); this.logger = { - loader: debug(`core:${activeStream.type}-hybrid-loader`), - engine: debug(`core:${activeStream.type}-hybrid-loader-engine`), + loader: debug(`core:hybrid-loader-${activeStream.type}`), + engine: debug(`core:hybrid-loader-${activeStream.type}-engine`), }; this.randomHttpDownloadInterval = window.setInterval( From 220b440a5f9af94c014fbe993d62886f0773e944 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Sun, 8 Oct 2023 23:12:01 +0300 Subject: [PATCH 084/127] And onPeerClose event to Peer class. --- .../src/hybrid-loader.ts | 30 ++++++++++++------- .../p2p-media-loader-core/src/p2p-loader.ts | 6 ++++ packages/p2p-media-loader-core/src/peer.ts | 2 ++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 16d24880..00078acb 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -19,7 +19,7 @@ export class HybridLoader { private readonly playback: Playback; private lastQueueProcessingTimeStamp?: number; private readonly segmentAvgDuration: number; - private readonly randomHttpDownloadInterval: number; + private randomHttpDownloadInterval!: number; private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; constructor( @@ -54,15 +54,25 @@ export class HybridLoader { this.settings ); - this.logger = { - loader: debug(`core:hybrid-loader-${activeStream.type}`), - engine: debug(`core:hybrid-loader-${activeStream.type}-engine`), - }; + const loader = debug(`core:hybrid-loader-${activeStream.type}`); + const engine = debug(`core:hybrid-loader-${activeStream.type}-engine`); + loader.color = "coral"; + engine.color = "orange"; + this.logger = { loader, engine }; - this.randomHttpDownloadInterval = window.setInterval( - this.loadRandomThroughHttp.bind(this), - 1500 - ); + // this.randomHttpDownloadInterval = window.setInterval( + // this.loadRandomThroughHttp.bind(this), + // 1500 + // ); + this.setIntervalLoading(); + } + + private setIntervalLoading() { + const randomTimeout = (Math.random() * 2 + 1) * 1000; + this.randomHttpDownloadInterval = window.setTimeout(() => { + this.loadRandomThroughHttp(); + this.setIntervalLoading(); + }, randomTimeout); } // api method for engines @@ -254,7 +264,7 @@ export class HybridLoader { }); if (!queue.length) return; const peersAmount = connectedPeersAmount + 1; - const probability = Math.min(queue.length / peersAmount, 1) / 2; + const probability = Math.min(queue.length / peersAmount, 1); const shouldLoad = Math.random() < probability; if (!shouldLoad) return; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index c510fa16..5951d8a1 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -89,6 +89,7 @@ export class P2PLoader { candidate, { onPeerConnected: this.onPeerConnected.bind(this), + onPeerClosed: this.onPeerClosed.bind(this), onSegmentRequested: this.onSegmentRequested.bind(this), }, this.settings @@ -156,6 +157,11 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(this.announcement); } + private onPeerClosed(peer: Peer) { + this.logger(`peer closed: ${peer.localId}`); + this.peers.delete(peer.id); + } + private updateAndBroadcastAnnouncement = () => { this.updateSegmentAnnouncement(); this.broadcastSegmentAnnouncement(); diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index aa81eb09..22f456aa 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -15,6 +15,7 @@ import { PeerRequestError } from "./errors"; type PeerEventHandlers = { onPeerConnected: (peer: Peer) => void; + onPeerClosed: (peer: Peer) => void; onSegmentRequested: (peer: Peer, segmentId: string) => void; }; @@ -63,6 +64,7 @@ export class Peer { if (this.connection === candidate) { this.connection = undefined; this.cancelSegmentRequest("peer-closed"); + this.eventHandlers.onPeerClosed(this); } }); // eslint-disable-next-line @typescript-eslint/no-empty-function From a4809598e7cfbd9f6b19bff6d2e3885d3e8b0ec7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 10 Oct 2023 12:32:25 +0300 Subject: [PATCH 085/127] Fix issue with wrong array buffer slicing. Modify choosing peer connection logic. --- .../src/declarations.d.ts | 8 ++-- .../p2p-media-loader-core/src/p2p-loader.ts | 16 ++++---- packages/p2p-media-loader-core/src/peer.ts | 41 +++++++++---------- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index 0d198026..d989094f 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -25,7 +25,7 @@ declare module "bittorrent-tracker" { export type TrackerEventHandler = E extends "update" ? (data: object) => void : E extends "peer" - ? (peer: PeerCandidate) => void + ? (peer: PeerConnection) => void : E extends "warning" ? (warning: unknown) => void : E extends "error" @@ -34,7 +34,7 @@ declare module "bittorrent-tracker" { type PeerEvent = "connect" | "data" | "close" | "error"; - export type PeerCandidateEventHandler = + export type PeerConnectionEventHandler = E extends "connect" ? () => void : E extends "data" @@ -45,12 +45,12 @@ declare module "bittorrent-tracker" { ? (error?: unknown) => void : never; - export type PeerCandidate = { + export type PeerConnection = { id: string; initiator: boolean; on( event: E, - handler: PeerCandidateEventHandler + handler: PeerConnectionEventHandler ): void; send(data: string | ArrayBuffer | Blob): void; write(data: string | ArrayBuffer | Blob): void; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 5951d8a1..c1782c09 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -1,4 +1,4 @@ -import TrackerClient, { PeerCandidate } from "bittorrent-tracker"; +import TrackerClient, { PeerConnection } from "bittorrent-tracker"; import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./utils/peer-utils"; @@ -58,10 +58,10 @@ export class P2PLoader { // TODO: tracker event handlers // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", (data) => {}); - trackerClient.on("peer", (candidate) => { - const peer = this.peers.get(candidate.id); - if (peer) peer.addCandidate(candidate); - else this.createPeer(candidate); + trackerClient.on("peer", (peerConnection) => { + const peer = this.peers.get(peerConnection.id); + if (peer) peer.setConnection(peerConnection); + else this.createPeer(peerConnection); }); // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("warning", (warning) => { @@ -79,14 +79,14 @@ export class P2PLoader { }); } - private createPeer(candidate: PeerCandidate) { + private createPeer(connection: PeerConnection) { const peerLocalId = `${LoggerUtils.getStreamString(this.stream)}-${ this.peers.size + 1 }`; this.logger(`create new peer: ${peerLocalId}`); const peer = new Peer( peerLocalId, - candidate, + connection, { onPeerConnected: this.onPeerConnected.bind(this), onPeerClosed: this.onPeerClosed.bind(this), @@ -94,7 +94,7 @@ export class P2PLoader { }, this.settings ); - this.peers.set(candidate.id, peer); + this.peers.set(connection.id, peer); } async downloadSegment(segment: Segment): Promise { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 22f456aa..2e942ded 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -1,4 +1,4 @@ -import { PeerCandidate } from "bittorrent-tracker"; +import { PeerConnection } from "bittorrent-tracker"; import { JsonSegmentAnnouncement, PeerCommand, @@ -35,43 +35,43 @@ type PeerSettings = Pick< export class Peer { readonly id: string; - private readonly candidates = new Set(); - private connection?: PeerCandidate; + private connection?: PeerConnection; private segments = new Map(); private request?: PeerRequest; private isSendingData = false; constructor( readonly localId: string, - candidate: PeerCandidate, + connection: PeerConnection, private readonly eventHandlers: PeerEventHandlers, private readonly settings: PeerSettings ) { - this.id = candidate.id; + this.id = connection.id; this.eventHandlers = eventHandlers; - this.addCandidate(candidate); + this.setConnection(connection); } - addCandidate(candidate: PeerCandidate) { - candidate.on("connect", () => { - if (candidate !== this.connection) { - this.connection = candidate; + setConnection(connection: PeerConnection) { + connection.on("connect", () => { + if (!this.connection) { + this.connection = connection; this.eventHandlers.onPeerConnected(this); + } else { + connection.destroy(); } }); - candidate.on("data", this.onReceiveData.bind(this)); - candidate.on("close", () => { - if (this.connection === candidate) { + connection.on("data", this.onReceiveData.bind(this)); + connection.on("close", () => { + if (this.connection === connection) { this.connection = undefined; this.cancelSegmentRequest("peer-closed"); this.eventHandlers.onPeerClosed(this); } }); // eslint-disable-next-line @typescript-eslint/no-empty-function - candidate.on("error", (error) => { + connection.on("error", (error) => { console.log("PEER ERROR:", error); }); - this.candidates.add(candidate); } get isConnected() { @@ -163,13 +163,12 @@ export class Peer { this.sendCommand(command); this.isSendingData = true; - const sendChunk = async (data: ArrayBuffer) => this.connection?.write(data); for (const chunk of getBufferChunks( data, this.settings.webRtcMaxMessageSize )) { if (!this.isSendingData) break; - void sendChunk(chunk); + this.connection?.write(chunk); } this.isSendingData = false; } @@ -257,20 +256,20 @@ export class Peer { destroy() { this.cancelSegmentRequest("destroy"); this.connection?.destroy(); - this.candidates.clear(); } } function* getBufferChunks( data: ArrayBuffer, maxChunkSize: number -): Generator { +): Generator { let bytesLeft = data.byteLength; while (bytesLeft > 0) { const bytesToSend = bytesLeft >= maxChunkSize ? maxChunkSize : bytesLeft; - const buffer = Buffer.from(data, data.byteLength - bytesLeft, bytesToSend); + const from = data.byteLength - bytesLeft; + const buffer = data.slice(from, from + bytesToSend); bytesLeft -= bytesToSend; - yield buffer; + yield Buffer.from(buffer); } } From 08203dfea979563444a658891789ebd237217f91 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 10 Oct 2023 16:51:10 +0300 Subject: [PATCH 086/127] Add peer logger. Use utf-8 string for peer ids instead of hashes. --- p2p-media-loader-demo/src/App.tsx | 1 + .../src/declarations.d.ts | 8 ++--- .../p2p-media-loader-core/src/p2p-loader.ts | 34 +++++++------------ packages/p2p-media-loader-core/src/peer.ts | 19 +++++++++-- .../src/utils/peer-utils.ts | 9 ++--- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index 001e6a1a..10000046 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -430,5 +430,6 @@ const loggers = [ "core:hybrid-loader-secondary", "core:hybrid-loader-secondary-engine", "core:p2p-manager", + "core:peer", "core:p2p-loaders-container", ] as const; diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index d989094f..e78e3c8b 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -1,8 +1,8 @@ declare module "bittorrent-tracker" { export default class Client { constructor(options: { - infoHash: string | ArrayBuffer; - peerId: string | ArrayBuffer; + infoHash: string | Buffer; + peerId: string | Buffer; announce: string[]; port: number; rtcConfig?: RTCConfiguration; @@ -52,8 +52,8 @@ declare module "bittorrent-tracker" { event: E, handler: PeerConnectionEventHandler ): void; - send(data: string | ArrayBuffer | Blob): void; - write(data: string | ArrayBuffer | Blob): void; + send(data: string | Blob | Buffer): void; + write(data: string | Blob | Buffer): void; destroy(): void; }; } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index c1782c09..248aa52d 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -1,5 +1,4 @@ import TrackerClient, { PeerConnection } from "bittorrent-tracker"; -import * as RIPEMD160 from "ripemd160"; import { Peer } from "./peer"; import * as PeerUtil from "./utils/peer-utils"; import { Segment, Settings, StreamWithSegments } from "./types"; @@ -13,8 +12,7 @@ import debug from "debug"; export class P2PLoader { private readonly streamExternalId: string; - private readonly streamHash: string; - private readonly peerHash: string; + private readonly peerId: string; private readonly trackerClient: TrackerClient; private readonly peers = new Map(); private announcement: JsonSegmentAnnouncement = { i: "" }; @@ -27,20 +25,20 @@ export class P2PLoader { private readonly segmentStorage: SegmentsMemoryStorage, private readonly settings: Settings ) { - const peerId = PeerUtil.generatePeerId(); + this.peerId = PeerUtil.generatePeerId(); this.streamExternalId = Utils.getStreamExternalId( this.streamManifestUrl, this.stream ); - this.streamHash = getHash(this.streamExternalId); - this.peerHash = getHash(peerId); this.trackerClient = createTrackerClient({ - streamHash: this.streamHash, - peerHash: this.peerHash, + streamHash: Buffer.from(this.streamExternalId, "utf-8"), + peerHash: Buffer.from(this.peerId, "utf-8"), }); this.logger( - `create tracker client: ${LoggerUtils.getStreamString(stream)}` + `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ + this.peerId + }` ); this.subscribeOnTrackerEvents(this.trackerClient); this.segmentStorage.subscribeOnUpdate( @@ -80,12 +78,7 @@ export class P2PLoader { } private createPeer(connection: PeerConnection) { - const peerLocalId = `${LoggerUtils.getStreamString(this.stream)}-${ - this.peers.size + 1 - }`; - this.logger(`create new peer: ${peerLocalId}`); const peer = new Peer( - peerLocalId, connection, { onPeerConnected: this.onPeerConnected.bind(this), @@ -94,6 +87,7 @@ export class P2PLoader { }, this.settings ); + this.logger(`create new peer: ${peer.id}`); this.peers.set(connection.id, peer); } @@ -153,12 +147,12 @@ export class P2PLoader { } private onPeerConnected(peer: Peer) { - this.logger(`connected with peer: ${peer.localId}`); + this.logger(`connected with peer: ${peer.id}`); peer.sendSegmentsAnnouncement(this.announcement); } private onPeerClosed(peer: Peer) { - this.logger(`peer closed: ${peer.localId}`); + this.logger(`peer closed: ${peer.id}`); this.peers.delete(peer.id); } @@ -204,16 +198,12 @@ export class P2PLoader { } } -function getHash(data: string) { - return new RIPEMD160().update(data).digest("hex"); -} - function createTrackerClient({ streamHash, peerHash, }: { - streamHash: string; - peerHash: string; + streamHash: Buffer; + peerHash: Buffer; }) { return new TrackerClient({ infoHash: streamHash, diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 2e942ded..e4d6238f 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -12,6 +12,7 @@ import { P2PRequest } from "./request"; import { Segment, Settings } from "./types"; import * as Utils from "./utils/utils"; import { PeerRequestError } from "./errors"; +import debug from "debug"; type PeerEventHandlers = { onPeerConnected: (peer: Peer) => void; @@ -39,14 +40,14 @@ export class Peer { private segments = new Map(); private request?: PeerRequest; private isSendingData = false; + private readonly logger = debug("core:peer"); constructor( - readonly localId: string, connection: PeerConnection, private readonly eventHandlers: PeerEventHandlers, private readonly settings: PeerSettings ) { - this.id = connection.id; + this.id = hexToUtf8(connection.id); this.eventHandlers = eventHandlers; this.setConnection(connection); } @@ -56,6 +57,7 @@ export class Peer { if (!this.connection) { this.connection = connection; this.eventHandlers.onPeerConnected(this); + this.logger(`connected with peer: ${this.id}`); } else { connection.destroy(); } @@ -65,12 +67,13 @@ export class Peer { if (this.connection === connection) { this.connection = undefined; this.cancelSegmentRequest("peer-closed"); + this.logger(`connection with peer closed: ${this.id}`); this.eventHandlers.onPeerClosed(this); } }); // eslint-disable-next-line @typescript-eslint/no-empty-function connection.on("error", (error) => { - console.log("PEER ERROR:", error); + this.logger(`peer error: ${this.id} ${error}`); }); } @@ -284,3 +287,13 @@ function joinChunks(chunks: ArrayBuffer[]): ArrayBuffer { return buffer; } + +function hexToUtf8(hexString: string) { + const bytes = new Uint8Array(hexString.length / 2); + + for (let i = 0; i < hexString.length; i += 2) { + bytes[i / 2] = parseInt(hexString.slice(i, i + 2), 16); + } + const decoder = new TextDecoder(); + return decoder.decode(bytes); +} diff --git a/packages/p2p-media-loader-core/src/utils/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer-utils.ts index f570661b..7b30b518 100644 --- a/packages/p2p-media-loader-core/src/utils/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/peer-utils.ts @@ -1,22 +1,23 @@ import { JsonSegmentAnnouncement, PeerCommand } from "../internal-types"; import * as TypeGuard from "../type-guards"; import { PeerSegmentStatus } from "../enums"; -import * as RIPEMD160 from "ripemd160"; export function generatePeerId(): string { + // Base64 characters const PEER_ID_SYMBOLS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const PEER_ID_LENGTH = 20; - let peerId = ""; + let peerId = "PEER:"; + const randomCharsAmount = PEER_ID_LENGTH - peerId.length; - for (let i = 0; i < PEER_ID_LENGTH - peerId.length; i++) { + for (let i = 0; i < randomCharsAmount; i++) { peerId += PEER_ID_SYMBOLS.charAt( Math.floor(Math.random() * PEER_ID_SYMBOLS.length) ); } - return new RIPEMD160().update(peerId).digest("hex"); + return peerId; } export function getPeerCommandFromArrayBuffer( From 321cc6be7aac4bd6c7bec9b21f758dc5807499fb Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 10 Oct 2023 16:52:47 +0300 Subject: [PATCH 087/127] Remove ripemd160 package. --- packages/p2p-media-loader-core/package.json | 6 +----- pnpm-lock.yaml | 19 ++----------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index 5857eeb4..9d907e44 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -28,10 +28,6 @@ "type-check": "npx tsc --noEmit" }, "dependencies": { - "bittorrent-tracker": "^9.19.0", - "ripemd160": "^2.0.2" - }, - "devDependencies": { - "@types/ripemd160": "^2.0.0" + "bittorrent-tracker": "^9.19.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fceced56..c8240382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,13 +90,6 @@ importers: bittorrent-tracker: specifier: ^9.19.0 version: 9.19.0 - ripemd160: - specifier: ^2.0.2 - version: 2.0.2 - devDependencies: - '@types/ripemd160': - specifier: ^2.0.0 - version: 2.0.0 packages/p2p-media-loader-hlsjs: dependencies: @@ -732,10 +725,6 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true - /@types/node@20.6.0: - resolution: {integrity: sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==} - dev: true - /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} dev: true @@ -754,12 +743,6 @@ packages: csstype: 3.1.2 dev: true - /@types/ripemd160@2.0.0: - resolution: {integrity: sha512-LD6AO/+8cAa1ghXax9NG9iPDLPUEGB2WWPjd//04KYfXxTwHvlDEfL0NRjrM5z9XWBi6WbKw75Are0rDyn3PSA==} - dependencies: - '@types/node': 20.6.0 - dev: true - /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: true @@ -1957,6 +1940,7 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 safe-buffer: 5.2.1 + dev: true /hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -2662,6 +2646,7 @@ packages: dependencies: hash-base: 3.1.0 inherits: 2.0.4 + dev: true /rollup@3.25.1: resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} From 9a14429da4ffcdbaf292efb4f0f428dff6c18f21 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 11 Oct 2023 14:49:22 +0300 Subject: [PATCH 088/127] Install node types. Patch bittorrent-tracker. Remove modules ignoring for browsers in bittorrent-tracker package.json. --- package.json | 6 + packages/p2p-media-loader-core/package.json | 2 +- patches/bittorrent-tracker@10.0.12.patch | 138 ++++++++++++++++ pnpm-lock.yaml | 168 +++++++++++++------- 4 files changed, 258 insertions(+), 56 deletions(-) create mode 100644 patches/bittorrent-tracker@10.0.12.patch diff --git a/package.json b/package.json index d03156ac..dda489bb 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@types/debug": "^4.1.8", + "@types/node": "^20.8.4", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", "eslint": "^8.39.0", @@ -26,5 +27,10 @@ }, "dependencies": { "debug": "^4.3.4" + }, + "pnpm": { + "patchedDependencies": { + "bittorrent-tracker@10.0.12": "patches/bittorrent-tracker@10.0.12.patch" + } } } diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index 9d907e44..716f9a4f 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -28,6 +28,6 @@ "type-check": "npx tsc --noEmit" }, "dependencies": { - "bittorrent-tracker": "^9.19.0" + "bittorrent-tracker": "10.0.12" } } diff --git a/patches/bittorrent-tracker@10.0.12.patch b/patches/bittorrent-tracker@10.0.12.patch new file mode 100644 index 00000000..c8507428 --- /dev/null +++ b/patches/bittorrent-tracker@10.0.12.patch @@ -0,0 +1,138 @@ +diff --git a/.idea/.gitignore b/.idea/.gitignore +new file mode 100644 +index 0000000000000000000000000000000000000000..b58b603fea78041071d125a30db58d79b3d49217 +--- /dev/null ++++ b/.idea/.gitignore +@@ -0,0 +1,5 @@ ++# Default ignored files ++/shelf/ ++/workspace.xml ++# Editor-based HTTP Client requests ++/httpRequests/ +diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..54ecbb2b9134d6a566fcd38f6f3c390218f5498b +--- /dev/null ++++ b/.idea/codeStyles/Project.xml +@@ -0,0 +1,44 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..79ee123c2b23e069e35ed634d687e17f731cc702 +--- /dev/null ++++ b/.idea/codeStyles/codeStyleConfig.xml +@@ -0,0 +1,5 @@ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/e50e68725add4f8d6518bfa3a5479dee.iml b/.idea/e50e68725add4f8d6518bfa3a5479dee.iml +new file mode 100644 +index 0000000000000000000000000000000000000000..ebbda94b7d0f898ddad4c9aa59459fc21676874c +--- /dev/null ++++ b/.idea/e50e68725add4f8d6518bfa3a5479dee.iml +@@ -0,0 +1,15 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..0be03f464545c7fdfc45458031b80a2b1b9bff17 +--- /dev/null ++++ b/.idea/inspectionProfiles/Project_Default.xml +@@ -0,0 +1,6 @@ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/modules.xml b/.idea/modules.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..1481ef72f8fc054af042eea2e463400b7aabea70 +--- /dev/null ++++ b/.idea/modules.xml +@@ -0,0 +1,8 @@ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/package.json b/package.json +index 9d537a8a1dd93687f4535bf2882b7b80139ec594..fe85f2f94b6d98f98ac926223ad077368ab80561 100644 +--- a/package.json ++++ b/package.json +@@ -12,9 +12,6 @@ + }, + "browser": { + "./lib/common-node.js": false, +- "./lib/client/http-tracker.js": false, +- "./lib/client/udp-tracker.js": false, +- "./server.js": false, + "socks": false + }, + "chromeapp": { \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8240382..101654d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,10 @@ lockfileVersion: '6.0' +patchedDependencies: + bittorrent-tracker@10.0.12: + hash: 3bacck7ok4ioq2ztv47aeh7t7e + path: patches/bittorrent-tracker@10.0.12.patch + importers: .: @@ -11,6 +16,9 @@ importers: '@types/debug': specifier: ^4.1.8 version: 4.1.8 + '@types/node': + specifier: ^20.8.4 + version: 20.8.4 '@typescript-eslint/eslint-plugin': specifier: ^5.59.2 version: 5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@5.0.2) @@ -34,7 +42,7 @@ importers: version: 5.0.2 vite: specifier: ^4.3.2 - version: 4.3.2 + version: 4.3.2(@types/node@20.8.4) p2p-media-loader-demo: dependencies: @@ -88,8 +96,8 @@ importers: packages/p2p-media-loader-core: dependencies: bittorrent-tracker: - specifier: ^9.19.0 - version: 9.19.0 + specifier: 10.0.12 + version: 10.0.12(patch_hash=3bacck7ok4ioq2ztv47aeh7t7e) packages/p2p-media-loader-hlsjs: dependencies: @@ -703,6 +711,33 @@ packages: picomatch: 2.3.1 dev: true + /@thaunknown/simple-peer@9.12.1: + resolution: {integrity: sha512-IS5BXvXx7cvBAzaxqotJf4s4rJCPk5JABLK6Gbnn7oAmWVcH4hYABabBBrvvJtv/xyUqR4v/H3LalnGRJJfEog==} + dependencies: + debug: 4.3.4 + err-code: 3.0.1 + get-browser-rtc: 1.1.0 + queue-microtask: 1.2.3 + streamx: 2.15.1 + uint8-util: 2.2.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@thaunknown/simple-websocket@9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-vzQloFWRodRZqZhpxMpBljFtISesY8TihA8T5uKwCYdj2I1ImMhE/gAeTCPsCGOtxJfGKu3hw/is6MXauWLjOg==} + dependencies: + debug: 4.3.4 + queue-microtask: 1.2.3 + streamx: 2.15.1 + uint8-util: 2.2.4 + ws: 8.14.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /@types/debug@4.1.8: resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} dependencies: @@ -725,6 +760,12 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true + /@types/node@20.8.4: + resolution: {integrity: sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==} + dependencies: + undici-types: 5.25.3 + dev: true + /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} dev: true @@ -891,7 +932,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.5) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.5) react-refresh: 0.14.0 - vite: 4.3.2 + vite: 4.3.2(@types/node@20.8.4) transitivePeerDependencies: - supports-color dev: true @@ -910,8 +951,9 @@ packages: hasBin: true dev: true - /addr-to-ip-port@1.5.4: - resolution: {integrity: sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg==} + /addr-to-ip-port@2.0.0: + resolution: {integrity: sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==} + engines: {node: '>=12.20.0'} dev: false /ajv@6.12.6: @@ -1007,25 +1049,35 @@ packages: resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} dev: false + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true - /bencode@2.0.3: - resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==} + /bencode@4.0.0: + resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} + engines: {node: '>=12.20.0'} + dependencies: + uint8-util: 2.2.4 dev: false /bittorrent-peerid@1.3.6: resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==} dev: false - /bittorrent-tracker@9.19.0: - resolution: {integrity: sha512-09d0aD2b+MC+zWvWajkUAKkYMynYW4tMbTKiRSthKtJZbafzEoNQSUHyND24SoCe3ZOb2fKfa6fu2INAESL9wA==} - engines: {node: '>=12'} + /bittorrent-tracker@10.0.12(patch_hash=3bacck7ok4ioq2ztv47aeh7t7e): + resolution: {integrity: sha512-EYQEwhOYkrRiiwkCFcM9pbzJInsAe7UVmUgevW133duwlZzjwf5ABwDE7pkkmNRS6iwN0b8LbI/94q16dYqiow==} + engines: {node: '>=12.20.0'} hasBin: true dependencies: - bencode: 2.0.3 + '@thaunknown/simple-peer': 9.12.1 + '@thaunknown/simple-websocket': 9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + bencode: 4.0.0 bittorrent-peerid: 1.3.6 - bn.js: 5.2.1 chrome-dgram: 3.0.6 clone: 2.1.2 compact2string: 1.4.1 @@ -1036,22 +1088,21 @@ packages: once: 1.4.0 queue-microtask: 1.2.3 random-iterate: 1.0.1 - randombytes: 2.1.0 run-parallel: 1.2.0 run-series: 1.1.9 simple-get: 4.0.1 - simple-peer: 9.11.1 - simple-websocket: 9.1.0(bufferutil@4.0.7)(utf-8-validate@5.0.10) socks: 2.7.1 - string2compact: 1.3.2 + string2compact: 2.0.1 + uint8-util: 2.2.4 unordered-array-remove: 1.0.2 - ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@5.0.10) + ws: 8.14.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) optionalDependencies: bufferutil: 4.0.7 utf-8-validate: 5.0.10 transitivePeerDependencies: - supports-color dev: false + patched: true /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} @@ -1059,6 +1110,7 @@ packages: /bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: true /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1172,6 +1224,7 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: true /bufferutil@4.0.7: resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} @@ -1687,6 +1740,10 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1967,6 +2024,7 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} @@ -2557,6 +2615,10 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + /random-iterate@1.0.1: resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==} dev: false @@ -2565,6 +2627,7 @@ packages: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: safe-buffer: 5.2.1 + dev: true /randomfill@1.0.4: resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} @@ -2602,6 +2665,7 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + dev: true /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -2667,6 +2731,7 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2747,34 +2812,6 @@ packages: simple-concat: 1.0.1 dev: false - /simple-peer@9.11.1: - resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==} - dependencies: - buffer: 6.0.3 - debug: 4.3.4 - err-code: 3.0.1 - get-browser-rtc: 1.1.0 - queue-microtask: 1.2.3 - randombytes: 2.1.0 - readable-stream: 3.6.2 - transitivePeerDependencies: - - supports-color - dev: false - - /simple-websocket@9.1.0(bufferutil@4.0.7)(utf-8-validate@5.0.10): - resolution: {integrity: sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==} - dependencies: - debug: 4.3.4 - queue-microtask: 1.2.3 - randombytes: 2.1.0 - readable-stream: 3.6.2 - ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2814,6 +2851,13 @@ packages: xtend: 4.0.2 dev: true + /streamx@2.15.1: + resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2832,10 +2876,11 @@ packages: strip-ansi: 7.1.0 dev: true - /string2compact@1.3.2: - resolution: {integrity: sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw==} + /string2compact@2.0.1: + resolution: {integrity: sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==} + engines: {node: '>=12.20.0'} dependencies: - addr-to-ip-port: 1.5.4 + addr-to-ip-port: 2.0.0 ipaddr.js: 2.1.0 dev: false @@ -2843,6 +2888,7 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 + dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -2941,6 +2987,16 @@ packages: hasBin: true dev: true + /uint8-util@2.2.4: + resolution: {integrity: sha512-uEI5lLozmKQPYEevfEhP9LY3Je5ZmrQhaWXrzTVqrLNQl36xsRh8NiAxYwB9J+2BAt99TRbmCkROQB2ZKhx4UA==} + dependencies: + base64-arraybuffer: 1.0.2 + dev: false + + /undici-types@5.25.3: + resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} + dev: true + /unordered-array-remove@1.0.2: resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==} dev: false @@ -2979,6 +3035,7 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true /util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} @@ -2999,12 +3056,12 @@ packages: buffer-polyfill: /buffer@6.0.3 node-stdlib-browser: 1.2.0 process: 0.11.10 - vite: 4.3.2 + vite: 4.3.2(@types/node@20.8.4) transitivePeerDependencies: - rollup dev: true - /vite@4.3.2: + /vite@4.3.2(@types/node@20.8.4): resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -3029,6 +3086,7 @@ packages: terser: optional: true dependencies: + '@types/node': 20.8.4 esbuild: 0.17.19 postcss: 8.4.24 rollup: 3.25.1 @@ -3085,12 +3143,12 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /ws@7.5.9(bufferutil@4.0.7)(utf-8-validate@5.0.10): - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} + /ws@8.14.2(bufferutil@4.0.7)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: optional: true From 548320dcefd820f300b54e0965a112e2610c871e Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 11 Oct 2023 15:13:54 +0300 Subject: [PATCH 089/127] Sent ArrayBuffer to peer instead of Buffer. Use custom function to change string from utf-8 to hex. --- .../p2p-media-loader-core/src/declarations.d.ts | 8 ++++---- .../p2p-media-loader-core/src/p2p-loader.ts | 17 +++++++++++++---- packages/p2p-media-loader-core/src/peer.ts | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index e78e3c8b..aa9c8e88 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -1,8 +1,8 @@ declare module "bittorrent-tracker" { export default class Client { constructor(options: { - infoHash: string | Buffer; - peerId: string | Buffer; + infoHash: string; + peerId: string; announce: string[]; port: number; rtcConfig?: RTCConfiguration; @@ -52,8 +52,8 @@ declare module "bittorrent-tracker" { event: E, handler: PeerConnectionEventHandler ): void; - send(data: string | Blob | Buffer): void; - write(data: string | Blob | Buffer): void; + send(data: string | ArrayBuffer): void; + write(data: string | ArrayBuffer): void; destroy(): void; }; } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 248aa52d..34f41e87 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -32,8 +32,8 @@ export class P2PLoader { ); this.trackerClient = createTrackerClient({ - streamHash: Buffer.from(this.streamExternalId, "utf-8"), - peerHash: Buffer.from(this.peerId, "utf-8"), + streamHash: utf8ToHex(this.streamExternalId), + peerHash: utf8ToHex(this.peerId), }); this.logger( `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ @@ -202,8 +202,8 @@ function createTrackerClient({ streamHash, peerHash, }: { - streamHash: Buffer; - peerHash: Buffer; + streamHash: string; + peerHash: string; }) { return new TrackerClient({ infoHash: streamHash, @@ -225,3 +225,12 @@ function createTrackerClient({ }, }); } + +function utf8ToHex(utf8String: string) { + let result = ""; + for (let i = 0; i < utf8String.length; i++) { + result += utf8String.charCodeAt(i).toString(16); + } + + return result; +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index e4d6238f..5eca7939 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -265,14 +265,14 @@ export class Peer { function* getBufferChunks( data: ArrayBuffer, maxChunkSize: number -): Generator { +): Generator { let bytesLeft = data.byteLength; while (bytesLeft > 0) { const bytesToSend = bytesLeft >= maxChunkSize ? maxChunkSize : bytesLeft; const from = data.byteLength - bytesLeft; const buffer = data.slice(from, from + bytesToSend); bytesLeft -= bytesToSend; - yield Buffer.from(buffer); + yield buffer; } } From 0bd2822b9aa3ddb30a2e37d8ead8b3d591f281f0 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 11 Oct 2023 22:39:36 +0300 Subject: [PATCH 090/127] Add remain time predicting logic. --- .../src/declarations.d.ts | 2 +- .../src/hybrid-loader.ts | 93 ++++++++++--------- .../p2p-media-loader-core/src/p2p-loader.ts | 17 ++-- packages/p2p-media-loader-core/src/peer.ts | 28 +++--- packages/p2p-media-loader-core/src/request.ts | 5 + 5 files changed, 80 insertions(+), 65 deletions(-) diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index aa9c8e88..7e88edd6 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -42,7 +42,7 @@ declare module "bittorrent-tracker" { : E extends "close" ? () => void : E extends "error" - ? (error?: unknown) => void + ? (error: { code: string }) => void : never; export type PeerConnection = { diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 00078acb..ae7c4f3e 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -4,7 +4,11 @@ import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; -import { RequestContainer, EngineCallbacks } from "./request"; +import { + RequestContainer, + EngineCallbacks, + HybridLoaderRequest, +} from "./request"; import * as QueueUtils from "./utils/queue-utils"; import * as LoggerUtils from "./utils/logger"; import { FetchError } from "./errors"; @@ -60,10 +64,6 @@ export class HybridLoader { engine.color = "orange"; this.logger = { loader, engine }; - // this.randomHttpDownloadInterval = window.setInterval( - // this.loadRandomThroughHttp.bind(this), - // 1500 - // ); this.setIntervalLoading(); } @@ -80,7 +80,7 @@ export class HybridLoader { this.logger.engine(`requests: ${LoggerUtils.getSegmentString(segment)}`); const { stream } = segment; if (stream !== this.lastRequestedSegment.stream) { - this.logger.loader( + this.logger.engine( `STREAM CHANGED ${LoggerUtils.getStreamString(stream)}` ); this.p2pLoaders.changeActiveLoader(stream); @@ -127,23 +127,27 @@ export class HybridLoader { for (const item of queue) { const { statuses, segment } = item; - // const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); + const request = this.requests.get(segment); + const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); + if (statuses.isHighDemand) { - if (this.requests.isHttpRequested(segment)) continue; - // const request = this.requests.get(segment.localId); - // if (request?.loaderRequest?.type === "p2p") { - // const remainingDownloadTime = getPredictedRemainingDownloadTime( - // request.loaderRequest - // ); - // if ( - // remainingDownloadTime === undefined || - // remainingDownloadTime > timeToPlayback - // ) { - // request.loaderRequest.abort(); - // } else { - // continue; - // } - // } + if (request?.type === "http") continue; + console.log("timeToPlayback", timeToPlayback); + console.log(this.bandwidthApproximator.getBandwidth() / 1024 ** 2); + + if (request?.type === "p2p") { + const remainingDownloadTime = + getPredictedRemainingDownloadTime(request); + console.log("remainingDownloadTime", remainingDownloadTime); + if ( + remainingDownloadTime === undefined || + remainingDownloadTime > timeToPlayback + ) { + request.abort(); + } else { + continue; + } + } if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { void this.loadThroughHttp(item); continue; @@ -225,16 +229,10 @@ export class HybridLoader { } private async loadThroughP2P(item: QueueItem) { - const { segment, statuses } = item; const p2pLoader = this.p2pLoaders.activeLoader; try { - const segmentString = LoggerUtils.getSegmentString(segment); - const statusesStr = LoggerUtils.getStatusesString(statuses); - const data = await p2pLoader.downloadSegment(segment); - if (data) { - this.logger.loader(`p2p loaded: ${segmentString} | ${statusesStr}`); - this.onSegmentLoaded(segment, data, "p2p"); - } + const data = await p2pLoader.downloadSegment(item); + if (data) this.onSegmentLoaded(item.segment, data, "p2p"); } catch (error) { console.log(""); console.log(JSON.stringify(error)); @@ -362,22 +360,25 @@ function* arrayBackwards(arr: T[]) { } } -// function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { -// return Math.max(segment.startTime - playback.position, 0) / playback.rate; -// } -// -// function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { -// const { startTimestamp, progress } = request; -// if (!progress || progress.percent === 0) return undefined; -// const now = performance.now(); -// const bandwidth = -// progress.percent / (progress.lastLoadedChunkTimestamp - startTimestamp); -// const remainingDownloadPercent = 100 - progress.percent; -// const predictedRemainingTimeFromLastDownload = -// remainingDownloadPercent / bandwidth; -// const timeFromLastDownload = now - progress.lastLoadedChunkTimestamp; -// return predictedRemainingTimeFromLastDownload - timeFromLastDownload; -// } +function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { + return Math.max(segment.startTime - playback.position, 0) / playback.rate; +} + +function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { + const { startTimestamp, progress } = request; + if (!progress || progress.lastLoadedChunkTimestamp === undefined) { + return undefined; + } + + const now = performance.now(); + const bandwidth = + progress.percent / (progress.lastLoadedChunkTimestamp - startTimestamp); + const remainingDownloadPercent = 100 - progress.percent; + const predictedRemainingTimeFromLastDownload = + remainingDownloadPercent / bandwidth; + const timeFromLastDownload = now - progress.lastLoadedChunkTimestamp; + return (predictedRemainingTimeFromLastDownload - timeFromLastDownload) / 1000; +} function getSegmentAvgDuration(stream: StreamWithSegments) { const { segments } = stream; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 34f41e87..1e20f199 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -2,7 +2,7 @@ import TrackerClient, { PeerConnection } from "bittorrent-tracker"; import { Peer } from "./peer"; import * as PeerUtil from "./utils/peer-utils"; import { Segment, Settings, StreamWithSegments } from "./types"; -import { JsonSegmentAnnouncement } from "./internal-types"; +import { JsonSegmentAnnouncement, QueueItem } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils/utils"; import * as LoggerUtils from "./utils/logger"; @@ -61,7 +61,6 @@ export class P2PLoader { if (peer) peer.setConnection(peerConnection); else this.createPeer(peerConnection); }); - // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("warning", (warning) => { this.logger( `tracker warning (${LoggerUtils.getStreamString( @@ -69,7 +68,6 @@ export class P2PLoader { )}: ${warning})` ); }); - // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("error", (error) => { this.logger( `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` @@ -91,8 +89,9 @@ export class P2PLoader { this.peers.set(connection.id, peer); } - async downloadSegment(segment: Segment): Promise { + async downloadSegment(item: QueueItem): Promise { const peerWithSegment: Peer[] = []; + const { segment, statuses } = item; for (const peer of this.peers.values()) { if ( @@ -108,9 +107,15 @@ export class P2PLoader { const peer = peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; const request = peer.requestSegment(segment); - this.logger(`p2p request ${segment.externalId}`); + this.logger( + `p2p request ${segment.externalId} | ${LoggerUtils.getStatusesString( + statuses + )}` + ); + const data = await request.promise; this.requests.addLoaderRequest(segment, request); - return request.promise; + this.logger(`p2p loaded: ${segment.externalId}`); + return data; } isLoadingOrLoadedBySomeone(segment: Segment): boolean { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 5eca7939..50b63ef7 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -64,16 +64,17 @@ export class Peer { }); connection.on("data", this.onReceiveData.bind(this)); connection.on("close", () => { - if (this.connection === connection) { - this.connection = undefined; - this.cancelSegmentRequest("peer-closed"); - this.logger(`connection with peer closed: ${this.id}`); - this.eventHandlers.onPeerClosed(this); - } + this.connection = undefined; + this.cancelSegmentRequest("peer-closed"); + this.logger(`connection with peer closed: ${this.id}`); + this.eventHandlers.onPeerClosed(this); }); - // eslint-disable-next-line @typescript-eslint/no-empty-function connection.on("error", (error) => { - this.logger(`peer error: ${this.id} ${error}`); + if (error.code === "ERR_DATA_CHANNEL") { + this.logger(`peer error: ${this.id} ${error.code}`); + this.destroy(); + this.eventHandlers.onPeerClosed(this); + } }); } @@ -131,7 +132,7 @@ export class Peer { private sendCommand(command: PeerCommand) { if (!this.connection) return; - this.connection.write(JSON.stringify(command)); + this.connection.send(JSON.stringify(command)); } requestSegment(segment: Segment) { @@ -158,6 +159,7 @@ export class Peer { sendSegmentData(segmentExternalId: string, data: ArrayBuffer) { if (!this.connection) return; + this.logger(`send segment ${segmentExternalId} to peer ${this.id}`); const command: PeerSendSegmentCommand = { c: PeerCommandType.SegmentData, i: segmentExternalId, @@ -171,12 +173,13 @@ export class Peer { this.settings.webRtcMaxMessageSize )) { if (!this.isSendingData) break; - this.connection?.write(chunk); + this.connection?.send(chunk); } this.isSendingData = false; } stopSendSegmentData() { + // TODO: revise sending cancellation this.isSendingData = false; } @@ -199,7 +202,6 @@ export class Peer { chunks: [], p2pRequest: { type: "p2p", - startTimestamp: performance.now(), promise, abort: () => this.cancelSegmentRequest("abort"), @@ -208,7 +210,6 @@ export class Peer { } private receiveSegmentChunk(chunk: ArrayBuffer): void { - // TODO: check can be chunk received before peer command answer const { request } = this; const progress = request?.p2pRequest?.progress; if (!request || !progress) return; @@ -232,6 +233,9 @@ export class Peer { } private cancelSegmentRequest(type: PeerRequestError["type"]) { + this.logger( + `cancel segment ${this.request?.segment.externalId} request (${type})` + ); const error = new PeerRequestError(type); if (!this.request) return; if (!["segment-absent", "peer-closed"].includes(type)) { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index e75eaca5..605083e1 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -69,6 +69,11 @@ export class RequestContainer { return count; } + get(segment: Segment) { + const id = getRequestItemId(segment); + return this.requests.get(id)?.loaderRequest; + } + addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { const segmentId = getRequestItemId(segment); const existingRequest = this.requests.get(segmentId); From 4c01697160adfcd903a6fd4910ae0dfc11606c58 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Sun, 15 Oct 2023 22:37:47 +0300 Subject: [PATCH 091/127] Create universal bandwidth-approximator. --- .../src/bandwidth-approximator.ts | 87 +++++++++---------- .../p2p-media-loader-core/src/http-loader.ts | 87 ++++++++++--------- .../src/hybrid-loader.ts | 19 ++-- .../p2p-media-loader-core/src/p2p-loader.ts | 10 ++- packages/p2p-media-loader-core/src/peer.ts | 16 ++-- packages/p2p-media-loader-core/src/request.ts | 7 +- 6 files changed, 121 insertions(+), 105 deletions(-) diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts index 7e7381d6..6f595ccc 100644 --- a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts +++ b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts @@ -1,58 +1,53 @@ -const SMOOTH_INTERVAL = 15 * 1000; -const MEASURE_INTERVAL = 60 * 1000; - -type NumberWithTime = { - readonly value: number; - readonly timeStamp: number; -}; +import { LoadProgress } from "./request"; export class BandwidthApproximator { - private lastBytes: NumberWithTime[] = []; - private currentBytesSum = 0; - private lastBandwidth: NumberWithTime[] = []; - - addBytes(bytes: number): void { - const timeStamp = performance.now(); - this.lastBytes.push({ value: bytes, timeStamp }); - this.currentBytesSum += bytes; - - while (timeStamp - this.lastBytes[0].timeStamp > SMOOTH_INTERVAL) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.currentBytesSum -= this.lastBytes.shift()!.value; - } + private readonly loadings: LoadProgress[] = []; - const interval = Math.min(SMOOTH_INTERVAL, timeStamp); - this.lastBandwidth.push({ - value: (this.currentBytesSum * 8000) / interval, - timeStamp, - }); + addLoading(progress: LoadProgress) { + this.loadings.push(progress); } - // in bits per seconds - getBandwidth(): number { - const timeStamp = performance.now(); - while ( - this.lastBandwidth.length !== 0 && - timeStamp - this.lastBandwidth[0].timeStamp > MEASURE_INTERVAL - ) { - this.lastBandwidth.shift(); - } + getBandwidth() { + this.clearStale(); + return getBandwidthByProgressList(this.loadings); + } - let maxBandwidth = 0; - for (const bandwidth of this.lastBandwidth) { - if (bandwidth.value > maxBandwidth) { - maxBandwidth = bandwidth.value; - } + private clearStale() { + const now = performance.now(); + for (const { startTimestamp } of this.loadings) { + if (now - startTimestamp <= 15000) break; + this.loadings.shift(); } - - return maxBandwidth; } +} - getSmoothInterval(): number { - return SMOOTH_INTERVAL; - } +function getBandwidthByProgressList(loadings: LoadProgress[]) { + let currentRange: { from: number; to: number } | undefined; + let totalLoadingTime = 0; + let totalBytes = 0; + const now = performance.now(); + + for (let { + // eslint-disable-next-line prefer-const + startTimestamp: from, + lastLoadedChunkTimestamp: to, + // eslint-disable-next-line prefer-const + loadedBytes, + } of loadings) { + totalBytes += loadedBytes; + if (to === undefined) to = now; + + if (!currentRange || from > currentRange.to) { + currentRange = { from, to }; + totalLoadingTime += to - from; + continue; + } - getMeasureInterval(): number { - return MEASURE_INTERVAL; + if (from <= currentRange.to && to > currentRange.to) { + totalLoadingTime += to - currentRange.to; + currentRange.to = to; + } } + + return (totalBytes * 8000) / totalLoadingTime; } diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index f0a5327a..71df348f 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,15 +1,14 @@ import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; import { HttpRequest, LoadProgress } from "./request"; +import * as process from "process"; export function getHttpSegmentRequest(segment: Segment): Readonly { - const { promise, abortController, progress, startTimestamp } = - fetchSegmentData(segment); + const { promise, abortController, progress } = fetchSegmentData(segment); return { type: "http", promise, progress, - startTimestamp, abort: () => abortController.abort(), }; } @@ -25,7 +24,13 @@ function fetchSegmentData(segment: Segment) { } const abortController = new AbortController(); - let progress: LoadProgress | undefined; + const progress: LoadProgress = { + canBeTracked: false, + totalBytes: 0, + loadedBytes: 0, + percent: 0, + startTimestamp: performance.now(), + }; const loadSegmentData = async () => { try { const response = await window.fetch(url, { @@ -34,12 +39,10 @@ function fetchSegmentData(segment: Segment) { }); if (response.ok) { - const result = getDataPromiseAndMonitorProgress(response); - progress = result.progress; + const data = await getDataPromiseAndMonitorProgress(response, progress); // Don't return dataPromise immediately // should await it for catch correct working - const resultData = await result.dataPromise; - return resultData; + return data; } throw new FetchError( response.statusText ?? `Network response was not for ${segmentId}`, @@ -58,48 +61,54 @@ function fetchSegmentData(segment: Segment) { promise: loadSegmentData(), abortController, progress, - startTimestamp: performance.now(), }; } -function getDataPromiseAndMonitorProgress(response: Response): { - progress?: LoadProgress; - dataPromise: Promise; -} { +async function getDataPromiseAndMonitorProgress( + response: Response, + progress: LoadProgress +): Promise { const totalBytesString = response.headers.get("Content-Length"); - if (totalBytesString === null || !response.body) { - return { dataPromise: response.arrayBuffer() }; + if (!response.body) { + return response.arrayBuffer().then((data) => { + progress.loadedBytes = data.byteLength; + progress.totalBytes = data.byteLength; + progress.lastLoadedChunkTimestamp = performance.now(); + progress.percent = 100; + return data; + }); + } + + if (totalBytesString) { + progress.totalBytes = +totalBytesString; + progress.canBeTracked = true; } - const totalBytes = +totalBytesString; - const progress: LoadProgress = { - percent: 0, - loadedBytes: 0, - totalBytes, - }; const reader = response.body.getReader(); - const getDataPromise = async () => { - const chunks: Uint8Array[] = []; - for await (const chunk of readStream(reader)) { - chunks.push(chunk); - progress.loadedBytes += chunk.length; - progress.percent = (progress.loadedBytes / totalBytes) * 100; - progress.lastLoadedChunkTimestamp = performance.now(); + const chunks: Uint8Array[] = []; + for await (const chunk of readStream(reader)) { + chunks.push(chunk); + progress.loadedBytes += chunk.length; + progress.lastLoadedChunkTimestamp = performance.now(); + if (progress.canBeTracked) { + progress.percent = (progress.loadedBytes / progress.totalBytes) * 100; } + } - const resultBuffer = new ArrayBuffer(progress.loadedBytes); - const view = new Uint8Array(resultBuffer); - - let offset = 0; - for (const chunk of chunks) { - view.set(chunk, offset); - offset += chunk.length; - } + if (!progress.canBeTracked) { + progress.totalBytes = progress.loadedBytes; + progress.percent = 100; + } + const resultBuffer = new ArrayBuffer(progress.loadedBytes); + const view = new Uint8Array(resultBuffer); - return resultBuffer; - }; - return { progress, dataPromise: getDataPromise() }; + let offset = 0; + for (const chunk of chunks) { + view.set(chunk, offset); + offset += chunk.length; + } + return resultBuffer; } async function* readStream( diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index ae7c4f3e..c88b639d 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -130,10 +130,9 @@ export class HybridLoader { const request = this.requests.get(segment); const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); + // console.log(this.bandwidthApp.getAverageBandwidth() / 1024 ** 2); if (statuses.isHighDemand) { if (request?.type === "http") continue; - console.log("timeToPlayback", timeToPlayback); - console.log(this.bandwidthApproximator.getBandwidth() / 1024 ** 2); if (request?.type === "p2p") { const remainingDownloadTime = @@ -217,10 +216,11 @@ export class HybridLoader { } this.requests.addLoaderRequest(segment, httpRequest); + this.bandwidthApproximator.addLoading(httpRequest.progress); data = await httpRequest.promise; - + if (!data) return; this.logger.loader(`http responses: ${segment.externalId}`); - if (data) this.onSegmentLoaded(segment, data, "http"); + this.onSegmentLoaded(segment, data, "http"); } catch (err) { if (err instanceof FetchError) { // TODO: handle error @@ -280,8 +280,12 @@ export class HybridLoader { type: "http" | "p2p" ) { const byteLength = data.byteLength; - this.bandwidthApproximator.addBytes(data.byteLength); + console.log( + "approx: ", + this.bandwidthApproximator.getBandwidth() / 1024 ** 2 + ); void this.segmentStorage.storeSegment(segment, data); + this.requests.resolveEngineRequest(segment, { data, bandwidth: this.bandwidthApproximator.getBandwidth(), @@ -365,14 +369,15 @@ function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { } function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { - const { startTimestamp, progress } = request; + const { progress } = request; if (!progress || progress.lastLoadedChunkTimestamp === undefined) { return undefined; } const now = performance.now(); const bandwidth = - progress.percent / (progress.lastLoadedChunkTimestamp - startTimestamp); + progress.percent / + (progress.lastLoadedChunkTimestamp - progress.startTimestamp); const remainingDownloadPercent = 100 - progress.percent; const predictedRemainingTimeFromLastDownload = remainingDownloadPercent / bandwidth; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 1e20f199..0b5945f0 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -107,15 +107,17 @@ export class P2PLoader { const peer = peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; const request = peer.requestSegment(segment); + this.requests.addLoaderRequest(segment, request); this.logger( `p2p request ${segment.externalId} | ${LoggerUtils.getStatusesString( statuses )}` ); - const data = await request.promise; - this.requests.addLoaderRequest(segment, request); - this.logger(`p2p loaded: ${segment.externalId}`); - return data; + request.promise.then(() => { + this.logger(`p2p loaded: ${segment.externalId}`); + }); + + return request.promise; } isLoadingOrLoadedBySomeone(segment: Segment): boolean { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 50b63ef7..76dafd07 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -109,11 +109,9 @@ export class Peer { case PeerCommandType.SegmentData: if (this.request?.segment.externalId === command.i) { - this.request.p2pRequest.progress = { - percent: 0, - loadedBytes: 0, - totalBytes: command.s, - }; + const { progress } = this.request.p2pRequest; + progress.totalBytes = command.s; + progress.canBeTracked = true; } break; @@ -202,7 +200,13 @@ export class Peer { chunks: [], p2pRequest: { type: "p2p", - startTimestamp: performance.now(), + progress: { + canBeTracked: false, + totalBytes: 0, + loadedBytes: 0, + percent: 0, + startTimestamp: performance.now(), + }, promise, abort: () => this.cancelSegmentRequest("abort"), }, diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 605083e1..aa88e20a 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -11,17 +11,18 @@ export type EngineCallbacks = { }; export type LoadProgress = { + startTimestamp: number; + lastLoadedChunkTimestamp?: number; percent: number; loadedBytes: number; totalBytes: number; - lastLoadedChunkTimestamp?: number; + canBeTracked: boolean; }; type RequestBase = { promise: Promise; abort: () => void; - progress?: LoadProgress; - startTimestamp: number; + progress: LoadProgress; }; export type HttpRequest = RequestBase & { From 1315281b2c1a9636c0f468618ce477d0a61c1b50 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 16 Oct 2023 23:24:50 +0300 Subject: [PATCH 092/127] Add bitrate adaptation logic. --- .../src/bandwidth-approximator.ts | 63 ++++++++++++++++++- packages/p2p-media-loader-core/src/core.ts | 2 +- .../p2p-media-loader-core/src/http-loader.ts | 1 - .../src/hybrid-loader.ts | 52 ++++++++++----- packages/p2p-media-loader-hlsjs/src/engine.ts | 9 +++ 5 files changed, 110 insertions(+), 17 deletions(-) diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts index 6f595ccc..72f2274f 100644 --- a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts +++ b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts @@ -7,7 +7,8 @@ export class BandwidthApproximator { this.loadings.push(progress); } - getBandwidth() { + // in bits per second + getBandwidth(): number { this.clearStale(); return getBandwidthByProgressList(this.loadings); } @@ -22,6 +23,7 @@ export class BandwidthApproximator { } function getBandwidthByProgressList(loadings: LoadProgress[]) { + if (!loadings.length) return 0; let currentRange: { from: number; to: number } | undefined; let totalLoadingTime = 0; let totalBytes = 0; @@ -51,3 +53,62 @@ function getBandwidthByProgressList(loadings: LoadProgress[]) { return (totalBytes * 8000) / totalLoadingTime; } + +const SMOOTH_INTERVAL = 15 * 1000; +const MEASURE_INTERVAL = 60 * 1000; + +type NumberWithTime = { + readonly value: number; + readonly timeStamp: number; +}; + +export class BandwidthApproximator1 { + private lastBytes: NumberWithTime[] = []; + private currentBytesSum = 0; + private lastBandwidth: NumberWithTime[] = []; + + addBytes(bytes: number): void { + const timeStamp = performance.now(); + this.lastBytes.push({ value: bytes, timeStamp }); + this.currentBytesSum += bytes; + + while (timeStamp - this.lastBytes[0].timeStamp > SMOOTH_INTERVAL) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.currentBytesSum -= this.lastBytes.shift()!.value; + } + + const interval = Math.min(SMOOTH_INTERVAL, timeStamp); + this.lastBandwidth.push({ + value: (this.currentBytesSum * 8000) / interval, + timeStamp, + }); + } + + // in bits per seconds + getBandwidth(): number { + const timeStamp = performance.now(); + while ( + this.lastBandwidth.length !== 0 && + timeStamp - this.lastBandwidth[0].timeStamp > MEASURE_INTERVAL + ) { + this.lastBandwidth.shift(); + } + + let maxBandwidth = 0; + for (const bandwidth of this.lastBandwidth) { + if (bandwidth.value > maxBandwidth) { + maxBandwidth = bandwidth.value; + } + } + + return maxBandwidth; + } + + getSmoothInterval(): number { + return SMOOTH_INTERVAL; + } + + getMeasureInterval(): number { + return MEASURE_INTERVAL; + } +} diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index ab9a4f5f..987ed033 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -19,7 +19,7 @@ export class Core { private readonly settings: Settings = { simultaneousHttpDownloads: 2, simultaneousP2PDownloads: 3, - highDemandTimeWindow: 10, + highDemandTimeWindow: 15, httpDownloadTimeWindow: 60, p2pDownloadTimeWindow: 60, cachedSegmentExpiration: 120 * 1000, diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 71df348f..cbda2f7a 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,7 +1,6 @@ import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; import { HttpRequest, LoadProgress } from "./request"; -import * as process from "process"; export function getHttpSegmentRequest(segment: Segment): Readonly { const { promise, abortController, progress } = fetchSegmentData(segment); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index c88b639d..ae05c3a1 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -2,8 +2,11 @@ import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; -import { BandwidthApproximator } from "./bandwidth-approximator"; -import { Playback, QueueItem } from "./internal-types"; +import { + BandwidthApproximator, + BandwidthApproximator1, +} from "./bandwidth-approximator"; +import { Playback, QueueItem, QueueItemStatuses } from "./internal-types"; import { RequestContainer, EngineCallbacks, @@ -25,6 +28,8 @@ export class HybridLoader { private readonly segmentAvgDuration: number; private randomHttpDownloadInterval!: number; private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; + private readonly approximator = new BandwidthApproximator1(); + private readonly levelBandwidth = { value: 0, refreshCount: 0 }; constructor( private streamManifestUrl: string, @@ -84,6 +89,7 @@ export class HybridLoader { `STREAM CHANGED ${LoggerUtils.getStreamString(stream)}` ); this.p2pLoaders.changeActiveLoader(stream); + this.refreshLevelBandwidth(true); } this.lastRequestedSegment = segment; this.requests.addEngineCallbacks(segment, callbacks); @@ -94,7 +100,7 @@ export class HybridLoader { if (data) { this.requests.resolveEngineRequest(segment, { data, - bandwidth: this.bandwidthApproximator.getBandwidth(), + bandwidth: this.levelBandwidth.value, }); } } @@ -204,7 +210,7 @@ export class HybridLoader { } private async loadThroughHttp(item: QueueItem, isRandom = false) { - const { segment } = item; + const { segment, statuses } = item; let data: ArrayBuffer | undefined; try { const httpRequest = getHttpSegmentRequest(segment); @@ -220,7 +226,7 @@ export class HybridLoader { data = await httpRequest.promise; if (!data) return; this.logger.loader(`http responses: ${segment.externalId}`); - this.onSegmentLoaded(segment, data, "http"); + this.onSegmentLoaded(item, "http", data); } catch (err) { if (err instanceof FetchError) { // TODO: handle error @@ -232,7 +238,7 @@ export class HybridLoader { const p2pLoader = this.p2pLoaders.activeLoader; try { const data = await p2pLoader.downloadSegment(item); - if (data) this.onSegmentLoaded(item.segment, data, "p2p"); + if (data) this.onSegmentLoaded(item, "p2p", data); } catch (error) { console.log(""); console.log(JSON.stringify(error)); @@ -275,21 +281,30 @@ export class HybridLoader { } private onSegmentLoaded( - segment: Segment, - data: ArrayBuffer, - type: "http" | "p2p" + queueItem: QueueItem, + type: "http" | "p2p", + data: ArrayBuffer ) { + const { segment, statuses } = queueItem; const byteLength = data.byteLength; console.log( - "approx: ", + "mine: ", this.bandwidthApproximator.getBandwidth() / 1024 ** 2 ); + if (type === "http" && statuses.isHighDemand) { + this.refreshLevelBandwidth(true); + } + this.approximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); - this.requests.resolveEngineRequest(segment, { - data, - bandwidth: this.bandwidthApproximator.getBandwidth(), - }); + console.log("is high demand: ", statuses.isHighDemand); + const bandwidth = statuses.isHighDemand + ? this.bandwidthApproximator.getBandwidth() + : this.levelBandwidth.value; + + console.log("BAND: ", bandwidth / 1024 ** 2); + + this.requests.resolveEngineRequest(segment, { data, bandwidth }); this.eventHandlers?.onDataLoaded?.(byteLength, type); this.processQueue(); } @@ -322,6 +337,15 @@ export class HybridLoader { } } + private refreshLevelBandwidth(levelChanged = false) { + if (levelChanged) this.levelBandwidth.refreshCount = 0; + if (this.levelBandwidth.refreshCount < 3) { + const currentBandwidth = this.bandwidthApproximator.getBandwidth(); + this.levelBandwidth.value = currentBandwidth ?? 0; + this.levelBandwidth.refreshCount++; + } + } + updatePlayback(position: number, rate: number) { const isRateChanged = this.playback.rate !== rate; const isPositionChanged = this.playback.position !== position; diff --git a/packages/p2p-media-loader-hlsjs/src/engine.ts b/packages/p2p-media-loader-hlsjs/src/engine.ts index c419b3e8..5091b585 100644 --- a/packages/p2p-media-loader-hlsjs/src/engine.ts +++ b/packages/p2p-media-loader-hlsjs/src/engine.ts @@ -30,6 +30,11 @@ export class Engine { this.core.setManifestResponseUrl(networkDetails.url); } this.segmentManager.processMasterManifest(data); + + console.log( + "LEVELS: ", + data.levels.map((i) => (i.bitrate / 1024 ** 2).toFixed(3)) + ); }); hls.on("hlsLevelUpdated" as Events.LEVEL_UPDATED, (event, data) => { @@ -75,6 +80,10 @@ export class Engine { this.core.updatePlayback(media.currentTime, media.playbackRate); }); }); + + hls.on("hlsLevelSwitching" as Events.LEVEL_SWITCHING, (event, data) => { + console.log("BITRATE: ", (data.bitrate / 1024 ** 2).toFixed(3)); + }); } destroy() { From 8bc001fbec30c90016aa23686f310d61a2c83ca0 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 17 Oct 2023 09:45:06 +0300 Subject: [PATCH 093/127] Rename p2p-loader logger. --- p2p-media-loader-demo/src/App.tsx | 2 +- packages/p2p-media-loader-core/src/p2p-loader.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index 10000046..1acc4ecd 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -429,7 +429,7 @@ const loggers = [ "core:hybrid-loader-main-engine", "core:hybrid-loader-secondary", "core:hybrid-loader-secondary-engine", - "core:p2p-manager", + "core:p2p-loader", "core:peer", "core:p2p-loaders-container", ] as const; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 0b5945f0..c16e6004 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -16,7 +16,7 @@ export class P2PLoader { private readonly trackerClient: TrackerClient; private readonly peers = new Map(); private announcement: JsonSegmentAnnouncement = { i: "" }; - private readonly logger = debug("core:p2p-manager"); + private readonly logger = debug("core:p2p-loader"); constructor( private streamManifestUrl: string, From 11cbfa304d382ec0afbab31dc7f5facb3f0c7d83 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 17 Oct 2023 11:53:59 +0300 Subject: [PATCH 094/127] Use simple number variable instead of object creation. --- .../src/bandwidth-approximator.ts | 79 +++---------------- .../p2p-media-loader-core/src/http-loader.ts | 1 + .../src/hybrid-loader.ts | 32 +------- .../p2p-media-loader-core/src/p2p-loader.ts | 1 - packages/p2p-media-loader-core/src/request.ts | 2 +- packages/p2p-media-loader-hlsjs/src/engine.ts | 12 --- 6 files changed, 14 insertions(+), 113 deletions(-) diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts index 72f2274f..2f60626f 100644 --- a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts +++ b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts @@ -4,6 +4,7 @@ export class BandwidthApproximator { private readonly loadings: LoadProgress[] = []; addLoading(progress: LoadProgress) { + this.clearStale(); this.loadings.push(progress); } @@ -24,91 +25,29 @@ export class BandwidthApproximator { function getBandwidthByProgressList(loadings: LoadProgress[]) { if (!loadings.length) return 0; - let currentRange: { from: number; to: number } | undefined; + let margin: number | undefined; let totalLoadingTime = 0; let totalBytes = 0; const now = performance.now(); - for (let { - // eslint-disable-next-line prefer-const + for (const { startTimestamp: from, - lastLoadedChunkTimestamp: to, - // eslint-disable-next-line prefer-const + lastLoadedChunkTimestamp: to = now, loadedBytes, } of loadings) { totalBytes += loadedBytes; - if (to === undefined) to = now; - if (!currentRange || from > currentRange.to) { - currentRange = { from, to }; + if (margin === undefined || from > margin) { + margin = to; totalLoadingTime += to - from; continue; } - if (from <= currentRange.to && to > currentRange.to) { - totalLoadingTime += to - currentRange.to; - currentRange.to = to; + if (from <= margin && to > margin) { + totalLoadingTime += to - margin; + margin = to; } } return (totalBytes * 8000) / totalLoadingTime; } - -const SMOOTH_INTERVAL = 15 * 1000; -const MEASURE_INTERVAL = 60 * 1000; - -type NumberWithTime = { - readonly value: number; - readonly timeStamp: number; -}; - -export class BandwidthApproximator1 { - private lastBytes: NumberWithTime[] = []; - private currentBytesSum = 0; - private lastBandwidth: NumberWithTime[] = []; - - addBytes(bytes: number): void { - const timeStamp = performance.now(); - this.lastBytes.push({ value: bytes, timeStamp }); - this.currentBytesSum += bytes; - - while (timeStamp - this.lastBytes[0].timeStamp > SMOOTH_INTERVAL) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.currentBytesSum -= this.lastBytes.shift()!.value; - } - - const interval = Math.min(SMOOTH_INTERVAL, timeStamp); - this.lastBandwidth.push({ - value: (this.currentBytesSum * 8000) / interval, - timeStamp, - }); - } - - // in bits per seconds - getBandwidth(): number { - const timeStamp = performance.now(); - while ( - this.lastBandwidth.length !== 0 && - timeStamp - this.lastBandwidth[0].timeStamp > MEASURE_INTERVAL - ) { - this.lastBandwidth.shift(); - } - - let maxBandwidth = 0; - for (const bandwidth of this.lastBandwidth) { - if (bandwidth.value > maxBandwidth) { - maxBandwidth = bandwidth.value; - } - } - - return maxBandwidth; - } - - getSmoothInterval(): number { - return SMOOTH_INTERVAL; - } - - getMeasureInterval(): number { - return MEASURE_INTERVAL; - } -} diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index cbda2f7a..293ef5a7 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -85,6 +85,7 @@ async function getDataPromiseAndMonitorProgress( const reader = response.body.getReader(); + progress.startTimestamp = performance.now(); const chunks: Uint8Array[] = []; for await (const chunk of readStream(reader)) { chunks.push(chunk); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index ae05c3a1..a43e1bc3 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -2,11 +2,8 @@ import { Segment, StreamWithSegments } from "./index"; import { getHttpSegmentRequest } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; -import { - BandwidthApproximator, - BandwidthApproximator1, -} from "./bandwidth-approximator"; -import { Playback, QueueItem, QueueItemStatuses } from "./internal-types"; +import { BandwidthApproximator } from "./bandwidth-approximator"; +import { Playback, QueueItem } from "./internal-types"; import { RequestContainer, EngineCallbacks, @@ -28,7 +25,6 @@ export class HybridLoader { private readonly segmentAvgDuration: number; private randomHttpDownloadInterval!: number; private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; - private readonly approximator = new BandwidthApproximator1(); private readonly levelBandwidth = { value: 0, refreshCount: 0 }; constructor( @@ -136,14 +132,12 @@ export class HybridLoader { const request = this.requests.get(segment); const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); - // console.log(this.bandwidthApp.getAverageBandwidth() / 1024 ** 2); if (statuses.isHighDemand) { if (request?.type === "http") continue; if (request?.type === "p2p") { const remainingDownloadTime = getPredictedRemainingDownloadTime(request); - console.log("remainingDownloadTime", remainingDownloadTime); if ( remainingDownloadTime === undefined || remainingDownloadTime > timeToPlayback @@ -191,16 +185,6 @@ export class HybridLoader { } break; } - - // console.log( - // [...this.requests.values()].map((req) => { - // const { loaderRequest, engineCallbacks, segment } = req; - // - // return `${getSegmentStringId(segment)}-l${loaderRequest ? 1 : 0}-e${ - // engineCallbacks ? 1 : 0 - // }`; - // }) - // ); } // api method for engines @@ -210,7 +194,7 @@ export class HybridLoader { } private async loadThroughHttp(item: QueueItem, isRandom = false) { - const { segment, statuses } = item; + const { segment } = item; let data: ArrayBuffer | undefined; try { const httpRequest = getHttpSegmentRequest(segment); @@ -240,9 +224,7 @@ export class HybridLoader { const data = await p2pLoader.downloadSegment(item); if (data) this.onSegmentLoaded(item, "p2p", data); } catch (error) { - console.log(""); console.log(JSON.stringify(error)); - console.log(""); } } @@ -287,23 +269,15 @@ export class HybridLoader { ) { const { segment, statuses } = queueItem; const byteLength = data.byteLength; - console.log( - "mine: ", - this.bandwidthApproximator.getBandwidth() / 1024 ** 2 - ); if (type === "http" && statuses.isHighDemand) { this.refreshLevelBandwidth(true); } - this.approximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); - console.log("is high demand: ", statuses.isHighDemand); const bandwidth = statuses.isHighDemand ? this.bandwidthApproximator.getBandwidth() : this.levelBandwidth.value; - console.log("BAND: ", bandwidth / 1024 ** 2); - this.requests.resolveEngineRequest(segment, { data, bandwidth }); this.eventHandlers?.onDataLoaded?.(byteLength, type); this.processQueue(); diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index c16e6004..719569ba 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -53,7 +53,6 @@ export class P2PLoader { } private subscribeOnTrackerEvents(trackerClient: TrackerClient) { - // TODO: tracker event handlers // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", (data) => {}); trackerClient.on("peer", (peerConnection) => { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index aa88e20a..45afa58f 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,5 +1,5 @@ import { Segment, SegmentResponse } from "./types"; -import { RequestAbortError, FetchError } from "./errors"; +import { RequestAbortError } from "./errors"; import { Subscriptions } from "./segments-storage"; import Debug from "debug"; diff --git a/packages/p2p-media-loader-hlsjs/src/engine.ts b/packages/p2p-media-loader-hlsjs/src/engine.ts index 5091b585..c918ae21 100644 --- a/packages/p2p-media-loader-hlsjs/src/engine.ts +++ b/packages/p2p-media-loader-hlsjs/src/engine.ts @@ -30,11 +30,6 @@ export class Engine { this.core.setManifestResponseUrl(networkDetails.url); } this.segmentManager.processMasterManifest(data); - - console.log( - "LEVELS: ", - data.levels.map((i) => (i.bitrate / 1024 ** 2).toFixed(3)) - ); }); hls.on("hlsLevelUpdated" as Events.LEVEL_UPDATED, (event, data) => { @@ -66,24 +61,17 @@ export class Engine { hls.on("hlsMediaAttached" as Events.MEDIA_ATTACHED, (event, data) => { const { media } = data; media.addEventListener("timeupdate", () => { - // console.log("playhead time: ", media.currentTime); this.core.updatePlayback(media.currentTime, media.playbackRate); }); media.addEventListener("seeking", () => { - // console.log("playhead time: ", media.currentTime); this.core.updatePlayback(media.currentTime, media.playbackRate); }); media.addEventListener("ratechange", () => { - // console.log("playback rate: ", media.playbackRate); this.core.updatePlayback(media.currentTime, media.playbackRate); }); }); - - hls.on("hlsLevelSwitching" as Events.LEVEL_SWITCHING, (event, data) => { - console.log("BITRATE: ", (data.bitrate / 1024 ** 2).toFixed(3)); - }); } destroy() { From 62e312a4d02d59c99038519913fc980df5049820 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 18 Oct 2023 15:17:24 +0300 Subject: [PATCH 095/127] Modify uploading cancellation logic. --- .../src/declarations.d.ts | 1 + .../src/hybrid-loader.ts | 11 +- .../p2p-media-loader-core/src/p2p-loader.ts | 27 ++++- packages/p2p-media-loader-core/src/peer.ts | 110 ++++++++++++++---- 4 files changed, 116 insertions(+), 33 deletions(-) diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index 7e88edd6..f85dacec 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -48,6 +48,7 @@ declare module "bittorrent-tracker" { export type PeerConnection = { id: string; initiator: boolean; + _channel: RTCDataChannel; on( event: E, handler: PeerConnectionEventHandler diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index a43e1bc3..a3c7877c 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -130,12 +130,15 @@ export class HybridLoader { for (const item of queue) { const { statuses, segment } = item; const request = this.requests.get(segment); - const timeToPlayback = getTimeToSegmentPlayback(segment, this.playback); if (statuses.isHighDemand) { if (request?.type === "http") continue; if (request?.type === "p2p") { + const timeToPlayback = getTimeToSegmentPlayback( + segment, + this.playback + ); const remainingDownloadTime = getPredictedRemainingDownloadTime(request); if ( @@ -172,7 +175,7 @@ export class HybridLoader { break; } if (statuses.isP2PDownloadable) { - if (this.requests.isP2PRequested(segment)) continue; + if (request) continue; if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { void this.loadThroughP2P(item); continue; @@ -213,7 +216,7 @@ export class HybridLoader { this.onSegmentLoaded(item, "http", data); } catch (err) { if (err instanceof FetchError) { - // TODO: handle error + this.processQueue(); } } } @@ -224,7 +227,7 @@ export class HybridLoader { const data = await p2pLoader.downloadSegment(item); if (data) this.onSegmentLoaded(item, "p2p", data); } catch (error) { - console.log(JSON.stringify(error)); + this.processQueue(); } } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 719569ba..f7e7dd01 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -56,6 +56,7 @@ export class P2PLoader { // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", (data) => {}); trackerClient.on("peer", (peerConnection) => { + console.log(peerConnection); const peer = this.peers.get(peerConnection.id); if (peer) peer.setConnection(peerConnection); else this.createPeer(peerConnection); @@ -89,22 +90,32 @@ export class P2PLoader { } async downloadSegment(item: QueueItem): Promise { - const peerWithSegment: Peer[] = []; const { segment, statuses } = item; + const untestedPeers: Peer[] = []; + let fastestPeer: Peer | undefined; + let fastedPeerBandwidth = 0; for (const peer of this.peers.values()) { if ( !peer.downloadingSegment && peer.getSegmentStatus(segment) === PeerSegmentStatus.Loaded ) { - peerWithSegment.push(peer); + const { bandwidth } = peer; + if (bandwidth === undefined) { + untestedPeers.push(peer); + } else if (bandwidth > fastedPeerBandwidth) { + fastedPeerBandwidth = bandwidth; + fastestPeer = peer; + } } } - if (peerWithSegment.length === 0) return undefined; + const peer = untestedPeers.length + ? getRandomItem(untestedPeers) + : fastestPeer; + + if (!peer) return; - const peer = - peerWithSegment[Math.floor(Math.random() * peerWithSegment.length)]; const request = peer.requestSegment(segment); this.requests.addLoaderRequest(segment, request); this.logger( @@ -174,7 +185,7 @@ export class P2PLoader { ); const segmentData = segment && (await this.segmentStorage.getSegmentData(segment)); - if (segmentData) peer.sendSegmentData(segmentExternalId, segmentData); + if (segmentData) void peer.sendSegmentData(segmentExternalId, segmentData); else peer.sendSegmentAbsent(segmentExternalId); } @@ -240,3 +251,7 @@ function utf8ToHex(utf8String: string) { return result; } + +function getRandomItem(items: T[]): T { + return items[Math.floor(Math.random() * items.length)]; +} diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 76dafd07..942610eb 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -37,10 +37,16 @@ type PeerSettings = Pick< export class Peer { readonly id: string; private connection?: PeerConnection; + private connections = new Set(); private segments = new Map(); private request?: PeerRequest; - private isSendingData = false; private readonly logger = debug("core:peer"); + private readonly bandwidthMeasurer = new BandwidthMeasurer(); + private uploadingPromise?: { + promise: Promise; + resolve: () => void; + reject: () => void; + }; constructor( connection: PeerConnection, @@ -53,23 +59,30 @@ export class Peer { } setConnection(connection: PeerConnection) { + if (this.connection && connection !== this.connection) connection.destroy(); + connection.on("connect", () => { - if (!this.connection) { - this.connection = connection; - this.eventHandlers.onPeerConnected(this); - this.logger(`connected with peer: ${this.id}`); - } else { - connection.destroy(); + this.connection = connection; + for (const item of this.connections) { + if (item !== connection) { + this.connections.delete(item); + item.destroy(); + } } + this.eventHandlers.onPeerConnected(this); + this.logger(`connected with peer: ${this.id}`); }); connection.on("data", this.onReceiveData.bind(this)); connection.on("close", () => { + if (connection !== this.connection) return; this.connection = undefined; this.cancelSegmentRequest("peer-closed"); this.logger(`connection with peer closed: ${this.id}`); + this.destroy(); this.eventHandlers.onPeerClosed(this); }); connection.on("error", (error) => { + if (connection !== this.connection) return; if (error.code === "ERR_DATA_CHANNEL") { this.logger(`peer error: ${this.id} ${error.code}`); this.destroy(); @@ -86,6 +99,10 @@ export class Peer { return this.request?.segment; } + get bandwidth(): number | undefined { + return this.bandwidthMeasurer.getBandwidth(); + } + getSegmentStatus(segment: Segment): PeerSegmentStatus | undefined { const { externalId } = segment; return this.segments.get(externalId); @@ -123,7 +140,7 @@ export class Peer { break; case PeerCommandType.CancelSegmentRequest: - this.stopSendSegmentData(); + this.stopUploadingSegmentData(); break; } } @@ -148,6 +165,7 @@ export class Peer { } sendSegmentsAnnouncement(announcement: JsonSegmentAnnouncement) { + if (!announcement.i) return; const command: PeerSegmentAnnouncementCommand = { c: PeerCommandType.SegmentsAnnouncement, a: announcement, @@ -155,9 +173,9 @@ export class Peer { this.sendCommand(command); } - sendSegmentData(segmentExternalId: string, data: ArrayBuffer) { + async sendSegmentData(segmentExternalId: string, data: ArrayBuffer) { if (!this.connection) return; - this.logger(`send segment ${segmentExternalId} to peer ${this.id}`); + this.logger(`send segment ${segmentExternalId} to ${this.id}`); const command: PeerSendSegmentCommand = { c: PeerCommandType.SegmentData, i: segmentExternalId, @@ -165,20 +183,40 @@ export class Peer { }; this.sendCommand(command); - this.isSendingData = true; - for (const chunk of getBufferChunks( - data, - this.settings.webRtcMaxMessageSize - )) { - if (!this.isSendingData) break; - this.connection?.send(chunk); + const chunks = getBufferChunks(data, this.settings.webRtcMaxMessageSize); + + const connection = this.connection; + const channel = connection._channel; + this.uploadingPromise = Utils.getControlledPromise(); + const sendChunk = () => { + while (channel.bufferedAmount <= channel.bufferedAmountLowThreshold) { + if (!this.uploadingPromise) break; + const chunk = chunks.next().value; + if (!chunk) { + this.uploadingPromise.resolve(); + break; + } + connection.send(chunk); + } + }; + this.connection._channel.addEventListener("bufferedamountlow", sendChunk); + sendChunk(); + try { + await this.uploadingPromise?.promise; + this.logger(`segment ${segmentExternalId} has been sent to ${this.id}`); + this.uploadingPromise = undefined; + } catch (err) { + // ignore + } finally { + this.connection._channel.removeEventListener( + "bufferedamountlow", + sendChunk + ); } - this.isSendingData = false; } - stopSendSegmentData() { - // TODO: revise sending cancellation - this.isSendingData = false; + stopUploadingSegmentData() { + this.uploadingPromise?.reject(); } sendSegmentAbsent(segmentExternalId: string) { @@ -225,6 +263,10 @@ export class Peer { if (progress.loadedBytes === progress.totalBytes) { const segmentData = joinChunks(request.chunks); + const { lastLoadedChunkTimestamp, startTimestamp, loadedBytes } = + progress; + const loadingDuration = lastLoadedChunkTimestamp - startTimestamp; + this.bandwidthMeasurer.addMeasurement(loadedBytes, loadingDuration); this.approveRequest(segmentData); } else if (progress.loadedBytes > progress.totalBytes) { this.cancelSegmentRequest("response-bytes-mismatch"); @@ -237,11 +279,11 @@ export class Peer { } private cancelSegmentRequest(type: PeerRequestError["type"]) { + if (!this.request) return; this.logger( - `cancel segment ${this.request?.segment.externalId} request (${type})` + `cancel segment request ${this.request?.segment.externalId} (${type})` ); const error = new PeerRequestError(type); - if (!this.request) return; if (!["segment-absent", "peer-closed"].includes(type)) { this.sendCommand({ c: PeerCommandType.CancelSegmentRequest, @@ -267,6 +309,28 @@ export class Peer { destroy() { this.cancelSegmentRequest("destroy"); this.connection?.destroy(); + this.connection = undefined; + } +} + +const SMOOTHING_COEF = 0.5; + +class BandwidthMeasurer { + private bandwidth?: number; + + addMeasurement(bytes: number, loadingDurationMs: number) { + const bits = bytes * 8; + const currentBandwidth = (bits * 1000) / loadingDurationMs; + + this.bandwidth = + this.bandwidth !== undefined + ? currentBandwidth * SMOOTHING_COEF + + (1 - SMOOTHING_COEF) * this.bandwidth + : currentBandwidth; + } + + getBandwidth() { + return this.bandwidth; } } From ebb843b02e5ae856979eab7130c41350d5ea8982 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 18 Oct 2023 16:00:58 +0300 Subject: [PATCH 096/127] Broadcast announcement not more than 1 time for macrotask. --- packages/p2p-media-loader-core/src/p2p-loader.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index f7e7dd01..5eceb190 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -9,6 +9,7 @@ import * as LoggerUtils from "./utils/logger"; import { PeerSegmentStatus } from "./enums"; import { RequestContainer } from "./request"; import debug from "debug"; +import { windows } from "rimraf"; export class P2PLoader { private readonly streamExternalId: string; @@ -17,6 +18,7 @@ export class P2PLoader { private readonly peers = new Map(); private announcement: JsonSegmentAnnouncement = { i: "" }; private readonly logger = debug("core:p2p-loader"); + private broadcastAnnouncementTaskId?: number; constructor( private streamManifestUrl: string, @@ -174,8 +176,14 @@ export class P2PLoader { } private updateAndBroadcastAnnouncement = () => { - this.updateSegmentAnnouncement(); - this.broadcastSegmentAnnouncement(); + if (this.broadcastAnnouncementTaskId) return; + + // for only execution for macrotask + this.broadcastAnnouncementTaskId = window.setTimeout(() => { + this.updateSegmentAnnouncement(); + this.broadcastSegmentAnnouncement(); + this.broadcastAnnouncementTaskId = undefined; + }, 0); }; private async onSegmentRequested(peer: Peer, segmentExternalId: string) { @@ -190,6 +198,7 @@ export class P2PLoader { } private broadcastSegmentAnnouncement() { + console.log("BROADCAST ANNOUNCEMENT"); for (const peer of this.peers.values()) { if (!peer.isConnected) continue; peer.sendSegmentsAnnouncement(this.announcement); @@ -212,6 +221,7 @@ export class P2PLoader { } this.peers.clear(); this.trackerClient.destroy(); + clearTimeout(this.broadcastAnnouncementTaskId); } } From a033a7f96ca3659e6e59578027a9779e9f3a9e7b Mon Sep 17 00:00:00 2001 From: igor Date: Tue, 24 Oct 2023 16:28:20 +0300 Subject: [PATCH 097/127] Generate user-friendly stream hash. --- packages/p2p-media-loader-core/package.json | 6 ++++- .../p2p-media-loader-core/src/p2p-loader.ts | 7 +++--- .../src/utils/peer-utils.ts | 21 +++++++++++++--- packages/p2p-media-loader-core/tsconfig.json | 3 ++- pnpm-lock.yaml | 24 ++++++++++++++----- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index 716f9a4f..fab4c20e 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -28,6 +28,10 @@ "type-check": "npx tsc --noEmit" }, "dependencies": { - "bittorrent-tracker": "10.0.12" + "bittorrent-tracker": "10.0.12", + "ripemd160": "^2.0.2" + }, + "devDependencies": { + "@types/ripemd160": "^2.0.2" } } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 5eceb190..f8da5a4a 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -9,10 +9,10 @@ import * as LoggerUtils from "./utils/logger"; import { PeerSegmentStatus } from "./enums"; import { RequestContainer } from "./request"; import debug from "debug"; -import { windows } from "rimraf"; export class P2PLoader { private readonly streamExternalId: string; + private readonly streamHash: string; private readonly peerId: string; private readonly trackerClient: TrackerClient; private readonly peers = new Map(); @@ -32,9 +32,10 @@ export class P2PLoader { this.streamManifestUrl, this.stream ); + this.streamHash = PeerUtil.getStreamHash(this.streamManifestUrl); this.trackerClient = createTrackerClient({ - streamHash: utf8ToHex(this.streamExternalId), + streamHash: utf8ToHex(this.streamHash), peerHash: utf8ToHex(this.peerId), }); this.logger( @@ -58,7 +59,6 @@ export class P2PLoader { // eslint-disable-next-line @typescript-eslint/no-empty-function trackerClient.on("update", (data) => {}); trackerClient.on("peer", (peerConnection) => { - console.log(peerConnection); const peer = this.peers.get(peerConnection.id); if (peer) peer.setConnection(peerConnection); else this.createPeer(peerConnection); @@ -198,7 +198,6 @@ export class P2PLoader { } private broadcastSegmentAnnouncement() { - console.log("BROADCAST ANNOUNCEMENT"); for (const peer of this.peers.values()) { if (!peer.isConnected) continue; peer.sendSegmentsAnnouncement(this.announcement); diff --git a/packages/p2p-media-loader-core/src/utils/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer-utils.ts index 7b30b518..ba8088b4 100644 --- a/packages/p2p-media-loader-core/src/utils/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/peer-utils.ts @@ -1,10 +1,25 @@ import { JsonSegmentAnnouncement, PeerCommand } from "../internal-types"; import * as TypeGuard from "../type-guards"; import { PeerSegmentStatus } from "../enums"; +import RIPEMD160 from "ripemd160"; + +export function getStreamHash(string: string): string { + const HASH_SYMBOLS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const symbolsCount = HASH_SYMBOLS.length; + const bytes = new RIPEMD160().update(string).digest(); + let hash = ""; + + for (const byte of bytes) { + hash += HASH_SYMBOLS[byte % symbolsCount]; + } + + return hash; +} export function generatePeerId(): string { // Base64 characters - const PEER_ID_SYMBOLS = + const HASH_SYMBOLS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const PEER_ID_LENGTH = 20; @@ -12,8 +27,8 @@ export function generatePeerId(): string { const randomCharsAmount = PEER_ID_LENGTH - peerId.length; for (let i = 0; i < randomCharsAmount; i++) { - peerId += PEER_ID_SYMBOLS.charAt( - Math.floor(Math.random() * PEER_ID_SYMBOLS.length) + peerId += HASH_SYMBOLS.charAt( + Math.floor(Math.random() * HASH_SYMBOLS.length) ); } diff --git a/packages/p2p-media-loader-core/tsconfig.json b/packages/p2p-media-loader-core/tsconfig.json index c5e79e55..3c05f5de 100644 --- a/packages/p2p-media-loader-core/tsconfig.json +++ b/packages/p2p-media-loader-core/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "tsBuildInfoFile": "./build/.tsbuildinfo" + "tsBuildInfoFile": "./build/.tsbuildinfo", + "allowSyntheticDefaultImports": true }, "include": ["src/**/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 101654d0..0be7a912 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + patchedDependencies: bittorrent-tracker@10.0.12: hash: 3bacck7ok4ioq2ztv47aeh7t7e @@ -98,6 +102,13 @@ importers: bittorrent-tracker: specifier: 10.0.12 version: 10.0.12(patch_hash=3bacck7ok4ioq2ztv47aeh7t7e) + ripemd160: + specifier: ^2.0.2 + version: 2.0.2 + devDependencies: + '@types/ripemd160': + specifier: ^2.0.2 + version: 2.0.2 packages/p2p-media-loader-hlsjs: dependencies: @@ -784,6 +795,12 @@ packages: csstype: 3.1.2 dev: true + /@types/ripemd160@2.0.2: + resolution: {integrity: sha512-hv3Oh/+ldCqp1xBRGi/1G6y2fxV6wUiiwjPt2Q7fe4UgmbD52ChdmxJyjDsGCRb9yuTBS9281UhH4D9gO85k7A==} + dependencies: + '@types/node': 20.8.4 + dev: true + /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: true @@ -1997,7 +2014,6 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 safe-buffer: 5.2.1 - dev: true /hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -2356,6 +2372,7 @@ packages: /node-gyp-build@4.6.1: resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} hasBin: true + requiresBuild: true dev: false /node-releases@2.0.12: @@ -2665,7 +2682,6 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} @@ -2710,7 +2726,6 @@ packages: dependencies: hash-base: 3.1.0 inherits: 2.0.4 - dev: true /rollup@3.25.1: resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} @@ -2731,7 +2746,6 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2888,7 +2902,6 @@ packages: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 - dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -3035,7 +3048,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true /util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} From 1f3b760e2dbc4ae553807f4471644956717e84d9 Mon Sep 17 00:00:00 2001 From: igor Date: Wed, 25 Oct 2023 15:17:49 +0300 Subject: [PATCH 098/127] Configure tsconfig module, target options. --- p2p-media-loader-demo/src/vite-env.d.ts | 1 + p2p-media-loader-demo/tsconfig.json | 9 +- p2p-media-loader-demo/tsconfig.node.json | 2 +- package.json | 1 - packages/p2p-media-loader-core/src/index.ts | 2 + .../p2p-media-loader-core/src/p2p-loader.ts | 2 +- packages/p2p-media-loader-core/tsconfig.json | 1 - packages/tsconfig.base.json | 7 +- pnpm-lock.yaml | 511 ++++++++++++++++-- 9 files changed, 491 insertions(+), 45 deletions(-) create mode 100644 p2p-media-loader-demo/src/vite-env.d.ts diff --git a/p2p-media-loader-demo/src/vite-env.d.ts b/p2p-media-loader-demo/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/p2p-media-loader-demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/p2p-media-loader-demo/tsconfig.json b/p2p-media-loader-demo/tsconfig.json index f0bb2433..f1d172fa 100644 --- a/p2p-media-loader-demo/tsconfig.json +++ b/p2p-media-loader-demo/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "ESNext", - "lib": ["DOM", "DOM.Iterable", "ESNext"], + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, - "moduleResolution": "node", - "allowImportingTsExtensions": false, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, @@ -15,6 +15,7 @@ "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, + "useDefineForClassFields": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/p2p-media-loader-demo/tsconfig.node.json b/p2p-media-loader-demo/tsconfig.node.json index cfa1ab5b..3adda81a 100644 --- a/p2p-media-loader-demo/tsconfig.node.json +++ b/p2p-media-loader-demo/tsconfig.node.json @@ -3,7 +3,7 @@ "composite": true, "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "Bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] diff --git a/package.json b/package.json index dda489bb..e9d0c288 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ }, "devDependencies": { "@types/debug": "^4.1.8", - "@types/node": "^20.8.4", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", "eslint": "^8.39.0", diff --git a/packages/p2p-media-loader-core/src/index.ts b/packages/p2p-media-loader-core/src/index.ts index 780df27e..036287ad 100644 --- a/packages/p2p-media-loader-core/src/index.ts +++ b/packages/p2p-media-loader-core/src/index.ts @@ -1,3 +1,5 @@ +/// + export { Core } from "./core"; export * from "./errors"; export type * from "./types"; diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index f8da5a4a..970183a6 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -57,7 +57,7 @@ export class P2PLoader { private subscribeOnTrackerEvents(trackerClient: TrackerClient) { // eslint-disable-next-line @typescript-eslint/no-empty-function - trackerClient.on("update", (data) => {}); + trackerClient.on("update", () => {}); trackerClient.on("peer", (peerConnection) => { const peer = this.peers.get(peerConnection.id); if (peer) peer.setConnection(peerConnection); diff --git a/packages/p2p-media-loader-core/tsconfig.json b/packages/p2p-media-loader-core/tsconfig.json index 3c05f5de..df1def43 100644 --- a/packages/p2p-media-loader-core/tsconfig.json +++ b/packages/p2p-media-loader-core/tsconfig.json @@ -4,7 +4,6 @@ "outDir": "lib", "rootDir": "src", "tsBuildInfoFile": "./build/.tsbuildinfo", - "allowSyntheticDefaultImports": true }, "include": ["src/**/*"] } diff --git a/packages/tsconfig.base.json b/packages/tsconfig.base.json index 54a1580d..cc86eadc 100644 --- a/packages/tsconfig.base.json +++ b/packages/tsconfig.base.json @@ -5,7 +5,7 @@ "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, - "moduleResolution": "Node", + "moduleResolution": "Bundler", "allowImportingTsExtensions": false, "resolveJsonModule": false, "isolatedModules": true, @@ -20,6 +20,7 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, - "composite": true - } + "composite": true, + "allowSyntheticDefaultImports": true + }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0be7a912..48fd3510 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@types/debug': specifier: ^4.1.8 version: 4.1.8 - '@types/node': - specifier: ^20.8.4 - version: 20.8.4 '@typescript-eslint/eslint-plugin': specifier: ^5.59.2 version: 5.59.2(@typescript-eslint/parser@5.59.2)(eslint@8.39.0)(typescript@5.0.2) @@ -46,7 +43,7 @@ importers: version: 5.0.2 vite: specifier: ^4.3.2 - version: 4.3.2(@types/node@20.8.4) + version: 4.3.2 p2p-media-loader-demo: dependencies: @@ -86,16 +83,16 @@ importers: version: 18.2.4 '@vitejs/plugin-react': specifier: ^4.0.0 - version: 4.0.0(vite@4.3.2) + version: 4.0.0(vite@4.5.0) eslint-plugin-react-hooks: specifier: ^4.6.0 - version: 4.6.0(eslint@8.39.0) + version: 4.6.0(eslint@8.52.0) eslint-plugin-react-refresh: specifier: ^0.4.1 - version: 0.4.1(eslint@8.39.0) + version: 0.4.1(eslint@8.52.0) vite-plugin-node-polyfills: specifier: ^0.14.1 - version: 0.14.1(vite@4.3.2) + version: 0.14.1(vite@4.5.0) packages/p2p-media-loader-core: dependencies: @@ -130,6 +127,11 @@ importers: packages: + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -374,6 +376,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.17.19: resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} engines: {node: '>=12'} @@ -383,6 +394,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.17.19: resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} engines: {node: '>=12'} @@ -392,6 +412,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.17.19: resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} engines: {node: '>=12'} @@ -401,6 +430,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.17.19: resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} engines: {node: '>=12'} @@ -410,6 +448,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.17.19: resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} engines: {node: '>=12'} @@ -419,6 +466,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.17.19: resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} engines: {node: '>=12'} @@ -428,6 +484,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.17.19: resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} engines: {node: '>=12'} @@ -437,6 +502,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.17.19: resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} engines: {node: '>=12'} @@ -446,6 +520,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.17.19: resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} engines: {node: '>=12'} @@ -455,6 +538,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.17.19: resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} engines: {node: '>=12'} @@ -464,6 +556,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.17.19: resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} engines: {node: '>=12'} @@ -473,6 +574,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.17.19: resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} engines: {node: '>=12'} @@ -482,6 +592,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.17.19: resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} engines: {node: '>=12'} @@ -491,6 +610,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.17.19: resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} engines: {node: '>=12'} @@ -500,6 +628,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.17.19: resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} engines: {node: '>=12'} @@ -509,6 +646,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.17.19: resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} engines: {node: '>=12'} @@ -518,6 +664,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.17.19: resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} engines: {node: '>=12'} @@ -527,6 +682,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.17.19: resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} engines: {node: '>=12'} @@ -536,6 +700,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.17.19: resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} engines: {node: '>=12'} @@ -545,6 +718,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.17.19: resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} engines: {node: '>=12'} @@ -554,6 +736,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.17.19: resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} engines: {node: '>=12'} @@ -563,6 +754,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.39.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -573,11 +773,26 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.52.0 + eslint-visitor-keys: 3.4.1 + dev: true + /@eslint-community/regexpp@4.5.1: resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true + /@eslint-community/regexpp@4.9.1: + resolution: {integrity: sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint/eslintrc@2.0.3: resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -595,11 +810,33 @@ packages: - supports-color dev: true + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.23.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /@eslint/js@8.39.0: resolution: {integrity: sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.52.0: + resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -611,6 +848,17 @@ packages: - supports-color dev: true + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -620,6 +868,10 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -735,14 +987,14 @@ packages: - supports-color dev: false - /@thaunknown/simple-websocket@9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10): + /@thaunknown/simple-websocket@9.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): resolution: {integrity: sha512-vzQloFWRodRZqZhpxMpBljFtISesY8TihA8T5uKwCYdj2I1ImMhE/gAeTCPsCGOtxJfGKu3hw/is6MXauWLjOg==} dependencies: debug: 4.3.4 queue-microtask: 1.2.3 streamx: 2.15.1 uint8-util: 2.2.4 - ws: 8.14.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) + ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - supports-color @@ -771,8 +1023,8 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true - /@types/node@20.8.4: - resolution: {integrity: sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==} + /@types/node@20.8.8: + resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} dependencies: undici-types: 5.25.3 dev: true @@ -798,7 +1050,7 @@ packages: /@types/ripemd160@2.0.2: resolution: {integrity: sha512-hv3Oh/+ldCqp1xBRGi/1G6y2fxV6wUiiwjPt2Q7fe4UgmbD52ChdmxJyjDsGCRb9yuTBS9281UhH4D9gO85k7A==} dependencies: - '@types/node': 20.8.4 + '@types/node': 20.8.8 dev: true /@types/scheduler@0.16.3: @@ -939,7 +1191,11 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@vitejs/plugin-react@4.0.0(vite@4.3.2): + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.0.0(vite@4.5.0): resolution: {integrity: sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -949,11 +1205,19 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.5) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.5) react-refresh: 0.14.0 - vite: 4.3.2(@types/node@20.8.4) + vite: 4.5.0 transitivePeerDependencies: - supports-color dev: true + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -962,6 +1226,12 @@ packages: acorn: 8.8.2 dev: true + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} engines: {node: '>=0.4.0'} @@ -1092,7 +1362,7 @@ packages: hasBin: true dependencies: '@thaunknown/simple-peer': 9.12.1 - '@thaunknown/simple-websocket': 9.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + '@thaunknown/simple-websocket': 9.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) bencode: 4.0.0 bittorrent-peerid: 1.3.6 chrome-dgram: 3.0.6 @@ -1112,9 +1382,9 @@ packages: string2compact: 2.0.1 uint8-util: 2.2.4 unordered-array-remove: 1.0.2 - ws: 8.14.2(bufferutil@4.0.7)(utf-8-validate@5.0.10) + ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) optionalDependencies: - bufferutil: 4.0.7 + bufferutil: 4.0.8 utf-8-validate: 5.0.10 transitivePeerDependencies: - supports-color @@ -1243,8 +1513,8 @@ packages: ieee754: 1.2.1 dev: true - /bufferutil@4.0.7: - resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} + /bufferutil@4.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} requiresBuild: true dependencies: @@ -1577,6 +1847,36 @@ packages: '@esbuild/win32-x64': 0.17.19 dev: true + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -1608,21 +1908,21 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.39.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.52.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.39.0 + eslint: 8.52.0 dev: true - /eslint-plugin-react-refresh@0.4.1(eslint@8.39.0): + /eslint-plugin-react-refresh@0.4.1(eslint@8.52.0): resolution: {integrity: sha512-QgrvtRJkmV+m4w953LS146+6RwEe5waouubFVNLBfOjXJf6MLczjymO8fOcKj9jMS8aKkTCMJqiPu2WEeFI99A==} peerDependencies: eslint: '>=7' dependencies: - eslint: 8.39.0 + eslint: 8.52.0 dev: true /eslint-scope@5.1.1: @@ -1641,11 +1941,24 @@ packages: estraverse: 5.3.0 dev: true + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + /eslint-visitor-keys@3.4.1: resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /eslint@8.39.0: resolution: {integrity: sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1695,6 +2008,53 @@ packages: - supports-color dev: true + /eslint@8.52.0: + resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/regexpp': 4.9.1 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.52.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.23.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /espree@9.5.2: resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1704,6 +2064,15 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} @@ -1859,8 +2228,8 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -1945,6 +2314,13 @@ packages: type-fest: 0.20.2 dev: true + /globals@13.23.0: + resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1967,6 +2343,10 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -2456,6 +2836,18 @@ packages: word-wrap: 1.2.3 dev: true + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + /os-browserify@0.3.0: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} dev: true @@ -2567,6 +2959,15 @@ packages: source-map-js: 1.0.2 dev: true + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2732,7 +3133,15 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 + dev: true + + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 dev: true /run-parallel@1.2.0: @@ -3059,7 +3468,7 @@ packages: which-typed-array: 1.1.11 dev: true - /vite-plugin-node-polyfills@0.14.1(vite@4.3.2): + /vite-plugin-node-polyfills@0.14.1(vite@4.5.0): resolution: {integrity: sha512-S5ofYUkXea/d94AHzDwiTA7Pv/yEwzagnjgVEuBZdy7E72GBfK17qpljAlyK3CD+CRcDzAwwl/4bEjKdvZmTGQ==} peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 @@ -3068,12 +3477,12 @@ packages: buffer-polyfill: /buffer@6.0.3 node-stdlib-browser: 1.2.0 process: 0.11.10 - vite: 4.3.2(@types/node@20.8.4) + vite: 4.5.0 transitivePeerDependencies: - rollup dev: true - /vite@4.3.2(@types/node@20.8.4): + /vite@4.3.2: resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -3098,12 +3507,46 @@ packages: terser: optional: true dependencies: - '@types/node': 20.8.4 esbuild: 0.17.19 postcss: 8.4.24 rollup: 3.25.1 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 + dev: true + + /vite@4.5.0: + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.18.20 + postcss: 8.4.31 + rollup: 3.29.4 + optionalDependencies: + fsevents: 2.3.3 dev: true /vm-browserify@1.1.2: @@ -3155,7 +3598,7 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /ws@8.14.2(bufferutil@4.0.7)(utf-8-validate@5.0.10): + /ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} engines: {node: '>=10.0.0'} peerDependencies: @@ -3167,7 +3610,7 @@ packages: utf-8-validate: optional: true dependencies: - bufferutil: 4.0.7 + bufferutil: 4.0.8 utf-8-validate: 5.0.10 dev: false From f3bf5275e00bdac5ca0ba0467d76b692dce7bab9 Mon Sep 17 00:00:00 2001 From: igor Date: Wed, 25 Oct 2023 18:21:48 +0300 Subject: [PATCH 099/127] Change event handler name to onSegmentLoaded. --- p2p-media-loader-demo/src/App.tsx | 2 +- packages/p2p-media-loader-core/package.json | 1 - packages/p2p-media-loader-core/src/errors.ts | 14 -------------- .../p2p-media-loader-core/src/hybrid-loader.ts | 4 ++-- packages/p2p-media-loader-core/src/peer.ts | 15 ++++++++++++++- .../src/{types.ts => types.d.ts} | 2 +- 6 files changed, 18 insertions(+), 20 deletions(-) rename packages/p2p-media-loader-core/src/{types.ts => types.d.ts} (95%) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index 1acc4ecd..1b4736a5 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -73,7 +73,7 @@ function App() { const hlsEngine = useRef( new HlsJsEngine({ - onDataLoaded: (byteLength, type) => { + onSegmentLoaded: (byteLength, type) => { const MBytes = getMBFromBytes(byteLength); if (type === "http") { setHttpLoaded((prev) => round(prev + MBytes)); diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index fab4c20e..b0619783 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -13,7 +13,6 @@ "types": "lib/index.d.ts" }, "sideEffects": false, - "private": false, "type": "module", "scripts": { "dev": "vite", diff --git a/packages/p2p-media-loader-core/src/errors.ts b/packages/p2p-media-loader-core/src/errors.ts index c052ebec..421932e9 100644 --- a/packages/p2p-media-loader-core/src/errors.ts +++ b/packages/p2p-media-loader-core/src/errors.ts @@ -14,17 +14,3 @@ export class RequestAbortError extends Error { super(message); } } - -export class PeerRequestError extends Error { - constructor( - readonly type: - | "abort" - | "request-timeout" - | "response-bytes-mismatch" - | "segment-absent" - | "peer-closed" - | "destroy" - ) { - super(); - } -} diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index a3c7877c..ad46cbe0 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -33,7 +33,7 @@ export class HybridLoader { private readonly settings: Settings, private readonly bandwidthApproximator: BandwidthApproximator, private readonly segmentStorage: SegmentsMemoryStorage, - private readonly eventHandlers?: Pick + private readonly eventHandlers?: Pick ) { this.lastRequestedSegment = requestedSegment; const activeStream = requestedSegment.stream; @@ -282,7 +282,7 @@ export class HybridLoader { : this.levelBandwidth.value; this.requests.resolveEngineRequest(segment, { data, bandwidth }); - this.eventHandlers?.onDataLoaded?.(byteLength, type); + this.eventHandlers?.onSegmentLoaded?.(byteLength, type); this.processQueue(); } diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 942610eb..625bbeae 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -11,9 +11,22 @@ import * as PeerUtil from "./utils/peer-utils"; import { P2PRequest } from "./request"; import { Segment, Settings } from "./types"; import * as Utils from "./utils/utils"; -import { PeerRequestError } from "./errors"; import debug from "debug"; +export class PeerRequestError extends Error { + constructor( + readonly type: + | "abort" + | "request-timeout" + | "response-bytes-mismatch" + | "segment-absent" + | "peer-closed" + | "destroy" + ) { + super(); + } +} + type PeerEventHandlers = { onPeerConnected: (peer: Peer) => void; onPeerClosed: (peer: Peer) => void; diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.d.ts similarity index 95% rename from packages/p2p-media-loader-core/src/types.ts rename to packages/p2p-media-loader-core/src/types.d.ts index a50c7327..bf0536ed 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -63,5 +63,5 @@ export type Settings = { }; export type CoreEventHandlers = { - onDataLoaded?: (byteLength: number, type: "http" | "p2p") => void; + onSegmentLoaded?: (byteLength: number, type: "http" | "p2p") => void; }; From 1297e87629ded6840e5cbefc118f00a91bb5dd0c Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 30 Oct 2023 18:30:10 +0200 Subject: [PATCH 100/127] Add core event handlers to shaka. --- p2p-media-loader-demo/src/App.tsx | 51 +++++++++++-------- packages/p2p-media-loader-shaka/src/engine.ts | 15 +++--- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index 1b4736a5..44dcecd7 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -43,6 +43,8 @@ const streamUrl = { mss: "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest", audioOnly: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/a1/prog_index.m3u8", + dash1: + "http://dash.akamaized.net/dash264/TestCases/1a/qualcomm/1/MultiRate.mpd", }; function App() { @@ -54,8 +56,6 @@ function App() { const hlsInstance = useRef(); const containerRef = useRef(null); const videoRef = useRef(null); - const shakaEngine = useRef(new ShakaEngine(shakaLib)); - const [httpLoaded, setHttpLoaded] = useState(0); const [p2pLoaded, setP2PLoaded] = useState(0); const [httpLoadedGlob, setHttpLoadedGlob] = useLocalStorageItem( @@ -71,20 +71,27 @@ function App() { (v) => (v !== null ? +v : 0) ); - const hlsEngine = useRef( - new HlsJsEngine({ - onSegmentLoaded: (byteLength, type) => { - const MBytes = getMBFromBytes(byteLength); - if (type === "http") { - setHttpLoaded((prev) => round(prev + MBytes)); - setHttpLoadedGlob((prev) => round(prev + MBytes)); - } else if (type === "p2p") { - setP2PLoaded((prev) => round(prev + MBytes)); - setP2PLoadedGlob((prev) => round(prev + MBytes)); - } - }, - }) - ); + const hlsEngine = useRef(); + const shakaEngine = useRef(); + + const onSegmentLoaded = (byteLength: number, type: "http" | "p2p") => { + const MBytes = getMBFromBytes(byteLength); + if (type === "http") { + setHttpLoaded((prev) => round(prev + MBytes)); + setHttpLoadedGlob((prev) => round(prev + MBytes)); + } else if (type === "p2p") { + setP2PLoaded((prev) => round(prev + MBytes)); + setP2PLoadedGlob((prev) => round(prev + MBytes)); + } + }; + + if (!hlsEngine.current) { + hlsEngine.current = new HlsJsEngine({ onSegmentLoaded }); + } + + if (!shakaEngine.current) { + shakaEngine.current = new ShakaEngine(shakaLib, { onSegmentLoaded }); + } useEffect(() => { if ( @@ -110,7 +117,8 @@ function App() { }; const initShakaDplayer = (url: string) => { - const engine = shakaEngine.current; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = shakaEngine.current!; const player = new DPlayer({ container: containerRef.current, video: { @@ -140,7 +148,8 @@ function App() { const initShakaPlayer = (url: string) => { if (!videoRef.current) return; - const engine = shakaEngine.current; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = shakaEngine.current!; const player = new shakaLib.Player(videoRef.current); const onError = (error: { code: unknown }) => { @@ -158,7 +167,8 @@ function App() { const initHlsJsPlayer = (url: string) => { if (!videoRef.current) return; - const engine = hlsEngine.current; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = hlsEngine.current!; const hls = new Hls({ ...engine.getConfig(), }); @@ -170,7 +180,8 @@ function App() { }; const initHlsDplayer = (url: string) => { - const engine = hlsEngine.current; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = hlsEngine.current!; const player = new DPlayer({ container: containerRef.current, video: { diff --git a/packages/p2p-media-loader-shaka/src/engine.ts b/packages/p2p-media-loader-shaka/src/engine.ts index 0970bcb9..7c6783fd 100644 --- a/packages/p2p-media-loader-shaka/src/engine.ts +++ b/packages/p2p-media-loader-shaka/src/engine.ts @@ -8,25 +8,22 @@ import Debug from "debug"; import { StreamInfo, StreamProtocol, Shaka, Stream } from "./types"; import { LoadingHandler } from "./loading-handler"; import { decorateMethod } from "./utils"; -import { Core } from "p2p-media-loader-core"; +import { Core, CoreEventHandlers } from "p2p-media-loader-core"; export class Engine { private readonly shaka: Shaka; - private player!: shaka.Player; private readonly streamInfo: StreamInfo = {}; - private readonly core = new Core(); - private readonly segmentManager = new SegmentManager( - this.streamInfo, - this.core - ); + private readonly core: Core; + private readonly segmentManager: SegmentManager; private debugDestroying = Debug("shaka:destroying"); - constructor(shaka?: unknown) { + constructor(shaka?: unknown, eventHandlers?: CoreEventHandlers) { this.shaka = (shaka as Shaka | undefined) ?? window.shaka; + this.core = new Core(eventHandlers); + this.segmentManager = new SegmentManager(this.streamInfo, this.core); } initShakaPlayer(player: shaka.Player) { - this.player = player; this.initializeNetworkingEngine(); this.registerParsers(); From 2e78864c5f2015307b33a203036e962c20966bb7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 30 Oct 2023 18:32:34 +0200 Subject: [PATCH 101/127] Fix issues with shaka streams playback. --- packages/p2p-media-loader-core/src/core.ts | 4 ++-- .../p2p-media-loader-core/src/hybrid-loader.ts | 13 ++++++++----- packages/p2p-media-loader-core/src/p2p-loader.ts | 5 ++++- .../src/utils/peer-utils.ts | 11 +++++++++-- .../src/utils/queue-utils.ts | 16 +++------------- .../src/manifest-parser-decorator.ts | 2 +- .../src/segment-manager.ts | 2 +- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 987ed033..d5919c49 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -20,8 +20,8 @@ export class Core { simultaneousHttpDownloads: 2, simultaneousP2PDownloads: 3, highDemandTimeWindow: 15, - httpDownloadTimeWindow: 60, - p2pDownloadTimeWindow: 60, + httpDownloadTimeWindow: 45, + p2pDownloadTimeWindow: 45, cachedSegmentExpiration: 120 * 1000, cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index ad46cbe0..a71f5bb4 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -88,18 +88,21 @@ export class HybridLoader { this.refreshLevelBandwidth(true); } this.lastRequestedSegment = segment; - this.requests.addEngineCallbacks(segment, callbacks); - this.processQueue(); if (this.segmentStorage.hasSegment(segment)) { + // TODO: error handling const data = await this.segmentStorage.getSegmentData(segment); if (data) { - this.requests.resolveEngineRequest(segment, { + callbacks.onSuccess({ data, bandwidth: this.levelBandwidth.value, }); } + } else { + this.requests.addEngineCallbacks(segment, callbacks); } + + this.processQueue(); } private processQueue(force = true) { @@ -107,7 +110,7 @@ export class HybridLoader { if ( !force && this.lastQueueProcessingTimeStamp !== undefined && - now - this.lastQueueProcessingTimeStamp <= 950 + now - this.lastQueueProcessingTimeStamp <= 1000 ) { return; } @@ -334,7 +337,7 @@ export class HybridLoader { 0.5; if (isPositionChanged) this.playback.position = position; - if (isRateChanged) this.playback.rate = rate; + if (isRateChanged && rate !== 0) this.playback.rate = rate; if (isPositionSignificantlyChanged) { this.logger.engine("position significantly changed"); } diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index 970183a6..c94a7216 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -32,7 +32,10 @@ export class P2PLoader { this.streamManifestUrl, this.stream ); - this.streamHash = PeerUtil.getStreamHash(this.streamManifestUrl); + this.streamHash = PeerUtil.getStreamHash( + this.streamManifestUrl, + this.stream + ); this.trackerClient = createTrackerClient({ streamHash: utf8ToHex(this.streamHash), diff --git a/packages/p2p-media-loader-core/src/utils/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer-utils.ts index ba8088b4..8f6410ff 100644 --- a/packages/p2p-media-loader-core/src/utils/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/peer-utils.ts @@ -2,12 +2,19 @@ import { JsonSegmentAnnouncement, PeerCommand } from "../internal-types"; import * as TypeGuard from "../type-guards"; import { PeerSegmentStatus } from "../enums"; import RIPEMD160 from "ripemd160"; +import { Stream } from "../types"; -export function getStreamHash(string: string): string { +export function getStreamHash( + masterManifestUrl: string, + stream: Stream +): string { + const { type } = stream; + const versionPrefix = "V2:"; + const urlWithPrefix = versionPrefix + masterManifestUrl + type; const HASH_SYMBOLS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const symbolsCount = HASH_SYMBOLS.length; - const bytes = new RIPEMD160().update(string).digest(); + const bytes = new RIPEMD160().update(urlWithPrefix).digest(); let hash = ""; for (const byte of bytes) { diff --git a/packages/p2p-media-loader-core/src/utils/queue-utils.ts b/packages/p2p-media-loader-core/src/utils/queue-utils.ts index 2d166fc7..66a99e38 100644 --- a/packages/p2p-media-loader-core/src/utils/queue-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/queue-utils.ts @@ -27,27 +27,17 @@ export function generateQueue({ const queue: QueueItem[] = []; const queueSegmentIds = new Set(); - const nextSegment = stream.segments.getNextTo( - lastRequestedSegment.localId - )?.[1]; - const isNextSegmentHighDemand = !!( - nextSegment && - getSegmentLoadStatuses(nextSegment, bufferRanges).isHighDemand - ); - let i = 0; for (const segment of stream.segments.values(requestedSegmentId)) { const statuses = getSegmentLoadStatuses(segment, bufferRanges); const isNotActual = isNotActualStatuses(statuses); - if (isNotActual && !(i === 0 && isNextSegmentHighDemand)) { - break; - } + if (isNotActual && i !== 0) break; + i++; if (skipSegment(segment, statuses)) continue; - queueSegmentIds.add(segment.localId); if (isNotActual) statuses.isHighDemand = true; queue.push({ segment, statuses }); - i++; + queueSegmentIds.add(segment.localId); } return { queue, queueSegmentIds }; diff --git a/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts b/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts index 27f6cd11..84f09e52 100644 --- a/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts +++ b/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts @@ -84,7 +84,7 @@ export class ManifestParserDecorator implements shaka.extern.ManifestParser { processStream(video, "main", videoCount++); } if (audio && !processedStreams.has(audio.id)) { - processStream(audio, !video ? "secondary" : "main", audioCount++); + processStream(audio, !video ? "main" : "secondary", audioCount++); } } } diff --git a/packages/p2p-media-loader-shaka/src/segment-manager.ts b/packages/p2p-media-loader-shaka/src/segment-manager.ts index 5d502a8c..856610eb 100644 --- a/packages/p2p-media-loader-shaka/src/segment-manager.ts +++ b/packages/p2p-media-loader-shaka/src/segment-manager.ts @@ -63,7 +63,7 @@ export class SegmentManager { const staleSegmentsIds = new Set(managerStream.segments.keys()); const newSegments: SegmentBase[] = []; for (const reference of segmentReferences) { - const externalId = reference.getStartTime().toString(); + const externalId = (+reference.getStartTime().toFixed(3)).toString(); const segmentLocalId = Utils.getSegmentLocalIdFromReference(reference); if (!managerStream.segments.has(segmentLocalId)) { From 0932063eec31af8a02c99487d126688a31b73836 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 31 Oct 2023 13:35:51 +0200 Subject: [PATCH 102/127] Revise and refactor code. --- p2p-media-loader-demo/src/App.tsx | 10 +- packages/p2p-media-loader-core/src/core.ts | 1 - .../p2p-media-loader-core/src/http-loader.ts | 14 +-- .../src/hybrid-loader.ts | 12 ++- .../src/internal-types.ts | 6 +- .../p2p-media-loader-core/src/p2p-loader.ts | 61 +++++------- .../src/p2p-loaders-container.ts | 29 +++--- packages/p2p-media-loader-core/src/peer.ts | 93 ++++++++++--------- packages/p2p-media-loader-core/src/request.ts | 18 ---- .../src/segments-storage.ts | 9 +- packages/p2p-media-loader-core/src/types.d.ts | 1 - .../p2p-media-loader-core/src/utils/logger.ts | 5 - .../src/utils/peer-utils.ts | 57 ++++-------- .../p2p-media-loader-core/src/utils/utils.ts | 11 +-- 14 files changed, 128 insertions(+), 199 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index 44dcecd7..eecfaa1d 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -419,13 +419,13 @@ function useLocalStorageItem( else localStorage.removeItem(prop); } }, []); - const eventHandler = useCallback((event: StorageEvent) => { - if (event.key !== prop) return; - const value = event.newValue; - setValue(storageItemToValue(value)); - }, []); useEffect(() => { + const eventHandler = (event: StorageEvent) => { + if (event.key !== prop) return; + const value = event.newValue; + setValue(storageItemToValue(value)); + }; window.addEventListener("storage", eventHandler); return () => { window.removeEventListener("storage", eventHandler); diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index d5919c49..f4710933 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -26,7 +26,6 @@ export class Core { cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, p2pSegmentDownloadTimeout: 5000, - storageCleanupInterval: 5000, p2pLoaderDestroyTimeout: 30 * 1000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 293ef5a7..bdacc079 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -38,10 +38,7 @@ function fetchSegmentData(segment: Segment) { }); if (response.ok) { - const data = await getDataPromiseAndMonitorProgress(response, progress); - // Don't return dataPromise immediately - // should await it for catch correct working - return data; + return await getDataPromiseAndMonitorProgress(response, progress); } throw new FetchError( response.statusText ?? `Network response was not for ${segmentId}`, @@ -49,7 +46,7 @@ function fetchSegmentData(segment: Segment) { response ); } catch (error) { - if (isAbortFetchError(error)) { + if (error instanceof Error && error.name === "AbortError") { throw new RequestAbortError(`Segment fetch was aborted ${segmentId}`); } throw error; @@ -120,10 +117,3 @@ async function* readStream( yield value; } } - -function isAbortFetchError(error: unknown) { - return ( - typeof error === "object" && - (error as { name?: string }).name === "AbortError" - ); -} diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index a71f5bb4..c5534d4b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -84,7 +84,7 @@ export class HybridLoader { this.logger.engine( `STREAM CHANGED ${LoggerUtils.getStreamString(stream)}` ); - this.p2pLoaders.changeActiveLoader(stream); + this.p2pLoaders.changeCurrentLoader(stream); this.refreshLevelBandwidth(true); } this.lastRequestedSegment = segment; @@ -225,10 +225,12 @@ export class HybridLoader { } private async loadThroughP2P(item: QueueItem) { - const p2pLoader = this.p2pLoaders.activeLoader; + const p2pLoader = this.p2pLoaders.currentLoader; try { - const data = await p2pLoader.downloadSegment(item); - if (data) this.onSegmentLoaded(item, "p2p", data); + const downloadPromise = p2pLoader.downloadSegment(item); + if (downloadPromise === undefined) return; + const data = await downloadPromise; + this.onSegmentLoaded(item, "p2p", data); } catch (error) { this.processQueue(); } @@ -236,7 +238,7 @@ export class HybridLoader { private loadRandomThroughHttp() { const { simultaneousHttpDownloads } = this.settings; - const p2pLoader = this.p2pLoaders.activeLoader; + const p2pLoader = this.p2pLoaders.currentLoader; const connectedPeersAmount = p2pLoader.connectedPeersAmount; if ( this.requests.httpRequestsCount >= simultaneousHttpDownloads || diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts index b841cca9..f11f12dd 100644 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ b/packages/p2p-media-loader-core/src/internal-types.ts @@ -29,10 +29,10 @@ export type BasePeerCommand = { c: T; }; -// {i: segmentExternalId[]; s: segment status separator position in ids array} +// {l: loadedSegmentsExternalIds; p: loadingInProcessSegmentExternalIds} export type JsonSegmentAnnouncement = { - i: string; - s?: number; + l: string; + p: string; }; export type PeerSegmentCommand = BasePeerCommand< diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p-loader.ts index c94a7216..f94ef187 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p-loader.ts @@ -2,7 +2,7 @@ import TrackerClient, { PeerConnection } from "bittorrent-tracker"; import { Peer } from "./peer"; import * as PeerUtil from "./utils/peer-utils"; import { Segment, Settings, StreamWithSegments } from "./types"; -import { JsonSegmentAnnouncement, QueueItem } from "./internal-types"; +import { QueueItem } from "./internal-types"; import { SegmentsMemoryStorage } from "./segments-storage"; import * as Utils from "./utils/utils"; import * as LoggerUtils from "./utils/logger"; @@ -11,12 +11,10 @@ import { RequestContainer } from "./request"; import debug from "debug"; export class P2PLoader { - private readonly streamExternalId: string; private readonly streamHash: string; private readonly peerId: string; private readonly trackerClient: TrackerClient; private readonly peers = new Map(); - private announcement: JsonSegmentAnnouncement = { i: "" }; private readonly logger = debug("core:p2p-loader"); private broadcastAnnouncementTaskId?: number; @@ -28,14 +26,11 @@ export class P2PLoader { private readonly settings: Settings ) { this.peerId = PeerUtil.generatePeerId(); - this.streamExternalId = Utils.getStreamExternalId( - this.streamManifestUrl, - this.stream - ); - this.streamHash = PeerUtil.getStreamHash( + const streamExternalId = Utils.getStreamExternalId( this.streamManifestUrl, this.stream ); + this.streamHash = PeerUtil.getStreamHash(streamExternalId); this.trackerClient = createTrackerClient({ streamHash: utf8ToHex(this.streamHash), @@ -49,12 +44,9 @@ export class P2PLoader { this.subscribeOnTrackerEvents(this.trackerClient); this.segmentStorage.subscribeOnUpdate( this.stream, - this.updateAndBroadcastAnnouncement + this.broadcastAnnouncement ); - this.requests.subscribeOnHttpRequestsUpdate( - this.updateAndBroadcastAnnouncement - ); - this.updateSegmentAnnouncement(); + this.requests.subscribeOnHttpRequestsUpdate(this.broadcastAnnouncement); this.trackerClient.start(); } @@ -63,7 +55,7 @@ export class P2PLoader { trackerClient.on("update", () => {}); trackerClient.on("peer", (peerConnection) => { const peer = this.peers.get(peerConnection.id); - if (peer) peer.setConnection(peerConnection); + if (peer) peer.addConnection(peerConnection); else this.createPeer(peerConnection); }); trackerClient.on("warning", (warning) => { @@ -94,11 +86,11 @@ export class P2PLoader { this.peers.set(connection.id, peer); } - async downloadSegment(item: QueueItem): Promise { + downloadSegment(item: QueueItem): Promise | undefined { const { segment, statuses } = item; const untestedPeers: Peer[] = []; let fastestPeer: Peer | undefined; - let fastedPeerBandwidth = 0; + let fastestPeerBandwidth = 0; for (const peer of this.peers.values()) { if ( @@ -108,8 +100,8 @@ export class P2PLoader { const { bandwidth } = peer; if (bandwidth === undefined) { untestedPeers.push(peer); - } else if (bandwidth > fastedPeerBandwidth) { - fastedPeerBandwidth = bandwidth; + } else if (bandwidth > fastestPeerBandwidth) { + fastestPeerBandwidth = bandwidth; fastestPeer = peer; } } @@ -150,7 +142,7 @@ export class P2PLoader { return count; } - private updateSegmentAnnouncement() { + private getSegmentsAnnouncement() { const loaded: string[] = this.segmentStorage.getStoredSegmentExternalIdsOfStream(this.stream); const httpLoading: string[] = []; @@ -161,16 +153,13 @@ export class P2PLoader { httpLoading.push(segment.externalId); } - - this.announcement = PeerUtil.getJsonSegmentsAnnouncement( - loaded, - httpLoading - ); + return PeerUtil.getJsonSegmentsAnnouncement(loaded, httpLoading); } private onPeerConnected(peer: Peer) { this.logger(`connected with peer: ${peer.id}`); - peer.sendSegmentsAnnouncement(this.announcement); + const announcement = this.getSegmentsAnnouncement(); + peer.sendSegmentsAnnouncement(announcement); } private onPeerClosed(peer: Peer) { @@ -178,13 +167,16 @@ export class P2PLoader { this.peers.delete(peer.id); } - private updateAndBroadcastAnnouncement = () => { + private broadcastAnnouncement = () => { if (this.broadcastAnnouncementTaskId) return; // for only execution for macrotask this.broadcastAnnouncementTaskId = window.setTimeout(() => { - this.updateSegmentAnnouncement(); - this.broadcastSegmentAnnouncement(); + const announcement = this.getSegmentsAnnouncement(); + for (const peer of this.peers.values()) { + if (!peer.isConnected) continue; + peer.sendSegmentsAnnouncement(announcement); + } this.broadcastAnnouncementTaskId = undefined; }, 0); }; @@ -200,24 +192,15 @@ export class P2PLoader { else peer.sendSegmentAbsent(segmentExternalId); } - private broadcastSegmentAnnouncement() { - for (const peer of this.peers.values()) { - if (!peer.isConnected) continue; - peer.sendSegmentsAnnouncement(this.announcement); - } - } - destroy() { this.logger( `destroy tracker client: ${LoggerUtils.getStreamString(this.stream)}` ); this.segmentStorage.unsubscribeFromUpdate( this.stream, - this.updateAndBroadcastAnnouncement - ); - this.requests.unsubscribeFromHttpRequestsUpdate( - this.updateAndBroadcastAnnouncement + this.broadcastAnnouncement ); + this.requests.unsubscribeFromHttpRequestsUpdate(this.broadcastAnnouncement); for (const peer of this.peers.values()) { peer.destroy(); } diff --git a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts b/packages/p2p-media-loader-core/src/p2p-loaders-container.ts index ddb68656..5ef53ed4 100644 --- a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts +++ b/packages/p2p-media-loader-core/src/p2p-loaders-container.ts @@ -14,7 +14,7 @@ type P2PLoaderContainerItem = { export class P2PLoadersContainer { private readonly loaders = new Map(); - private _activeLoaderItem!: P2PLoaderContainerItem; + private _currentLoaderItem!: P2PLoaderContainerItem; private readonly logger = debug("core:p2p-loaders-container"); constructor( @@ -24,7 +24,7 @@ export class P2PLoadersContainer { private readonly segmentStorage: SegmentsMemoryStorage, private readonly settings: Settings ) { - this.changeActiveLoader(stream); + this.changeCurrentLoader(stream); } private createLoader(stream: StreamWithSegments): P2PLoaderContainerItem { @@ -47,26 +47,29 @@ export class P2PLoadersContainer { }; } - changeActiveLoader(stream: StreamWithSegments) { + changeCurrentLoader(stream: StreamWithSegments) { const loaderItem = this.loaders.get(stream.localId); - const prevActive = this._activeLoaderItem; + const prev = this._currentLoaderItem; if (loaderItem) { - this._activeLoaderItem = loaderItem; + this._currentLoaderItem = loaderItem; clearTimeout(loaderItem.destroyTimeoutId); + loaderItem.destroyTimeoutId = undefined; } else { const loader = this.createLoader(stream); this.loaders.set(stream.localId, loader); - this._activeLoaderItem = loader; + this._currentLoaderItem = loader; } this.logger( - `change active p2p loader: ${LoggerUtils.getStreamString(stream)}` + `change current p2p loader: ${LoggerUtils.getStreamString(stream)}` ); - if (!prevActive) return; + if (!prev) return; - const ids = this.segmentStorage.getStoredSegmentExternalIdsOfStream(stream); - if (!ids.length) this.destroyAndRemoveLoader(prevActive); - else this.setLoaderDestroyTimeout(prevActive); + const ids = this.segmentStorage.getStoredSegmentExternalIdsOfStream( + prev.stream + ); + if (!ids.length) this.destroyAndRemoveLoader(prev); + else this.setLoaderDestroyTimeout(prev); } private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { @@ -82,8 +85,8 @@ export class P2PLoadersContainer { this.logger(`destroy p2p loader: `, item.loggerInfo); } - get activeLoader() { - return this._activeLoaderItem.loader; + get currentLoader() { + return this._currentLoaderItem.loader; } destroy() { diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/peer.ts index 625bbeae..672de4f5 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/peer.ts @@ -55,11 +55,7 @@ export class Peer { private request?: PeerRequest; private readonly logger = debug("core:peer"); private readonly bandwidthMeasurer = new BandwidthMeasurer(); - private uploadingPromise?: { - promise: Promise; - resolve: () => void; - reject: () => void; - }; + private isUploadingSegment = false; constructor( connection: PeerConnection, @@ -68,13 +64,19 @@ export class Peer { ) { this.id = hexToUtf8(connection.id); this.eventHandlers = eventHandlers; - this.setConnection(connection); + this.addConnection(connection); } - setConnection(connection: PeerConnection) { - if (this.connection && connection !== this.connection) connection.destroy(); + addConnection(connection: PeerConnection) { + if (this.connection && connection !== this.connection) { + connection.destroy(); + return; + } + this.connections.add(connection); connection.on("connect", () => { + if (this.connection) return; + this.connection = connection; for (const item of this.connections) { if (item !== connection) { @@ -84,23 +86,22 @@ export class Peer { } this.eventHandlers.onPeerConnected(this); this.logger(`connected with peer: ${this.id}`); - }); - connection.on("data", this.onReceiveData.bind(this)); - connection.on("close", () => { - if (connection !== this.connection) return; - this.connection = undefined; - this.cancelSegmentRequest("peer-closed"); - this.logger(`connection with peer closed: ${this.id}`); - this.destroy(); - this.eventHandlers.onPeerClosed(this); - }); - connection.on("error", (error) => { - if (connection !== this.connection) return; - if (error.code === "ERR_DATA_CHANNEL") { - this.logger(`peer error: ${this.id} ${error.code}`); + + connection.on("data", this.onReceiveData.bind(this)); + connection.on("close", () => { + this.connection = undefined; + this.cancelSegmentRequest("peer-closed"); + this.logger(`connection with peer closed: ${this.id}`); this.destroy(); this.eventHandlers.onPeerClosed(this); - } + }); + connection.on("error", (error) => { + if (error.code === "ERR_DATA_CHANNEL") { + this.logger(`peer error: ${this.id} ${error.code}`); + this.destroy(); + this.eventHandlers.onPeerClosed(this); + } + }); }); } @@ -153,7 +154,7 @@ export class Peer { break; case PeerCommandType.CancelSegmentRequest: - this.stopUploadingSegmentData(); + this.isUploadingSegment = false; break; } } @@ -178,7 +179,6 @@ export class Peer { } sendSegmentsAnnouncement(announcement: JsonSegmentAnnouncement) { - if (!announcement.i) return; const command: PeerSegmentAnnouncementCommand = { c: PeerCommandType.SegmentsAnnouncement, a: announcement, @@ -197,41 +197,38 @@ export class Peer { this.sendCommand(command); const chunks = getBufferChunks(data, this.settings.webRtcMaxMessageSize); - const connection = this.connection; const channel = connection._channel; - this.uploadingPromise = Utils.getControlledPromise(); + const { promise, resolve, reject } = Utils.getControlledPromise(); + const sendChunk = () => { while (channel.bufferedAmount <= channel.bufferedAmountLowThreshold) { - if (!this.uploadingPromise) break; const chunk = chunks.next().value; if (!chunk) { - this.uploadingPromise.resolve(); + resolve(); + break; + } + if (chunk && !this.isUploadingSegment) { + reject(); break; } connection.send(chunk); } }; - this.connection._channel.addEventListener("bufferedamountlow", sendChunk); - sendChunk(); try { - await this.uploadingPromise?.promise; + channel.addEventListener("bufferedamountlow", sendChunk); + this.isUploadingSegment = true; + sendChunk(); + await promise; this.logger(`segment ${segmentExternalId} has been sent to ${this.id}`); - this.uploadingPromise = undefined; } catch (err) { - // ignore + this.logger(`cancel segment uploading ${segmentExternalId}`); } finally { - this.connection._channel.removeEventListener( - "bufferedamountlow", - sendChunk - ); + channel.removeEventListener("bufferedamountlow", sendChunk); + this.isUploadingSegment = false; } } - stopUploadingSegmentData() { - this.uploadingPromise?.reject(); - } - sendSegmentAbsent(segmentExternalId: string) { const command: PeerSegmentCommand = { c: PeerCommandType.SegmentAbsent, @@ -297,7 +294,13 @@ export class Peer { `cancel segment request ${this.request?.segment.externalId} (${type})` ); const error = new PeerRequestError(type); - if (!["segment-absent", "peer-closed"].includes(type)) { + const sendCancelCommandTypes: PeerRequestError["type"][] = [ + "destroy", + "abort", + "request-timeout", + "response-bytes-mismatch", + ]; + if (sendCancelCommandTypes.includes(type)) { this.sendCommand({ c: PeerCommandType.CancelSegmentRequest, i: this.request.segment.externalId, @@ -323,6 +326,10 @@ export class Peer { this.cancelSegmentRequest("destroy"); this.connection?.destroy(); this.connection = undefined; + for (const connection of this.connections) { + connection.destroy(); + } + this.connections.clear(); } } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 45afa58f..1dfaa25c 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -3,8 +3,6 @@ import { RequestAbortError } from "./errors"; import { Subscriptions } from "./segments-storage"; import Debug from "debug"; -type SetRequired = Omit & Required>; - export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; onError: (reason?: unknown) => void; @@ -41,8 +39,6 @@ type Request = { engineCallbacks?: Readonly; }; -type RequestWithEngineCallbacks = SetRequired; - function getRequestItemId(segment: Segment) { return segment.localId; } @@ -52,10 +48,6 @@ export class RequestContainer { private readonly onHttpRequestsHandlers = new Subscriptions(); private readonly debug = Debug("core:request-container"); - get totalCount() { - return this.requests.size; - } - get httpRequestsCount() { let count = 0; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -141,21 +133,11 @@ export class RequestContainer { } } - *engineRequests(): Generator { - for (const request of this.requests.values()) { - if (request.engineCallbacks) yield request as RequestWithEngineCallbacks; - } - } - resolveEngineRequest(segment: Segment, response: SegmentResponse) { const id = getRequestItemId(segment); this.requests.get(id)?.engineCallbacks?.onSuccess(response); } - isRequestedByEngine(segmentId: string): boolean { - return !!this.requests.get(segmentId)?.engineCallbacks; - } - isHttpRequested(segment: Segment): boolean { const id = getRequestItemId(segment); return this.requests.get(id)?.loaderRequest?.type === "http"; diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index 64f9d2f4..0b6d9dd6 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -2,7 +2,7 @@ import { Segment, Settings, Stream } from "./types"; type StorageSettings = Pick< Settings, - "cachedSegmentExpiration" | "cachedSegmentsCount" | "storageCleanupInterval" + "cachedSegmentExpiration" | "cachedSegmentsCount" >; function getStreamShortExternalId(stream: Readonly) { @@ -56,7 +56,6 @@ type StorageItem = { export class SegmentsMemoryStorage { private cache = new Map(); private _isInitialized = false; - private cleanupIntervalId?: number; private readonly isSegmentLockedPredicates: (( segment: Segment ) => boolean)[] = []; @@ -69,10 +68,6 @@ export class SegmentsMemoryStorage { async initialize() { this._isInitialized = true; - this.cleanupIntervalId = window.setInterval( - () => this.clear(), - this.settings.storageCleanupInterval - ); } get isInitialized(): boolean { @@ -96,6 +91,7 @@ export class SegmentsMemoryStorage { lastAccessed: performance.now(), }); this.fireOnUpdateSubscriptions(streamId); + void this.clear(); } async getSegmentData(segment: Segment): Promise { @@ -198,6 +194,5 @@ export class SegmentsMemoryStorage { this.cache.clear(); this.onUpdateHandlers.clear(); this._isInitialized = false; - clearInterval(this.cleanupIntervalId); } } diff --git a/packages/p2p-media-loader-core/src/types.d.ts b/packages/p2p-media-loader-core/src/types.d.ts index bf0536ed..3c373c5a 100644 --- a/packages/p2p-media-loader-core/src/types.d.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -58,7 +58,6 @@ export type Settings = { cachedSegmentsCount: number; webRtcMaxMessageSize: number; p2pSegmentDownloadTimeout: number; - storageCleanupInterval: number; p2pLoaderDestroyTimeout: number; }; diff --git a/packages/p2p-media-loader-core/src/utils/logger.ts b/packages/p2p-media-loader-core/src/utils/logger.ts index 9f802f2b..c479f828 100644 --- a/packages/p2p-media-loader-core/src/utils/logger.ts +++ b/packages/p2p-media-loader-core/src/utils/logger.ts @@ -10,11 +10,6 @@ export function getSegmentString(segment: Segment) { return `(${getStreamString(segment.stream)} | ${externalId})`; } -export function getSegmentFullString(segment: Segment) { - const { externalId } = segment; - return `(${getStreamString(segment.stream)} | ${externalId})`; -} - export function getStatusesString(statuses: QueueItemStatuses): string { const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; if (isHighDemand) return "high-demand"; diff --git a/packages/p2p-media-loader-core/src/utils/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer-utils.ts index 8f6410ff..f1ab3136 100644 --- a/packages/p2p-media-loader-core/src/utils/peer-utils.ts +++ b/packages/p2p-media-loader-core/src/utils/peer-utils.ts @@ -2,19 +2,14 @@ import { JsonSegmentAnnouncement, PeerCommand } from "../internal-types"; import * as TypeGuard from "../type-guards"; import { PeerSegmentStatus } from "../enums"; import RIPEMD160 from "ripemd160"; -import { Stream } from "../types"; -export function getStreamHash( - masterManifestUrl: string, - stream: Stream -): string { - const { type } = stream; - const versionPrefix = "V2:"; - const urlWithPrefix = versionPrefix + masterManifestUrl + type; - const HASH_SYMBOLS = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +const HASH_SYMBOLS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +const PEER_ID_LENGTH = 20; + +export function getStreamHash(streamId: string): string { const symbolsCount = HASH_SYMBOLS.length; - const bytes = new RIPEMD160().update(urlWithPrefix).digest(); + const bytes = new RIPEMD160().update(streamId).digest(); let hash = ""; for (const byte of bytes) { @@ -25,11 +20,6 @@ export function getStreamHash( } export function generatePeerId(): string { - // Base64 characters - const HASH_SYMBOLS = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const PEER_ID_LENGTH = 20; - let peerId = "PEER:"; const randomCharsAmount = PEER_ID_LENGTH - peerId.length; @@ -67,33 +57,22 @@ export function getSegmentsFromPeerAnnouncement( announcement: JsonSegmentAnnouncement ): Map { const segmentStatusMap = new Map(); - const separator = announcement.s; - const ids = announcement.i.split("|"); - if (!separator) { - return new Map(ids.map((id) => [id, PeerSegmentStatus.Loaded])); - } - for (const [index, segmentExternalId] of ids.entries()) { - if (index < separator) { - segmentStatusMap.set(segmentExternalId, PeerSegmentStatus.Loaded); - } else { - segmentStatusMap.set(segmentExternalId, PeerSegmentStatus.LoadingByHttp); - } - } + announcement.l + .split("|") + .forEach((id) => segmentStatusMap.set(id, PeerSegmentStatus.Loaded)); + + announcement.p + .split("|") + .forEach((id) => segmentStatusMap.set(id, PeerSegmentStatus.LoadingByHttp)); return segmentStatusMap; } export function getJsonSegmentsAnnouncement( - storedSegmentExternalIds: string[], + loadedSegmentExternalIds: string[], loadingByHttpSegmentExternalIds: string[] ): JsonSegmentAnnouncement { - let segmentsListing = storedSegmentExternalIds.join("|"); - if (loadingByHttpSegmentExternalIds.length) { - if (segmentsListing) segmentsListing += "|"; - segmentsListing += loadingByHttpSegmentExternalIds.join("|"); - } - const announcement: JsonSegmentAnnouncement = { i: segmentsListing }; - if (loadingByHttpSegmentExternalIds.length) { - announcement.s = storedSegmentExternalIds.length; - } - return announcement; + return { + l: loadedSegmentExternalIds.join("|"), + p: loadingByHttpSegmentExternalIds.join("|"), + }; } diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index 9570efc3..d9fb72fd 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -1,18 +1,13 @@ import { Segment, Stream, StreamWithSegments } from "../index"; +const PEER_PROTOCOL_VERSION = "V1"; + export function getStreamExternalId( manifestResponseUrl: string, stream: Readonly ): string { const { type, index } = stream; - return `${manifestResponseUrl}-${type}-${index}`; -} - -export function getSegmentFullExternalId( - externalStreamId: string, - externalSegmentId: string -) { - return `${externalStreamId}|${externalSegmentId}`; + return `${PEER_PROTOCOL_VERSION}:${manifestResponseUrl}-${type}-${index}`; } export function getSegmentFromStreamsMap( From 4162ab8b5a6b28849d2eaffd361864da537dd562 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 31 Oct 2023 13:49:31 +0200 Subject: [PATCH 103/127] Move p2p files to separate p2p directory. --- .../p2p-media-loader-core/src/hybrid-loader.ts | 4 ++-- .../src/{ => p2p}/p2p-loader.ts | 16 ++++++++-------- .../src/{ => p2p}/p2p-loaders-container.ts | 8 ++++---- .../p2p-media-loader-core/src/{ => p2p}/peer.ts | 12 ++++++------ .../src/utils/{peer-utils.ts => peer.ts} | 0 .../src/utils/{queue-utils.ts => queue.ts} | 0 6 files changed, 20 insertions(+), 20 deletions(-) rename packages/p2p-media-loader-core/src/{ => p2p}/p2p-loader.ts (94%) rename packages/p2p-media-loader-core/src/{ => p2p}/p2p-loaders-container.ts (92%) rename packages/p2p-media-loader-core/src/{ => p2p}/peer.ts (97%) rename packages/p2p-media-loader-core/src/utils/{peer-utils.ts => peer.ts} (100%) rename packages/p2p-media-loader-core/src/utils/{queue-utils.ts => queue.ts} (100%) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index c5534d4b..7655c4af 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -9,10 +9,10 @@ import { EngineCallbacks, HybridLoaderRequest, } from "./request"; -import * as QueueUtils from "./utils/queue-utils"; +import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import { FetchError } from "./errors"; -import { P2PLoadersContainer } from "./p2p-loaders-container"; +import { P2PLoadersContainer } from "./p2p/p2p-loaders-container"; import debug from "debug"; export class HybridLoader { diff --git a/packages/p2p-media-loader-core/src/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p/p2p-loader.ts similarity index 94% rename from packages/p2p-media-loader-core/src/p2p-loader.ts rename to packages/p2p-media-loader-core/src/p2p/p2p-loader.ts index f94ef187..e9c3a361 100644 --- a/packages/p2p-media-loader-core/src/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/p2p-loader.ts @@ -1,13 +1,13 @@ import TrackerClient, { PeerConnection } from "bittorrent-tracker"; import { Peer } from "./peer"; -import * as PeerUtil from "./utils/peer-utils"; -import { Segment, Settings, StreamWithSegments } from "./types"; -import { QueueItem } from "./internal-types"; -import { SegmentsMemoryStorage } from "./segments-storage"; -import * as Utils from "./utils/utils"; -import * as LoggerUtils from "./utils/logger"; -import { PeerSegmentStatus } from "./enums"; -import { RequestContainer } from "./request"; +import * as PeerUtil from "../utils/peer"; +import { Segment, Settings, StreamWithSegments } from "../types"; +import { QueueItem } from "../internal-types"; +import { SegmentsMemoryStorage } from "../segments-storage"; +import * as Utils from "../utils/utils"; +import * as LoggerUtils from "../utils/logger"; +import { PeerSegmentStatus } from "../enums"; +import { RequestContainer } from "../request"; import debug from "debug"; export class P2PLoader { diff --git a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts b/packages/p2p-media-loader-core/src/p2p/p2p-loaders-container.ts similarity index 92% rename from packages/p2p-media-loader-core/src/p2p-loaders-container.ts rename to packages/p2p-media-loader-core/src/p2p/p2p-loaders-container.ts index 5ef53ed4..55398f74 100644 --- a/packages/p2p-media-loader-core/src/p2p-loaders-container.ts +++ b/packages/p2p-media-loader-core/src/p2p/p2p-loaders-container.ts @@ -1,9 +1,9 @@ import { P2PLoader } from "./p2p-loader"; import debug from "debug"; -import { Settings, Stream, StreamWithSegments } from "./index"; -import { RequestContainer } from "./request"; -import { SegmentsMemoryStorage } from "./segments-storage"; -import * as LoggerUtils from "./utils/logger"; +import { Settings, Stream, StreamWithSegments } from "../index"; +import { RequestContainer } from "../request"; +import { SegmentsMemoryStorage } from "../segments-storage"; +import * as LoggerUtils from "../utils/logger"; type P2PLoaderContainerItem = { stream: Stream; diff --git a/packages/p2p-media-loader-core/src/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts similarity index 97% rename from packages/p2p-media-loader-core/src/peer.ts rename to packages/p2p-media-loader-core/src/p2p/peer.ts index 672de4f5..fc5cb42a 100644 --- a/packages/p2p-media-loader-core/src/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -5,12 +5,12 @@ import { PeerSegmentAnnouncementCommand, PeerSegmentCommand, PeerSendSegmentCommand, -} from "./internal-types"; -import { PeerCommandType, PeerSegmentStatus } from "./enums"; -import * as PeerUtil from "./utils/peer-utils"; -import { P2PRequest } from "./request"; -import { Segment, Settings } from "./types"; -import * as Utils from "./utils/utils"; +} from "../internal-types"; +import { PeerCommandType, PeerSegmentStatus } from "../enums"; +import * as PeerUtil from "../utils/peer"; +import { P2PRequest } from "../request"; +import { Segment, Settings } from "../types"; +import * as Utils from "../utils/utils"; import debug from "debug"; export class PeerRequestError extends Error { diff --git a/packages/p2p-media-loader-core/src/utils/peer-utils.ts b/packages/p2p-media-loader-core/src/utils/peer.ts similarity index 100% rename from packages/p2p-media-loader-core/src/utils/peer-utils.ts rename to packages/p2p-media-loader-core/src/utils/peer.ts diff --git a/packages/p2p-media-loader-core/src/utils/queue-utils.ts b/packages/p2p-media-loader-core/src/utils/queue.ts similarity index 100% rename from packages/p2p-media-loader-core/src/utils/queue-utils.ts rename to packages/p2p-media-loader-core/src/utils/queue.ts From 19ec6af47310dfdd3dacc4cc8892b51d44126ab9 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 31 Oct 2023 13:51:59 +0200 Subject: [PATCH 104/127] Rename request container file and p2p loader and container files. --- packages/p2p-media-loader-core/src/bandwidth-approximator.ts | 2 +- packages/p2p-media-loader-core/src/core.ts | 2 +- packages/p2p-media-loader-core/src/http-loader.ts | 2 +- packages/p2p-media-loader-core/src/hybrid-loader.ts | 4 ++-- .../src/{internal-types.ts => internal-types.d.ts} | 0 .../src/p2p/{p2p-loader.ts => loader.ts} | 2 +- .../p2p/{p2p-loaders-container.ts => loaders-container.ts} | 4 ++-- packages/p2p-media-loader-core/src/p2p/peer.ts | 2 +- .../src/{request.ts => request-container.ts} | 0 packages/p2p-media-loader-core/src/types.d.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename packages/p2p-media-loader-core/src/{internal-types.ts => internal-types.d.ts} (100%) rename packages/p2p-media-loader-core/src/p2p/{p2p-loader.ts => loader.ts} (99%) rename packages/p2p-media-loader-core/src/p2p/{p2p-loaders-container.ts => loaders-container.ts} (96%) rename packages/p2p-media-loader-core/src/{request.ts => request-container.ts} (100%) diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts index 2f60626f..79216e53 100644 --- a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts +++ b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts @@ -1,4 +1,4 @@ -import { LoadProgress } from "./request"; +import { LoadProgress } from "./request-container"; export class BandwidthApproximator { private readonly loadings: LoadProgress[] = []; diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index f4710933..69700d8b 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -10,7 +10,7 @@ import { import * as Utils from "./utils/utils"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; -import { EngineCallbacks } from "./request"; +import { EngineCallbacks } from "./request-container"; import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index bdacc079..efd00a9c 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,6 +1,6 @@ import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; -import { HttpRequest, LoadProgress } from "./request"; +import { HttpRequest, LoadProgress } from "./request-container"; export function getHttpSegmentRequest(segment: Segment): Readonly { const { promise, abortController, progress } = fetchSegmentData(segment); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 7655c4af..2d14dcc9 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -8,11 +8,11 @@ import { RequestContainer, EngineCallbacks, HybridLoaderRequest, -} from "./request"; +} from "./request-container"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import { FetchError } from "./errors"; -import { P2PLoadersContainer } from "./p2p/p2p-loaders-container"; +import { P2PLoadersContainer } from "./p2p/loaders-container"; import debug from "debug"; export class HybridLoader { diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.d.ts similarity index 100% rename from packages/p2p-media-loader-core/src/internal-types.ts rename to packages/p2p-media-loader-core/src/internal-types.d.ts diff --git a/packages/p2p-media-loader-core/src/p2p/p2p-loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts similarity index 99% rename from packages/p2p-media-loader-core/src/p2p/p2p-loader.ts rename to packages/p2p-media-loader-core/src/p2p/loader.ts index e9c3a361..3f7426b4 100644 --- a/packages/p2p-media-loader-core/src/p2p/p2p-loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -7,7 +7,7 @@ import { SegmentsMemoryStorage } from "../segments-storage"; import * as Utils from "../utils/utils"; import * as LoggerUtils from "../utils/logger"; import { PeerSegmentStatus } from "../enums"; -import { RequestContainer } from "../request"; +import { RequestContainer } from "../request-container"; import debug from "debug"; export class P2PLoader { diff --git a/packages/p2p-media-loader-core/src/p2p/p2p-loaders-container.ts b/packages/p2p-media-loader-core/src/p2p/loaders-container.ts similarity index 96% rename from packages/p2p-media-loader-core/src/p2p/p2p-loaders-container.ts rename to packages/p2p-media-loader-core/src/p2p/loaders-container.ts index 55398f74..73d8aeba 100644 --- a/packages/p2p-media-loader-core/src/p2p/p2p-loaders-container.ts +++ b/packages/p2p-media-loader-core/src/p2p/loaders-container.ts @@ -1,7 +1,7 @@ -import { P2PLoader } from "./p2p-loader"; +import { P2PLoader } from "./loader"; import debug from "debug"; import { Settings, Stream, StreamWithSegments } from "../index"; -import { RequestContainer } from "../request"; +import { RequestContainer } from "../request-container"; import { SegmentsMemoryStorage } from "../segments-storage"; import * as LoggerUtils from "../utils/logger"; diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index fc5cb42a..9fb4d43f 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -8,7 +8,7 @@ import { } from "../internal-types"; import { PeerCommandType, PeerSegmentStatus } from "../enums"; import * as PeerUtil from "../utils/peer"; -import { P2PRequest } from "../request"; +import { P2PRequest } from "../request-container"; import { Segment, Settings } from "../types"; import * as Utils from "../utils/utils"; import debug from "debug"; diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request-container.ts similarity index 100% rename from packages/p2p-media-loader-core/src/request.ts rename to packages/p2p-media-loader-core/src/request-container.ts diff --git a/packages/p2p-media-loader-core/src/types.d.ts b/packages/p2p-media-loader-core/src/types.d.ts index 3c373c5a..10ad7cd9 100644 --- a/packages/p2p-media-loader-core/src/types.d.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -1,6 +1,6 @@ import { LinkedMap } from "./linked-map"; -export type { EngineCallbacks } from "./request"; +export type { EngineCallbacks } from "./request-container"; export type StreamType = "main" | "secondary"; From bcec5bea8b358e36f0dc5ee4912bedee2fab5278 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 31 Oct 2023 17:16:22 +0200 Subject: [PATCH 105/127] Add request-container, storage loggers. Use queueMicrotask instead of setTimeout. --- p2p-media-loader-demo/src/App.tsx | 25 ++++++++----- .../src/hybrid-loader.ts | 9 ++--- .../p2p-media-loader-core/src/p2p/loader.ts | 17 +++++---- .../src/p2p/loaders-container.ts | 4 +-- .../src/request-container.ts | 35 ++++++++++++++----- .../src/segments-storage.ts | 10 +++++- packages/p2p-media-loader-core/src/types.d.ts | 6 +++- 7 files changed, 72 insertions(+), 34 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index eecfaa1d..e3d03820 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -303,14 +303,18 @@ function App() {
-
- - - +
+
+ + +
+
+ +
); @@ -370,7 +374,7 @@ function LoggersSelect() { value={activeLoggers} onChange={onChange} multiple - style={{ width: 300, height: 150 }} + style={{ width: 300, height: 200 }} > {loggers.map((logger) => (