From 8efe6f09aa5c8ce5f2e3722c2a8c454ef6b00adb Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 9 Nov 2023 13:16:02 +0200 Subject: [PATCH 01/22] Reduce LoadProgress object type. --- packages/p2p-media-loader-core/src/core.ts | 1 + .../src/declarations.d.ts | 2 +- packages/p2p-media-loader-core/src/errors.ts | 16 --- .../p2p-media-loader-core/src/http-loader.ts | 99 +++++++++-------- .../src/hybrid-loader.ts | 60 +++++++--- packages/p2p-media-loader-core/src/index.ts | 1 - .../p2p-media-loader-core/src/p2p/loader.ts | 2 +- .../p2p-media-loader-core/src/p2p/peer.ts | 54 ++++----- .../src/request-container.ts | 103 ++++++------------ packages/p2p-media-loader-core/src/types.d.ts | 1 + .../p2p-media-loader-core/src/utils/utils.ts | 17 +++ 11 files changed, 171 insertions(+), 185 deletions(-) delete mode 100644 packages/p2p-media-loader-core/src/errors.ts diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 69700d8b..1a8a9159 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -27,6 +27,7 @@ export class Core { webRtcMaxMessageSize: 64 * 1024 - 1, p2pSegmentDownloadTimeout: 5000, p2pLoaderDestroyTimeout: 30 * 1000, + httpRequestTimeout: 5000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); private segmentStorage?: SegmentsMemoryStorage; diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index f85dacec..de428c70 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -38,7 +38,7 @@ declare module "bittorrent-tracker" { E extends "connect" ? () => void : E extends "data" - ? (data: ArrayBuffer) => void + ? (data: Uint8Array) => void : E extends "close" ? () => void : E extends "error" diff --git a/packages/p2p-media-loader-core/src/errors.ts b/packages/p2p-media-loader-core/src/errors.ts deleted file mode 100644 index 421932e9..00000000 --- a/packages/p2p-media-loader-core/src/errors.ts +++ /dev/null @@ -1,16 +0,0 @@ -export class FetchError extends Error { - public code: number; - public details: object; - - constructor(message: string, code: number, details: object) { - super(message); - this.code = code; - this.details = details; - } -} - -export class RequestAbortError 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 efd00a9c..a30cf2a6 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,36 +1,32 @@ -import { RequestAbortError, FetchError } from "./errors"; -import { Segment } from "./types"; +import { Segment, Settings } from "./types"; import { HttpRequest, LoadProgress } from "./request-container"; +import * as Utils from "./utils/utils"; -export function getHttpSegmentRequest(segment: Segment): Readonly { - const { promise, abortController, progress } = fetchSegmentData(segment); - return { - type: "http", - promise, - progress, - abort: () => abortController.abort(), - }; -} - -function fetchSegmentData(segment: Segment) { +export function getHttpSegmentRequest( + segment: Segment, + settings: Pick +): Readonly { const headers = new Headers(); - const { url, byteRange, localId: segmentId } = segment; + const { url, byteRange } = segment; if (byteRange) { const { start, end } = byteRange; const byteRangeString = `bytes=${start}-${end}`; headers.set("Range", byteRangeString); } - const abortController = new AbortController(); + const abortController = new AbortController(); const progress: LoadProgress = { - canBeTracked: false, - totalBytes: 0, loadedBytes: 0, - percent: 0, startTimestamp: performance.now(), + chunks: [], }; const loadSegmentData = async () => { + const requestAbortTimeout = setTimeout(() => { + const errorType: HttpLoaderError["type"] = "request-timeout"; + abortController.abort(errorType); + }, settings.httpRequestTimeout); + try { const response = await window.fetch(url, { headers, @@ -38,25 +34,36 @@ function fetchSegmentData(segment: Segment) { }); if (response.ok) { - return await getDataPromiseAndMonitorProgress(response, progress); + const data = await getDataPromiseAndMonitorProgress(response, progress); + clearTimeout(requestAbortTimeout); + return data; } - throw new FetchError( - response.statusText ?? `Network response was not for ${segmentId}`, - response.status, - response - ); + throw new HttpLoaderError("fetch-error", response.statusText); } catch (error) { - if (error instanceof Error && error.name === "AbortError") { - throw new RequestAbortError(`Segment fetch was aborted ${segmentId}`); + if (error instanceof Error) { + if ((error.name as HttpLoaderError["type"]) === "manual-abort") { + throw new HttpLoaderError("manual-abort"); + } + if ((error.name as HttpLoaderError["type"]) === "request-timeout") { + throw new HttpLoaderError("request-timeout"); + } + if (!(error instanceof HttpLoaderError)) { + throw new HttpLoaderError("fetch-error", error.message); + } } + throw error; } }; return { + type: "http", promise: loadSegmentData(), - abortController, progress, + abort: () => { + const abortErrorType: HttpLoaderError["type"] = "manual-abort"; + abortController.abort(abortErrorType); + }, }; } @@ -70,42 +77,25 @@ async function getDataPromiseAndMonitorProgress( 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; - } + if (totalBytesString) progress.totalBytes = +totalBytesString; const reader = response.body.getReader(); - progress.startTimestamp = performance.now(); - const chunks: Uint8Array[] = []; + + progress.chunks = []; for await (const chunk of readStream(reader)) { - chunks.push(chunk); + progress.chunks.push(chunk); progress.loadedBytes += chunk.length; progress.lastLoadedChunkTimestamp = performance.now(); - if (progress.canBeTracked) { - progress.percent = (progress.loadedBytes / progress.totalBytes) * 100; - } } - if (!progress.canBeTracked) { - progress.totalBytes = progress.loadedBytes; - progress.percent = 100; - } - const resultBuffer = new ArrayBuffer(progress.loadedBytes); - const view = new Uint8Array(resultBuffer); + progress.totalBytes = progress.loadedBytes; - let offset = 0; - for (const chunk of chunks) { - view.set(chunk, offset); - offset += chunk.length; - } - return resultBuffer; + return Utils.joinChunks(progress.chunks, progress.totalBytes); } async function* readStream( @@ -117,3 +107,12 @@ async function* readStream( yield value; } } + +export class HttpLoaderError extends Error { + constructor( + readonly type: "request-timeout" | "fetch-error" | "manual-abort", + message?: string + ) { + super(message); + } +} diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 41b0c00c..777e24b6 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, StreamWithSegments } from "./index"; -import { getHttpSegmentRequest } from "./http-loader"; +import { getHttpSegmentRequest, HttpLoaderError } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; @@ -11,8 +11,8 @@ import { } from "./request-container"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; -import { FetchError } from "./errors"; import { P2PLoadersContainer } from "./p2p/loaders-container"; +import { PeerRequestError } from "./p2p/peer"; import debug from "debug"; export class HybridLoader { @@ -26,6 +26,7 @@ export class HybridLoader { private randomHttpDownloadInterval!: number; private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; private readonly levelBandwidth = { value: 0, refreshCount: 0 }; + private isProcessQueueMicrotaskCreated = false; constructor( private streamManifestUrl: string, @@ -106,7 +107,7 @@ export class HybridLoader { this.processQueue(); } - private processQueue(force = true) { + private createProcessQueueMicrotask(force = true) { const now = performance.now(); if ( !force && @@ -115,8 +116,16 @@ export class HybridLoader { ) { return; } - this.lastQueueProcessingTimeStamp = now; + this.isProcessQueueMicrotaskCreated = true; + queueMicrotask(() => { + this.processQueue(); + this.lastQueueProcessingTimeStamp = now; + this.isProcessQueueMicrotaskCreated = false; + }); + } + + private processQueue() { const { queue, queueSegmentIds } = QueueUtils.generateQueue({ lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, @@ -124,16 +133,27 @@ export class HybridLoader { skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); - this.requests.abortAllNotRequestedByEngine((segment) => - queueSegmentIds.has(segment.localId) - ); + for (const { + segment, + loaderRequest, + engineCallbacks, + } of this.requests.values()) { + if ( + !engineCallbacks && + loaderRequest && + !queueSegmentIds.has(segment.localId) + ) { + loaderRequest.abort(); + this.requests.remove(segment); + } + } const { simultaneousHttpDownloads, simultaneousP2PDownloads } = this.settings; for (const item of queue) { const { statuses, segment } = item; - const request = this.requests.get(segment); + const request = this.requests.getHybridLoaderRequest(segment); if (statuses.isHighDemand) { if (request?.type === "http") continue; @@ -204,7 +224,7 @@ export class HybridLoader { const { segment } = item; let data: ArrayBuffer | undefined; try { - const httpRequest = getHttpSegmentRequest(segment); + const httpRequest = getHttpSegmentRequest(segment, this.settings); if (!isRandom) { this.logger.loader( @@ -212,20 +232,25 @@ export class HybridLoader { ); } - this.requests.addLoaderRequest(segment, httpRequest); + this.requests.addHybridLoaderRequest(segment, httpRequest); this.bandwidthApproximator.addLoading(httpRequest.progress); data = await httpRequest.promise; if (!data) return; this.logger.loader(`http responses: ${segment.externalId}`); this.onSegmentLoaded(item, "http", data); - } catch (err) { - if (err instanceof FetchError) { - this.processQueue(); + } catch (error) { + if ( + !(error instanceof HttpLoaderError) || + error.type === "manual-abort" + ) { + return; } + this.processQueue(); } } private async loadThroughP2P(item: QueueItem) { + const { segment } = item; const p2pLoader = this.p2pLoaders.currentLoader; try { const downloadPromise = p2pLoader.downloadSegment(item); @@ -233,6 +258,13 @@ export class HybridLoader { const data = await downloadPromise; this.onSegmentLoaded(item, "p2p", data); } catch (error) { + if ( + !(error instanceof PeerRequestError) || + error.type === "manual-abort" + ) { + return; + } + const request = this.requests.get(segment); this.processQueue(); } } @@ -287,7 +319,7 @@ export class HybridLoader { ? this.bandwidthApproximator.getBandwidth() : this.levelBandwidth.value; - this.requests.resolveEngineRequest(segment, { data, bandwidth }); + this.requests.resolveAndRemoveRequest(segment, { data, bandwidth }); this.eventHandlers?.onSegmentLoaded?.(byteLength, type); this.processQueue(); } diff --git a/packages/p2p-media-loader-core/src/index.ts b/packages/p2p-media-loader-core/src/index.ts index 036287ad..723b2f2d 100644 --- a/packages/p2p-media-loader-core/src/index.ts +++ b/packages/p2p-media-loader-core/src/index.ts @@ -1,5 +1,4 @@ /// 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 da25f186..ff81470a 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -114,7 +114,7 @@ export class P2PLoader { if (!peer) return; const request = peer.requestSegment(segment); - this.requests.addLoaderRequest(segment, request); + this.requests.addHybridLoaderRequest(segment, request); this.logger( `p2p request ${segment.externalId} | ${LoggerUtils.getStatusesString( statuses diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index 9fb4d43f..c051d35c 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -16,7 +16,7 @@ import debug from "debug"; export class PeerRequestError extends Error { constructor( readonly type: - | "abort" + | "manual-abort" | "request-timeout" | "response-bytes-mismatch" | "segment-absent" @@ -38,7 +38,6 @@ type PeerRequest = { p2pRequest: P2PRequest; resolve: (data: ArrayBuffer) => void; reject: (error: PeerRequestError) => void; - chunks: ArrayBuffer[]; responseTimeoutId: number; }; @@ -122,7 +121,7 @@ export class Peer { return this.segments.get(externalId); } - private onReceiveData(data: ArrayBuffer) { + private onReceiveData(data: Uint8Array) { const command = PeerUtil.getPeerCommandFromArrayBuffer(data); if (!command) { this.receiveSegmentChunk(data); @@ -142,7 +141,6 @@ export class Peer { if (this.request?.segment.externalId === command.i) { const { progress } = this.request.p2pRequest; progress.totalBytes = command.s; - progress.canBeTracked = true; } break; @@ -245,49 +243,47 @@ export class Peer { resolve, reject, responseTimeoutId: this.setRequestTimeout(), - chunks: [], p2pRequest: { type: "p2p", progress: { - canBeTracked: false, - totalBytes: 0, loadedBytes: 0, - percent: 0, startTimestamp: performance.now(), + chunks: [], }, promise, - abort: () => this.cancelSegmentRequest("abort"), + abort: () => this.cancelSegmentRequest("manual-abort"), }, }; } - private receiveSegmentChunk(chunk: ArrayBuffer): void { + private receiveSegmentChunk(chunk: Uint8Array): void { const { request } = this; - const progress = request?.p2pRequest?.progress; - if (!request || !progress) return; + if (!request) return; + const { progress } = request.p2pRequest; progress.loadedBytes += chunk.byteLength; - progress.percent = (progress.loadedBytes / progress.loadedBytes) * 100; progress.lastLoadedChunkTimestamp = performance.now(); - request.chunks.push(chunk); + progress.chunks.push(chunk); if (progress.loadedBytes === progress.totalBytes) { - const segmentData = joinChunks(request.chunks); + const segmentData = Utils.joinChunks( + progress.chunks, + progress.totalBytes + ); const { lastLoadedChunkTimestamp, startTimestamp, loadedBytes } = progress; const loadingDuration = lastLoadedChunkTimestamp - startTimestamp; this.bandwidthMeasurer.addMeasurement(loadedBytes, loadingDuration); - this.approveRequest(segmentData); - } else if (progress.loadedBytes > progress.totalBytes) { + request.resolve(segmentData); + this.clearRequest(); + } else if ( + progress.totalBytes !== undefined && + progress.loadedBytes > progress.totalBytes + ) { this.cancelSegmentRequest("response-bytes-mismatch"); } } - private approveRequest(data: ArrayBuffer) { - this.request?.resolve(data); - this.clearRequest(); - } - private cancelSegmentRequest(type: PeerRequestError["type"]) { if (!this.request) return; this.logger( @@ -296,7 +292,7 @@ export class Peer { const error = new PeerRequestError(type); const sendCancelCommandTypes: PeerRequestError["type"][] = [ "destroy", - "abort", + "manual-abort", "request-timeout", "response-bytes-mismatch", ]; @@ -368,18 +364,6 @@ function* getBufferChunks( } } -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; -} - function hexToUtf8(hexString: string) { const bytes = new Uint8Array(hexString.length / 2); diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 78140899..9926fb0f 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,42 +1,45 @@ import { Segment, SegmentResponse, StreamType } from "./types"; -import { RequestAbortError } from "./errors"; import { Subscriptions } from "./segments-storage"; +import { PeerRequestError } from "./p2p/peer"; +import { HttpLoaderError } from "./http-loader"; import Debug from "debug"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; - onError: (reason?: unknown) => void; + onError: (reason: "failed" | "abort") => void; }; export type LoadProgress = { startTimestamp: number; lastLoadedChunkTimestamp?: number; - percent: number; loadedBytes: number; - totalBytes: number; - canBeTracked: boolean; + totalBytes?: number; + chunks: Uint8Array[]; }; -type RequestBase = { +type HybridLoaderRequestBase = { promise: Promise; abort: () => void; progress: LoadProgress; }; -export type HttpRequest = RequestBase & { +export type HttpRequest = HybridLoaderRequestBase & { type: "http"; + error?: HttpLoaderError; }; -export type P2PRequest = RequestBase & { +export type P2PRequest = HybridLoaderRequestBase & { type: "p2p"; + error?: PeerRequestError; }; export type HybridLoaderRequest = HttpRequest | P2PRequest; -type Request = { +type RequestItem = { segment: Readonly; - loaderRequest?: Readonly; + loaderRequest?: HybridLoaderRequest; engineCallbacks?: Readonly; + prevAttempts: HybridLoaderRequest[]; }; function getRequestItemId(segment: Segment) { @@ -44,7 +47,7 @@ function getRequestItemId(segment: Segment) { } export class RequestsContainer { - private readonly requests = new Map(); + private readonly requests = new Map(); private readonly onHttpRequestsHandlers = new Subscriptions(); private readonly logger: Debug.Debugger; @@ -68,11 +71,21 @@ export class RequestsContainer { } get(segment: Segment) { + const id = getRequestItemId(segment); + return this.requests.get(id); + } + + getHybridLoaderRequest(segment: Segment) { const id = getRequestItemId(segment); return this.requests.get(id)?.loaderRequest; } - addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { + remove(segment: Segment) { + const id = getRequestItemId(segment); + this.requests.delete(id); + } + + addHybridLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { const segmentId = getRequestItemId(segment); const existingRequest = this.requests.get(segmentId); if (existingRequest) { @@ -81,18 +94,12 @@ export class RequestsContainer { this.requests.set(segmentId, { segment, loaderRequest, + prevAttempts: [], }); } this.logger( `add loader request: ${loaderRequest.type} ${segment.externalId}` ); - - const clearRequestItem = () => this.clearRequestItem(segmentId, "loader"); - loaderRequest.promise - .then(() => clearRequestItem()) - .catch((err) => { - if (err instanceof RequestAbortError) clearRequestItem(); - }); if (loaderRequest.type === "http") this.onHttpRequestsHandlers.fire(); } @@ -100,25 +107,13 @@ export class RequestsContainer { const segmentId = getRequestItemId(segment); const requestItem = this.requests.get(segmentId); - 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 { this.requests.set(segmentId, { segment, engineCallbacks, + prevAttempts: [], }); } this.logger(`add engine request ${segment.externalId}`); @@ -128,21 +123,24 @@ export class RequestsContainer { return this.requests.values(); } - *httpRequests(): Generator { + *httpRequests(): Generator { for (const request of this.requests.values()) { if (request.loaderRequest?.type === "http") yield request; } } - *p2pRequests(): Generator { + *p2pRequests(): Generator { for (const request of this.requests.values()) { if (request.loaderRequest?.type === "p2p") yield request; } } - resolveEngineRequest(segment: Segment, response: SegmentResponse) { + resolveAndRemoveRequest(segment: Segment, response: SegmentResponse) { const id = getRequestItemId(segment); - this.requests.get(id)?.engineCallbacks?.onSuccess(response); + const request = this.requests.get(id); + if (!request) return; + request.engineCallbacks?.onSuccess(response); + this.requests.delete(id); } isHttpRequested(segment: Segment): boolean { @@ -165,7 +163,7 @@ export class RequestsContainer { const request = this.requests.get(id); if (!request) return; - request.engineCallbacks?.onError(new RequestAbortError()); + // request.engineCallbacks?.onError(new RequestAbortError()); request.loaderRequest?.abort(); } @@ -174,35 +172,6 @@ export class RequestsContainer { this.requests.get(id)?.loaderRequest?.abort(); } - private clearRequestItem( - requestItemId: string, - type: "loader" | "engine" - ): void { - const requestItem = this.requests.get(requestItemId); - if (!requestItem) return; - const { segment, loaderRequest } = requestItem; - const segmentExternalId = segment.externalId; - - if (type === "engine") { - this.logger(`remove engine callbacks: ${segmentExternalId}`); - delete requestItem.engineCallbacks; - } - if (type === "loader" && loaderRequest) { - this.logger( - `remove loader request: ${loaderRequest.type} ${segmentExternalId}` - ); - if (loaderRequest.type === "http") { - this.onHttpRequestsHandlers.fire(); - } - delete requestItem.loaderRequest; - } - if (!requestItem.engineCallbacks && !requestItem.loaderRequest) { - this.logger(`remove request item ${segmentExternalId}`); - const segmentId = getRequestItemId(segment); - this.requests.delete(segmentId); - } - } - abortAllNotRequestedByEngine(isLocked?: (segment: Segment) => boolean) { const isSegmentLocked = isLocked ? isLocked : () => false; for (const { @@ -226,7 +195,7 @@ export class RequestsContainer { destroy() { for (const request of this.requests.values()) { request.loaderRequest?.abort(); - request.engineCallbacks?.onError(); + request.engineCallbacks?.onError("failed"); } this.requests.clear(); } diff --git a/packages/p2p-media-loader-core/src/types.d.ts b/packages/p2p-media-loader-core/src/types.d.ts index a894961a..16e7d14d 100644 --- a/packages/p2p-media-loader-core/src/types.d.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -60,6 +60,7 @@ export type Settings = { webRtcMaxMessageSize: number; p2pSegmentDownloadTimeout: number; p2pLoaderDestroyTimeout: number; + httpRequestTimeout: number; }; export type CoreEventHandlers = { diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index d9fb72fd..7267ac7a 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -45,3 +45,20 @@ export function getControlledPromise() { reject: reject!, }; } + +export function joinChunks( + chunks: Uint8Array[], + totalBytes?: number +): ArrayBuffer { + if (totalBytes === undefined) { + totalBytes = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + } + const buffer = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + buffer.set(chunk, offset); + offset += chunk.byteLength; + } + + return buffer; +} From f7225e6424bdd4094a40543e0d22d08f0b7d3bd7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 9 Nov 2023 13:17:05 +0200 Subject: [PATCH 02/22] Create microtask for process queue. --- packages/p2p-media-loader-core/src/hybrid-loader.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 777e24b6..bd503081 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -104,7 +104,7 @@ export class HybridLoader { this.requests.addEngineCallbacks(segment, callbacks); } - this.processQueue(); + this.createProcessQueueMicrotask(); } private createProcessQueueMicrotask(force = true) { @@ -245,7 +245,7 @@ export class HybridLoader { ) { return; } - this.processQueue(); + this.createProcessQueueMicrotask(); } } @@ -265,7 +265,7 @@ export class HybridLoader { return; } const request = this.requests.get(segment); - this.processQueue(); + this.createProcessQueueMicrotask(); } } @@ -321,7 +321,7 @@ export class HybridLoader { this.requests.resolveAndRemoveRequest(segment, { data, bandwidth }); this.eventHandlers?.onSegmentLoaded?.(byteLength, type); - this.processQueue(); + this.createProcessQueueMicrotask(); } private abortLastHttpLoadingAfter(queue: QueueItem[], segment: Segment) { @@ -376,13 +376,13 @@ export class HybridLoader { if (isPositionSignificantlyChanged) { this.logger.engine("position significantly changed"); } - void this.processQueue(isPositionSignificantlyChanged); + void this.createProcessQueueMicrotask(isPositionSignificantlyChanged); } updateStream(stream: StreamWithSegments) { if (stream !== this.lastRequestedSegment.stream) return; this.logger.engine(`update stream: ${LoggerUtils.getStreamString(stream)}`); - this.processQueue(); + this.createProcessQueueMicrotask(); } destroy() { From 7408c873b7511b1b699c68a9d94bbbb5930bc71b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 13 Nov 2023 16:47:39 +0200 Subject: [PATCH 03/22] Add event dispatcher class. --- .../src/event-dispatcher.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/p2p-media-loader-core/src/event-dispatcher.ts diff --git a/packages/p2p-media-loader-core/src/event-dispatcher.ts b/packages/p2p-media-loader-core/src/event-dispatcher.ts new file mode 100644 index 00000000..ed648b45 --- /dev/null +++ b/packages/p2p-media-loader-core/src/event-dispatcher.ts @@ -0,0 +1,30 @@ +export class EventDispatcher< + T extends { [key: string]: (...args: any) => any }, + K extends keyof T = keyof T +> { + private readonly listeners = new Map>(); + + subscribe(eventType: K, listener: T[K]) { + let eventListeners = this.listeners.get(eventType); + if (!eventListeners) { + eventListeners = new Set(); + this.listeners.set(eventType, eventListeners); + } + eventListeners.add(listener); + } + + unsubscribe(eventType: K, listener: T[K]) { + const eventListeners = this.listeners.get(eventType); + if (!eventListeners) return; + eventListeners.delete(listener); + if (!eventListeners.size) this.listeners.delete(eventType); + } + + dispatch(eventType: K, ...args: Parameters) { + const eventListeners = this.listeners.get(eventType); + if (!eventListeners) return; + for (const listener of eventListeners) { + listener(args); + } + } +} From e0198702bb73bc36ae2a51a1584189b758a89176 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 13 Nov 2023 16:47:58 +0200 Subject: [PATCH 04/22] Create Request class. --- .../p2p-media-loader-core/src/http-loader.ts | 129 ++++---- .../src/hybrid-loader.ts | 125 ++++---- .../p2p-media-loader-core/src/p2p/loader.ts | 12 +- .../p2p-media-loader-core/src/p2p/peer.ts | 97 ++---- .../src/request-container.ts | 285 +++++++++++------- .../src/segments-storage.ts | 101 ++----- .../p2p-media-loader-core/src/utils/stream.ts | 34 +++ .../p2p-media-loader-core/src/utils/utils.ts | 35 +-- 8 files changed, 393 insertions(+), 425 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/utils/stream.ts diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index a30cf2a6..fb58d23f 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,12 +1,12 @@ -import { Segment, Settings } from "./types"; -import { HttpRequest, LoadProgress } from "./request-container"; -import * as Utils from "./utils/utils"; +import { Settings } from "./types"; +import { Request } from "./request-container"; -export function getHttpSegmentRequest( - segment: Segment, +export async function fulfillHttpSegmentRequest( + request: Request, settings: Pick -): Readonly { +) { const headers = new Headers(); + const { segment } = request; const { url, byteRange } = segment; if (byteRange) { @@ -16,86 +16,61 @@ export function getHttpSegmentRequest( } const abortController = new AbortController(); - const progress: LoadProgress = { - loadedBytes: 0, - startTimestamp: performance.now(), - chunks: [], - }; - const loadSegmentData = async () => { - const requestAbortTimeout = setTimeout(() => { - const errorType: HttpLoaderError["type"] = "request-timeout"; - abortController.abort(errorType); - }, settings.httpRequestTimeout); - - try { - const response = await window.fetch(url, { - headers, - signal: abortController.signal, - }); - - if (response.ok) { - const data = await getDataPromiseAndMonitorProgress(response, progress); - clearTimeout(requestAbortTimeout); - return data; - } - throw new HttpLoaderError("fetch-error", response.statusText); - } catch (error) { - if (error instanceof Error) { - if ((error.name as HttpLoaderError["type"]) === "manual-abort") { - throw new HttpLoaderError("manual-abort"); - } - if ((error.name as HttpLoaderError["type"]) === "request-timeout") { - throw new HttpLoaderError("request-timeout"); - } - if (!(error instanceof HttpLoaderError)) { - throw new HttpLoaderError("fetch-error", error.message); - } - } - throw error; - } - }; + const requestAbortTimeout = setTimeout(() => { + const errorType: HttpLoaderError["type"] = "request-timeout"; + abortController.abort(errorType); + }, settings.httpRequestTimeout); - return { - type: "http", - promise: loadSegmentData(), - progress, - abort: () => { - const abortErrorType: HttpLoaderError["type"] = "manual-abort"; - abortController.abort(abortErrorType); - }, + const abortManually = () => { + const abortErrorType: HttpLoaderError["type"] = "manual-abort"; + abortController.abort(abortErrorType); }; -} -async function getDataPromiseAndMonitorProgress( - response: Response, - progress: LoadProgress -): Promise { - const totalBytesString = response.headers.get("Content-Length"); - if (!response.body) { - return response.arrayBuffer().then((data) => { - progress.loadedBytes = data.byteLength; - progress.totalBytes = data.byteLength; - progress.lastLoadedChunkTimestamp = performance.now(); - return data; + const requestControls = request.start("http", abortManually); + try { + const fetchResponse = await window.fetch(url, { + headers, + signal: abortController.signal, }); - } - if (totalBytesString) progress.totalBytes = +totalBytesString; + if (fetchResponse.ok) { + const totalBytesString = fetchResponse.headers.get("Content-Length"); + if (!fetchResponse.body) { + fetchResponse.arrayBuffer().then((data) => { + requestControls.addLoadedChunk(data); + requestControls.completeOnSuccess(); + }); + return; + } - const reader = response.body.getReader(); - progress.startTimestamp = performance.now(); + if (totalBytesString) request.setTotalBytes(+totalBytesString); - progress.chunks = []; - for await (const chunk of readStream(reader)) { - progress.chunks.push(chunk); - progress.loadedBytes += chunk.length; - progress.lastLoadedChunkTimestamp = performance.now(); + const reader = fetchResponse.body.getReader(); + for await (const chunk of readStream(reader)) { + requestControls.addLoadedChunk(chunk); + } + requestControls.completeOnSuccess(); + clearTimeout(requestAbortTimeout); + } + throw new HttpLoaderError("fetch-error", fetchResponse.statusText); + } catch (error) { + if (error instanceof Error) { + let httpLoaderError: HttpLoaderError; + if ((error.name as HttpLoaderError["type"]) === "manual-abort") { + httpLoaderError = new HttpLoaderError("manual-abort"); + } else if ( + (error.name as HttpLoaderError["type"]) === "request-timeout" + ) { + httpLoaderError = new HttpLoaderError("request-timeout"); + } else if (!(error instanceof HttpLoaderError)) { + httpLoaderError = new HttpLoaderError("fetch-error", error.message); + } else { + httpLoaderError = error; + } + requestControls.cancelOnError(httpLoaderError); + } } - - progress.totalBytes = progress.loadedBytes; - - return Utils.joinChunks(progress.chunks, progress.totalBytes); } 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 bd503081..f0f4f415 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, StreamWithSegments } from "./index"; -import { getHttpSegmentRequest, HttpLoaderError } from "./http-loader"; +import { fulfillHttpSegmentRequest } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; @@ -8,6 +8,7 @@ import { RequestsContainer, EngineCallbacks, HybridLoaderRequest, + Request, } from "./request-container"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; @@ -40,7 +41,10 @@ export class HybridLoader { const activeStream = requestedSegment.stream; this.playback = { position: requestedSegment.startTime, rate: 1 }; this.segmentAvgDuration = getSegmentAvgDuration(activeStream); - this.requests = new RequestsContainer(requestedSegment.stream.type); + this.requests = new RequestsContainer( + requestedSegment.stream.type, + this.bandwidthApproximator + ); if (!this.segmentStorage.isInitialized) { throw new Error("Segment storage is not initialized."); @@ -101,7 +105,8 @@ export class HybridLoader { }); } } else { - this.requests.addEngineCallbacks(segment, callbacks); + const request = this.requests.getOrCreateRequest(segment); + request.engineCallbacks = callbacks; } this.createProcessQueueMicrotask(); @@ -110,9 +115,10 @@ export class HybridLoader { private createProcessQueueMicrotask(force = true) { const now = performance.now(); if ( - !force && - this.lastQueueProcessingTimeStamp !== undefined && - now - this.lastQueueProcessingTimeStamp <= 1000 + (!force && + this.lastQueueProcessingTimeStamp !== undefined && + now - this.lastQueueProcessingTimeStamp <= 1000) || + this.isProcessQueueMicrotaskCreated ) { return; } @@ -133,18 +139,14 @@ export class HybridLoader { skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); - for (const { - segment, - loaderRequest, - engineCallbacks, - } of this.requests.values()) { + for (const request of this.requests.values()) { if ( - !engineCallbacks && - loaderRequest && - !queueSegmentIds.has(segment.localId) + !request.isSegmentRequestedByEngine && + request.status === "loading" && + !queueSegmentIds.has(request.segment.localId) ) { - loaderRequest.abort(); - this.requests.remove(segment); + request.abort(); + this.requests.remove(request); } } @@ -153,7 +155,7 @@ export class HybridLoader { for (const item of queue) { const { statuses, segment } = item; - const request = this.requests.getHybridLoaderRequest(segment); + const request = this.requests.get(segment); if (statuses.isHighDemand) { if (request?.type === "http") continue; @@ -174,39 +176,39 @@ export class HybridLoader { continue; } } - if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + if (this.requests.executingHttpCount < simultaneousHttpDownloads) { void this.loadThroughHttp(item); continue; } this.abortLastHttpLoadingAfter(queue, segment); - if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + if (this.requests.executingHttpCount < simultaneousHttpDownloads) { void this.loadThroughHttp(item); continue; } if (this.requests.isP2PRequested(segment)) continue; - if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + if (this.requests.executingP2PCount < simultaneousP2PDownloads) { void this.loadThroughP2P(item); continue; } this.abortLastP2PLoadingAfter(queue, segment); - if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + if (this.requests.executingP2PCount < simultaneousP2PDownloads) { void this.loadThroughP2P(item); } break; } if (statuses.isP2PDownloadable) { if (request) continue; - if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + if (this.requests.executingP2PCount < simultaneousP2PDownloads) { void this.loadThroughP2P(item); continue; } this.abortLastP2PLoadingAfter(queue, segment); - if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + if (this.requests.executingP2PCount < simultaneousP2PDownloads) { void this.loadThroughP2P(item); } } @@ -222,59 +224,48 @@ export class HybridLoader { private async loadThroughHttp(item: QueueItem, isRandom = false) { const { segment } = item; - let data: ArrayBuffer | undefined; - try { - const httpRequest = getHttpSegmentRequest(segment, this.settings); - if (!isRandom) { - this.logger.loader( - `http request: ${LoggerUtils.getQueueItemString(item)}` - ); - } + const request = this.requests.getOrCreateRequest(segment); + request.subscribe("onCompleted", this.onRequestCompleted); + request.subscribe("onError", this.onRequestError); - this.requests.addHybridLoaderRequest(segment, httpRequest); - this.bandwidthApproximator.addLoading(httpRequest.progress); - data = await httpRequest.promise; - if (!data) return; - this.logger.loader(`http responses: ${segment.externalId}`); - this.onSegmentLoaded(item, "http", data); - } catch (error) { - if ( - !(error instanceof HttpLoaderError) || - error.type === "manual-abort" - ) { - return; - } - this.createProcessQueueMicrotask(); + void fulfillHttpSegmentRequest(request, this.settings); + if (!isRandom) { + this.logger.loader( + `http request: ${LoggerUtils.getQueueItemString(item)}` + ); } } private async loadThroughP2P(item: QueueItem) { - const { segment } = item; const p2pLoader = this.p2pLoaders.currentLoader; - try { - const downloadPromise = p2pLoader.downloadSegment(item); - if (downloadPromise === undefined) return; - const data = await downloadPromise; - this.onSegmentLoaded(item, "p2p", data); - } catch (error) { - if ( - !(error instanceof PeerRequestError) || - error.type === "manual-abort" - ) { - return; - } - const request = this.requests.get(segment); - this.createProcessQueueMicrotask(); - } + const request = p2pLoader.downloadSegment(item); + if (request === undefined) return; + + request.subscribe("onCompleted", this.onRequestCompleted); + request.subscribe("onError", this.onRequestError); } + private onRequestCompleted = (request: Request, data: ArrayBuffer) => { + const { segment } = request; + this.logger.loader(`http responses: ${segment.externalId}`); + this.eventHandlers?.onSegmentLoaded?.(data.byteLength, "http"); + this.createProcessQueueMicrotask(); + }; + + private onRequestError = (request: Request, error: Error) => { + if (!(error instanceof PeerRequestError) || error.type === "manual-abort") { + return; + } + this.createProcessQueueMicrotask(); + }; + private loadRandomThroughHttp() { const { simultaneousHttpDownloads } = this.settings; const p2pLoader = this.p2pLoaders.currentLoader; const connectedPeersAmount = p2pLoader.connectedPeersAmount; if ( - this.requests.httpRequestsCount >= simultaneousHttpDownloads || + this.requests.executingHttpCount >= simultaneousHttpDownloads || !connectedPeersAmount ) { return; @@ -314,12 +305,6 @@ export class HybridLoader { this.refreshLevelBandwidth(true); } void this.segmentStorage.storeSegment(segment, data); - - const bandwidth = statuses.isHighDemand - ? this.bandwidthApproximator.getBandwidth() - : this.levelBandwidth.value; - - this.requests.resolveAndRemoveRequest(segment, { data, bandwidth }); this.eventHandlers?.onSegmentLoaded?.(byteLength, type); this.createProcessQueueMicrotask(); } @@ -328,7 +313,7 @@ export class HybridLoader { for (const { segment: itemSegment } of arrayBackwards(queue)) { if (itemSegment.localId === segment.localId) break; if (this.requests.isHttpRequested(segment)) { - this.requests.abortLoaderRequest(segment); + this.requests.get(segment)?.abort(); this.logger.loader( "http aborted: ", LoggerUtils.getSegmentString(segment) @@ -342,7 +327,7 @@ export class HybridLoader { for (const { segment: itemSegment } of arrayBackwards(queue)) { if (itemSegment.localId === segment.localId) break; if (this.requests.isP2PRequested(segment)) { - this.requests.abortLoaderRequest(segment); + this.requests.get(segment)?.abort(); this.logger.loader( "p2p aborted: ", LoggerUtils.getSegmentString(segment) diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index ff81470a..619492c6 100644 --- a/packages/p2p-media-loader-core/src/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 { RequestsContainer } from "../request-container"; +import { Request, RequestsContainer } from "../request-container"; import debug from "debug"; export class P2PLoader { @@ -86,7 +86,7 @@ export class P2PLoader { this.peers.set(connection.id, peer); } - downloadSegment(item: QueueItem): Promise | undefined { + downloadSegment(item: QueueItem): Request | undefined { const { segment, statuses } = item; const untestedPeers: Peer[] = []; let fastestPeer: Peer | undefined; @@ -113,18 +113,18 @@ export class P2PLoader { if (!peer) return; - const request = peer.requestSegment(segment); - this.requests.addHybridLoaderRequest(segment, request); + const request = this.requests.getOrCreateRequest(segment); + peer.fulfillSegmentRequest(request); this.logger( `p2p request ${segment.externalId} | ${LoggerUtils.getStatusesString( statuses )}` ); - request.promise.then(() => { + request.subscribe("onCompleted", () => { this.logger(`p2p loaded: ${segment.externalId}`); }); - return request.promise; + return request; } isLoadingOrLoadedBySomeone(segment: Segment): boolean { diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index c051d35c..cb9da03f 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-container"; +import { Request, RequestControls } from "../request-container"; import { Segment, Settings } from "../types"; import * as Utils from "../utils/utils"; import debug from "debug"; @@ -33,14 +33,6 @@ type PeerEventHandlers = { onSegmentRequested: (peer: Peer, segmentId: string) => void; }; -type PeerRequest = { - segment: Segment; - p2pRequest: P2PRequest; - resolve: (data: ArrayBuffer) => void; - reject: (error: PeerRequestError) => void; - responseTimeoutId: number; -}; - type PeerSettings = Pick< Settings, "p2pSegmentDownloadTimeout" | "webRtcMaxMessageSize" @@ -51,7 +43,7 @@ export class Peer { private connection?: PeerConnection; private connections = new Set(); private segments = new Map(); - private request?: PeerRequest; + private requestData?: { request: Request; controls: RequestControls }; private readonly logger = debug("core:peer"); private readonly bandwidthMeasurer = new BandwidthMeasurer(); private isUploadingSegment = false; @@ -109,7 +101,7 @@ export class Peer { } get downloadingSegment(): Segment | undefined { - return this.request?.segment; + return this.requestData?.request.segment; } get bandwidth(): number | undefined { @@ -138,14 +130,13 @@ export class Peer { break; case PeerCommandType.SegmentData: - if (this.request?.segment.externalId === command.i) { - const { progress } = this.request.p2pRequest; - progress.totalBytes = command.s; + if (this.requestData?.request.segment.externalId === command.i) { + this.requestData.request.setTotalBytes(command.s); } break; case PeerCommandType.SegmentAbsent: - if (this.request?.segment.externalId === command.i) { + if (this.requestData?.request.segment.externalId === command.i) { this.cancelSegmentRequest("segment-absent"); this.segments.delete(command.i); } @@ -162,18 +153,21 @@ export class Peer { this.connection.send(JSON.stringify(command)); } - requestSegment(segment: Segment) { - if (this.request) { + fulfillSegmentRequest(request: Request) { + if (this.requestData) { throw new Error("Segment already is downloading"); } - const { externalId } = segment; + this.requestData = { + request, + controls: request.start("p2p", () => + this.cancelSegmentRequest("manual-abort") + ), + }; const command: PeerSegmentCommand = { c: PeerCommandType.SegmentRequest, - i: externalId, + i: request.segment.externalId, }; this.sendCommand(command); - this.request = this.createPeerRequest(segment); - return this.request.p2pRequest; } sendSegmentsAnnouncement(announcement: JsonSegmentAnnouncement) { @@ -235,60 +229,27 @@ export class Peer { this.sendCommand(command); } - private createPeerRequest(segment: Segment): PeerRequest { - const { promise, resolve, reject } = - Utils.getControlledPromise(); - return { - segment, - resolve, - reject, - responseTimeoutId: this.setRequestTimeout(), - p2pRequest: { - type: "p2p", - progress: { - loadedBytes: 0, - startTimestamp: performance.now(), - chunks: [], - }, - promise, - abort: () => this.cancelSegmentRequest("manual-abort"), - }, - }; - } - private receiveSegmentChunk(chunk: Uint8Array): void { - const { request } = this; - if (!request) return; - - const { progress } = request.p2pRequest; - progress.loadedBytes += chunk.byteLength; - progress.lastLoadedChunkTimestamp = performance.now(); - progress.chunks.push(chunk); - - if (progress.loadedBytes === progress.totalBytes) { - const segmentData = Utils.joinChunks( - progress.chunks, - progress.totalBytes - ); - const { lastLoadedChunkTimestamp, startTimestamp, loadedBytes } = - progress; - const loadingDuration = lastLoadedChunkTimestamp - startTimestamp; - this.bandwidthMeasurer.addMeasurement(loadedBytes, loadingDuration); - request.resolve(segmentData); + if (!this.requestData) return; + const { request, controls } = this.requestData; + controls.addLoadedChunk(chunk); + + if (request.loadedBytes === request.totalBytes) { + controls.completeOnSuccess(); this.clearRequest(); } else if ( - progress.totalBytes !== undefined && - progress.loadedBytes > progress.totalBytes + request.totalBytes !== undefined && + request.loadedBytes > request.totalBytes ) { this.cancelSegmentRequest("response-bytes-mismatch"); } } private cancelSegmentRequest(type: PeerRequestError["type"]) { - if (!this.request) return; - this.logger( - `cancel segment request ${this.request?.segment.externalId} (${type})` - ); + if (!this.requestData) return; + const { request, controls } = this.requestData; + const { segment } = request; + this.logger(`cancel segment request ${segment.externalId} (${type})`); const error = new PeerRequestError(type); const sendCancelCommandTypes: PeerRequestError["type"][] = [ "destroy", @@ -299,10 +260,10 @@ export class Peer { if (sendCancelCommandTypes.includes(type)) { this.sendCommand({ c: PeerCommandType.CancelSegmentRequest, - i: this.request.segment.externalId, + i: segment.externalId, }); } - this.request.reject(error); + controls.cancelOnError(error); this.clearRequest(); } diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 9926fb0f..2185bf26 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,8 +1,10 @@ import { Segment, SegmentResponse, StreamType } from "./types"; -import { Subscriptions } from "./segments-storage"; import { PeerRequestError } from "./p2p/peer"; import { HttpLoaderError } from "./http-loader"; import Debug from "debug"; +import { EventDispatcher } from "./event-dispatcher"; +import * as Utils from "./utils/utils"; +import { BandwidthApproximator } from "./bandwidth-approximator"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -14,11 +16,9 @@ export type LoadProgress = { lastLoadedChunkTimestamp?: number; loadedBytes: number; totalBytes?: number; - chunks: Uint8Array[]; }; type HybridLoaderRequestBase = { - promise: Promise; abort: () => void; progress: LoadProgress; }; @@ -35,38 +35,179 @@ export type P2PRequest = HybridLoaderRequestBase & { export type HybridLoaderRequest = HttpRequest | P2PRequest; -type RequestItem = { - segment: Readonly; - loaderRequest?: HybridLoaderRequest; - engineCallbacks?: Readonly; - prevAttempts: HybridLoaderRequest[]; +type RequestEvents = { + onCompleted: (request: Request, data: ArrayBuffer) => void; + onError: (request: Request, data: Error) => void; +}; + +type RequestStatus = + | "not-started" + | "loading" + | "succeed" + | "failed" + | "aborted"; + +export class Request extends EventDispatcher { + readonly id: string; + private _engineCallbacks?: EngineCallbacks; + private hybridLoaderRequest?: HybridLoaderRequest; + private prevAttempts: HybridLoaderRequest[] = []; + private chunks: Uint8Array[] = []; + private _loadedBytes = 0; + private _totalBytes?: number; + private _status: RequestStatus = "not-started"; + + constructor( + readonly segment: Segment, + private readonly bandwidthApproximator: BandwidthApproximator + ) { + super(); + this.id = getRequestItemId(segment); + } + + get status() { + return this._status; + } + + get isSegmentRequestedByEngine(): boolean { + return !!this._engineCallbacks; + } + + get type() { + return this.hybridLoaderRequest?.type; + } + + get loadedBytes() { + return this._loadedBytes; + } + + set engineCallbacks(callbacks: EngineCallbacks) { + if (this._engineCallbacks) { + throw new Error("Segment is already requested by engine"); + } + this._engineCallbacks = callbacks; + } + + get totalBytes(): number | undefined { + return this._totalBytes; + } + + setTotalBytes(value: number) { + if (this._totalBytes !== undefined) { + throw new Error("Request total bytes value is already set"); + } + this._totalBytes = value; + } + + get loadedPercent() { + if (!this._totalBytes) return; + return Utils.getPercent(this.loadedBytes, this._totalBytes); + } + + start(type: "http" | "p2p", abortLoading: () => void): RequestControls { + if (this._status === "loading") { + throw new Error("Request has been already started."); + } + + this._status = "loading"; + this.hybridLoaderRequest = { + type, + abort: abortLoading, + progress: { + loadedBytes: 0, + startTimestamp: performance.now(), + }, + }; + + return { + addLoadedChunk: this.addLoadedChunk, + completeOnSuccess: this.completeOnSuccess, + cancelOnError: this.cancelOnError, + }; + } + + abort() { + if (!this.hybridLoaderRequest) return; + this.hybridLoaderRequest.abort(); + this._status = "aborted"; + } + + private completeOnSuccess = () => { + this.throwErrorIfNotLoadingStatus(); + const data = Utils.joinChunks(this.chunks); + this._status = "succeed"; + this._engineCallbacks?.onSuccess({ + data, + bandwidth: this.bandwidthApproximator.getBandwidth(), + }); + this.dispatch("onCompleted", this, data); + }; + + private addLoadedChunk = (chunk: Uint8Array) => { + this.throwErrorIfNotLoadingStatus(); + const { hybridLoaderRequest: request } = this; + if (!request) return; + this.chunks.push(chunk); + request.progress.lastLoadedChunkTimestamp = performance.now(); + this._loadedBytes += chunk.length; + }; + + private cancelOnError = (error: Error) => { + this.throwErrorIfNotLoadingStatus(); + if (!this.hybridLoaderRequest) return; + this._status = "failed"; + this.hybridLoaderRequest.error = error; + this.prevAttempts.push(this.hybridLoaderRequest); + this.dispatch("onError", this, error); + }; + + private throwErrorIfNotLoadingStatus() { + if (this._status !== "loading") { + throw new Error("Request has been already completed/aborted/failed."); + } + } +} + +export type RequestControls = { + addLoadedChunk: Request["addLoadedChunk"]; + completeOnSuccess: Request["completeOnSuccess"]; + cancelOnError: Request["cancelOnError"]; }; function getRequestItemId(segment: Segment) { return segment.localId; } +type RequestsContainerEvents = { + httpRequestsUpdated: () => void; +}; + export class RequestsContainer { - private readonly requests = new Map(); - private readonly onHttpRequestsHandlers = new Subscriptions(); + private readonly requests = new Map(); private readonly logger: Debug.Debugger; + private readonly events = new EventDispatcher(); - constructor(streamType: StreamType) { + constructor( + streamType: StreamType, + private readonly bandwidthApproximator: BandwidthApproximator + ) { this.logger = Debug(`core:requests-container-${streamType}`); this.logger.color = "LightSeaGreen"; } - get httpRequestsCount() { + get executingHttpCount() { let count = 0; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const request of this.httpRequests()) count++; + for (const request of this.httpRequests()) { + if (request.status === "loading") count++; + } return count; } - get p2pRequestsCount() { + get executingP2PCount() { let count = 0; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const request of this.p2pRequests()) count++; + for (const request of this.p2pRequests()) { + if (request.status === "loading") count++; + } return count; } @@ -75,126 +216,60 @@ export class RequestsContainer { return this.requests.get(id); } - getHybridLoaderRequest(segment: Segment) { + getOrCreateRequest(segment: Segment) { const id = getRequestItemId(segment); - return this.requests.get(id)?.loaderRequest; + let request = this.requests.get(id); + if (!request) { + request = new Request(segment, this.bandwidthApproximator); + request.subscribe("onCompleted", this.onRequestCompleted); + this.requests.set(request.id, request); + } + return request; } - remove(segment: Segment) { - const id = getRequestItemId(segment); - this.requests.delete(id); - } + private onRequestCompleted: RequestEvents["onCompleted"] = (request) => { + this.requests.delete(request.id); + }; - addHybridLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { - const segmentId = getRequestItemId(segment); - const existingRequest = this.requests.get(segmentId); - if (existingRequest) { - existingRequest.loaderRequest = loaderRequest; - } else { - this.requests.set(segmentId, { - segment, - loaderRequest, - prevAttempts: [], - }); - } - this.logger( - `add loader request: ${loaderRequest.type} ${segment.externalId}` - ); - if (loaderRequest.type === "http") this.onHttpRequestsHandlers.fire(); - } - - addEngineCallbacks(segment: Segment, engineCallbacks: EngineCallbacks) { - const segmentId = getRequestItemId(segment); - const requestItem = this.requests.get(segmentId); - - if (requestItem) { - requestItem.engineCallbacks = engineCallbacks; - } else { - this.requests.set(segmentId, { - segment, - engineCallbacks, - prevAttempts: [], - }); - } - this.logger(`add engine request ${segment.externalId}`); + remove(value: Segment | Request) { + const id = value instanceof Request ? value.id : getRequestItemId(value); + this.requests.delete(id); } values() { return this.requests.values(); } - *httpRequests(): Generator { + *httpRequests(): Generator { for (const request of this.requests.values()) { - if (request.loaderRequest?.type === "http") yield request; + if (request.type === "http") yield request; } } - *p2pRequests(): Generator { + *p2pRequests(): Generator { for (const request of this.requests.values()) { - if (request.loaderRequest?.type === "p2p") yield request; + if (request.type === "p2p") yield request; } } - resolveAndRemoveRequest(segment: Segment, response: SegmentResponse) { - const id = getRequestItemId(segment); - const request = this.requests.get(id); - if (!request) return; - request.engineCallbacks?.onSuccess(response); - this.requests.delete(id); - } - isHttpRequested(segment: Segment): boolean { const id = getRequestItemId(segment); - return this.requests.get(id)?.loaderRequest?.type === "http"; + return this.requests.get(id)?.type === "http"; } isP2PRequested(segment: Segment): boolean { const id = getRequestItemId(segment); - return this.requests.get(id)?.loaderRequest?.type === "p2p"; + return this.requests.get(id)?.type === "p2p"; } isHybridLoaderRequested(segment: Segment): boolean { const id = getRequestItemId(segment); - return !!this.requests.get(id)?.loaderRequest; - } - - abortEngineRequest(segment: Segment) { - const id = getRequestItemId(segment); - const request = this.requests.get(id); - if (!request) return; - - // request.engineCallbacks?.onError(new RequestAbortError()); - request.loaderRequest?.abort(); - } - - abortLoaderRequest(segment: Segment) { - const id = getRequestItemId(segment); - this.requests.get(id)?.loaderRequest?.abort(); - } - - abortAllNotRequestedByEngine(isLocked?: (segment: Segment) => boolean) { - const isSegmentLocked = isLocked ? isLocked : () => false; - for (const { - loaderRequest, - engineCallbacks, - segment, - } of this.requests.values()) { - if (engineCallbacks || !loaderRequest) continue; - if (!isSegmentLocked(segment)) loaderRequest.abort(); - } - } - - subscribeOnHttpRequestsUpdate(handler: () => void) { - this.onHttpRequestsHandlers.add(handler); - } - - unsubscribeFromHttpRequestsUpdate(handler: () => void) { - this.onHttpRequestsHandlers.remove(handler); + return !!this.requests.get(id)?.type; } destroy() { for (const request of this.requests.values()) { - request.loaderRequest?.abort(); + request.abort(); request.engineCallbacks?.onError("failed"); } this.requests.clear(); diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index e8b1a1db..545ee965 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -1,4 +1,6 @@ import { Segment, Settings, Stream } from "./types"; +import { EventDispatcher } from "./event-dispatcher"; +import * as StreamUtils from "./utils/stream"; import Debug from "debug"; type StorageSettings = Pick< @@ -6,62 +8,29 @@ type StorageSettings = Pick< "cachedSegmentExpiration" | "cachedSegmentsCount" >; -function getStreamShortExternalId(stream: Readonly) { - const { type, index } = stream; - return `${type}-${index}`; -} - function getStorageItemId(segment: Segment) { - const streamExternalId = getStreamShortExternalId(segment.stream); + const streamExternalId = StreamUtils.getStreamShortId(segment.stream); return `${streamExternalId}|${segment.externalId}`; } -export class Subscriptions< - T extends (...args: unknown[]) => void = () => void -> { - private readonly list: Set; - - constructor(handlers?: T | T[]) { - if (handlers) { - this.list = new Set(Array.isArray(handlers) ? handlers : [handlers]); - } else { - this.list = new Set(); - } - } - - 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 = { segment: Segment; data: ArrayBuffer; lastAccessed: number; }; +type StorageEventHandlers = { + [key in `onStorageUpdated${string}`]: (steam: Stream) => void; +}; + export class SegmentsMemoryStorage { private cache = new Map(); private _isInitialized = false; private readonly isSegmentLockedPredicates: (( segment: Segment ) => boolean)[] = []; - private onUpdateHandlers = new Map(); private readonly logger: Debug.Debugger; + private readonly events = new EventDispatcher(); constructor( private readonly masterManifestUrl: string, @@ -90,14 +59,13 @@ export class SegmentsMemoryStorage { async storeSegment(segment: Segment, data: ArrayBuffer) { const id = getStorageItemId(segment); - const streamId = getStreamShortExternalId(segment.stream); this.cache.set(id, { segment, data, lastAccessed: performance.now(), }); this.logger(`add segment: ${id}`); - this.fireOnUpdateSubscriptions(streamId); + this.dispatchStorageUpdatedEvent(segment.stream); void this.clear(); } @@ -116,10 +84,10 @@ export class SegmentsMemoryStorage { } getStoredSegmentExternalIdsOfStream(stream: Stream) { - const streamId = getStreamShortExternalId(stream); + const streamId = StreamUtils.getStreamShortId(stream); const externalIds: string[] = []; for (const { segment } of this.cache.values()) { - const itemStreamId = getStreamShortExternalId(segment.stream); + const itemStreamId = StreamUtils.getStreamShortId(segment.stream); if (itemStreamId === streamId) externalIds.push(segment.externalId); } return externalIds; @@ -128,7 +96,7 @@ export class SegmentsMemoryStorage { private async clear(): Promise { const itemsToDelete: string[] = []; const remainingItems: [string, StorageItem][] = []; - const streamIdsOfChangedItems = new Set(); + const streamsOfChangedItems = new Set(); // Delete old segments const now = performance.now(); @@ -138,9 +106,8 @@ export class SegmentsMemoryStorage { 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); + streamsOfChangedItems.add(segment.stream); } } else { remainingItems.push(entry); @@ -155,9 +122,8 @@ export class SegmentsMemoryStorage { for (const [itemId, { segment }] of remainingItems) { if (!this.isSegmentLocked(segment)) { - const streamId = getStreamShortExternalId(segment.stream); itemsToDelete.push(itemId); - streamIdsOfChangedItems.add(streamId); + streamsOfChangedItems.add(segment.stream); countOverhead--; if (countOverhead === 0) break; } @@ -167,40 +133,39 @@ export class SegmentsMemoryStorage { if (itemsToDelete.length) { this.logger(`cleared ${itemsToDelete.length} segments`); itemsToDelete.forEach((id) => this.cache.delete(id)); - for (const streamId of streamIdsOfChangedItems) { - this.fireOnUpdateSubscriptions(streamId); + for (const stream of streamsOfChangedItems) { + this.dispatchStorageUpdatedEvent(stream); } } return itemsToDelete.length > 0; } - subscribeOnUpdate(stream: Stream, handler: () => void) { - const streamId = getStreamShortExternalId(stream); - const handlers = this.onUpdateHandlers.get(streamId); - if (!handlers) { - this.onUpdateHandlers.set(streamId, new Subscriptions(handler)); - } else { - handlers.add(handler); - } + subscribeOnUpdate( + stream: Stream, + listener: StorageEventHandlers["onStorageUpdated"] + ) { + const localId = StreamUtils.getStreamShortId(stream); + this.events.subscribe(`onStorageUpdated-${localId}`, listener); } - unsubscribeFromUpdate(stream: Stream, handler: () => void) { - const streamId = getStreamShortExternalId(stream); - const handlers = this.onUpdateHandlers.get(streamId); - if (handlers) { - handlers.remove(handler); - if (handlers.isEmpty) this.onUpdateHandlers.delete(streamId); - } + unsubscribeFromUpdate( + stream: Stream, + listener: StorageEventHandlers["onStorageUpdated"] + ) { + const localId = StreamUtils.getStreamShortId(stream); + this.events.unsubscribe(`onStorageUpdated-${localId}`, listener); } - private fireOnUpdateSubscriptions(streamId: string) { - this.onUpdateHandlers.get(streamId)?.fire(); + private dispatchStorageUpdatedEvent(stream: Stream) { + this.events.dispatch( + `onStorageUpdated${StreamUtils.getStreamShortId(stream)}`, + stream + ); } public async destroy() { this.cache.clear(); - this.onUpdateHandlers.clear(); this._isInitialized = false; } } diff --git a/packages/p2p-media-loader-core/src/utils/stream.ts b/packages/p2p-media-loader-core/src/utils/stream.ts new file mode 100644 index 00000000..5845e0ac --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/stream.ts @@ -0,0 +1,34 @@ +import { Segment, Stream, StreamWithSegments } from "../types"; + +const PEER_PROTOCOL_VERSION = "V1"; + +export function getStreamExternalId( + manifestResponseUrl: string, + stream: Readonly +): string { + const { type, index } = stream; + return `${PEER_PROTOCOL_VERSION}:${manifestResponseUrl}-${type}-${index}`; +} + +export function getSegmentFromStreamsMap( + streams: Map, + segmentId: string +): Segment | undefined { + for (const stream of streams.values()) { + const segment = stream.segments.get(segmentId); + 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; + } +} + +export function getStreamShortId(stream: Stream) { + return `${stream.type}-${stream.index}`; +} diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index 7267ac7a..8db9b2bb 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -1,34 +1,3 @@ -import { Segment, Stream, StreamWithSegments } from "../index"; - -const PEER_PROTOCOL_VERSION = "V1"; - -export function getStreamExternalId( - manifestResponseUrl: string, - stream: Readonly -): string { - const { type, index } = stream; - return `${PEER_PROTOCOL_VERSION}:${manifestResponseUrl}-${type}-${index}`; -} - -export function getSegmentFromStreamsMap( - streams: Map, - segmentId: string -): Segment | undefined { - for (const stream of streams.values()) { - const segment = stream.segments.get(segmentId); - 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; - } -} - export function getControlledPromise() { let resolve: (value: T) => void; let reject: (reason?: unknown) => void; @@ -62,3 +31,7 @@ export function joinChunks( return buffer; } + +export function getPercent(numerator: number, denominator: number): number { + return (numerator / denominator) * 100; +} From eb1ba54730d562a317250236465060a9082b016d Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 13 Nov 2023 17:52:35 +0200 Subject: [PATCH 05/22] Move Request file to separate file. --- .../p2p-media-loader-core/src/http-loader.ts | 10 +- .../src/hybrid-loader.ts | 69 +------ .../p2p-media-loader-core/src/p2p/loader.ts | 16 +- .../p2p-media-loader-core/src/p2p/peer.ts | 2 +- .../src/request-container.ts | 191 +----------------- packages/p2p-media-loader-core/src/request.ts | 183 +++++++++++++++++ .../p2p-media-loader-core/src/utils/stream.ts | 17 ++ .../p2p-media-loader-core/src/utils/utils.ts | 4 + 8 files changed, 233 insertions(+), 259 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 fb58d23f..10011317 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -35,15 +35,9 @@ export async function fulfillHttpSegmentRequest( }); if (fetchResponse.ok) { - const totalBytesString = fetchResponse.headers.get("Content-Length"); - if (!fetchResponse.body) { - fetchResponse.arrayBuffer().then((data) => { - requestControls.addLoadedChunk(data); - requestControls.completeOnSuccess(); - }); - return; - } + if (!fetchResponse.body) return; + const totalBytesString = fetchResponse.headers.get("Content-Length"); if (totalBytesString) request.setTotalBytes(+totalBytesString); const reader = fetchResponse.body.getReader(); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index f0f4f415..3f5cc42a 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -4,14 +4,12 @@ import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; -import { - RequestsContainer, - EngineCallbacks, - HybridLoaderRequest, - Request, -} from "./request-container"; +import { RequestsContainer } from "./request-container"; +import { Request, EngineCallbacks, HybridLoaderRequest } from "./request"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; +import * as StreamUtils from "./utils/stream"; +import * as Utils from "./utils/utils"; import { P2PLoadersContainer } from "./p2p/loaders-container"; import { PeerRequestError } from "./p2p/peer"; import debug from "debug"; @@ -40,7 +38,7 @@ export class HybridLoader { this.lastRequestedSegment = requestedSegment; const activeStream = requestedSegment.stream; this.playback = { position: requestedSegment.startTime, rate: 1 }; - this.segmentAvgDuration = getSegmentAvgDuration(activeStream); + this.segmentAvgDuration = StreamUtils.getSegmentAvgDuration(activeStream); this.requests = new RequestsContainer( requestedSegment.stream.type, this.bandwidthApproximator @@ -159,23 +157,6 @@ export class HybridLoader { if (statuses.isHighDemand) { if (request?.type === "http") continue; - - if (request?.type === "p2p") { - const timeToPlayback = getTimeToSegmentPlayback( - segment, - this.playback - ); - const remainingDownloadTime = - getPredictedRemainingDownloadTime(request); - if ( - remainingDownloadTime === undefined || - remainingDownloadTime > timeToPlayback - ) { - request.abort(); - } else { - continue; - } - } if (this.requests.executingHttpCount < simultaneousHttpDownloads) { void this.loadThroughHttp(item); continue; @@ -218,8 +199,11 @@ export class HybridLoader { // api method for engines abortSegment(segment: Segment) { + const request = this.requests.get(segment); + if (!request) return; + request.abort(); + this.createProcessQueueMicrotask(); this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); - this.requests.abortEngineRequest(segment); } private async loadThroughHttp(item: QueueItem, isRandom = false) { @@ -286,7 +270,7 @@ export class HybridLoader { const shouldLoad = Math.random() < probability; if (!shouldLoad) return; - const item = queue[Math.floor(Math.random() * queue.length)]; + const item = Utils.getRandomItem(queue); void this.loadThroughHttp(item, true); this.logger.loader( @@ -387,36 +371,3 @@ function* arrayBackwards(arr: T[]) { yield arr[i]; } } - -function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { - return Math.max(segment.startTime - playback.position, 0) / playback.rate; -} - -function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { - const { progress } = request; - if (!progress || progress.lastLoadedChunkTimestamp === undefined) { - return undefined; - } - - const now = performance.now(); - const bandwidth = - progress.percent / - (progress.lastLoadedChunkTimestamp - progress.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; - let sumDuration = 0; - const size = segments.size; - for (const segment of segments.values()) { - const duration = segment.endTime - segment.startTime; - sumDuration += duration; - } - - return sumDuration / size; -} diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index 619492c6..a8e04762 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -4,10 +4,12 @@ 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 * as StreamUtils from "../utils/stream"; +import * as Utils from "../utils/utils"; import { PeerSegmentStatus } from "../enums"; -import { Request, RequestsContainer } from "../request-container"; +import { RequestsContainer } from "../request-container"; +import { Request } from "../request"; import debug from "debug"; export class P2PLoader { @@ -26,7 +28,7 @@ export class P2PLoader { private readonly settings: Settings ) { this.peerId = PeerUtil.generatePeerId(); - const streamExternalId = Utils.getStreamExternalId( + const streamExternalId = StreamUtils.getStreamExternalId( this.streamManifestUrl, this.stream ); @@ -108,7 +110,7 @@ export class P2PLoader { } const peer = untestedPeers.length - ? getRandomItem(untestedPeers) + ? Utils.getRandomItem(untestedPeers) : fastestPeer; if (!peer) return; @@ -182,7 +184,7 @@ export class P2PLoader { }; private async onSegmentRequested(peer: Peer, segmentExternalId: string) { - const segment = Utils.getSegmentFromStreamByExternalId( + const segment = StreamUtils.getSegmentFromStreamByExternalId( this.stream, segmentExternalId ); @@ -245,7 +247,3 @@ 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/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index cb9da03f..d3d52594 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 { Request, RequestControls } from "../request-container"; +import { Request, RequestControls } from "../request"; 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-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 2185bf26..03f41338 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,182 +1,8 @@ -import { Segment, SegmentResponse, StreamType } from "./types"; -import { PeerRequestError } from "./p2p/peer"; -import { HttpLoaderError } from "./http-loader"; +import { Segment, StreamType } from "./types"; import Debug from "debug"; import { EventDispatcher } from "./event-dispatcher"; -import * as Utils from "./utils/utils"; import { BandwidthApproximator } from "./bandwidth-approximator"; - -export type EngineCallbacks = { - onSuccess: (response: SegmentResponse) => void; - onError: (reason: "failed" | "abort") => void; -}; - -export type LoadProgress = { - startTimestamp: number; - lastLoadedChunkTimestamp?: number; - loadedBytes: number; - totalBytes?: number; -}; - -type HybridLoaderRequestBase = { - abort: () => void; - progress: LoadProgress; -}; - -export type HttpRequest = HybridLoaderRequestBase & { - type: "http"; - error?: HttpLoaderError; -}; - -export type P2PRequest = HybridLoaderRequestBase & { - type: "p2p"; - error?: PeerRequestError; -}; - -export type HybridLoaderRequest = HttpRequest | P2PRequest; - -type RequestEvents = { - onCompleted: (request: Request, data: ArrayBuffer) => void; - onError: (request: Request, data: Error) => void; -}; - -type RequestStatus = - | "not-started" - | "loading" - | "succeed" - | "failed" - | "aborted"; - -export class Request extends EventDispatcher { - readonly id: string; - private _engineCallbacks?: EngineCallbacks; - private hybridLoaderRequest?: HybridLoaderRequest; - private prevAttempts: HybridLoaderRequest[] = []; - private chunks: Uint8Array[] = []; - private _loadedBytes = 0; - private _totalBytes?: number; - private _status: RequestStatus = "not-started"; - - constructor( - readonly segment: Segment, - private readonly bandwidthApproximator: BandwidthApproximator - ) { - super(); - this.id = getRequestItemId(segment); - } - - get status() { - return this._status; - } - - get isSegmentRequestedByEngine(): boolean { - return !!this._engineCallbacks; - } - - get type() { - return this.hybridLoaderRequest?.type; - } - - get loadedBytes() { - return this._loadedBytes; - } - - set engineCallbacks(callbacks: EngineCallbacks) { - if (this._engineCallbacks) { - throw new Error("Segment is already requested by engine"); - } - this._engineCallbacks = callbacks; - } - - get totalBytes(): number | undefined { - return this._totalBytes; - } - - setTotalBytes(value: number) { - if (this._totalBytes !== undefined) { - throw new Error("Request total bytes value is already set"); - } - this._totalBytes = value; - } - - get loadedPercent() { - if (!this._totalBytes) return; - return Utils.getPercent(this.loadedBytes, this._totalBytes); - } - - start(type: "http" | "p2p", abortLoading: () => void): RequestControls { - if (this._status === "loading") { - throw new Error("Request has been already started."); - } - - this._status = "loading"; - this.hybridLoaderRequest = { - type, - abort: abortLoading, - progress: { - loadedBytes: 0, - startTimestamp: performance.now(), - }, - }; - - return { - addLoadedChunk: this.addLoadedChunk, - completeOnSuccess: this.completeOnSuccess, - cancelOnError: this.cancelOnError, - }; - } - - abort() { - if (!this.hybridLoaderRequest) return; - this.hybridLoaderRequest.abort(); - this._status = "aborted"; - } - - private completeOnSuccess = () => { - this.throwErrorIfNotLoadingStatus(); - const data = Utils.joinChunks(this.chunks); - this._status = "succeed"; - this._engineCallbacks?.onSuccess({ - data, - bandwidth: this.bandwidthApproximator.getBandwidth(), - }); - this.dispatch("onCompleted", this, data); - }; - - private addLoadedChunk = (chunk: Uint8Array) => { - this.throwErrorIfNotLoadingStatus(); - const { hybridLoaderRequest: request } = this; - if (!request) return; - this.chunks.push(chunk); - request.progress.lastLoadedChunkTimestamp = performance.now(); - this._loadedBytes += chunk.length; - }; - - private cancelOnError = (error: Error) => { - this.throwErrorIfNotLoadingStatus(); - if (!this.hybridLoaderRequest) return; - this._status = "failed"; - this.hybridLoaderRequest.error = error; - this.prevAttempts.push(this.hybridLoaderRequest); - this.dispatch("onError", this, error); - }; - - private throwErrorIfNotLoadingStatus() { - if (this._status !== "loading") { - throw new Error("Request has been already completed/aborted/failed."); - } - } -} - -export type RequestControls = { - addLoadedChunk: Request["addLoadedChunk"]; - completeOnSuccess: Request["completeOnSuccess"]; - cancelOnError: Request["cancelOnError"]; -}; - -function getRequestItemId(segment: Segment) { - return segment.localId; -} +import { Request, RequestEvents } from "./request"; type RequestsContainerEvents = { httpRequestsUpdated: () => void; @@ -212,12 +38,12 @@ export class RequestsContainer { } get(segment: Segment) { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return this.requests.get(id); } getOrCreateRequest(segment: Segment) { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); let request = this.requests.get(id); if (!request) { request = new Request(segment, this.bandwidthApproximator); @@ -232,7 +58,8 @@ export class RequestsContainer { }; remove(value: Segment | Request) { - const id = value instanceof Request ? value.id : getRequestItemId(value); + const id = + value instanceof Request ? value.id : Request.getRequestItemId(value); this.requests.delete(id); } @@ -253,17 +80,17 @@ export class RequestsContainer { } isHttpRequested(segment: Segment): boolean { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return this.requests.get(id)?.type === "http"; } isP2PRequested(segment: Segment): boolean { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return this.requests.get(id)?.type === "p2p"; } isHybridLoaderRequested(segment: Segment): boolean { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return !!this.requests.get(id)?.type; } 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..b20a1057 --- /dev/null +++ b/packages/p2p-media-loader-core/src/request.ts @@ -0,0 +1,183 @@ +import { EventDispatcher } from "./event-dispatcher"; +import { Segment, SegmentResponse } from "./types"; +import { BandwidthApproximator } from "./bandwidth-approximator"; +import * as Utils from "./utils/utils"; +import { HttpLoaderError } from "./http-loader"; +import { PeerRequestError } from "./p2p/peer"; + +export type EngineCallbacks = { + onSuccess: (response: SegmentResponse) => void; + onError: (reason: "failed" | "abort") => void; +}; + +export type LoadProgress = { + startTimestamp: number; + lastLoadedChunkTimestamp?: number; + loadedBytes: number; + totalBytes?: number; +}; + +type HybridLoaderRequestBase = { + abort: () => void; + progress: LoadProgress; +}; + +type HttpRequest = HybridLoaderRequestBase & { + type: "http"; + error?: HttpLoaderError; +}; + +type P2PRequest = HybridLoaderRequestBase & { + type: "p2p"; + error?: PeerRequestError; +}; + +export type HybridLoaderRequest = HttpRequest | P2PRequest; + +export type RequestEvents = { + onCompleted: (request: Request, data: ArrayBuffer) => void; + onError: (request: Request, data: Error) => void; +}; + +export type RequestControls = { + addLoadedChunk: Request["addLoadedChunk"]; + completeOnSuccess: Request["completeOnSuccess"]; + cancelOnError: Request["cancelOnError"]; +}; + +type RequestStatus = + | "not-started" + | "loading" + | "succeed" + | "failed" + | "aborted"; + +export class Request extends EventDispatcher { + readonly id: string; + private _engineCallbacks?: EngineCallbacks; + private hybridLoaderRequest?: HybridLoaderRequest; + private prevAttempts: HybridLoaderRequest[] = []; + private chunks: Uint8Array[] = []; + private _loadedBytes = 0; + private _totalBytes?: number; + private _status: RequestStatus = "not-started"; + + constructor( + readonly segment: Segment, + private readonly bandwidthApproximator: BandwidthApproximator + ) { + super(); + this.id = Request.getRequestItemId(segment); + } + + get status() { + return this._status; + } + + get isSegmentRequestedByEngine(): boolean { + return !!this._engineCallbacks; + } + + get type() { + return this.hybridLoaderRequest?.type; + } + + get loadedBytes() { + return this._loadedBytes; + } + + set engineCallbacks(callbacks: EngineCallbacks) { + if (this._engineCallbacks) { + throw new Error("Segment is already requested by engine"); + } + this._engineCallbacks = callbacks; + } + + get totalBytes(): number | undefined { + return this._totalBytes; + } + + setTotalBytes(value: number) { + if (this._totalBytes !== undefined) { + throw new Error("Request total bytes value is already set"); + } + this._totalBytes = value; + } + + get loadedPercent() { + if (!this._totalBytes) return; + return Utils.getPercent(this.loadedBytes, this._totalBytes); + } + + start(type: "http" | "p2p", abortLoading: () => void): RequestControls { + if (this._status === "loading") { + throw new Error("Request has been already started."); + } + + this._status = "loading"; + this.hybridLoaderRequest = { + type, + abort: abortLoading, + progress: { + loadedBytes: 0, + startTimestamp: performance.now(), + }, + }; + + return { + addLoadedChunk: this.addLoadedChunk, + completeOnSuccess: this.completeOnSuccess, + cancelOnError: this.cancelOnError, + }; + } + + abort() { + if (!this.hybridLoaderRequest) return; + this.hybridLoaderRequest.abort(); + this._status = "aborted"; + } + + abortEngineRequest() { + this._engineCallbacks?.onError("abort"); + this._engineCallbacks = undefined; + } + + private completeOnSuccess = () => { + this.throwErrorIfNotLoadingStatus(); + const data = Utils.joinChunks(this.chunks); + this._status = "succeed"; + this._engineCallbacks?.onSuccess({ + data, + bandwidth: this.bandwidthApproximator.getBandwidth(), + }); + this.dispatch("onCompleted", this, data); + }; + + private addLoadedChunk = (chunk: Uint8Array) => { + this.throwErrorIfNotLoadingStatus(); + const { hybridLoaderRequest: request } = this; + if (!request) return; + this.chunks.push(chunk); + request.progress.lastLoadedChunkTimestamp = performance.now(); + this._loadedBytes += chunk.length; + }; + + private cancelOnError = (error: Error) => { + this.throwErrorIfNotLoadingStatus(); + if (!this.hybridLoaderRequest) return; + this._status = "failed"; + this.hybridLoaderRequest.error = error; + this.prevAttempts.push(this.hybridLoaderRequest); + this.dispatch("onError", this, error); + }; + + private throwErrorIfNotLoadingStatus() { + if (this._status !== "loading") { + throw new Error("Request has been already completed/aborted/failed."); + } + } + + static getRequestItemId(segment: Segment) { + return segment.localId; + } +} diff --git a/packages/p2p-media-loader-core/src/utils/stream.ts b/packages/p2p-media-loader-core/src/utils/stream.ts index 5845e0ac..39d63cba 100644 --- a/packages/p2p-media-loader-core/src/utils/stream.ts +++ b/packages/p2p-media-loader-core/src/utils/stream.ts @@ -1,4 +1,5 @@ import { Segment, Stream, StreamWithSegments } from "../types"; +import { Playback } from "../internal-types"; const PEER_PROTOCOL_VERSION = "V1"; @@ -32,3 +33,19 @@ export function getSegmentFromStreamByExternalId( export function getStreamShortId(stream: Stream) { return `${stream.type}-${stream.index}`; } + +export 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; +} + +export function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { + return Math.max(segment.startTime - playback.position, 0) / playback.rate; +} diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index 8db9b2bb..a47d2a18 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -35,3 +35,7 @@ export function joinChunks( export function getPercent(numerator: number, denominator: number): number { return (numerator / denominator) * 100; } + +export function getRandomItem(items: T[]): T { + return items[Math.floor(Math.random() * items.length)]; +} From 51fc027b503483825f78552ab825335d7c74843b Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Mon, 13 Nov 2023 17:52:35 +0200 Subject: [PATCH 06/22] Change style of bittorrent-tracker declarations event handlers types. --- .../src/declarations.d.ts | 47 ++--- .../p2p-media-loader-core/src/http-loader.ts | 10 +- .../src/hybrid-loader.ts | 69 +------ .../p2p-media-loader-core/src/p2p/loader.ts | 16 +- .../p2p-media-loader-core/src/p2p/peer.ts | 2 +- .../src/request-container.ts | 191 +----------------- packages/p2p-media-loader-core/src/request.ts | 183 +++++++++++++++++ .../p2p-media-loader-core/src/utils/stream.ts | 17 ++ .../p2p-media-loader-core/src/utils/utils.ts | 4 + 9 files changed, 251 insertions(+), 288 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/request.ts diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts index de428c70..65768074 100644 --- a/packages/p2p-media-loader-core/src/declarations.d.ts +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -9,7 +9,10 @@ declare module "bittorrent-tracker" { getAnnounceOpts?: () => object; }); - on(event: E, handler: TrackerEventHandler): void; + on( + event: E, + handler: TrackerClientEvents[E] + ): void; start(): void; @@ -20,39 +23,25 @@ declare module "bittorrent-tracker" { destroy(): void; } - export type TrackerEvent = "update" | "peer" | "warning" | "error"; - - export type TrackerEventHandler = E extends "update" - ? (data: object) => void - : E extends "peer" - ? (peer: PeerConnection) => void - : E extends "warning" - ? (warning: unknown) => void - : E extends "error" - ? (error: unknown) => void - : never; - - type PeerEvent = "connect" | "data" | "close" | "error"; - - export type PeerConnectionEventHandler = - E extends "connect" - ? () => void - : E extends "data" - ? (data: Uint8Array) => void - : E extends "close" - ? () => void - : E extends "error" - ? (error: { code: string }) => void - : never; + export type TrackerClientEvents = { + update: (data: object) => void; + peer: (peer: PeerConnection) => void; + warning: (warning: unknown) => void; + error: (error: unknown) => void; + }; + + export type PeerEvents = { + connect: () => void; + data: (data: Uint8Array) => void; + close: () => void; + error: (error: { code: string }) => void; + }; export type PeerConnection = { id: string; initiator: boolean; _channel: RTCDataChannel; - on( - event: E, - handler: PeerConnectionEventHandler - ): void; + on(event: E, handler: PeerEvents[E]): void; send(data: string | ArrayBuffer): void; write(data: string | ArrayBuffer): void; 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 fb58d23f..10011317 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -35,15 +35,9 @@ export async function fulfillHttpSegmentRequest( }); if (fetchResponse.ok) { - const totalBytesString = fetchResponse.headers.get("Content-Length"); - if (!fetchResponse.body) { - fetchResponse.arrayBuffer().then((data) => { - requestControls.addLoadedChunk(data); - requestControls.completeOnSuccess(); - }); - return; - } + if (!fetchResponse.body) return; + const totalBytesString = fetchResponse.headers.get("Content-Length"); if (totalBytesString) request.setTotalBytes(+totalBytesString); const reader = fetchResponse.body.getReader(); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index f0f4f415..3f5cc42a 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -4,14 +4,12 @@ import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; -import { - RequestsContainer, - EngineCallbacks, - HybridLoaderRequest, - Request, -} from "./request-container"; +import { RequestsContainer } from "./request-container"; +import { Request, EngineCallbacks, HybridLoaderRequest } from "./request"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; +import * as StreamUtils from "./utils/stream"; +import * as Utils from "./utils/utils"; import { P2PLoadersContainer } from "./p2p/loaders-container"; import { PeerRequestError } from "./p2p/peer"; import debug from "debug"; @@ -40,7 +38,7 @@ export class HybridLoader { this.lastRequestedSegment = requestedSegment; const activeStream = requestedSegment.stream; this.playback = { position: requestedSegment.startTime, rate: 1 }; - this.segmentAvgDuration = getSegmentAvgDuration(activeStream); + this.segmentAvgDuration = StreamUtils.getSegmentAvgDuration(activeStream); this.requests = new RequestsContainer( requestedSegment.stream.type, this.bandwidthApproximator @@ -159,23 +157,6 @@ export class HybridLoader { if (statuses.isHighDemand) { if (request?.type === "http") continue; - - if (request?.type === "p2p") { - const timeToPlayback = getTimeToSegmentPlayback( - segment, - this.playback - ); - const remainingDownloadTime = - getPredictedRemainingDownloadTime(request); - if ( - remainingDownloadTime === undefined || - remainingDownloadTime > timeToPlayback - ) { - request.abort(); - } else { - continue; - } - } if (this.requests.executingHttpCount < simultaneousHttpDownloads) { void this.loadThroughHttp(item); continue; @@ -218,8 +199,11 @@ export class HybridLoader { // api method for engines abortSegment(segment: Segment) { + const request = this.requests.get(segment); + if (!request) return; + request.abort(); + this.createProcessQueueMicrotask(); this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); - this.requests.abortEngineRequest(segment); } private async loadThroughHttp(item: QueueItem, isRandom = false) { @@ -286,7 +270,7 @@ export class HybridLoader { const shouldLoad = Math.random() < probability; if (!shouldLoad) return; - const item = queue[Math.floor(Math.random() * queue.length)]; + const item = Utils.getRandomItem(queue); void this.loadThroughHttp(item, true); this.logger.loader( @@ -387,36 +371,3 @@ function* arrayBackwards(arr: T[]) { yield arr[i]; } } - -function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { - return Math.max(segment.startTime - playback.position, 0) / playback.rate; -} - -function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { - const { progress } = request; - if (!progress || progress.lastLoadedChunkTimestamp === undefined) { - return undefined; - } - - const now = performance.now(); - const bandwidth = - progress.percent / - (progress.lastLoadedChunkTimestamp - progress.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; - let sumDuration = 0; - const size = segments.size; - for (const segment of segments.values()) { - const duration = segment.endTime - segment.startTime; - sumDuration += duration; - } - - return sumDuration / size; -} diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index 619492c6..a8e04762 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -4,10 +4,12 @@ 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 * as StreamUtils from "../utils/stream"; +import * as Utils from "../utils/utils"; import { PeerSegmentStatus } from "../enums"; -import { Request, RequestsContainer } from "../request-container"; +import { RequestsContainer } from "../request-container"; +import { Request } from "../request"; import debug from "debug"; export class P2PLoader { @@ -26,7 +28,7 @@ export class P2PLoader { private readonly settings: Settings ) { this.peerId = PeerUtil.generatePeerId(); - const streamExternalId = Utils.getStreamExternalId( + const streamExternalId = StreamUtils.getStreamExternalId( this.streamManifestUrl, this.stream ); @@ -108,7 +110,7 @@ export class P2PLoader { } const peer = untestedPeers.length - ? getRandomItem(untestedPeers) + ? Utils.getRandomItem(untestedPeers) : fastestPeer; if (!peer) return; @@ -182,7 +184,7 @@ export class P2PLoader { }; private async onSegmentRequested(peer: Peer, segmentExternalId: string) { - const segment = Utils.getSegmentFromStreamByExternalId( + const segment = StreamUtils.getSegmentFromStreamByExternalId( this.stream, segmentExternalId ); @@ -245,7 +247,3 @@ 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/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index cb9da03f..d3d52594 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 { Request, RequestControls } from "../request-container"; +import { Request, RequestControls } from "../request"; 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-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 2185bf26..03f41338 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,182 +1,8 @@ -import { Segment, SegmentResponse, StreamType } from "./types"; -import { PeerRequestError } from "./p2p/peer"; -import { HttpLoaderError } from "./http-loader"; +import { Segment, StreamType } from "./types"; import Debug from "debug"; import { EventDispatcher } from "./event-dispatcher"; -import * as Utils from "./utils/utils"; import { BandwidthApproximator } from "./bandwidth-approximator"; - -export type EngineCallbacks = { - onSuccess: (response: SegmentResponse) => void; - onError: (reason: "failed" | "abort") => void; -}; - -export type LoadProgress = { - startTimestamp: number; - lastLoadedChunkTimestamp?: number; - loadedBytes: number; - totalBytes?: number; -}; - -type HybridLoaderRequestBase = { - abort: () => void; - progress: LoadProgress; -}; - -export type HttpRequest = HybridLoaderRequestBase & { - type: "http"; - error?: HttpLoaderError; -}; - -export type P2PRequest = HybridLoaderRequestBase & { - type: "p2p"; - error?: PeerRequestError; -}; - -export type HybridLoaderRequest = HttpRequest | P2PRequest; - -type RequestEvents = { - onCompleted: (request: Request, data: ArrayBuffer) => void; - onError: (request: Request, data: Error) => void; -}; - -type RequestStatus = - | "not-started" - | "loading" - | "succeed" - | "failed" - | "aborted"; - -export class Request extends EventDispatcher { - readonly id: string; - private _engineCallbacks?: EngineCallbacks; - private hybridLoaderRequest?: HybridLoaderRequest; - private prevAttempts: HybridLoaderRequest[] = []; - private chunks: Uint8Array[] = []; - private _loadedBytes = 0; - private _totalBytes?: number; - private _status: RequestStatus = "not-started"; - - constructor( - readonly segment: Segment, - private readonly bandwidthApproximator: BandwidthApproximator - ) { - super(); - this.id = getRequestItemId(segment); - } - - get status() { - return this._status; - } - - get isSegmentRequestedByEngine(): boolean { - return !!this._engineCallbacks; - } - - get type() { - return this.hybridLoaderRequest?.type; - } - - get loadedBytes() { - return this._loadedBytes; - } - - set engineCallbacks(callbacks: EngineCallbacks) { - if (this._engineCallbacks) { - throw new Error("Segment is already requested by engine"); - } - this._engineCallbacks = callbacks; - } - - get totalBytes(): number | undefined { - return this._totalBytes; - } - - setTotalBytes(value: number) { - if (this._totalBytes !== undefined) { - throw new Error("Request total bytes value is already set"); - } - this._totalBytes = value; - } - - get loadedPercent() { - if (!this._totalBytes) return; - return Utils.getPercent(this.loadedBytes, this._totalBytes); - } - - start(type: "http" | "p2p", abortLoading: () => void): RequestControls { - if (this._status === "loading") { - throw new Error("Request has been already started."); - } - - this._status = "loading"; - this.hybridLoaderRequest = { - type, - abort: abortLoading, - progress: { - loadedBytes: 0, - startTimestamp: performance.now(), - }, - }; - - return { - addLoadedChunk: this.addLoadedChunk, - completeOnSuccess: this.completeOnSuccess, - cancelOnError: this.cancelOnError, - }; - } - - abort() { - if (!this.hybridLoaderRequest) return; - this.hybridLoaderRequest.abort(); - this._status = "aborted"; - } - - private completeOnSuccess = () => { - this.throwErrorIfNotLoadingStatus(); - const data = Utils.joinChunks(this.chunks); - this._status = "succeed"; - this._engineCallbacks?.onSuccess({ - data, - bandwidth: this.bandwidthApproximator.getBandwidth(), - }); - this.dispatch("onCompleted", this, data); - }; - - private addLoadedChunk = (chunk: Uint8Array) => { - this.throwErrorIfNotLoadingStatus(); - const { hybridLoaderRequest: request } = this; - if (!request) return; - this.chunks.push(chunk); - request.progress.lastLoadedChunkTimestamp = performance.now(); - this._loadedBytes += chunk.length; - }; - - private cancelOnError = (error: Error) => { - this.throwErrorIfNotLoadingStatus(); - if (!this.hybridLoaderRequest) return; - this._status = "failed"; - this.hybridLoaderRequest.error = error; - this.prevAttempts.push(this.hybridLoaderRequest); - this.dispatch("onError", this, error); - }; - - private throwErrorIfNotLoadingStatus() { - if (this._status !== "loading") { - throw new Error("Request has been already completed/aborted/failed."); - } - } -} - -export type RequestControls = { - addLoadedChunk: Request["addLoadedChunk"]; - completeOnSuccess: Request["completeOnSuccess"]; - cancelOnError: Request["cancelOnError"]; -}; - -function getRequestItemId(segment: Segment) { - return segment.localId; -} +import { Request, RequestEvents } from "./request"; type RequestsContainerEvents = { httpRequestsUpdated: () => void; @@ -212,12 +38,12 @@ export class RequestsContainer { } get(segment: Segment) { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return this.requests.get(id); } getOrCreateRequest(segment: Segment) { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); let request = this.requests.get(id); if (!request) { request = new Request(segment, this.bandwidthApproximator); @@ -232,7 +58,8 @@ export class RequestsContainer { }; remove(value: Segment | Request) { - const id = value instanceof Request ? value.id : getRequestItemId(value); + const id = + value instanceof Request ? value.id : Request.getRequestItemId(value); this.requests.delete(id); } @@ -253,17 +80,17 @@ export class RequestsContainer { } isHttpRequested(segment: Segment): boolean { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return this.requests.get(id)?.type === "http"; } isP2PRequested(segment: Segment): boolean { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return this.requests.get(id)?.type === "p2p"; } isHybridLoaderRequested(segment: Segment): boolean { - const id = getRequestItemId(segment); + const id = Request.getRequestItemId(segment); return !!this.requests.get(id)?.type; } 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..b20a1057 --- /dev/null +++ b/packages/p2p-media-loader-core/src/request.ts @@ -0,0 +1,183 @@ +import { EventDispatcher } from "./event-dispatcher"; +import { Segment, SegmentResponse } from "./types"; +import { BandwidthApproximator } from "./bandwidth-approximator"; +import * as Utils from "./utils/utils"; +import { HttpLoaderError } from "./http-loader"; +import { PeerRequestError } from "./p2p/peer"; + +export type EngineCallbacks = { + onSuccess: (response: SegmentResponse) => void; + onError: (reason: "failed" | "abort") => void; +}; + +export type LoadProgress = { + startTimestamp: number; + lastLoadedChunkTimestamp?: number; + loadedBytes: number; + totalBytes?: number; +}; + +type HybridLoaderRequestBase = { + abort: () => void; + progress: LoadProgress; +}; + +type HttpRequest = HybridLoaderRequestBase & { + type: "http"; + error?: HttpLoaderError; +}; + +type P2PRequest = HybridLoaderRequestBase & { + type: "p2p"; + error?: PeerRequestError; +}; + +export type HybridLoaderRequest = HttpRequest | P2PRequest; + +export type RequestEvents = { + onCompleted: (request: Request, data: ArrayBuffer) => void; + onError: (request: Request, data: Error) => void; +}; + +export type RequestControls = { + addLoadedChunk: Request["addLoadedChunk"]; + completeOnSuccess: Request["completeOnSuccess"]; + cancelOnError: Request["cancelOnError"]; +}; + +type RequestStatus = + | "not-started" + | "loading" + | "succeed" + | "failed" + | "aborted"; + +export class Request extends EventDispatcher { + readonly id: string; + private _engineCallbacks?: EngineCallbacks; + private hybridLoaderRequest?: HybridLoaderRequest; + private prevAttempts: HybridLoaderRequest[] = []; + private chunks: Uint8Array[] = []; + private _loadedBytes = 0; + private _totalBytes?: number; + private _status: RequestStatus = "not-started"; + + constructor( + readonly segment: Segment, + private readonly bandwidthApproximator: BandwidthApproximator + ) { + super(); + this.id = Request.getRequestItemId(segment); + } + + get status() { + return this._status; + } + + get isSegmentRequestedByEngine(): boolean { + return !!this._engineCallbacks; + } + + get type() { + return this.hybridLoaderRequest?.type; + } + + get loadedBytes() { + return this._loadedBytes; + } + + set engineCallbacks(callbacks: EngineCallbacks) { + if (this._engineCallbacks) { + throw new Error("Segment is already requested by engine"); + } + this._engineCallbacks = callbacks; + } + + get totalBytes(): number | undefined { + return this._totalBytes; + } + + setTotalBytes(value: number) { + if (this._totalBytes !== undefined) { + throw new Error("Request total bytes value is already set"); + } + this._totalBytes = value; + } + + get loadedPercent() { + if (!this._totalBytes) return; + return Utils.getPercent(this.loadedBytes, this._totalBytes); + } + + start(type: "http" | "p2p", abortLoading: () => void): RequestControls { + if (this._status === "loading") { + throw new Error("Request has been already started."); + } + + this._status = "loading"; + this.hybridLoaderRequest = { + type, + abort: abortLoading, + progress: { + loadedBytes: 0, + startTimestamp: performance.now(), + }, + }; + + return { + addLoadedChunk: this.addLoadedChunk, + completeOnSuccess: this.completeOnSuccess, + cancelOnError: this.cancelOnError, + }; + } + + abort() { + if (!this.hybridLoaderRequest) return; + this.hybridLoaderRequest.abort(); + this._status = "aborted"; + } + + abortEngineRequest() { + this._engineCallbacks?.onError("abort"); + this._engineCallbacks = undefined; + } + + private completeOnSuccess = () => { + this.throwErrorIfNotLoadingStatus(); + const data = Utils.joinChunks(this.chunks); + this._status = "succeed"; + this._engineCallbacks?.onSuccess({ + data, + bandwidth: this.bandwidthApproximator.getBandwidth(), + }); + this.dispatch("onCompleted", this, data); + }; + + private addLoadedChunk = (chunk: Uint8Array) => { + this.throwErrorIfNotLoadingStatus(); + const { hybridLoaderRequest: request } = this; + if (!request) return; + this.chunks.push(chunk); + request.progress.lastLoadedChunkTimestamp = performance.now(); + this._loadedBytes += chunk.length; + }; + + private cancelOnError = (error: Error) => { + this.throwErrorIfNotLoadingStatus(); + if (!this.hybridLoaderRequest) return; + this._status = "failed"; + this.hybridLoaderRequest.error = error; + this.prevAttempts.push(this.hybridLoaderRequest); + this.dispatch("onError", this, error); + }; + + private throwErrorIfNotLoadingStatus() { + if (this._status !== "loading") { + throw new Error("Request has been already completed/aborted/failed."); + } + } + + static getRequestItemId(segment: Segment) { + return segment.localId; + } +} diff --git a/packages/p2p-media-loader-core/src/utils/stream.ts b/packages/p2p-media-loader-core/src/utils/stream.ts index 5845e0ac..39d63cba 100644 --- a/packages/p2p-media-loader-core/src/utils/stream.ts +++ b/packages/p2p-media-loader-core/src/utils/stream.ts @@ -1,4 +1,5 @@ import { Segment, Stream, StreamWithSegments } from "../types"; +import { Playback } from "../internal-types"; const PEER_PROTOCOL_VERSION = "V1"; @@ -32,3 +33,19 @@ export function getSegmentFromStreamByExternalId( export function getStreamShortId(stream: Stream) { return `${stream.type}-${stream.index}`; } + +export 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; +} + +export function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { + return Math.max(segment.startTime - playback.position, 0) / playback.rate; +} diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts index 8db9b2bb..a47d2a18 100644 --- a/packages/p2p-media-loader-core/src/utils/utils.ts +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -35,3 +35,7 @@ export function joinChunks( export function getPercent(numerator: number, denominator: number): number { return (numerator / denominator) * 100; } + +export function getRandomItem(items: T[]): T { + return items[Math.floor(Math.random() * items.length)]; +} From bab7d15a7f551cfa7b661d683d82633bb635c590 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 14 Nov 2023 18:25:01 +0200 Subject: [PATCH 07/22] Create P2PTrackerClient class to encapsulate peer connection logic. --- .../src/event-dispatcher.ts | 4 +- .../p2p-media-loader-core/src/http-loader.ts | 2 +- .../p2p-media-loader-core/src/p2p/loader.ts | 237 +++++++++++------- .../p2p-media-loader-core/src/p2p/peer.ts | 73 ++---- 4 files changed, 166 insertions(+), 150 deletions(-) diff --git a/packages/p2p-media-loader-core/src/event-dispatcher.ts b/packages/p2p-media-loader-core/src/event-dispatcher.ts index ed648b45..a6583633 100644 --- a/packages/p2p-media-loader-core/src/event-dispatcher.ts +++ b/packages/p2p-media-loader-core/src/event-dispatcher.ts @@ -4,13 +4,13 @@ export class EventDispatcher< > { private readonly listeners = new Map>(); - subscribe(eventType: K, listener: T[K]) { + subscribe(eventType: K, ...listeners: T[K][]) { let eventListeners = this.listeners.get(eventType); if (!eventListeners) { eventListeners = new Set(); this.listeners.set(eventType, eventListeners); } - eventListeners.add(listener); + for (const listener of listeners) eventListeners.add(listener); } unsubscribe(eventType: K, listener: T[K]) { diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 10011317..99300c16 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 { Settings } from "./types"; -import { Request } from "./request-container"; +import { Request } from "./request"; export async function fulfillHttpSegmentRequest( request: Request, diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index a8e04762..f2ab86ce 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -1,4 +1,7 @@ -import TrackerClient, { PeerConnection } from "bittorrent-tracker"; +import TrackerClient, { + PeerConnection, + TrackerClientEvents, +} from "bittorrent-tracker"; import { Peer } from "./peer"; import * as PeerUtil from "../utils/peer"; import { Segment, Settings, StreamWithSegments } from "../types"; @@ -13,10 +16,8 @@ import { Request } from "../request"; import debug from "debug"; export class P2PLoader { - private readonly streamHash: string; private readonly peerId: string; - private readonly trackerClient: TrackerClient; - private readonly peers = new Map(); + private readonly trackerClient: P2PTrackerClient; private readonly logger = debug("core:p2p-loader"); private isAnnounceMicrotaskCreated = false; @@ -32,69 +33,33 @@ export class P2PLoader { this.streamManifestUrl, this.stream ); - this.streamHash = PeerUtil.getStreamHash(streamExternalId); - - this.trackerClient = createTrackerClient({ - streamHash: utf8ToHex(this.streamHash), - peerHash: utf8ToHex(this.peerId), - }); - this.logger( - `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ - this.peerId - }` + this.trackerClient = new P2PTrackerClient( + this.peerId, + streamExternalId, + this.stream, + { + onPeerConnected: this.onPeerConnected, + onSegmentRequested: this.onSegmentRequested, + }, + this.settings, + this.logger ); - this.subscribeOnTrackerEvents(this.trackerClient); + this.segmentStorage.subscribeOnUpdate( this.stream, this.broadcastAnnouncement ); - this.requests.subscribeOnHttpRequestsUpdate(this.broadcastAnnouncement); + // this.requests.subscribeOnHttpRequestsUpdate(this.broadcastAnnouncement); this.trackerClient.start(); } - private subscribeOnTrackerEvents(trackerClient: TrackerClient) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - trackerClient.on("update", () => {}); - trackerClient.on("peer", (peerConnection) => { - const peer = this.peers.get(peerConnection.id); - if (peer) peer.addConnection(peerConnection); - else this.createPeer(peerConnection); - }); - trackerClient.on("warning", (warning) => { - this.logger( - `tracker warning (${LoggerUtils.getStreamString( - this.stream - )}: ${warning})` - ); - }); - trackerClient.on("error", (error) => { - this.logger( - `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` - ); - }); - } - - private createPeer(connection: PeerConnection) { - const peer = new Peer( - connection, - { - onPeerConnected: this.onPeerConnected.bind(this), - onPeerClosed: this.onPeerClosed.bind(this), - onSegmentRequested: this.onSegmentRequested.bind(this), - }, - this.settings - ); - this.logger(`create new peer: ${peer.id}`); - this.peers.set(connection.id, peer); - } - downloadSegment(item: QueueItem): Request | undefined { const { segment, statuses } = item; const untestedPeers: Peer[] = []; let fastestPeer: Peer | undefined; let fastestPeerBandwidth = 0; - for (const peer of this.peers.values()) { + for (const peer of this.trackerClient.peers()) { if ( !peer.downloadingSegment && peer.getSegmentStatus(segment) === PeerSegmentStatus.Loaded @@ -130,7 +95,7 @@ export class P2PLoader { } isLoadingOrLoadedBySomeone(segment: Segment): boolean { - for (const peer of this.peers.values()) { + for (const peer of this.trackerClient.peers()) { if (peer.getSegmentStatus(segment)) return true; } return false; @@ -138,9 +103,7 @@ export class P2PLoader { get connectedPeersAmount() { let count = 0; - for (const peer of this.peers.values()) { - if (peer.isConnected) count++; - } + for (const peer of this.trackerClient.peers()) count++; return count; } @@ -164,19 +127,13 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(announcement); } - private onPeerClosed(peer: Peer) { - this.logger(`peer closed: ${peer.id}`); - this.peers.delete(peer.id); - } - private broadcastAnnouncement = () => { if (this.isAnnounceMicrotaskCreated) return; this.isAnnounceMicrotaskCreated = true; queueMicrotask(() => { const announcement = this.getSegmentsAnnouncement(); - for (const peer of this.peers.values()) { - if (!peer.isConnected) continue; + for (const peer of this.trackerClient.peers()) { peer.sendSegmentsAnnouncement(announcement); } this.isAnnounceMicrotaskCreated = false; @@ -202,41 +159,135 @@ export class P2PLoader { this.stream, this.broadcastAnnouncement ); - this.requests.unsubscribeFromHttpRequestsUpdate(this.broadcastAnnouncement); - for (const peer of this.peers.values()) { - peer.destroy(); - } - this.peers.clear(); + // this.requests.unsubscribeFromHttpRequestsUpdate(this.broadcastAnnouncement); this.trackerClient.destroy(); } } -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: [ +type PeerItem = { peer?: Peer; potentialConnections: Set }; + +type P2PTrackerClientEventHandlers = { + onPeerConnected: (peer: Peer) => void; + onSegmentRequested: (peer: Peer, segmentExternalId: string) => void; +}; + +class P2PTrackerClient { + private readonly client: TrackerClient; + private readonly _peers = new Map(); + private readonly streamHash: string; + + constructor( + private readonly peerId: string, + private readonly streamExternalId: string, + private readonly stream: StreamWithSegments, + private readonly eventHandlers: P2PTrackerClientEventHandlers, + private readonly settings: Settings, + private readonly logger: debug.Debugger + ) { + this.streamHash = PeerUtil.getStreamHash(streamExternalId); + this.client = new TrackerClient({ + infoHash: utf8ToHex(this.streamHash), + peerId: utf8ToHex(this.peerId), + 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.client.on("peer", this.onReceivePeerConnection); + this.client.on("warning", this.onTrackerClientWarning); + this.client.on("error", this.onTrackerClientError); + this.logger( + `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ + this.peerId + }` + ); + } + + start() { + this.client.start(); + } + + destroy() { + this.client.destroy(); + for (const { peer, potentialConnections } of this._peers.values()) { + peer?.destroy(); + for (const connection of potentialConnections) { + connection.destroy(); + } + } + } + + private onReceivePeerConnection: TrackerClientEvents["peer"] = ( + peerConnection + ) => { + let peerItem = this._peers.get(peerConnection.id); + + if (peerItem?.peer) { + peerConnection.destroy(); + return; + } else if (!peerItem) { + peerItem = { potentialConnections: new Set() }; + peerItem.potentialConnections.add(peerConnection); + const itemId = Peer.getPeerIdFromHexString(peerConnection.id); + this._peers.set(itemId, peerItem); + } + + peerConnection.on("connect", () => { + if (!peerItem) return; + + for (const connection of peerItem.potentialConnections) { + if (connection !== peerConnection) connection.destroy(); + } + peerItem.potentialConnections.clear(); + peerItem.peer = new Peer( + peerConnection, { - urls: [ - "stun:stun.l.google.com:19302", - "stun:global.stun.twilio.com:3478", - ], + onPeerClosed: this.onPeerClosed, + onSegmentRequested: this.eventHandlers.onSegmentRequested, }, - ], - }, - }); + this.settings + ); + this.eventHandlers.onPeerConnected(peerItem.peer); + }); + }; + + private onTrackerClientWarning: TrackerClientEvents["warning"] = ( + warning + ) => { + this.logger( + `tracker warning (${LoggerUtils.getStreamString( + this.stream + )}: ${warning})` + ); + }; + + private onTrackerClientError: TrackerClientEvents["error"] = (error) => { + this.logger( + `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` + ); + }; + + *peers() { + for (const peerItem of this._peers.values()) { + if (peerItem?.peer) yield peerItem.peer; + } + } + + private onPeerClosed = (peer: Peer) => { + this.logger(`peer closed: ${peer.id}`); + this._peers.delete(peer.id); + }; } function utf8ToHex(utf8String: string) { diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index d3d52594..89147315 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -28,7 +28,6 @@ export class PeerRequestError extends Error { } type PeerEventHandlers = { - onPeerConnected: (peer: Peer) => void; onPeerClosed: (peer: Peer) => void; onSegmentRequested: (peer: Peer, segmentId: string) => void; }; @@ -40,8 +39,6 @@ type PeerSettings = Pick< export class Peer { readonly id: string; - private connection?: PeerConnection; - private connections = new Set(); private segments = new Map(); private requestData?: { request: Request; controls: RequestControls }; private readonly logger = debug("core:peer"); @@ -49,57 +46,29 @@ export class Peer { private isUploadingSegment = false; constructor( - connection: PeerConnection, + private readonly connection: PeerConnection, private readonly eventHandlers: PeerEventHandlers, private readonly settings: PeerSettings ) { this.id = hexToUtf8(connection.id); this.eventHandlers = eventHandlers; - this.addConnection(connection); - } - - 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) { - 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", () => { - this.connection = undefined; - this.cancelSegmentRequest("peer-closed"); - this.logger(`connection with peer closed: ${this.id}`); + connection.on("data", this.onReceiveData.bind(this)); + connection.on("close", () => { + 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); - }); - connection.on("error", (error) => { - if (error.code === "ERR_DATA_CHANNEL") { - this.logger(`peer error: ${this.id} ${error.code}`); - this.destroy(); - this.eventHandlers.onPeerClosed(this); - } - }); + } }); } - get isConnected() { - return !!this.connection; - } - get downloadingSegment(): Segment | undefined { return this.requestData?.request.segment; } @@ -149,7 +118,6 @@ export class Peer { } private sendCommand(command: PeerCommand) { - if (!this.connection) return; this.connection.send(JSON.stringify(command)); } @@ -179,7 +147,6 @@ export class Peer { } async sendSegmentData(segmentExternalId: string, data: ArrayBuffer) { - if (!this.connection) return; this.logger(`send segment ${segmentExternalId} to ${this.id}`); const command: PeerSendSegmentCommand = { c: PeerCommandType.SegmentData, @@ -189,8 +156,7 @@ export class Peer { this.sendCommand(command); const chunks = getBufferChunks(data, this.settings.webRtcMaxMessageSize); - const connection = this.connection; - const channel = connection._channel; + const channel = this.connection._channel; const { promise, resolve, reject } = Utils.getControlledPromise(); const sendChunk = () => { @@ -204,7 +170,7 @@ export class Peer { reject(); break; } - connection.send(chunk); + this.connection.send(chunk); } }; try { @@ -281,12 +247,11 @@ export class Peer { destroy() { this.cancelSegmentRequest("destroy"); - this.connection?.destroy(); - this.connection = undefined; - for (const connection of this.connections) { - connection.destroy(); - } - this.connections.clear(); + this.connection.destroy(); + } + + static getPeerIdFromHexString(hex: string) { + return hexToUtf8(hex); } } From 0e4e204fa2425373b738ee9e6244be8dd3d4e6d2 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Tue, 14 Nov 2023 18:33:24 +0200 Subject: [PATCH 08/22] Add TODO task. --- packages/p2p-media-loader-core/src/request.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index b20a1057..253016aa 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -7,6 +7,7 @@ import { PeerRequestError } from "./p2p/peer"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; + // TODO: Error for engines onError: (reason: "failed" | "abort") => void; }; From 92021d3ca03ae3141ccec2ffa5b2ab1972d7eaf2 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Wed, 15 Nov 2023 20:09:03 +0200 Subject: [PATCH 09/22] Add requests error handling. --- .../p2p-media-loader-core/src/http-loader.ts | 48 ++-- .../src/hybrid-loader.ts | 31 +-- .../src/internal-types.d.ts | 12 +- .../p2p-media-loader-core/src/p2p/peer.ts | 118 +++++----- packages/p2p-media-loader-core/src/request.ts | 211 ++++++++++++++---- packages/p2p-media-loader-core/src/types.d.ts | 7 +- 6 files changed, 265 insertions(+), 162 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 99300c16..6fd36dd7 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,9 +1,9 @@ import { Settings } from "./types"; -import { Request } from "./request"; +import { Request, RequestError, HttpRequestErrorType } from "./request"; export async function fulfillHttpSegmentRequest( request: Request, - settings: Pick + settings: Pick ) { const headers = new Headers(); const { segment } = request; @@ -16,23 +16,19 @@ export async function fulfillHttpSegmentRequest( } const abortController = new AbortController(); - - const requestAbortTimeout = setTimeout(() => { - const errorType: HttpLoaderError["type"] = "request-timeout"; - abortController.abort(errorType); - }, settings.httpRequestTimeout); - - const abortManually = () => { - const abortErrorType: HttpLoaderError["type"] = "manual-abort"; - abortController.abort(abortErrorType); - }; - - const requestControls = request.start("http", abortManually); + const requestControls = request.start( + { type: "http" }, + { + abort: (errorType) => abortController.abort(errorType), + fullLoadingTimeoutMs: settings.httpDownloadTimeoutMs, + } + ); try { const fetchResponse = await window.fetch(url, { headers, signal: abortController.signal, }); + requestControls.firstBytesReceived(); if (fetchResponse.ok) { if (!fetchResponse.body) return; @@ -45,20 +41,13 @@ export async function fulfillHttpSegmentRequest( requestControls.addLoadedChunk(chunk); } requestControls.completeOnSuccess(); - clearTimeout(requestAbortTimeout); } - throw new HttpLoaderError("fetch-error", fetchResponse.statusText); + throw new RequestError("fetch-error", fetchResponse.statusText); } catch (error) { if (error instanceof Error) { - let httpLoaderError: HttpLoaderError; - if ((error.name as HttpLoaderError["type"]) === "manual-abort") { - httpLoaderError = new HttpLoaderError("manual-abort"); - } else if ( - (error.name as HttpLoaderError["type"]) === "request-timeout" - ) { - httpLoaderError = new HttpLoaderError("request-timeout"); - } else if (!(error instanceof HttpLoaderError)) { - httpLoaderError = new HttpLoaderError("fetch-error", error.message); + let httpLoaderError: RequestError; + if (!(error instanceof RequestError)) { + httpLoaderError = new RequestError("fetch-error", error.message); } else { httpLoaderError = error; } @@ -76,12 +65,3 @@ async function* readStream( yield value; } } - -export class HttpLoaderError extends Error { - constructor( - readonly type: "request-timeout" | "fetch-error" | "manual-abort", - message?: string - ) { - super(message); - } -} diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 3f5cc42a..ce6496f1 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -5,13 +5,12 @@ import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestsContainer } from "./request-container"; -import { Request, EngineCallbacks, HybridLoaderRequest } from "./request"; +import { Request, EngineCallbacks, RequestError } from "./request"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import * as StreamUtils from "./utils/stream"; import * as Utils from "./utils/utils"; import { P2PLoadersContainer } from "./p2p/loaders-container"; -import { PeerRequestError } from "./p2p/peer"; import debug from "debug"; export class HybridLoader { @@ -201,7 +200,7 @@ export class HybridLoader { abortSegment(segment: Segment) { const request = this.requests.get(segment); if (!request) return; - request.abort(); + request.abortEngineRequest(); this.createProcessQueueMicrotask(); this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); } @@ -210,7 +209,7 @@ export class HybridLoader { const { segment } = item; const request = this.requests.getOrCreateRequest(segment); - request.subscribe("onCompleted", this.onRequestCompleted); + request.subscribe("onSuccess", this.onRequestSucceed); request.subscribe("onError", this.onRequestError); void fulfillHttpSegmentRequest(request, this.settings); @@ -226,21 +225,18 @@ export class HybridLoader { const request = p2pLoader.downloadSegment(item); if (request === undefined) return; - request.subscribe("onCompleted", this.onRequestCompleted); + request.subscribe("onSuccess", this.onRequestSucceed); request.subscribe("onError", this.onRequestError); } - private onRequestCompleted = (request: Request, data: ArrayBuffer) => { + private onRequestSucceed = (request: Request, data: ArrayBuffer) => { const { segment } = request; this.logger.loader(`http responses: ${segment.externalId}`); this.eventHandlers?.onSegmentLoaded?.(data.byteLength, "http"); this.createProcessQueueMicrotask(); }; - private onRequestError = (request: Request, error: Error) => { - if (!(error instanceof PeerRequestError) || error.type === "manual-abort") { - return; - } + private onRequestError = (request: Request, error: RequestError) => { this.createProcessQueueMicrotask(); }; @@ -278,21 +274,6 @@ export class HybridLoader { ); } - private onSegmentLoaded( - queueItem: QueueItem, - type: HybridLoaderRequest["type"], - data: ArrayBuffer - ) { - const { segment, statuses } = queueItem; - const byteLength = data.byteLength; - if (type === "http" && statuses.isHighDemand) { - this.refreshLevelBandwidth(true); - } - void this.segmentStorage.storeSegment(segment, data); - this.eventHandlers?.onSegmentLoaded?.(byteLength, type); - this.createProcessQueueMicrotask(); - } - private abortLastHttpLoadingAfter(queue: QueueItem[], segment: Segment) { for (const { segment: itemSegment } of arrayBackwards(queue)) { if (itemSegment.localId === segment.localId) break; diff --git a/packages/p2p-media-loader-core/src/internal-types.d.ts b/packages/p2p-media-loader-core/src/internal-types.d.ts index f11f12dd..74a8ab05 100644 --- a/packages/p2p-media-loader-core/src/internal-types.d.ts +++ b/packages/p2p-media-loader-core/src/internal-types.d.ts @@ -36,13 +36,18 @@ export type JsonSegmentAnnouncement = { }; export type PeerSegmentCommand = BasePeerCommand< - | PeerCommandType.SegmentRequest - | PeerCommandType.SegmentAbsent - | PeerCommandType.CancelSegmentRequest + PeerCommandType.SegmentAbsent | PeerCommandType.CancelSegmentRequest > & { i: string; }; +export type PeerSegmentRequestCommand = + BasePeerCommand & { + i: string; + // start byte of range + b?: number; + }; + export type PeerSegmentAnnouncementCommand = BasePeerCommand & { a: JsonSegmentAnnouncement; @@ -56,5 +61,6 @@ export type PeerSendSegmentCommand = export type PeerCommand = | PeerSegmentCommand + | PeerSegmentRequestCommand | PeerSegmentAnnouncementCommand | PeerSendSegmentCommand; diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index 89147315..3c175748 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -4,29 +4,22 @@ import { PeerCommand, PeerSegmentAnnouncementCommand, PeerSegmentCommand, + PeerSegmentRequestCommand, PeerSendSegmentCommand, } from "../internal-types"; import { PeerCommandType, PeerSegmentStatus } from "../enums"; import * as PeerUtil from "../utils/peer"; -import { Request, RequestControls } from "../request"; +import { + Request, + RequestControls, + RequestError, + PeerRequestErrorType, + RequestInnerErrorType, +} from "../request"; import { Segment, Settings } from "../types"; import * as Utils from "../utils/utils"; import debug from "debug"; -export class PeerRequestError extends Error { - constructor( - readonly type: - | "manual-abort" - | "request-timeout" - | "response-bytes-mismatch" - | "segment-absent" - | "peer-closed" - | "destroy" - ) { - super(); - } -} - type PeerEventHandlers = { onPeerClosed: (peer: Peer) => void; onSegmentRequested: (peer: Peer, segmentId: string) => void; @@ -34,13 +27,15 @@ type PeerEventHandlers = { type PeerSettings = Pick< Settings, - "p2pSegmentDownloadTimeout" | "webRtcMaxMessageSize" + | "p2pSegmentDownloadTimeoutMs" + | "p2pSegmentFirstBytesTimeoutMs" + | "webRtcMaxMessageSize" >; export class Peer { readonly id: string; private segments = new Map(); - private requestData?: { request: Request; controls: RequestControls }; + private requestContext?: { request: Request; controls: RequestControls }; private readonly logger = debug("core:peer"); private readonly bandwidthMeasurer = new BandwidthMeasurer(); private isUploadingSegment = false; @@ -55,7 +50,6 @@ export class Peer { connection.on("data", this.onReceiveData.bind(this)); connection.on("close", () => { - this.cancelSegmentRequest("peer-closed"); this.logger(`connection with peer closed: ${this.id}`); this.destroy(); this.eventHandlers.onPeerClosed(this); @@ -70,7 +64,7 @@ export class Peer { } get downloadingSegment(): Segment | undefined { - return this.requestData?.request.segment; + return this.requestContext?.request.segment; } get bandwidth(): number | undefined { @@ -99,14 +93,21 @@ export class Peer { break; case PeerCommandType.SegmentData: - if (this.requestData?.request.segment.externalId === command.i) { - this.requestData.request.setTotalBytes(command.s); + { + const request = this.requestContext?.request; + this.requestContext?.controls.firstBytesReceived(); + if ( + request?.segment.externalId === command.i && + request.totalBytes === undefined + ) { + request.setTotalBytes(command.s); + } } break; case PeerCommandType.SegmentAbsent: - if (this.requestData?.request.segment.externalId === command.i) { - this.cancelSegmentRequest("segment-absent"); + if (this.requestContext?.request.segment.externalId === command.i) { + this.cancelSegmentRequest("peer-segment-absent"); this.segments.delete(command.i); } break; @@ -122,19 +123,25 @@ export class Peer { } fulfillSegmentRequest(request: Request) { - if (this.requestData) { + if (this.requestContext) { throw new Error("Segment already is downloading"); } - this.requestData = { + this.requestContext = { request, - controls: request.start("p2p", () => - this.cancelSegmentRequest("manual-abort") + controls: request.start( + { type: "p2p", peerId: this.id }, + { + abort: this.abortRequest, + firstBytesTimeoutMs: this.settings.p2pSegmentFirstBytesTimeoutMs, + fullLoadingTimeoutMs: this.settings.p2pSegmentDownloadTimeoutMs, + } ), }; - const command: PeerSegmentCommand = { + const command: PeerSegmentRequestCommand = { c: PeerCommandType.SegmentRequest, i: request.segment.externalId, }; + if (request.loadedBytes) command.b = request.loadedBytes; this.sendCommand(command); } @@ -196,57 +203,50 @@ export class Peer { } private receiveSegmentChunk(chunk: Uint8Array): void { - if (!this.requestData) return; - const { request, controls } = this.requestData; + if (!this.requestContext) return; + const { request, controls } = this.requestContext; controls.addLoadedChunk(chunk); if (request.loadedBytes === request.totalBytes) { controls.completeOnSuccess(); - this.clearRequest(); + this.requestContext = undefined; } else if ( request.totalBytes !== undefined && request.loadedBytes > request.totalBytes ) { - this.cancelSegmentRequest("response-bytes-mismatch"); + this.cancelSegmentRequest("peer-response-bytes-mismatch"); } } - private cancelSegmentRequest(type: PeerRequestError["type"]) { - if (!this.requestData) return; - const { request, controls } = this.requestData; + private abortRequest = (reason: RequestInnerErrorType) => { + if (!this.requestContext) return; + const { request } = this.requestContext; + this.sendCancelSegmentRequestCommand(request.segment); + this.requestContext = undefined; + }; + + private cancelSegmentRequest(type: PeerRequestErrorType) { + if (!this.requestContext) return; + const { request, controls } = this.requestContext; const { segment } = request; this.logger(`cancel segment request ${segment.externalId} (${type})`); - const error = new PeerRequestError(type); - const sendCancelCommandTypes: PeerRequestError["type"][] = [ - "destroy", - "manual-abort", - "request-timeout", - "response-bytes-mismatch", - ]; - if (sendCancelCommandTypes.includes(type)) { - this.sendCommand({ - c: PeerCommandType.CancelSegmentRequest, - i: segment.externalId, - }); + const error = new RequestError(type); + if (type === "peer-response-bytes-mismatch") { + this.sendCancelSegmentRequestCommand(request.segment); } controls.cancelOnError(error); - this.clearRequest(); + this.requestContext = undefined; } - private setRequestTimeout(): number { - return window.setTimeout( - () => this.cancelSegmentRequest("request-timeout"), - this.settings.p2pSegmentDownloadTimeout - ); - } - - private clearRequest() { - clearTimeout(this.request?.responseTimeoutId); - this.request = undefined; + private sendCancelSegmentRequestCommand(segment: Segment) { + this.sendCommand({ + c: PeerCommandType.CancelSegmentRequest, + i: segment.externalId, + }); } destroy() { - this.cancelSegmentRequest("destroy"); + this.cancelSegmentRequest("peer-closed"); this.connection.destroy(); } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 253016aa..efa3cf7e 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -2,8 +2,6 @@ import { EventDispatcher } from "./event-dispatcher"; import { Segment, SegmentResponse } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import * as Utils from "./utils/utils"; -import { HttpLoaderError } from "./http-loader"; -import { PeerRequestError } from "./p2p/peer"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -14,37 +12,39 @@ export type EngineCallbacks = { export type LoadProgress = { startTimestamp: number; lastLoadedChunkTimestamp?: number; + startFromByte?: number; loadedBytes: number; - totalBytes?: number; }; -type HybridLoaderRequestBase = { - abort: () => void; - progress: LoadProgress; -}; - -type HttpRequest = HybridLoaderRequestBase & { +type HttpRequestAttempt = { type: "http"; - error?: HttpLoaderError; + error?: RequestError; }; -type P2PRequest = HybridLoaderRequestBase & { +type P2PRequestAttempt = { type: "p2p"; - error?: PeerRequestError; + peerId: string; + error?: RequestError; }; -export type HybridLoaderRequest = HttpRequest | P2PRequest; +export type RequestAttempt = HttpRequestAttempt | P2PRequestAttempt; export type RequestEvents = { - onCompleted: (request: Request, data: ArrayBuffer) => void; - onError: (request: Request, data: Error) => void; + onSuccess: (request: Request, data: ArrayBuffer) => void; + onError: (request: Request, data: RequestError) => void; }; -export type RequestControls = { +export type RequestControls = Readonly<{ + firstBytesReceived: Request["firstBytesReceived"]; addLoadedChunk: Request["addLoadedChunk"]; completeOnSuccess: Request["completeOnSuccess"]; cancelOnError: Request["cancelOnError"]; -}; +}>; + +type OmitEncapsulated = Omit; +type StartRequestParameters = + | OmitEncapsulated + | OmitEncapsulated; type RequestStatus = | "not-started" @@ -56,12 +56,16 @@ type RequestStatus = export class Request extends EventDispatcher { readonly id: string; private _engineCallbacks?: EngineCallbacks; - private hybridLoaderRequest?: HybridLoaderRequest; - private prevAttempts: HybridLoaderRequest[] = []; + private currentAttempt?: RequestAttempt; + private prevAttempts: RequestAttempt[] = []; private chunks: Uint8Array[] = []; private _loadedBytes = 0; private _totalBytes?: number; private _status: RequestStatus = "not-started"; + private progress?: LoadProgress; + private firstBytesTimeout: Timeout; + private fullBytesTimeout: Timeout; + private _abortRequestCallback?: (errorType: RequestInnerErrorType) => void; constructor( readonly segment: Segment, @@ -69,6 +73,8 @@ export class Request extends EventDispatcher { ) { super(); this.id = Request.getRequestItemId(segment); + this.firstBytesTimeout = new Timeout(this.onFirstBytesTimeout); + this.fullBytesTimeout = new Timeout(this.onFullBytesTimeout); } get status() { @@ -80,7 +86,7 @@ export class Request extends EventDispatcher { } get type() { - return this.hybridLoaderRequest?.type; + return this.currentAttempt?.type; } get loadedBytes() { @@ -110,22 +116,46 @@ export class Request extends EventDispatcher { return Utils.getPercent(this.loadedBytes, this._totalBytes); } - start(type: "http" | "p2p", abortLoading: () => void): RequestControls { + get requestAttempts(): ReadonlyArray> { + return this.prevAttempts; + } + + start( + requestData: StartRequestParameters, + controls: { + firstBytesTimeoutMs?: number; + fullLoadingTimeoutMs?: number; + abort: (errorType: RequestInnerErrorType) => void; + } + ): RequestControls { + if (this._status === "succeed") { + throw new Error("Request has been already succeed."); + } if (this._status === "loading") { throw new Error("Request has been already started."); } this._status = "loading"; - this.hybridLoaderRequest = { - type, - abort: abortLoading, - progress: { - loadedBytes: 0, - startTimestamp: performance.now(), - }, + const attempt: RequestAttempt = { + ...requestData, + }; + this.progress = { + startFromByte: this._loadedBytes, + loadedBytes: 0, + startTimestamp: performance.now(), }; + const { firstBytesTimeoutMs, fullLoadingTimeoutMs, abort } = controls; + this._abortRequestCallback = abort; + if (firstBytesTimeoutMs !== undefined) { + this.firstBytesTimeout.start(firstBytesTimeoutMs); + } + if (fullLoadingTimeoutMs !== undefined) { + this.fullBytesTimeout.start(fullLoadingTimeoutMs); + } + this.currentAttempt = attempt; return { + firstBytesReceived: this.firstBytesReceived, addLoadedChunk: this.addLoadedChunk, completeOnSuccess: this.completeOnSuccess, cancelOnError: this.cancelOnError, @@ -133,9 +163,11 @@ export class Request extends EventDispatcher { } abort() { - if (!this.hybridLoaderRequest) return; - this.hybridLoaderRequest.abort(); + this.throwErrorIfNotLoadingStatus(); + if (!this._abortRequestCallback) return; this._status = "aborted"; + this._abortRequestCallback("abort"); + this._abortRequestCallback = undefined; } abortEngineRequest() { @@ -145,31 +177,66 @@ export class Request extends EventDispatcher { private completeOnSuccess = () => { this.throwErrorIfNotLoadingStatus(); + if (!this.currentAttempt) return; + + this.fullBytesTimeout.stopAndClear(); const data = Utils.joinChunks(this.chunks); this._status = "succeed"; + this.prevAttempts.push(this.currentAttempt); + this.currentAttempt = undefined; + this._engineCallbacks?.onSuccess({ data, bandwidth: this.bandwidthApproximator.getBandwidth(), }); - this.dispatch("onCompleted", this, data); + this.dispatch("onSuccess", this, data); }; private addLoadedChunk = (chunk: Uint8Array) => { this.throwErrorIfNotLoadingStatus(); - const { hybridLoaderRequest: request } = this; - if (!request) return; + if (!this.currentAttempt || !this.progress) return; + this.chunks.push(chunk); - request.progress.lastLoadedChunkTimestamp = performance.now(); + this.progress.lastLoadedChunkTimestamp = performance.now(); + this.progress.loadedBytes += chunk.length; this._loadedBytes += chunk.length; }; - private cancelOnError = (error: Error) => { + private firstBytesReceived = () => { + this.throwErrorIfNotLoadingStatus(); + this.firstBytesTimeout.stopAndClear(); + }; + + private cancelOnError = (error: RequestError) => { + this.throwErrorIfNotLoadingStatus(); + this.throwRequestError(error, false); + }; + + private throwRequestError(error: RequestError, abort = true) { this.throwErrorIfNotLoadingStatus(); - if (!this.hybridLoaderRequest) return; + if (!this.currentAttempt) return; this._status = "failed"; - this.hybridLoaderRequest.error = error; - this.prevAttempts.push(this.hybridLoaderRequest); + if ( + abort && + this._abortRequestCallback && + RequestError.isRequestInnerErrorType(error) + ) { + this._abortRequestCallback(error.type); + } + this.currentAttempt.error = error; + this.prevAttempts.push(this.currentAttempt); + this.currentAttempt = undefined; this.dispatch("onError", this, error); + } + + private onFirstBytesTimeout = () => { + this.throwErrorIfNotLoadingStatus(); + this.throwRequestError(new RequestError("first-bytes-timeout"), true); + }; + + private onFullBytesTimeout = () => { + this.throwErrorIfNotLoadingStatus(); + this.throwRequestError(new RequestError("full-bytes-timeout"), true); }; private throwErrorIfNotLoadingStatus() { @@ -182,3 +249,71 @@ export class Request extends EventDispatcher { return segment.localId; } } + +const requestInnerErrorTypes = [ + "abort", + "first-bytes-timeout", + "full-bytes-timeout", +] as const; + +const httpRequestErrorTypes = ["fetch-error"] as const; + +const peerRequestErrorTypes = [ + "peer-response-bytes-mismatch", + "peer-segment-absent", + "peer-closed", +] as const; + +export type RequestInnerErrorType = (typeof requestInnerErrorTypes)[number]; +export type HttpRequestErrorType = (typeof httpRequestErrorTypes)[number]; +export type PeerRequestErrorType = (typeof peerRequestErrorTypes)[number]; + +export class RequestError< + T extends + | RequestInnerErrorType + | PeerRequestErrorType + | HttpRequestErrorType = + | RequestInnerErrorType + | PeerRequestErrorType + | HttpRequestErrorType +> extends Error { + constructor(readonly type: T, message?: string) { + super(message); + } + + static isRequestInnerErrorType( + error: RequestError + ): error is RequestError { + return requestInnerErrorTypes.includes(error.type as any); + } + + static isPeerErrorType( + error: RequestError + ): error is RequestError { + return peerRequestErrorTypes.includes(error.type as any); + } + + static isHttpErrorType( + error: RequestError + ): error is RequestError { + return peerRequestErrorTypes.includes(error.type as any); + } +} + +class Timeout { + private timeoutId?: number; + + constructor(private readonly action: () => void) {} + + start(ms: number) { + if (this.timeoutId) { + throw new Error("Timeout is already started."); + } + this.timeoutId = window.setTimeout(this.action, ms); + } + + stopAndClear() { + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + } +} diff --git a/packages/p2p-media-loader-core/src/types.d.ts b/packages/p2p-media-loader-core/src/types.d.ts index 16e7d14d..2f113f1e 100644 --- a/packages/p2p-media-loader-core/src/types.d.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -58,9 +58,10 @@ export type Settings = { cachedSegmentExpiration: number; cachedSegmentsCount: number; webRtcMaxMessageSize: number; - p2pSegmentDownloadTimeout: number; - p2pLoaderDestroyTimeout: number; - httpRequestTimeout: number; + p2pSegmentFirstBytesTimeoutMs: number; + p2pSegmentDownloadTimeoutMs: number; + p2pLoaderDestroyTimeoutMs: number; + httpDownloadTimeoutMs: number; }; export type CoreEventHandlers = { From 3b4dbdd39f2e28bb3fa30a09ea4e1c9837438a09 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 16 Nov 2023 13:51:54 +0200 Subject: [PATCH 10/22] Fix types errors. --- .../src/bandwidth-approximator.ts | 2 +- packages/p2p-media-loader-core/src/core.ts | 18 +++++--- .../src/hybrid-loader.ts | 10 ++--- packages/p2p-media-loader-core/src/index.ts | 2 + .../p2p-media-loader-core/src/p2p/loader.ts | 2 +- .../src/p2p/loaders-container.ts | 3 +- .../p2p-media-loader-core/src/p2p/peer.ts | 5 +-- .../src/request-container.ts | 12 ++---- packages/p2p-media-loader-core/src/request.ts | 25 ++++++----- packages/p2p-media-loader-core/src/types.d.ts | 9 +--- .../src/fragment-loader.ts | 26 +++++------ .../src/loading-handler.ts | 43 ++++++++++++++----- 12 files changed, 91 insertions(+), 66 deletions(-) diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts index 79216e53..2f60626f 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-container"; +import { LoadProgress } from "./request"; 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 1a8a9159..65de933f 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -7,10 +7,10 @@ import { SegmentBase, CoreEventHandlers, } from "./types"; -import * as Utils from "./utils/utils"; +import * as StreamUtils from "./utils/stream"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; -import { EngineCallbacks } from "./request-container"; +import { EngineCallbacks } from "./request"; import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { @@ -25,9 +25,10 @@ export class Core { cachedSegmentExpiration: 120 * 1000, cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, - p2pSegmentDownloadTimeout: 5000, - p2pLoaderDestroyTimeout: 30 * 1000, - httpRequestTimeout: 5000, + p2pSegmentFirstBytesTimeoutMs: 1000, + p2pSegmentDownloadTimeoutMs: 5000, + p2pLoaderDestroyTimeoutMs: 30 * 1000, + httpDownloadTimeoutMs: 5000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); private segmentStorage?: SegmentsMemoryStorage; @@ -41,7 +42,7 @@ export class Core { } hasSegment(segmentLocalId: string): boolean { - const segment = Utils.getSegmentFromStreamsMap( + const segment = StreamUtils.getSegmentFromStreamsMap( this.streams, segmentLocalId ); @@ -117,7 +118,10 @@ export class Core { throw new Error("Manifest response url is undefined"); } - const segment = Utils.getSegmentFromStreamsMap(this.streams, segmentId); + const segment = StreamUtils.getSegmentFromStreamsMap( + this.streams, + segmentId + ); if (!segment) { throw new Error(`Not found segment with id: ${segmentId}`); } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index ce6496f1..fd3f69ba 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -5,7 +5,7 @@ import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestsContainer } from "./request-container"; -import { Request, EngineCallbacks, RequestError } from "./request"; +import { Request, EngineCallbacks } from "./request"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import * as StreamUtils from "./utils/stream"; @@ -209,7 +209,7 @@ export class HybridLoader { const { segment } = item; const request = this.requests.getOrCreateRequest(segment); - request.subscribe("onSuccess", this.onRequestSucceed); + request.subscribe("onSuccess", this.onRequestSuccess); request.subscribe("onError", this.onRequestError); void fulfillHttpSegmentRequest(request, this.settings); @@ -225,18 +225,18 @@ export class HybridLoader { const request = p2pLoader.downloadSegment(item); if (request === undefined) return; - request.subscribe("onSuccess", this.onRequestSucceed); + request.subscribe("onSuccess", this.onRequestSuccess); request.subscribe("onError", this.onRequestError); } - private onRequestSucceed = (request: Request, data: ArrayBuffer) => { + private onRequestSuccess = (request: Request, data: ArrayBuffer) => { const { segment } = request; this.logger.loader(`http responses: ${segment.externalId}`); this.eventHandlers?.onSegmentLoaded?.(data.byteLength, "http"); this.createProcessQueueMicrotask(); }; - private onRequestError = (request: Request, error: RequestError) => { + private onRequestError = () => { this.createProcessQueueMicrotask(); }; diff --git a/packages/p2p-media-loader-core/src/index.ts b/packages/p2p-media-loader-core/src/index.ts index 723b2f2d..08df54ac 100644 --- a/packages/p2p-media-loader-core/src/index.ts +++ b/packages/p2p-media-loader-core/src/index.ts @@ -2,3 +2,5 @@ export { Core } from "./core"; export type * from "./types"; +export { CoreRequestError } from "./request"; +export type { EngineCallbacks } from "./request"; diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index f2ab86ce..0b774618 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -87,7 +87,7 @@ export class P2PLoader { statuses )}` ); - request.subscribe("onCompleted", () => { + request.subscribe("onSuccess", () => { this.logger(`p2p loaded: ${segment.externalId}`); }); 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 cbc8ced9..8a8d5640 100644 --- a/packages/p2p-media-loader-core/src/p2p/loaders-container.ts +++ b/packages/p2p-media-loader-core/src/p2p/loaders-container.ts @@ -73,9 +73,10 @@ export class P2PLoadersContainer { } private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { + // TODO: use Timeout class instead item.destroyTimeoutId = window.setTimeout( () => this.destroyAndRemoveLoader(item), - this.settings.p2pLoaderDestroyTimeout + this.settings.p2pLoaderDestroyTimeoutMs ); } diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index 3c175748..ce11cb42 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -8,15 +8,14 @@ import { PeerSendSegmentCommand, } from "../internal-types"; import { PeerCommandType, PeerSegmentStatus } from "../enums"; -import * as PeerUtil from "../utils/peer"; import { Request, RequestControls, RequestError, PeerRequestErrorType, - RequestInnerErrorType, } from "../request"; import { Segment, Settings } from "../types"; +import * as PeerUtil from "../utils/peer"; import * as Utils from "../utils/utils"; import debug from "debug"; @@ -218,7 +217,7 @@ export class Peer { } } - private abortRequest = (reason: RequestInnerErrorType) => { + private abortRequest = () => { if (!this.requestContext) return; const { request } = this.requestContext; this.sendCancelSegmentRequestCommand(request.segment); diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 03f41338..b8ffcb58 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,17 +1,11 @@ import { Segment, StreamType } from "./types"; import Debug from "debug"; -import { EventDispatcher } from "./event-dispatcher"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Request, RequestEvents } from "./request"; -type RequestsContainerEvents = { - httpRequestsUpdated: () => void; -}; - export class RequestsContainer { private readonly requests = new Map(); private readonly logger: Debug.Debugger; - private readonly events = new EventDispatcher(); constructor( streamType: StreamType, @@ -47,13 +41,13 @@ export class RequestsContainer { let request = this.requests.get(id); if (!request) { request = new Request(segment, this.bandwidthApproximator); - request.subscribe("onCompleted", this.onRequestCompleted); + request.subscribe("onSuccess", this.onRequestCompleted); this.requests.set(request.id, request); } return request; } - private onRequestCompleted: RequestEvents["onCompleted"] = (request) => { + private onRequestCompleted: RequestEvents["onSuccess"] = (request) => { this.requests.delete(request.id); }; @@ -97,7 +91,7 @@ export class RequestsContainer { destroy() { for (const request of this.requests.values()) { request.abort(); - request.engineCallbacks?.onError("failed"); + request.abortEngineRequest(); } this.requests.clear(); } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index efa3cf7e..35baae2e 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -5,8 +5,7 @@ import * as Utils from "./utils/utils"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; - // TODO: Error for engines - onError: (reason: "failed" | "abort") => void; + onError: (reason: CoreRequestError) => void; }; export type LoadProgress = { @@ -144,6 +143,7 @@ export class Request extends EventDispatcher { loadedBytes: 0, startTimestamp: performance.now(), }; + this.bandwidthApproximator.addLoading(this.progress); const { firstBytesTimeoutMs, fullLoadingTimeoutMs, abort } = controls; this._abortRequestCallback = abort; if (firstBytesTimeoutMs !== undefined) { @@ -171,7 +171,7 @@ export class Request extends EventDispatcher { } abortEngineRequest() { - this._engineCallbacks?.onError("abort"); + this._engineCallbacks?.onError(new CoreRequestError("aborted")); this._engineCallbacks = undefined; } @@ -268,14 +268,13 @@ export type RequestInnerErrorType = (typeof requestInnerErrorTypes)[number]; export type HttpRequestErrorType = (typeof httpRequestErrorTypes)[number]; export type PeerRequestErrorType = (typeof peerRequestErrorTypes)[number]; +type RequestErrorType = + | RequestInnerErrorType + | PeerRequestErrorType + | HttpRequestErrorType; + export class RequestError< - T extends - | RequestInnerErrorType - | PeerRequestErrorType - | HttpRequestErrorType = - | RequestInnerErrorType - | PeerRequestErrorType - | HttpRequestErrorType + T extends RequestErrorType = RequestErrorType > extends Error { constructor(readonly type: T, message?: string) { super(message); @@ -300,6 +299,12 @@ export class RequestError< } } +export class CoreRequestError extends Error { + constructor(readonly type: "failed" | "aborted") { + super(); + } +} + class Timeout { private timeoutId?: number; diff --git a/packages/p2p-media-loader-core/src/types.d.ts b/packages/p2p-media-loader-core/src/types.d.ts index 2f113f1e..2e0ba396 100644 --- a/packages/p2p-media-loader-core/src/types.d.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -1,7 +1,5 @@ import { LinkedMap } from "./linked-map"; -import { HybridLoaderRequest } from "./request-container"; - -export type { EngineCallbacks } from "./request-container"; +import { RequestAttempt } from "./request"; export type StreamType = "main" | "secondary"; @@ -65,8 +63,5 @@ export type Settings = { }; export type CoreEventHandlers = { - onSegmentLoaded?: ( - byteLength: number, - type: HybridLoaderRequest["type"] - ) => void; + onSegmentLoaded?: (byteLength: number, type: RequestAttempt["type"]) => void; }; diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index 8ed41e65..0bc75e7a 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -8,12 +8,7 @@ import type { LoaderStats, } from "hls.js"; import * as Utils from "./utils"; -import { - RequestAbortError, - Core, - FetchError, - SegmentResponse, -} from "p2p-media-loader-core"; +import { Core, SegmentResponse, CoreRequestError } from "p2p-media-loader-core"; const DEFAULT_DOWNLOAD_LATENCY = 10; @@ -89,7 +84,13 @@ export class FragmentLoaderBase implements Loader { }; const onError = (error: unknown) => { - if (error instanceof RequestAbortError && this.stats.aborted) return; + if ( + error instanceof CoreRequestError && + error.type === "aborted" && + this.stats.aborted + ) { + return; + } this.handleError(error); }; @@ -98,15 +99,16 @@ export class FragmentLoaderBase implements Loader { private handleError(thrownError: unknown) { const error = { code: 0, text: "" }; - let details: object | null = null; - if (thrownError instanceof FetchError) { - error.code = thrownError.code; + if ( + thrownError instanceof CoreRequestError && + thrownError.type === "failed" + ) { + // error.code = thrownError.code; error.text = thrownError.message; - details = thrownError.details; } else if (thrownError instanceof Error) { error.text = thrownError.message; } - this.callbacks?.onError(error, this.context, details, this.stats); + this.callbacks?.onError(error, this.context, null, this.stats); } private abortInternal() { diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index 99773cfd..cda4a8dd 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -2,7 +2,12 @@ import * as Utils from "./stream-utils"; import { SegmentManager } from "./segment-manager"; import { StreamInfo } from "./types"; import { Shaka, Stream } from "./types"; -import { Core, EngineCallbacks, SegmentResponse } from "p2p-media-loader-core"; +import { + Core, + CoreRequestError, + SegmentResponse, + EngineCallbacks, +} from "p2p-media-loader-core"; interface LoadingHandlerInterface { handleLoading: shaka.extern.SchemePlugin; @@ -70,15 +75,33 @@ export class LoadingHandler implements LoadingHandlerInterface { const loadSegment = async (): Promise => { const { request, callbacks } = getSegmentRequest(); - await this.core.loadSegment(segmentId, callbacks); - const { data, bandwidth } = await request; - return { - data, - headers: {}, - uri: segmentUrl, - originalUri: segmentUrl, - timeMs: getLoadingDurationBasedOnBandwidth(bandwidth, data.byteLength), - }; + void this.core.loadSegment(segmentId, callbacks); + try { + const { data, bandwidth } = await request; + return { + data, + headers: {}, + uri: segmentUrl, + originalUri: segmentUrl, + timeMs: getLoadingDurationBasedOnBandwidth( + bandwidth, + data.byteLength + ), + }; + } catch (error) { + // TODO: throw Shaka Errors + if (error instanceof CoreRequestError) { + const { Error: ShakaError } = this.shaka.util; + if (error.type === "aborted") { + throw new ShakaError( + ShakaError.Severity.RECOVERABLE, + ShakaError.Category.NETWORK, + this.shaka.util.Error.Code.OPERATION_ABORTED + ); + } + } + throw error; + } }; return new this.shaka.util.AbortableOperation(loadSegment(), async () => From 5d5865b3fa451a3c8ef4e1747abf6efaada336f7 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 16 Nov 2023 15:03:28 +0200 Subject: [PATCH 11/22] P2P announce segments on http requests state change. --- .../src/event-dispatcher.ts | 3 ++- .../src/hybrid-loader.ts | 23 +++++++++++++++---- .../p2p-media-loader-core/src/p2p/loader.ts | 7 +++--- .../src/p2p/loaders-container.ts | 1 - packages/p2p-media-loader-core/src/request.ts | 17 ++------------ 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/p2p-media-loader-core/src/event-dispatcher.ts b/packages/p2p-media-loader-core/src/event-dispatcher.ts index a6583633..7396e0e9 100644 --- a/packages/p2p-media-loader-core/src/event-dispatcher.ts +++ b/packages/p2p-media-loader-core/src/event-dispatcher.ts @@ -1,5 +1,6 @@ export class EventDispatcher< - T extends { [key: string]: (...args: any) => any }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends { [key: string]: (...args: any) => void | Promise }, K extends keyof T = keyof T > { private readonly listeners = new Map>(); diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index fd3f69ba..2318b8a4 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -22,7 +22,10 @@ export class HybridLoader { private lastQueueProcessingTimeStamp?: number; private readonly segmentAvgDuration: number; private randomHttpDownloadInterval!: number; - private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; + private readonly logger: { + engine: debug.Debugger; + loader: debug.Debugger; + }; private readonly levelBandwidth = { value: 0, refreshCount: 0 }; private isProcessQueueMicrotaskCreated = false; @@ -212,6 +215,7 @@ export class HybridLoader { request.subscribe("onSuccess", this.onRequestSuccess); request.subscribe("onError", this.onRequestError); + this.p2pLoaders.currentLoader.broadcastAnnouncement(); void fulfillHttpSegmentRequest(request, this.settings); if (!isRandom) { this.logger.loader( @@ -230,13 +234,22 @@ export class HybridLoader { } private onRequestSuccess = (request: Request, data: ArrayBuffer) => { - const { segment } = request; - this.logger.loader(`http responses: ${segment.externalId}`); - this.eventHandlers?.onSegmentLoaded?.(data.byteLength, "http"); + const requestType = request.type; + if (!requestType) return; + + if (requestType === "http") { + this.logger.loader(`http responses: ${request.segment.externalId}`); + this.p2pLoaders.currentLoader.broadcastAnnouncement(); + } + + this.eventHandlers?.onSegmentLoaded?.(data.byteLength, requestType); this.createProcessQueueMicrotask(); }; - private onRequestError = () => { + private onRequestError = (request: Request) => { + if (request.type === "http") { + this.p2pLoaders.currentLoader.broadcastAnnouncement(); + } this.createProcessQueueMicrotask(); }; diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index 0b774618..6b4d174a 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -49,7 +49,6 @@ export class P2PLoader { this.stream, this.broadcastAnnouncement ); - // this.requests.subscribeOnHttpRequestsUpdate(this.broadcastAnnouncement); this.trackerClient.start(); } @@ -103,6 +102,7 @@ export class P2PLoader { get connectedPeersAmount() { let count = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const peer of this.trackerClient.peers()) count++; return count; } @@ -127,7 +127,7 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(announcement); } - private broadcastAnnouncement = () => { + broadcastAnnouncement() { if (this.isAnnounceMicrotaskCreated) return; this.isAnnounceMicrotaskCreated = true; @@ -138,7 +138,7 @@ export class P2PLoader { } this.isAnnounceMicrotaskCreated = false; }); - }; + } private async onSegmentRequested(peer: Peer, segmentExternalId: string) { const segment = StreamUtils.getSegmentFromStreamByExternalId( @@ -159,7 +159,6 @@ export class P2PLoader { this.stream, this.broadcastAnnouncement ); - // this.requests.unsubscribeFromHttpRequestsUpdate(this.broadcastAnnouncement); this.trackerClient.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 8a8d5640..623fb00f 100644 --- a/packages/p2p-media-loader-core/src/p2p/loaders-container.ts +++ b/packages/p2p-media-loader-core/src/p2p/loaders-container.ts @@ -73,7 +73,6 @@ export class P2PLoadersContainer { } private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { - // TODO: use Timeout class instead item.destroyTimeoutId = window.setTimeout( () => this.destroyAndRemoveLoader(item), this.settings.p2pLoaderDestroyTimeoutMs diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 35baae2e..09783c31 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -183,7 +183,6 @@ export class Request extends EventDispatcher { const data = Utils.joinChunks(this.chunks); this._status = "succeed"; this.prevAttempts.push(this.currentAttempt); - this.currentAttempt = undefined; this._engineCallbacks?.onSuccess({ data, @@ -225,7 +224,6 @@ export class Request extends EventDispatcher { } this.currentAttempt.error = error; this.prevAttempts.push(this.currentAttempt); - this.currentAttempt = undefined; this.dispatch("onError", this, error); } @@ -283,20 +281,9 @@ export class RequestError< static isRequestInnerErrorType( error: RequestError ): error is RequestError { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return requestInnerErrorTypes.includes(error.type as any); } - - static isPeerErrorType( - error: RequestError - ): error is RequestError { - return peerRequestErrorTypes.includes(error.type as any); - } - - static isHttpErrorType( - error: RequestError - ): error is RequestError { - return peerRequestErrorTypes.includes(error.type as any); - } } export class CoreRequestError extends Error { @@ -305,7 +292,7 @@ export class CoreRequestError extends Error { } } -class Timeout { +export class Timeout { private timeoutId?: number; constructor(private readonly action: () => void) {} From 0d8c3bc6de75da584d1f1bbf3bd66bff2561c9a8 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 16 Nov 2023 18:03:24 +0200 Subject: [PATCH 12/22] Fix bugs. --- packages/p2p-media-loader-core/src/core.ts | 2 +- .../src/event-dispatcher.ts | 4 +- .../p2p-media-loader-core/src/http-loader.ts | 36 ++++++------- .../src/hybrid-loader.ts | 18 ++----- .../p2p-media-loader-core/src/p2p/loader.ts | 18 ++++--- .../src/request-container.ts | 27 ++++------ packages/p2p-media-loader-core/src/request.ts | 50 ++++++++++--------- 7 files changed, 75 insertions(+), 80 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 65de933f..786eb9df 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -17,7 +17,7 @@ export class Core { private manifestResponseUrl?: string; private readonly streams = new Map>(); private readonly settings: Settings = { - simultaneousHttpDownloads: 2, + simultaneousHttpDownloads: 1, simultaneousP2PDownloads: 3, highDemandTimeWindow: 15, httpDownloadTimeWindow: 45, diff --git a/packages/p2p-media-loader-core/src/event-dispatcher.ts b/packages/p2p-media-loader-core/src/event-dispatcher.ts index 7396e0e9..b37e621c 100644 --- a/packages/p2p-media-loader-core/src/event-dispatcher.ts +++ b/packages/p2p-media-loader-core/src/event-dispatcher.ts @@ -1,6 +1,6 @@ export class EventDispatcher< // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends { [key: string]: (...args: any) => void | Promise }, + T extends { [key: string]: (...args: any[]) => void | Promise }, K extends keyof T = keyof T > { private readonly listeners = new Map>(); @@ -25,7 +25,7 @@ export class EventDispatcher< const eventListeners = this.listeners.get(eventType); if (!eventListeners) return; for (const listener of eventListeners) { - listener(args); + listener(...args); } } } diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 6fd36dd7..a4f2e384 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -19,7 +19,7 @@ export async function fulfillHttpSegmentRequest( const requestControls = request.start( { type: "http" }, { - abort: (errorType) => abortController.abort(errorType), + abort: () => abortController.abort("abort"), fullLoadingTimeoutMs: settings.httpDownloadTimeoutMs, } ); @@ -30,27 +30,29 @@ export async function fulfillHttpSegmentRequest( }); requestControls.firstBytesReceived(); - if (fetchResponse.ok) { - if (!fetchResponse.body) return; + if (!fetchResponse.ok) { + throw new RequestError("fetch-error", fetchResponse.statusText); + } - const totalBytesString = fetchResponse.headers.get("Content-Length"); - if (totalBytesString) request.setTotalBytes(+totalBytesString); + if (!fetchResponse.body) return; + const totalBytesString = fetchResponse.headers.get("Content-Length"); + if (totalBytesString) request.setTotalBytes(+totalBytesString); - const reader = fetchResponse.body.getReader(); - for await (const chunk of readStream(reader)) { - requestControls.addLoadedChunk(chunk); - } - requestControls.completeOnSuccess(); + const reader = fetchResponse.body.getReader(); + for await (const chunk of readStream(reader)) { + requestControls.addLoadedChunk(chunk); } - throw new RequestError("fetch-error", fetchResponse.statusText); + requestControls.completeOnSuccess(); } catch (error) { if (error instanceof Error) { - let httpLoaderError: RequestError; - if (!(error instanceof RequestError)) { - httpLoaderError = new RequestError("fetch-error", error.message); - } else { - httpLoaderError = error; - } + if (error.name !== "abort") return; + + const httpLoaderError: RequestError = !( + error instanceof RequestError + ) + ? new RequestError("fetch-error", error.message) + : error; + console.log("HTTP ERROR"); requestControls.cancelOnError(httpLoaderError); } } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 2318b8a4..6ee40cdb 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -26,7 +26,6 @@ export class HybridLoader { engine: debug.Debugger; loader: debug.Debugger; }; - private readonly levelBandwidth = { value: 0, refreshCount: 0 }; private isProcessQueueMicrotaskCreated = false; constructor( @@ -91,7 +90,6 @@ export class HybridLoader { `stream changed to ${LoggerUtils.getStreamString(stream)}` ); this.p2pLoaders.changeCurrentLoader(stream); - this.refreshLevelBandwidth(true); } this.lastRequestedSegment = segment; @@ -101,12 +99,12 @@ export class HybridLoader { if (data) { callbacks.onSuccess({ data, - bandwidth: this.levelBandwidth.value, + bandwidth: this.bandwidthApproximator.getBandwidth(), }); } } else { const request = this.requests.getOrCreateRequest(segment); - request.engineCallbacks = callbacks; + request.setEngineCallbacks(callbacks); } this.createProcessQueueMicrotask(); @@ -146,7 +144,7 @@ export class HybridLoader { !queueSegmentIds.has(request.segment.localId) ) { request.abort(); - this.requests.remove(request); + this.requests.remove(request.segment); } } @@ -237,6 +235,7 @@ export class HybridLoader { const requestType = request.type; if (!requestType) return; + void this.segmentStorage.storeSegment(request.segment, data); if (requestType === "http") { this.logger.loader(`http responses: ${request.segment.externalId}`); this.p2pLoaders.currentLoader.broadcastAnnouncement(); @@ -315,15 +314,6 @@ 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-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index 6b4d174a..01fef1eb 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -3,10 +3,10 @@ import TrackerClient, { TrackerClientEvents, } from "bittorrent-tracker"; import { Peer } from "./peer"; -import * as PeerUtil from "../utils/peer"; import { Segment, Settings, StreamWithSegments } from "../types"; import { QueueItem } from "../internal-types"; import { SegmentsMemoryStorage } from "../segments-storage"; +import * as PeerUtil from "../utils/peer"; import * as LoggerUtils from "../utils/logger"; import * as StreamUtils from "../utils/stream"; import * as Utils from "../utils/utils"; @@ -121,11 +121,11 @@ export class P2PLoader { return PeerUtil.getJsonSegmentsAnnouncement(loaded, httpLoading); } - private onPeerConnected(peer: Peer) { + private onPeerConnected = (peer: Peer) => { this.logger(`connected with peer: ${peer.id}`); const announcement = this.getSegmentsAnnouncement(); peer.sendSegmentsAnnouncement(announcement); - } + }; broadcastAnnouncement() { if (this.isAnnounceMicrotaskCreated) return; @@ -140,7 +140,10 @@ export class P2PLoader { }); } - private async onSegmentRequested(peer: Peer, segmentExternalId: string) { + private onSegmentRequested = async ( + peer: Peer, + segmentExternalId: string + ) => { const segment = StreamUtils.getSegmentFromStreamByExternalId( this.stream, segmentExternalId @@ -149,7 +152,7 @@ export class P2PLoader { segment && (await this.segmentStorage.getSegmentData(segment)); if (segmentData) void peer.sendSegmentData(segmentExternalId, segmentData); else peer.sendSegmentAbsent(segmentExternalId); - } + }; destroy() { this.logger( @@ -163,7 +166,10 @@ export class P2PLoader { } } -type PeerItem = { peer?: Peer; potentialConnections: Set }; +type PeerItem = { + peer?: Peer; + potentialConnections: Set; +}; type P2PTrackerClientEventHandlers = { onPeerConnected: (peer: Peer) => void; diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index b8ffcb58..ce3e0e5f 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -4,7 +4,7 @@ import { BandwidthApproximator } from "./bandwidth-approximator"; import { Request, RequestEvents } from "./request"; export class RequestsContainer { - private readonly requests = new Map(); + private readonly requests = new Map(); private readonly logger: Debug.Debugger; constructor( @@ -32,29 +32,25 @@ export class RequestsContainer { } get(segment: Segment) { - const id = Request.getRequestItemId(segment); - return this.requests.get(id); + return this.requests.get(segment); } getOrCreateRequest(segment: Segment) { - const id = Request.getRequestItemId(segment); - let request = this.requests.get(id); + let request = this.requests.get(segment); if (!request) { request = new Request(segment, this.bandwidthApproximator); request.subscribe("onSuccess", this.onRequestCompleted); - this.requests.set(request.id, request); + this.requests.set(segment, request); } return request; } private onRequestCompleted: RequestEvents["onSuccess"] = (request) => { - this.requests.delete(request.id); + this.requests.delete(request.segment); }; - remove(value: Segment | Request) { - const id = - value instanceof Request ? value.id : Request.getRequestItemId(value); - this.requests.delete(id); + remove(segment: Segment) { + this.requests.delete(segment); } values() { @@ -74,18 +70,15 @@ export class RequestsContainer { } isHttpRequested(segment: Segment): boolean { - const id = Request.getRequestItemId(segment); - return this.requests.get(id)?.type === "http"; + return this.requests.get(segment)?.type === "http"; } isP2PRequested(segment: Segment): boolean { - const id = Request.getRequestItemId(segment); - return this.requests.get(id)?.type === "p2p"; + return this.requests.get(segment)?.type === "p2p"; } isHybridLoaderRequested(segment: Segment): boolean { - const id = Request.getRequestItemId(segment); - return !!this.requests.get(id)?.type; + return !!this.requests.get(segment)?.type; } destroy() { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 09783c31..3380d529 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -92,24 +92,10 @@ export class Request extends EventDispatcher { return this._loadedBytes; } - set engineCallbacks(callbacks: EngineCallbacks) { - if (this._engineCallbacks) { - throw new Error("Segment is already requested by engine"); - } - this._engineCallbacks = callbacks; - } - get totalBytes(): number | undefined { return this._totalBytes; } - setTotalBytes(value: number) { - if (this._totalBytes !== undefined) { - throw new Error("Request total bytes value is already set"); - } - this._totalBytes = value; - } - get loadedPercent() { if (!this._totalBytes) return; return Utils.getPercent(this.loadedBytes, this._totalBytes); @@ -119,6 +105,20 @@ export class Request extends EventDispatcher { return this.prevAttempts; } + setEngineCallbacks(callbacks: EngineCallbacks) { + if (this._engineCallbacks) { + throw new Error("Segment is already requested by engine"); + } + this._engineCallbacks = callbacks; + } + + setTotalBytes(value: number) { + if (this._totalBytes !== undefined) { + throw new Error("Request total bytes value is already set"); + } + this._totalBytes = value; + } + start( requestData: StartRequestParameters, controls: { @@ -135,9 +135,7 @@ export class Request extends EventDispatcher { } this._status = "loading"; - const attempt: RequestAttempt = { - ...requestData, - }; + this.currentAttempt = { ...requestData }; this.progress = { startFromByte: this._loadedBytes, loadedBytes: 0, @@ -152,8 +150,6 @@ export class Request extends EventDispatcher { if (fullLoadingTimeoutMs !== undefined) { this.fullBytesTimeout.start(fullLoadingTimeoutMs); } - - this.currentAttempt = attempt; return { firstBytesReceived: this.firstBytesReceived, addLoadedChunk: this.addLoadedChunk, @@ -168,6 +164,7 @@ export class Request extends EventDispatcher { this._status = "aborted"; this._abortRequestCallback("abort"); this._abortRequestCallback = undefined; + this.clearTimeouts(); } abortEngineRequest() { @@ -179,9 +176,10 @@ export class Request extends EventDispatcher { this.throwErrorIfNotLoadingStatus(); if (!this.currentAttempt) return; - this.fullBytesTimeout.stopAndClear(); + this.fullBytesTimeout.clear(); const data = Utils.joinChunks(this.chunks); this._status = "succeed"; + this._totalBytes = this._loadedBytes; this.prevAttempts.push(this.currentAttempt); this._engineCallbacks?.onSuccess({ @@ -203,7 +201,7 @@ export class Request extends EventDispatcher { private firstBytesReceived = () => { this.throwErrorIfNotLoadingStatus(); - this.firstBytesTimeout.stopAndClear(); + this.firstBytesTimeout.clear(); }; private cancelOnError = (error: RequestError) => { @@ -224,6 +222,7 @@ export class Request extends EventDispatcher { } this.currentAttempt.error = error; this.prevAttempts.push(this.currentAttempt); + this.clearTimeouts(); this.dispatch("onError", this, error); } @@ -237,9 +236,14 @@ export class Request extends EventDispatcher { this.throwRequestError(new RequestError("full-bytes-timeout"), true); }; + private clearTimeouts() { + this.firstBytesTimeout.clear(); + this.fullBytesTimeout.clear(); + } + private throwErrorIfNotLoadingStatus() { if (this._status !== "loading") { - throw new Error("Request has been already completed/aborted/failed."); + throw new Error(`Request has been already ${this.status}.`); } } @@ -304,7 +308,7 @@ export class Timeout { this.timeoutId = window.setTimeout(this.action, ms); } - stopAndClear() { + clear() { clearTimeout(this.timeoutId); this.timeoutId = undefined; } From 6475ce897be560ca7102c7b03f652cf270177601 Mon Sep 17 00:00:00 2001 From: Igor Zolotarenko Date: Thu, 16 Nov 2023 18:16:47 +0200 Subject: [PATCH 13/22] Make broadcastAnnouncement method as function expression. --- packages/p2p-media-loader-core/src/p2p/loader.ts | 4 ++-- 1 file changed, 2 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 01fef1eb..2629b5fe 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -127,7 +127,7 @@ export class P2PLoader { peer.sendSegmentsAnnouncement(announcement); }; - broadcastAnnouncement() { + broadcastAnnouncement = () => { if (this.isAnnounceMicrotaskCreated) return; this.isAnnounceMicrotaskCreated = true; @@ -138,7 +138,7 @@ export class P2PLoader { } this.isAnnounceMicrotaskCreated = false; }); - } + }; private onSegmentRequested = async ( peer: Peer, From e354c72c07e7d17329a5ebf047515e84a69bd36e Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 27 Nov 2023 11:47:55 +0200 Subject: [PATCH 14/22] Refactor Request code. Create separate flows for each type of abort. --- .../p2p-media-loader-core/src/http-loader.ts | 7 +- .../src/hybrid-loader.ts | 33 +++-- .../p2p-media-loader-core/src/p2p/loader.ts | 6 +- .../p2p-media-loader-core/src/p2p/peer.ts | 10 +- .../src/request-container.ts | 6 +- packages/p2p-media-loader-core/src/request.ts | 138 ++++++++---------- packages/p2p-media-loader-core/src/types.d.ts | 5 +- 7 files changed, 94 insertions(+), 111 deletions(-) diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index a4f2e384..d81d9d3b 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -3,7 +3,7 @@ import { Request, RequestError, HttpRequestErrorType } from "./request"; export async function fulfillHttpSegmentRequest( request: Request, - settings: Pick + settings: Pick ) { const headers = new Headers(); const { segment } = request; @@ -20,7 +20,7 @@ export async function fulfillHttpSegmentRequest( { type: "http" }, { abort: () => abortController.abort("abort"), - fullLoadingTimeoutMs: settings.httpDownloadTimeoutMs, + notReceivingBytesTimeoutMs: settings.httpNotReceivingBytesTimeoutMs, } ); try { @@ -52,8 +52,7 @@ export async function fulfillHttpSegmentRequest( ) ? new RequestError("fetch-error", error.message) : error; - console.log("HTTP ERROR"); - requestControls.cancelOnError(httpLoaderError); + requestControls.abortOnError(httpLoaderError); } } } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 6ee40cdb..f75c3976 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -107,10 +107,10 @@ export class HybridLoader { request.setEngineCallbacks(callbacks); } - this.createProcessQueueMicrotask(); + this.requestProcessQueueMicrotask(); } - private createProcessQueueMicrotask(force = true) { + private requestProcessQueueMicrotask(force = true) { const now = performance.now(); if ( (!force && @@ -123,9 +123,12 @@ export class HybridLoader { this.isProcessQueueMicrotaskCreated = true; queueMicrotask(() => { - this.processQueue(); - this.lastQueueProcessingTimeStamp = now; - this.isProcessQueueMicrotaskCreated = false; + try { + this.processQueue(); + this.lastQueueProcessingTimeStamp = now; + } finally { + this.isProcessQueueMicrotaskCreated = false; + } }); } @@ -137,13 +140,13 @@ export class HybridLoader { skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); - for (const request of this.requests.values()) { + for (const request of this.requests.items()) { if ( !request.isSegmentRequestedByEngine && request.status === "loading" && !queueSegmentIds.has(request.segment.localId) ) { - request.abort(); + request.abortFromProcessQueue(); this.requests.remove(request.segment); } } @@ -201,8 +204,8 @@ export class HybridLoader { abortSegment(segment: Segment) { const request = this.requests.get(segment); if (!request) return; - request.abortEngineRequest(); - this.createProcessQueueMicrotask(); + request.abortFromEngine(); + this.requestProcessQueueMicrotask(); this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); } @@ -242,14 +245,14 @@ export class HybridLoader { } this.eventHandlers?.onSegmentLoaded?.(data.byteLength, requestType); - this.createProcessQueueMicrotask(); + this.requestProcessQueueMicrotask(); }; private onRequestError = (request: Request) => { if (request.type === "http") { this.p2pLoaders.currentLoader.broadcastAnnouncement(); } - this.createProcessQueueMicrotask(); + this.requestProcessQueueMicrotask(); }; private loadRandomThroughHttp() { @@ -290,7 +293,7 @@ export class HybridLoader { for (const { segment: itemSegment } of arrayBackwards(queue)) { if (itemSegment.localId === segment.localId) break; if (this.requests.isHttpRequested(segment)) { - this.requests.get(segment)?.abort(); + this.requests.get(segment)?.abortFromProcessQueue(); this.logger.loader( "http aborted: ", LoggerUtils.getSegmentString(segment) @@ -304,7 +307,7 @@ export class HybridLoader { for (const { segment: itemSegment } of arrayBackwards(queue)) { if (itemSegment.localId === segment.localId) break; if (this.requests.isP2PRequested(segment)) { - this.requests.get(segment)?.abort(); + this.requests.get(segment)?.abortFromProcessQueue(); this.logger.loader( "p2p aborted: ", LoggerUtils.getSegmentString(segment) @@ -329,13 +332,13 @@ export class HybridLoader { if (isPositionSignificantlyChanged) { this.logger.engine("position significantly changed"); } - void this.createProcessQueueMicrotask(isPositionSignificantlyChanged); + void this.requestProcessQueueMicrotask(isPositionSignificantlyChanged); } updateStream(stream: StreamWithSegments) { if (stream !== this.lastRequestedSegment.stream) return; this.logger.engine(`update stream: ${LoggerUtils.getStreamString(stream)}`); - this.createProcessQueueMicrotask(); + this.requestProcessQueueMicrotask(); } destroy() { diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index 2629b5fe..4177ffb6 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -86,9 +86,9 @@ export class P2PLoader { statuses )}` ); - request.subscribe("onSuccess", () => { - this.logger(`p2p loaded: ${segment.externalId}`); - }); + // request.subscribe("onSuccess", () => { + // this.logger(`p2p loaded: ${segment.externalId}`); + // }); return request; } diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index ce11cb42..341c3695 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -26,9 +26,7 @@ type PeerEventHandlers = { type PeerSettings = Pick< Settings, - | "p2pSegmentDownloadTimeoutMs" - | "p2pSegmentFirstBytesTimeoutMs" - | "webRtcMaxMessageSize" + "p2pNotReceivingBytesTimeoutMs" | "webRtcMaxMessageSize" >; export class Peer { @@ -131,8 +129,8 @@ export class Peer { { type: "p2p", peerId: this.id }, { abort: this.abortRequest, - firstBytesTimeoutMs: this.settings.p2pSegmentFirstBytesTimeoutMs, - fullLoadingTimeoutMs: this.settings.p2pSegmentDownloadTimeoutMs, + notReceivingBytesTimeoutMs: + this.settings.p2pNotReceivingBytesTimeoutMs, } ), }; @@ -233,7 +231,7 @@ export class Peer { if (type === "peer-response-bytes-mismatch") { this.sendCancelSegmentRequestCommand(request.segment); } - controls.cancelOnError(error); + controls.abortOnError(error); this.requestContext = undefined; } diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index ce3e0e5f..af2bda0b 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -53,7 +53,7 @@ export class RequestsContainer { this.requests.delete(segment); } - values() { + items() { return this.requests.values(); } @@ -83,8 +83,8 @@ export class RequestsContainer { destroy() { for (const request of this.requests.values()) { - request.abort(); - request.abortEngineRequest(); + request.abortFromProcessQueue(); + request.abortFromEngine(); } this.requests.clear(); } diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 3380d529..49d30020 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -37,7 +37,7 @@ export type RequestControls = Readonly<{ firstBytesReceived: Request["firstBytesReceived"]; addLoadedChunk: Request["addLoadedChunk"]; completeOnSuccess: Request["completeOnSuccess"]; - cancelOnError: Request["cancelOnError"]; + abortOnError: Request["abortOnError"]; }>; type OmitEncapsulated = Omit; @@ -52,28 +52,25 @@ type RequestStatus = | "failed" | "aborted"; -export class Request extends EventDispatcher { +export class Request { readonly id: string; private _engineCallbacks?: EngineCallbacks; private currentAttempt?: RequestAttempt; - private prevAttempts: RequestAttempt[] = []; - private chunks: Uint8Array[] = []; + private _failedAttempts: RequestAttempt[] = []; + private bytes: Uint8Array[] = []; private _loadedBytes = 0; private _totalBytes?: number; private _status: RequestStatus = "not-started"; private progress?: LoadProgress; - private firstBytesTimeout: Timeout; - private fullBytesTimeout: Timeout; + private notReceivingBytesTimeout: Timeout; private _abortRequestCallback?: (errorType: RequestInnerErrorType) => void; constructor( readonly segment: Segment, private readonly bandwidthApproximator: BandwidthApproximator ) { - super(); this.id = Request.getRequestItemId(segment); - this.firstBytesTimeout = new Timeout(this.onFirstBytesTimeout); - this.fullBytesTimeout = new Timeout(this.onFullBytesTimeout); + this.notReceivingBytesTimeout = new Timeout(this.abortOnTimeout); } get status() { @@ -101,8 +98,8 @@ export class Request extends EventDispatcher { return Utils.getPercent(this.loadedBytes, this._totalBytes); } - get requestAttempts(): ReadonlyArray> { - return this.prevAttempts; + get failedAttempts(): ReadonlyArray> { + return this._failedAttempts; } setEngineCallbacks(callbacks: EngineCallbacks) { @@ -122,8 +119,7 @@ export class Request extends EventDispatcher { start( requestData: StartRequestParameters, controls: { - firstBytesTimeoutMs?: number; - fullLoadingTimeoutMs?: number; + notReceivingBytesTimeoutMs?: number; abort: (errorType: RequestInnerErrorType) => void; } ): RequestControls { @@ -142,58 +138,78 @@ export class Request extends EventDispatcher { startTimestamp: performance.now(), }; this.bandwidthApproximator.addLoading(this.progress); - const { firstBytesTimeoutMs, fullLoadingTimeoutMs, abort } = controls; + const { notReceivingBytesTimeoutMs, abort } = controls; this._abortRequestCallback = abort; - if (firstBytesTimeoutMs !== undefined) { - this.firstBytesTimeout.start(firstBytesTimeoutMs); - } - if (fullLoadingTimeoutMs !== undefined) { - this.fullBytesTimeout.start(fullLoadingTimeoutMs); + + if (notReceivingBytesTimeoutMs !== undefined) { + this.notReceivingBytesTimeout.start(notReceivingBytesTimeoutMs); } + return { firstBytesReceived: this.firstBytesReceived, addLoadedChunk: this.addLoadedChunk, completeOnSuccess: this.completeOnSuccess, - cancelOnError: this.cancelOnError, + abortOnError: this.abortOnError, }; } - abort() { + abortFromEngine() { + this._engineCallbacks?.onError(new CoreRequestError("aborted")); + this._engineCallbacks = undefined; + } + + abortFromProcessQueue() { this.throwErrorIfNotLoadingStatus(); - if (!this._abortRequestCallback) return; this._status = "aborted"; - this._abortRequestCallback("abort"); + this._abortRequestCallback?.("abort"); this._abortRequestCallback = undefined; - this.clearTimeouts(); + this.notReceivingBytesTimeout.clear(); } - abortEngineRequest() { - this._engineCallbacks?.onError(new CoreRequestError("aborted")); - this._engineCallbacks = undefined; - } + private abortOnTimeout = () => { + this.throwErrorIfNotLoadingStatus(); + if (!this.currentAttempt) return; + + this._status = "failed"; + const error = new RequestError("bytes-receiving-timeout"); + this._abortRequestCallback?.(error.type); + + this.currentAttempt.error = error; + this._failedAttempts.push(this.currentAttempt); + this.notReceivingBytesTimeout.clear(); + }; + + private abortOnError = (error: RequestError) => { + this.throwErrorIfNotLoadingStatus(); + if (!this.currentAttempt) return; + + this._status = "failed"; + this.currentAttempt.error = error; + this._failedAttempts.push(this.currentAttempt); + this.notReceivingBytesTimeout.clear(); + }; private completeOnSuccess = () => { this.throwErrorIfNotLoadingStatus(); if (!this.currentAttempt) return; - this.fullBytesTimeout.clear(); - const data = Utils.joinChunks(this.chunks); + this.notReceivingBytesTimeout.clear(); + const data = Utils.joinChunks(this.bytes); this._status = "succeed"; this._totalBytes = this._loadedBytes; - this.prevAttempts.push(this.currentAttempt); this._engineCallbacks?.onSuccess({ data, bandwidth: this.bandwidthApproximator.getBandwidth(), }); - this.dispatch("onSuccess", this, data); }; private addLoadedChunk = (chunk: Uint8Array) => { this.throwErrorIfNotLoadingStatus(); if (!this.currentAttempt || !this.progress) return; + this.notReceivingBytesTimeout.restart(); - this.chunks.push(chunk); + this.bytes.push(chunk); this.progress.lastLoadedChunkTimestamp = performance.now(); this.progress.loadedBytes += chunk.length; this._loadedBytes += chunk.length; @@ -201,46 +217,9 @@ export class Request extends EventDispatcher { private firstBytesReceived = () => { this.throwErrorIfNotLoadingStatus(); - this.firstBytesTimeout.clear(); - }; - - private cancelOnError = (error: RequestError) => { - this.throwErrorIfNotLoadingStatus(); - this.throwRequestError(error, false); + this.notReceivingBytesTimeout.restart(); }; - private throwRequestError(error: RequestError, abort = true) { - this.throwErrorIfNotLoadingStatus(); - if (!this.currentAttempt) return; - this._status = "failed"; - if ( - abort && - this._abortRequestCallback && - RequestError.isRequestInnerErrorType(error) - ) { - this._abortRequestCallback(error.type); - } - this.currentAttempt.error = error; - this.prevAttempts.push(this.currentAttempt); - this.clearTimeouts(); - this.dispatch("onError", this, error); - } - - private onFirstBytesTimeout = () => { - this.throwErrorIfNotLoadingStatus(); - this.throwRequestError(new RequestError("first-bytes-timeout"), true); - }; - - private onFullBytesTimeout = () => { - this.throwErrorIfNotLoadingStatus(); - this.throwRequestError(new RequestError("full-bytes-timeout"), true); - }; - - private clearTimeouts() { - this.firstBytesTimeout.clear(); - this.fullBytesTimeout.clear(); - } - private throwErrorIfNotLoadingStatus() { if (this._status !== "loading") { throw new Error(`Request has been already ${this.status}.`); @@ -252,11 +231,7 @@ export class Request extends EventDispatcher { } } -const requestInnerErrorTypes = [ - "abort", - "first-bytes-timeout", - "full-bytes-timeout", -] as const; +const requestInnerErrorTypes = ["abort", "bytes-receiving-timeout"] as const; const httpRequestErrorTypes = ["fetch-error"] as const; @@ -298,6 +273,7 @@ export class CoreRequestError extends Error { export class Timeout { private timeoutId?: number; + private ms?: number; constructor(private readonly action: () => void) {} @@ -305,7 +281,15 @@ export class Timeout { if (this.timeoutId) { throw new Error("Timeout is already started."); } - this.timeoutId = window.setTimeout(this.action, ms); + this.ms = ms; + this.timeoutId = window.setTimeout(this.action, this.ms); + } + + restart(ms?: number) { + if (this.timeoutId) clearTimeout(this.timeoutId); + if (ms) this.ms = ms; + if (!this.ms) return; + this.timeoutId = window.setTimeout(this.action, this.ms); } clear() { diff --git a/packages/p2p-media-loader-core/src/types.d.ts b/packages/p2p-media-loader-core/src/types.d.ts index 2e0ba396..85d65fd3 100644 --- a/packages/p2p-media-loader-core/src/types.d.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -56,10 +56,9 @@ export type Settings = { cachedSegmentExpiration: number; cachedSegmentsCount: number; webRtcMaxMessageSize: number; - p2pSegmentFirstBytesTimeoutMs: number; - p2pSegmentDownloadTimeoutMs: number; + p2pNotReceivingBytesTimeoutMs: number; p2pLoaderDestroyTimeoutMs: number; - httpDownloadTimeoutMs: number; + httpNotReceivingBytesTimeoutMs: number; }; export type CoreEventHandlers = { From c6080797796c91a3fef8bc3d0a033a0331407391 Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 27 Nov 2023 13:11:02 +0200 Subject: [PATCH 15/22] Remove event subscriptions to request instance. --- .../src/hybrid-loader.ts | 73 +++++++++---------- .../src/request-container.ts | 14 ++-- packages/p2p-media-loader-core/src/request.ts | 18 ++++- 3 files changed, 56 insertions(+), 49 deletions(-) diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index f75c3976..bf373204 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -5,7 +5,7 @@ import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestsContainer } from "./request-container"; -import { Request, EngineCallbacks } from "./request"; +import { EngineCallbacks, RequestStatus } from "./request"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import * as StreamUtils from "./utils/stream"; @@ -42,6 +42,7 @@ export class HybridLoader { this.segmentAvgDuration = StreamUtils.getSegmentAvgDuration(activeStream); this.requests = new RequestsContainer( requestedSegment.stream.type, + this.requestProcessQueueMicrotask, this.bandwidthApproximator ); @@ -110,7 +111,7 @@ export class HybridLoader { this.requestProcessQueueMicrotask(); } - private requestProcessQueueMicrotask(force = true) { + private requestProcessQueueMicrotask = (force = true) => { const now = performance.now(); if ( (!force && @@ -130,7 +131,7 @@ export class HybridLoader { this.isProcessQueueMicrotaskCreated = false; } }); - } + }; private processQueue() { const { queue, queueSegmentIds } = QueueUtils.generateQueue({ @@ -140,14 +141,41 @@ export class HybridLoader { skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); + const removeRequestStatuses: RequestStatus[] = ["not-started", "aborted"]; for (const request of this.requests.items()) { if ( - !request.isSegmentRequestedByEngine && request.status === "loading" && + !request.isSegmentRequestedByEngine && !queueSegmentIds.has(request.segment.localId) ) { request.abortFromProcessQueue(); - this.requests.remove(request.segment); + this.requests.remove(request); + continue; + } + + if (request.status === "succeed") { + if (request.type === "http") { + this.p2pLoaders.currentLoader.broadcastAnnouncement(); + } + this.eventHandlers?.onSegmentLoaded?.( + request.data!.byteLength, + request.type! + ); + continue; + } + + if (request.status === "failed") { + if (request.type === "http") { + this.p2pLoaders.currentLoader.broadcastAnnouncement(); + } + continue; + } + + if ( + removeRequestStatuses.includes(request.status) && + !request.isSegmentRequestedByEngine + ) { + this.requests.remove(request); } } @@ -205,19 +233,14 @@ export class HybridLoader { const request = this.requests.get(segment); if (!request) return; request.abortFromEngine(); - this.requestProcessQueueMicrotask(); this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); } private async loadThroughHttp(item: QueueItem, isRandom = false) { const { segment } = item; - const request = this.requests.getOrCreateRequest(segment); - request.subscribe("onSuccess", this.onRequestSuccess); - request.subscribe("onError", this.onRequestError); - - this.p2pLoaders.currentLoader.broadcastAnnouncement(); void fulfillHttpSegmentRequest(request, this.settings); + this.p2pLoaders.currentLoader.broadcastAnnouncement(); if (!isRandom) { this.logger.loader( `http request: ${LoggerUtils.getQueueItemString(item)}` @@ -226,35 +249,9 @@ export class HybridLoader { } private async loadThroughP2P(item: QueueItem) { - const p2pLoader = this.p2pLoaders.currentLoader; - const request = p2pLoader.downloadSegment(item); - if (request === undefined) return; - - request.subscribe("onSuccess", this.onRequestSuccess); - request.subscribe("onError", this.onRequestError); + this.p2pLoaders.currentLoader.downloadSegment(item); } - private onRequestSuccess = (request: Request, data: ArrayBuffer) => { - const requestType = request.type; - if (!requestType) return; - - void this.segmentStorage.storeSegment(request.segment, data); - if (requestType === "http") { - this.logger.loader(`http responses: ${request.segment.externalId}`); - this.p2pLoaders.currentLoader.broadcastAnnouncement(); - } - - this.eventHandlers?.onSegmentLoaded?.(data.byteLength, requestType); - this.requestProcessQueueMicrotask(); - }; - - private onRequestError = (request: Request) => { - if (request.type === "http") { - this.p2pLoaders.currentLoader.broadcastAnnouncement(); - } - this.requestProcessQueueMicrotask(); - }; - private loadRandomThroughHttp() { const { simultaneousHttpDownloads } = this.settings; const p2pLoader = this.p2pLoaders.currentLoader; diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index af2bda0b..4b6fd97b 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -9,6 +9,7 @@ export class RequestsContainer { constructor( streamType: StreamType, + private readonly requestProcessQueueCallback: () => void, private readonly bandwidthApproximator: BandwidthApproximator ) { this.logger = Debug(`core:requests-container-${streamType}`); @@ -38,19 +39,18 @@ export class RequestsContainer { getOrCreateRequest(segment: Segment) { let request = this.requests.get(segment); if (!request) { - request = new Request(segment, this.bandwidthApproximator); - request.subscribe("onSuccess", this.onRequestCompleted); + request = new Request( + segment, + this.requestProcessQueueCallback, + this.bandwidthApproximator + ); this.requests.set(segment, request); } return request; } - private onRequestCompleted: RequestEvents["onSuccess"] = (request) => { + remove(request: Request) { this.requests.delete(request.segment); - }; - - remove(segment: Segment) { - this.requests.delete(segment); } items() { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 49d30020..d2edabc4 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,4 +1,3 @@ -import { EventDispatcher } from "./event-dispatcher"; import { Segment, SegmentResponse } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import * as Utils from "./utils/utils"; @@ -45,7 +44,7 @@ type StartRequestParameters = | OmitEncapsulated | OmitEncapsulated; -type RequestStatus = +export type RequestStatus = | "not-started" | "loading" | "succeed" @@ -57,6 +56,7 @@ export class Request { private _engineCallbacks?: EngineCallbacks; private currentAttempt?: RequestAttempt; private _failedAttempts: RequestAttempt[] = []; + private finalData?: ArrayBuffer; private bytes: Uint8Array[] = []; private _loadedBytes = 0; private _totalBytes?: number; @@ -67,6 +67,7 @@ export class Request { constructor( readonly segment: Segment, + private readonly requestProcessQueueCallback: () => void, private readonly bandwidthApproximator: BandwidthApproximator ) { this.id = Request.getRequestItemId(segment); @@ -93,6 +94,11 @@ export class Request { return this._totalBytes; } + get data(): ArrayBuffer | undefined { + if (this.status !== "succeed") return; + return this.finalData ?? Utils.joinChunks(this.bytes); + } + get loadedPercent() { if (!this._totalBytes) return; return Utils.getPercent(this.loadedBytes, this._totalBytes); @@ -156,6 +162,7 @@ export class Request { abortFromEngine() { this._engineCallbacks?.onError(new CoreRequestError("aborted")); this._engineCallbacks = undefined; + this.requestProcessQueueCallback(); } abortFromProcessQueue() { @@ -163,6 +170,7 @@ export class Request { this._status = "aborted"; this._abortRequestCallback?.("abort"); this._abortRequestCallback = undefined; + this.currentAttempt = undefined; this.notReceivingBytesTimeout.clear(); } @@ -177,6 +185,7 @@ export class Request { this.currentAttempt.error = error; this._failedAttempts.push(this.currentAttempt); this.notReceivingBytesTimeout.clear(); + this.requestProcessQueueCallback(); }; private abortOnError = (error: RequestError) => { @@ -187,6 +196,7 @@ export class Request { this.currentAttempt.error = error; this._failedAttempts.push(this.currentAttempt); this.notReceivingBytesTimeout.clear(); + this.requestProcessQueueCallback(); }; private completeOnSuccess = () => { @@ -194,12 +204,12 @@ export class Request { if (!this.currentAttempt) return; this.notReceivingBytesTimeout.clear(); - const data = Utils.joinChunks(this.bytes); + this.finalData = Utils.joinChunks(this.bytes); this._status = "succeed"; this._totalBytes = this._loadedBytes; this._engineCallbacks?.onSuccess({ - data, + data: this.finalData, bandwidth: this.bandwidthApproximator.getBandwidth(), }); }; From 15f83589934af2c0434280aef45a0838ae16f203 Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 27 Nov 2023 13:29:50 +0200 Subject: [PATCH 16/22] Fix type errors. --- packages/p2p-media-loader-core/src/core.ts | 5 ++-- .../src/hybrid-loader.ts | 29 +++++++++---------- .../src/request-container.ts | 2 +- packages/p2p-media-loader-core/src/request.ts | 5 ---- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 786eb9df..58c40a09 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -25,10 +25,9 @@ export class Core { cachedSegmentExpiration: 120 * 1000, cachedSegmentsCount: 50, webRtcMaxMessageSize: 64 * 1024 - 1, - p2pSegmentFirstBytesTimeoutMs: 1000, - p2pSegmentDownloadTimeoutMs: 5000, + p2pNotReceivingBytesTimeoutMs: 1000, p2pLoaderDestroyTimeoutMs: 30 * 1000, - httpDownloadTimeoutMs: 5000, + httpNotReceivingBytesTimeoutMs: 1000, }; 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 bf373204..a4232b9b 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -5,7 +5,7 @@ import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; import { RequestsContainer } from "./request-container"; -import { EngineCallbacks, RequestStatus } from "./request"; +import { EngineCallbacks } from "./request"; import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import * as StreamUtils from "./utils/stream"; @@ -141,26 +141,25 @@ export class HybridLoader { skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); - const removeRequestStatuses: RequestStatus[] = ["not-started", "aborted"]; for (const request of this.requests.items()) { - if ( - request.status === "loading" && - !request.isSegmentRequestedByEngine && - !queueSegmentIds.has(request.segment.localId) - ) { - request.abortFromProcessQueue(); - this.requests.remove(request); + if (request.status === "loading") { + if ( + !request.isSegmentRequestedByEngine && + !queueSegmentIds.has(request.segment.localId) + ) { + request.abortFromProcessQueue(); + this.requests.remove(request); + } continue; } if (request.status === "succeed") { - if (request.type === "http") { + const { type, data } = request; + if (!type || !data) continue; + if (type === "http") { this.p2pLoaders.currentLoader.broadcastAnnouncement(); } - this.eventHandlers?.onSegmentLoaded?.( - request.data!.byteLength, - request.type! - ); + this.eventHandlers?.onSegmentLoaded?.(data.byteLength, type); continue; } @@ -172,7 +171,7 @@ export class HybridLoader { } if ( - removeRequestStatuses.includes(request.status) && + (request.status === "not-started" || request.status === "aborted") && !request.isSegmentRequestedByEngine ) { this.requests.remove(request); diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 4b6fd97b..de5ae927 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,7 +1,7 @@ import { Segment, StreamType } from "./types"; import Debug from "debug"; import { BandwidthApproximator } from "./bandwidth-approximator"; -import { Request, RequestEvents } from "./request"; +import { Request } from "./request"; export class RequestsContainer { private readonly requests = new Map(); diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index d2edabc4..8843e9ef 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -27,11 +27,6 @@ type P2PRequestAttempt = { export type RequestAttempt = HttpRequestAttempt | P2PRequestAttempt; -export type RequestEvents = { - onSuccess: (request: Request, data: ArrayBuffer) => void; - onError: (request: Request, data: RequestError) => void; -}; - export type RequestControls = Readonly<{ firstBytesReceived: Request["firstBytesReceived"]; addLoadedChunk: Request["addLoadedChunk"]; From bb4f57a996d636a64e3f6bdb3b9f2be673f54568 Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 27 Nov 2023 17:40:43 +0200 Subject: [PATCH 17/22] Fix bugs. --- packages/p2p-media-loader-core/src/core.ts | 10 ++++----- .../src/hybrid-loader.ts | 21 ++++++++++++++----- .../src/request-container.ts | 6 ++++++ packages/p2p-media-loader-core/src/request.ts | 21 +++++++++++++------ packages/p2p-media-loader-shaka/src/engine.ts | 7 ++++--- .../src/loading-handler.ts | 14 ++++++------- 6 files changed, 51 insertions(+), 28 deletions(-) diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index 58c40a09..8ee51cb7 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -17,7 +17,7 @@ export class Core { private manifestResponseUrl?: string; private readonly streams = new Map>(); private readonly settings: Settings = { - simultaneousHttpDownloads: 1, + simultaneousHttpDownloads: 3, simultaneousP2PDownloads: 3, highDemandTimeWindow: 15, httpDownloadTimeWindow: 45, @@ -93,11 +93,9 @@ export class Core { void loader.loadSegment(segment, callbacks); } - abortSegmentLoading(segmentId: string): void { - const segment = this.identifySegment(segmentId); - const streamType = segment.stream.type; - if (streamType === "main") this.mainStreamLoader?.abortSegment(segment); - else this.secondaryStreamLoader?.abortSegment(segment); + abortSegmentLoading(segmentLocalId: string): void { + this.mainStreamLoader?.abortSegmentRequest(segmentLocalId); + this.secondaryStreamLoader?.abortSegmentRequest(segmentLocalId); } 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 a4232b9b..7d391cc1 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -105,7 +105,7 @@ export class HybridLoader { } } else { const request = this.requests.getOrCreateRequest(segment); - request.setEngineCallbacks(callbacks); + request.setOrResolveEngineCallbacks(callbacks); } this.requestProcessQueueMicrotask(); @@ -138,7 +138,12 @@ export class HybridLoader { lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, settings: this.settings, - skipSegment: (segment) => this.segmentStorage.hasSegment(segment), + skipSegment: (segment) => { + return ( + this.requests.get(segment)?.status === "succeed" || + this.segmentStorage.hasSegment(segment) + ); + }, }); for (const request of this.requests.items()) { @@ -159,7 +164,9 @@ export class HybridLoader { if (type === "http") { this.p2pLoaders.currentLoader.broadcastAnnouncement(); } + void this.segmentStorage.storeSegment(request.segment, data); this.eventHandlers?.onSegmentLoaded?.(data.byteLength, type); + this.requests.remove(request); continue; } @@ -187,6 +194,7 @@ export class HybridLoader { if (statuses.isHighDemand) { if (request?.type === "http") continue; + if (this.requests.executingHttpCount < simultaneousHttpDownloads) { void this.loadThroughHttp(item); continue; @@ -228,11 +236,14 @@ export class HybridLoader { } // api method for engines - abortSegment(segment: Segment) { - const request = this.requests.get(segment); + abortSegmentRequest(segmentLocalId: string) { + const request = this.requests.getBySegmentLocalId(segmentLocalId); if (!request) return; request.abortFromEngine(); - this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); + this.logger.engine( + "abort: ", + LoggerUtils.getSegmentString(request.segment) + ); } private async loadThroughHttp(item: QueueItem, isRandom = false) { diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index de5ae927..4b98691d 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -36,6 +36,12 @@ export class RequestsContainer { return this.requests.get(segment); } + getBySegmentLocalId(id: string) { + for (const request of this.requests.values()) { + if (request.segment.localId === id) return request; + } + } + getOrCreateRequest(segment: Segment) { let request = this.requests.get(segment); if (!request) { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 8843e9ef..98198a92 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 { BandwidthApproximator } from "./bandwidth-approximator"; import * as Utils from "./utils/utils"; +import * as LoggerUtils from "./utils/logger"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -91,7 +92,8 @@ export class Request { get data(): ArrayBuffer | undefined { if (this.status !== "succeed") return; - return this.finalData ?? Utils.joinChunks(this.bytes); + if (!this.finalData) this.finalData = Utils.joinChunks(this.bytes); + return this.finalData; } get loadedPercent() { @@ -103,11 +105,12 @@ export class Request { return this._failedAttempts; } - setEngineCallbacks(callbacks: EngineCallbacks) { + setOrResolveEngineCallbacks(callbacks: EngineCallbacks) { if (this._engineCallbacks) { throw new Error("Segment is already requested by engine"); } this._engineCallbacks = callbacks; + if (this.finalData) this.resolveEngineCallbacksSuccessfully(this.finalData); } setTotalBytes(value: number) { @@ -154,6 +157,14 @@ export class Request { }; } + private resolveEngineCallbacksSuccessfully(data: ArrayBuffer) { + this._engineCallbacks?.onSuccess({ + data, + bandwidth: this.bandwidthApproximator.getBandwidth(), + }); + this._engineCallbacks = undefined; + } + abortFromEngine() { this._engineCallbacks?.onError(new CoreRequestError("aborted")); this._engineCallbacks = undefined; @@ -198,15 +209,13 @@ export class Request { this.throwErrorIfNotLoadingStatus(); if (!this.currentAttempt) return; + console.log("segment loaded", LoggerUtils.getSegmentString(this.segment)); this.notReceivingBytesTimeout.clear(); this.finalData = Utils.joinChunks(this.bytes); this._status = "succeed"; this._totalBytes = this._loadedBytes; - this._engineCallbacks?.onSuccess({ - data: this.finalData, - bandwidth: this.bandwidthApproximator.getBandwidth(), - }); + this.resolveEngineCallbacksSuccessfully(this.finalData); }; private addLoadedChunk = (chunk: Uint8Array) => { diff --git a/packages/p2p-media-loader-shaka/src/engine.ts b/packages/p2p-media-loader-shaka/src/engine.ts index 8ccb40d4..0992985b 100644 --- a/packages/p2p-media-loader-shaka/src/engine.ts +++ b/packages/p2p-media-loader-shaka/src/engine.ts @@ -13,7 +13,7 @@ import { HookedRequest, P2PMLShakaData, } from "./types"; -import { LoadingHandler } from "./loading-handler"; +import { Loader } from "./loading-handler"; import { decorateMethod } from "./utils"; import { Core, CoreEventHandlers } from "p2p-media-loader-core"; @@ -113,13 +113,14 @@ export class Engine { const { p2pml } = request; if (!p2pml) return this.shaka.net.HttpFetchPlugin.parse(...args); - const loadingHandler = new LoadingHandler( + console.log("HANDLE LOADING", p2pml); + const loadingHandler = new Loader( p2pml.shaka, p2pml.core, p2pml.streamInfo, p2pml.segmentManager ); - return loadingHandler.handleLoading(...args); + return loadingHandler.load(...args); }; this.shaka.net.NetworkingEngine.registerScheme("http", handleLoading); diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index cda4a8dd..512f75b8 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -9,15 +9,11 @@ import { EngineCallbacks, } from "p2p-media-loader-core"; -interface LoadingHandlerInterface { - handleLoading: shaka.extern.SchemePlugin; -} - type LoadingHandlerParams = Parameters; type Response = shaka.extern.Response; type LoadingHandlerResult = shaka.extern.IAbortableOperation; -export class LoadingHandler implements LoadingHandlerInterface { +export class Loader { private loadArgs!: LoadingHandlerParams; constructor( @@ -32,12 +28,12 @@ export class LoadingHandler implements LoadingHandlerInterface { return fetchPlugin.parse(...this.loadArgs); } - handleLoading(...args: LoadingHandlerParams): LoadingHandlerResult { + load(...args: LoadingHandlerParams): LoadingHandlerResult { this.loadArgs = args; const { RequestType } = this.shaka.net.NetworkingEngine; const [url, request, requestType] = args; if (requestType === RequestType.SEGMENT) { - return this.handleSegmentLoading(url, request.headers.Range); + return this.loadSegment(url, request.headers.Range); } const loading = this.defaultLoad(); @@ -66,7 +62,7 @@ export class LoadingHandler implements LoadingHandlerInterface { } } - private handleSegmentLoading( + private loadSegment( segmentUrl: string, byteRangeString: string ): LoadingHandlerResult { @@ -75,6 +71,7 @@ export class LoadingHandler implements LoadingHandlerInterface { const loadSegment = async (): Promise => { const { request, callbacks } = getSegmentRequest(); + console.log("segment url", segmentUrl); void this.core.loadSegment(segmentId, callbacks); try { const { data, bandwidth } = await request; @@ -89,6 +86,7 @@ export class LoadingHandler implements LoadingHandlerInterface { ), }; } catch (error) { + console.log(error); // TODO: throw Shaka Errors if (error instanceof CoreRequestError) { const { Error: ShakaError } = this.shaka.util; From 2275bd0c638283c1a442c7f883db32ec73204887 Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 27 Nov 2023 17:42:31 +0200 Subject: [PATCH 18/22] Fix lint warnings. --- packages/p2p-media-loader-core/src/request.ts | 2 -- packages/p2p-media-loader-shaka/src/engine.ts | 1 - packages/p2p-media-loader-shaka/src/loading-handler.ts | 2 -- 3 files changed, 5 deletions(-) diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 98198a92..8ff65c02 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,7 +1,6 @@ import { Segment, SegmentResponse } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import * as Utils from "./utils/utils"; -import * as LoggerUtils from "./utils/logger"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -209,7 +208,6 @@ export class Request { this.throwErrorIfNotLoadingStatus(); if (!this.currentAttempt) return; - console.log("segment loaded", LoggerUtils.getSegmentString(this.segment)); this.notReceivingBytesTimeout.clear(); this.finalData = Utils.joinChunks(this.bytes); this._status = "succeed"; diff --git a/packages/p2p-media-loader-shaka/src/engine.ts b/packages/p2p-media-loader-shaka/src/engine.ts index 0992985b..626fe8de 100644 --- a/packages/p2p-media-loader-shaka/src/engine.ts +++ b/packages/p2p-media-loader-shaka/src/engine.ts @@ -113,7 +113,6 @@ export class Engine { const { p2pml } = request; if (!p2pml) return this.shaka.net.HttpFetchPlugin.parse(...args); - console.log("HANDLE LOADING", p2pml); const loadingHandler = new Loader( p2pml.shaka, p2pml.core, diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index 512f75b8..eed55ddd 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -71,7 +71,6 @@ export class Loader { const loadSegment = async (): Promise => { const { request, callbacks } = getSegmentRequest(); - console.log("segment url", segmentUrl); void this.core.loadSegment(segmentId, callbacks); try { const { data, bandwidth } = await request; @@ -86,7 +85,6 @@ export class Loader { ), }; } catch (error) { - console.log(error); // TODO: throw Shaka Errors if (error instanceof CoreRequestError) { const { Error: ShakaError } = this.shaka.util; From fec7ce4b61603be714c4bcfa4b59585c37ab1bcc Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 27 Nov 2023 17:49:49 +0200 Subject: [PATCH 19/22] Move P2P tracker client to separate file. --- .../p2p-media-loader-core/src/p2p/loader.ts | 143 +---------------- .../src/p2p/tracker-client.ts | 146 ++++++++++++++++++ 2 files changed, 147 insertions(+), 142 deletions(-) create mode 100644 packages/p2p-media-loader-core/src/p2p/tracker-client.ts diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts index 4177ffb6..4cdcdcd8 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -1,7 +1,3 @@ -import TrackerClient, { - PeerConnection, - TrackerClientEvents, -} from "bittorrent-tracker"; import { Peer } from "./peer"; import { Segment, Settings, StreamWithSegments } from "../types"; import { QueueItem } from "../internal-types"; @@ -13,6 +9,7 @@ import * as Utils from "../utils/utils"; import { PeerSegmentStatus } from "../enums"; import { RequestsContainer } from "../request-container"; import { Request } from "../request"; +import { P2PTrackerClient } from "./tracker-client"; import debug from "debug"; export class P2PLoader { @@ -165,141 +162,3 @@ export class P2PLoader { this.trackerClient.destroy(); } } - -type PeerItem = { - peer?: Peer; - potentialConnections: Set; -}; - -type P2PTrackerClientEventHandlers = { - onPeerConnected: (peer: Peer) => void; - onSegmentRequested: (peer: Peer, segmentExternalId: string) => void; -}; - -class P2PTrackerClient { - private readonly client: TrackerClient; - private readonly _peers = new Map(); - private readonly streamHash: string; - - constructor( - private readonly peerId: string, - private readonly streamExternalId: string, - private readonly stream: StreamWithSegments, - private readonly eventHandlers: P2PTrackerClientEventHandlers, - private readonly settings: Settings, - private readonly logger: debug.Debugger - ) { - this.streamHash = PeerUtil.getStreamHash(streamExternalId); - this.client = new TrackerClient({ - infoHash: utf8ToHex(this.streamHash), - peerId: utf8ToHex(this.peerId), - 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.client.on("peer", this.onReceivePeerConnection); - this.client.on("warning", this.onTrackerClientWarning); - this.client.on("error", this.onTrackerClientError); - this.logger( - `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ - this.peerId - }` - ); - } - - start() { - this.client.start(); - } - - destroy() { - this.client.destroy(); - for (const { peer, potentialConnections } of this._peers.values()) { - peer?.destroy(); - for (const connection of potentialConnections) { - connection.destroy(); - } - } - } - - private onReceivePeerConnection: TrackerClientEvents["peer"] = ( - peerConnection - ) => { - let peerItem = this._peers.get(peerConnection.id); - - if (peerItem?.peer) { - peerConnection.destroy(); - return; - } else if (!peerItem) { - peerItem = { potentialConnections: new Set() }; - peerItem.potentialConnections.add(peerConnection); - const itemId = Peer.getPeerIdFromHexString(peerConnection.id); - this._peers.set(itemId, peerItem); - } - - peerConnection.on("connect", () => { - if (!peerItem) return; - - for (const connection of peerItem.potentialConnections) { - if (connection !== peerConnection) connection.destroy(); - } - peerItem.potentialConnections.clear(); - peerItem.peer = new Peer( - peerConnection, - { - onPeerClosed: this.onPeerClosed, - onSegmentRequested: this.eventHandlers.onSegmentRequested, - }, - this.settings - ); - this.eventHandlers.onPeerConnected(peerItem.peer); - }); - }; - - private onTrackerClientWarning: TrackerClientEvents["warning"] = ( - warning - ) => { - this.logger( - `tracker warning (${LoggerUtils.getStreamString( - this.stream - )}: ${warning})` - ); - }; - - private onTrackerClientError: TrackerClientEvents["error"] = (error) => { - this.logger( - `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` - ); - }; - - *peers() { - for (const peerItem of this._peers.values()) { - if (peerItem?.peer) yield peerItem.peer; - } - } - - private onPeerClosed = (peer: Peer) => { - this.logger(`peer closed: ${peer.id}`); - this._peers.delete(peer.id); - }; -} - -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/p2p/tracker-client.ts b/packages/p2p-media-loader-core/src/p2p/tracker-client.ts new file mode 100644 index 00000000..f3ccaaa1 --- /dev/null +++ b/packages/p2p-media-loader-core/src/p2p/tracker-client.ts @@ -0,0 +1,146 @@ +import TrackerClient, { + PeerConnection, + TrackerClientEvents, +} from "bittorrent-tracker"; +import { Settings, StreamWithSegments } from "../types"; +import debug from "debug"; +import * as PeerUtil from "../utils/peer"; +import * as LoggerUtils from "../utils/logger"; +import { Peer } from "./peer"; + +type PeerItem = { + peer?: Peer; + potentialConnections: Set; +}; +type P2PTrackerClientEventHandlers = { + onPeerConnected: (peer: Peer) => void; + onSegmentRequested: (peer: Peer, segmentExternalId: string) => void; +}; + +export class P2PTrackerClient { + private readonly client: TrackerClient; + private readonly _peers = new Map(); + private readonly streamHash: string; + + constructor( + private readonly peerId: string, + private readonly streamExternalId: string, + private readonly stream: StreamWithSegments, + private readonly eventHandlers: P2PTrackerClientEventHandlers, + private readonly settings: Settings, + private readonly logger: debug.Debugger + ) { + this.streamHash = PeerUtil.getStreamHash(streamExternalId); + this.client = new TrackerClient({ + infoHash: utf8ToHex(this.streamHash), + peerId: utf8ToHex(this.peerId), + 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.client.on("peer", this.onReceivePeerConnection); + this.client.on("warning", this.onTrackerClientWarning); + this.client.on("error", this.onTrackerClientError); + this.logger( + `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ + this.peerId + }` + ); + } + + start() { + this.client.start(); + } + + destroy() { + this.client.destroy(); + for (const { peer, potentialConnections } of this._peers.values()) { + peer?.destroy(); + for (const connection of potentialConnections) { + connection.destroy(); + } + } + } + + private onReceivePeerConnection: TrackerClientEvents["peer"] = ( + peerConnection + ) => { + let peerItem = this._peers.get(peerConnection.id); + + if (peerItem?.peer) { + peerConnection.destroy(); + return; + } else if (!peerItem) { + peerItem = { potentialConnections: new Set() }; + peerItem.potentialConnections.add(peerConnection); + const itemId = Peer.getPeerIdFromHexString(peerConnection.id); + this._peers.set(itemId, peerItem); + } + + peerConnection.on("connect", () => { + if (!peerItem) return; + + for (const connection of peerItem.potentialConnections) { + if (connection !== peerConnection) connection.destroy(); + } + peerItem.potentialConnections.clear(); + peerItem.peer = new Peer( + peerConnection, + { + onPeerClosed: this.onPeerClosed, + onSegmentRequested: this.eventHandlers.onSegmentRequested, + }, + this.settings + ); + this.eventHandlers.onPeerConnected(peerItem.peer); + }); + }; + + private onTrackerClientWarning: TrackerClientEvents["warning"] = ( + warning + ) => { + this.logger( + `tracker warning (${LoggerUtils.getStreamString( + this.stream + )}: ${warning})` + ); + }; + + private onTrackerClientError: TrackerClientEvents["error"] = (error) => { + this.logger( + `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` + ); + }; + + *peers() { + for (const peerItem of this._peers.values()) { + if (peerItem?.peer) yield peerItem.peer; + } + } + + private onPeerClosed = (peer: Peer) => { + this.logger(`peer closed: ${peer.id}`); + this._peers.delete(peer.id); + }; +} + +function utf8ToHex(utf8String: string) { + let result = ""; + for (let i = 0; i < utf8String.length; i++) { + result += utf8String.charCodeAt(i).toString(16); + } + + return result; +} From 1a08f15c6bf055e06c4fd7ee0838f2c6414aa63a Mon Sep 17 00:00:00 2001 From: igor Date: Tue, 28 Nov 2023 16:21:38 +0200 Subject: [PATCH 20/22] Fix more bugs. Rewrite loggers. --- p2p-media-loader-demo/src/App.tsx | 8 +- .../src/hybrid-loader.ts | 133 ++++++++---------- .../src/internal-types.d.ts | 20 +-- .../p2p-media-loader-core/src/p2p/loader.ts | 42 +----- .../p2p-media-loader-core/src/p2p/peer.ts | 28 +--- .../src/p2p/tracker-client.ts | 28 ++-- .../src/request-container.ts | 25 ++-- packages/p2p-media-loader-core/src/request.ts | 32 ++++- .../p2p-media-loader-core/src/utils/logger.ts | 9 +- .../p2p-media-loader-core/src/utils/queue.ts | 92 ++---------- .../p2p-media-loader-core/src/utils/stream.ts | 63 ++++++++- .../src/segment-manager.ts | 2 +- 12 files changed, 208 insertions(+), 274 deletions(-) diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index f6b1434c..18b3c620 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -443,13 +443,11 @@ function useLocalStorageItem( const loggers = [ "core:hybrid-loader-main", - "core:hybrid-loader-main-engine", "core:hybrid-loader-secondary", - "core:hybrid-loader-secondary-engine", - "core:p2p-loader", + "core:p2p-tracker-client", "core:peer", "core:p2p-loaders-container", - "core:requests-container-main", - "core:requests-container-secondary", + "core:request-main", + "core:request-secondary", "core:segment-memory-storage", ] 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 7d391cc1..4e84023f 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -3,6 +3,7 @@ import { fulfillHttpSegmentRequest } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; +import { P2PLoadersContainer } from "./p2p/loaders-container"; import { Playback, QueueItem } from "./internal-types"; import { RequestsContainer } from "./request-container"; import { EngineCallbacks } from "./request"; @@ -10,7 +11,6 @@ import * as QueueUtils from "./utils/queue"; import * as LoggerUtils from "./utils/logger"; import * as StreamUtils from "./utils/stream"; import * as Utils from "./utils/utils"; -import { P2PLoadersContainer } from "./p2p/loaders-container"; import debug from "debug"; export class HybridLoader { @@ -22,10 +22,7 @@ export class HybridLoader { private lastQueueProcessingTimeStamp?: number; private readonly segmentAvgDuration: number; private randomHttpDownloadInterval!: number; - private readonly logger: { - engine: debug.Debugger; - loader: debug.Debugger; - }; + private readonly logger: debug.Debugger; private isProcessQueueMicrotaskCreated = false; constructor( @@ -41,9 +38,10 @@ export class HybridLoader { this.playback = { position: requestedSegment.startTime, rate: 1 }; this.segmentAvgDuration = StreamUtils.getSegmentAvgDuration(activeStream); this.requests = new RequestsContainer( - requestedSegment.stream.type, this.requestProcessQueueMicrotask, - this.bandwidthApproximator + this.bandwidthApproximator, + this.playback, + this.settings ); if (!this.segmentStorage.isInitialized) { @@ -51,11 +49,11 @@ export class HybridLoader { } this.segmentStorage.addIsSegmentLockedPredicate((segment) => { if (segment.stream !== activeStream) return false; - const bufferRanges = QueueUtils.getLoadBufferRanges( + return StreamUtils.isSegmentActualInPlayback( + segment, this.playback, this.settings ); - return QueueUtils.isSegmentActual(segment, bufferRanges); }); this.p2pLoaders = new P2PLoadersContainer( this.streamManifestUrl, @@ -65,11 +63,8 @@ export class HybridLoader { this.settings ); - 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.logger = debug(`core:hybrid-loader-${activeStream.type}`); + this.logger.color = "coral"; this.setIntervalLoading(); } @@ -84,12 +79,10 @@ export class HybridLoader { // api method for engines async loadSegment(segment: Readonly, callbacks: EngineCallbacks) { - this.logger.engine(`requests: ${LoggerUtils.getSegmentString(segment)}`); + this.logger(`requests: ${LoggerUtils.getSegmentString(segment)}`); const { stream } = segment; if (stream !== this.lastRequestedSegment.stream) { - this.logger.engine( - `stream changed to ${LoggerUtils.getStreamString(stream)}` - ); + this.logger(`stream changed to ${LoggerUtils.getStreamString(stream)}`); this.p2pLoaders.changeCurrentLoader(stream); } this.lastRequestedSegment = segment; @@ -193,42 +186,48 @@ export class HybridLoader { const request = this.requests.get(segment); if (statuses.isHighDemand) { - if (request?.type === "http") continue; + if (request?.type === "http" && request.status === "loading") continue; if (this.requests.executingHttpCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(item); + void this.loadThroughHttp(segment); continue; } - this.abortLastHttpLoadingAfter(queue, segment); - if (this.requests.executingHttpCount < simultaneousHttpDownloads) { - void this.loadThroughHttp(item); + if ( + this.abortLastHttpLoadingInQueueAfterItem(queue, segment) && + this.requests.executingHttpCount < simultaneousHttpDownloads + ) { + void this.loadThroughHttp(segment); continue; } - if (this.requests.isP2PRequested(segment)) continue; + if (request?.type === "p2p" && request.status === "loading") continue; if (this.requests.executingP2PCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(item); + void this.loadThroughP2P(segment); continue; } - this.abortLastP2PLoadingAfter(queue, segment); - if (this.requests.executingP2PCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(item); + if ( + this.abortLastP2PLoadingInQueueAfterItem(queue, segment) && + this.requests.executingP2PCount < simultaneousP2PDownloads + ) { + void this.loadThroughP2P(segment); } break; } if (statuses.isP2PDownloadable) { - if (request) continue; + if (request?.status === "loading") continue; if (this.requests.executingP2PCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(item); + void this.loadThroughP2P(segment); continue; } - this.abortLastP2PLoadingAfter(queue, segment); - if (this.requests.executingP2PCount < simultaneousP2PDownloads) { - void this.loadThroughP2P(item); + if ( + this.abortLastP2PLoadingInQueueAfterItem(queue, segment) && + this.requests.executingP2PCount < simultaneousP2PDownloads + ) { + void this.loadThroughP2P(segment); } } break; @@ -240,26 +239,17 @@ export class HybridLoader { const request = this.requests.getBySegmentLocalId(segmentLocalId); if (!request) return; request.abortFromEngine(); - this.logger.engine( - "abort: ", - LoggerUtils.getSegmentString(request.segment) - ); + this.logger("abort: ", LoggerUtils.getSegmentString(request.segment)); } - private async loadThroughHttp(item: QueueItem, isRandom = false) { - const { segment } = item; + private async loadThroughHttp(segment: Segment) { const request = this.requests.getOrCreateRequest(segment); void fulfillHttpSegmentRequest(request, this.settings); this.p2pLoaders.currentLoader.broadcastAnnouncement(); - if (!isRandom) { - this.logger.loader( - `http request: ${LoggerUtils.getQueueItemString(item)}` - ); - } } - private async loadThroughP2P(item: QueueItem) { - this.p2pLoaders.currentLoader.downloadSegment(item); + private async loadThroughP2P(segment: Segment) { + this.p2pLoaders.currentLoader.downloadSegment(segment); } private loadRandomThroughHttp() { @@ -289,39 +279,37 @@ export class HybridLoader { if (!shouldLoad) return; const item = Utils.getRandomItem(queue); - void this.loadThroughHttp(item, true); - - this.logger.loader( - `http random request: ${LoggerUtils.getQueueItemString(item)}` - ); + void this.loadThroughHttp(item.segment); } - private abortLastHttpLoadingAfter(queue: QueueItem[], segment: Segment) { + private abortLastHttpLoadingInQueueAfterItem( + queue: QueueItem[], + segment: Segment + ): boolean { for (const { segment: itemSegment } of arrayBackwards(queue)) { - if (itemSegment.localId === segment.localId) break; - if (this.requests.isHttpRequested(segment)) { - this.requests.get(segment)?.abortFromProcessQueue(); - this.logger.loader( - "http aborted: ", - LoggerUtils.getSegmentString(segment) - ); - break; + if (itemSegment === segment) break; + const request = this.requests.get(itemSegment); + if (request?.type === "http" && request.status === "loading") { + request.abortFromProcessQueue(); + return true; } } + return false; } - private abortLastP2PLoadingAfter(queue: QueueItem[], segment: Segment) { + private abortLastP2PLoadingInQueueAfterItem( + queue: QueueItem[], + segment: Segment + ): boolean { for (const { segment: itemSegment } of arrayBackwards(queue)) { - if (itemSegment.localId === segment.localId) break; - if (this.requests.isP2PRequested(segment)) { - this.requests.get(segment)?.abortFromProcessQueue(); - this.logger.loader( - "p2p aborted: ", - LoggerUtils.getSegmentString(segment) - ); - break; + if (itemSegment === segment) break; + const request = this.requests.get(itemSegment); + if (request?.type === "p2p" && request.status === "loading") { + request.abortFromProcessQueue(); + return true; } } + return false; } updatePlayback(position: number, rate: number) { @@ -337,14 +325,14 @@ export class HybridLoader { if (isPositionChanged) this.playback.position = position; if (isRateChanged && rate !== 0) this.playback.rate = rate; if (isPositionSignificantlyChanged) { - this.logger.engine("position significantly changed"); + this.logger("position significantly changed"); } void this.requestProcessQueueMicrotask(isPositionSignificantlyChanged); } updateStream(stream: StreamWithSegments) { if (stream !== this.lastRequestedSegment.stream) return; - this.logger.engine(`update stream: ${LoggerUtils.getStreamString(stream)}`); + this.logger(`update stream: ${LoggerUtils.getStreamString(stream)}`); this.requestProcessQueueMicrotask(); } @@ -355,8 +343,7 @@ export class HybridLoader { void this.segmentStorage.destroy(); this.requests.destroy(); this.p2pLoaders.destroy(); - this.logger.loader.destroy(); - this.logger.engine.destroy(); + this.logger.destroy(); } } diff --git a/packages/p2p-media-loader-core/src/internal-types.d.ts b/packages/p2p-media-loader-core/src/internal-types.d.ts index 74a8ab05..dd307504 100644 --- a/packages/p2p-media-loader-core/src/internal-types.d.ts +++ b/packages/p2p-media-loader-core/src/internal-types.d.ts @@ -1,29 +1,13 @@ import { Segment } from "./types"; import { PeerCommandType } from "./enums"; +import { SegmentPlaybackStatuses } from "./utils/stream"; export type Playback = { position: number; rate: number; }; -export type NumberRange = { - from: number; - to: number; -}; - -export type LoadBufferRanges = { - highDemand: NumberRange; - http: NumberRange; - p2p: NumberRange; -}; - -export type QueueItemStatuses = { - isHighDemand: boolean; - isHttpDownloadable: boolean; - isP2PDownloadable: boolean; -}; - -export type QueueItem = { segment: Segment; statuses: QueueItemStatuses }; +export type QueueItem = { segment: Segment; statuses: SegmentPlaybackStatuses }; 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 4cdcdcd8..bd4d05fb 100644 --- a/packages/p2p-media-loader-core/src/p2p/loader.ts +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -1,21 +1,17 @@ import { Peer } from "./peer"; import { Segment, Settings, StreamWithSegments } from "../types"; -import { QueueItem } from "../internal-types"; import { SegmentsMemoryStorage } from "../segments-storage"; import * as PeerUtil from "../utils/peer"; -import * as LoggerUtils from "../utils/logger"; import * as StreamUtils from "../utils/stream"; import * as Utils from "../utils/utils"; import { PeerSegmentStatus } from "../enums"; import { RequestsContainer } from "../request-container"; import { Request } from "../request"; import { P2PTrackerClient } from "./tracker-client"; -import debug from "debug"; export class P2PLoader { private readonly peerId: string; private readonly trackerClient: P2PTrackerClient; - private readonly logger = debug("core:p2p-loader"); private isAnnounceMicrotaskCreated = false; constructor( @@ -38,8 +34,7 @@ export class P2PLoader { onPeerConnected: this.onPeerConnected, onSegmentRequested: this.onSegmentRequested, }, - this.settings, - this.logger + this.settings ); this.segmentStorage.subscribeOnUpdate( @@ -49,45 +44,22 @@ export class P2PLoader { this.trackerClient.start(); } - downloadSegment(item: QueueItem): Request | undefined { - const { segment, statuses } = item; - const untestedPeers: Peer[] = []; - let fastestPeer: Peer | undefined; - let fastestPeerBandwidth = 0; - + downloadSegment(segment: Segment): Request | undefined { + const peersWithSegment: Peer[] = []; for (const peer of this.trackerClient.peers()) { if ( !peer.downloadingSegment && peer.getSegmentStatus(segment) === PeerSegmentStatus.Loaded ) { - const { bandwidth } = peer; - if (bandwidth === undefined) { - untestedPeers.push(peer); - } else if (bandwidth > fastestPeerBandwidth) { - fastestPeerBandwidth = bandwidth; - fastestPeer = peer; - } + peersWithSegment.push(peer); } } - const peer = untestedPeers.length - ? Utils.getRandomItem(untestedPeers) - : fastestPeer; - + const peer = Utils.getRandomItem(peersWithSegment); if (!peer) return; const request = this.requests.getOrCreateRequest(segment); peer.fulfillSegmentRequest(request); - this.logger( - `p2p request ${segment.externalId} | ${LoggerUtils.getStatusesString( - statuses - )}` - ); - // request.subscribe("onSuccess", () => { - // this.logger(`p2p loaded: ${segment.externalId}`); - // }); - - return request; } isLoadingOrLoadedBySomeone(segment: Segment): boolean { @@ -119,7 +91,6 @@ export class P2PLoader { } private onPeerConnected = (peer: Peer) => { - this.logger(`connected with peer: ${peer.id}`); const announcement = this.getSegmentsAnnouncement(); peer.sendSegmentsAnnouncement(announcement); }; @@ -152,9 +123,6 @@ export class P2PLoader { }; destroy() { - this.logger( - `destroy tracker client: ${LoggerUtils.getStreamString(this.stream)}` - ); this.segmentStorage.unsubscribeFromUpdate( this.stream, this.broadcastAnnouncement diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts index 341c3695..d62f9bc4 100644 --- a/packages/p2p-media-loader-core/src/p2p/peer.ts +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -34,7 +34,6 @@ export class Peer { private segments = new Map(); private requestContext?: { request: Request; controls: RequestControls }; private readonly logger = debug("core:peer"); - private readonly bandwidthMeasurer = new BandwidthMeasurer(); private isUploadingSegment = false; constructor( @@ -42,7 +41,7 @@ export class Peer { private readonly eventHandlers: PeerEventHandlers, private readonly settings: PeerSettings ) { - this.id = hexToUtf8(connection.id); + this.id = Peer.getPeerIdFromHexString(connection.id); this.eventHandlers = eventHandlers; connection.on("data", this.onReceiveData.bind(this)); @@ -64,10 +63,6 @@ export class Peer { return this.requestContext?.request.segment; } - get bandwidth(): number | undefined { - return this.bandwidthMeasurer.getBandwidth(); - } - getSegmentStatus(segment: Segment): PeerSegmentStatus | undefined { const { externalId } = segment; return this.segments.get(externalId); @@ -252,27 +247,6 @@ export class Peer { } } -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; - } -} - function* getBufferChunks( data: ArrayBuffer, maxChunkSize: number diff --git a/packages/p2p-media-loader-core/src/p2p/tracker-client.ts b/packages/p2p-media-loader-core/src/p2p/tracker-client.ts index f3ccaaa1..ab476f83 100644 --- a/packages/p2p-media-loader-core/src/p2p/tracker-client.ts +++ b/packages/p2p-media-loader-core/src/p2p/tracker-client.ts @@ -20,19 +20,19 @@ type P2PTrackerClientEventHandlers = { export class P2PTrackerClient { private readonly client: TrackerClient; private readonly _peers = new Map(); - private readonly streamHash: string; + private readonly logger = debug("core:p2p-tracker-client"); constructor( private readonly peerId: string, - private readonly streamExternalId: string, + streamExternalId: string, private readonly stream: StreamWithSegments, private readonly eventHandlers: P2PTrackerClientEventHandlers, - private readonly settings: Settings, - private readonly logger: debug.Debugger + private readonly settings: Settings ) { - this.streamHash = PeerUtil.getStreamHash(streamExternalId); + const streamHash = PeerUtil.getStreamHash(streamExternalId); + const streamShortId = LoggerUtils.getStreamString(stream); this.client = new TrackerClient({ - infoHash: utf8ToHex(this.streamHash), + infoHash: utf8ToHex(streamHash), peerId: utf8ToHex(this.peerId), port: 6881, announce: [ @@ -54,9 +54,7 @@ export class P2PTrackerClient { this.client.on("warning", this.onTrackerClientWarning); this.client.on("error", this.onTrackerClientError); this.logger( - `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ - this.peerId - }` + `create new client; \nstream: ${streamShortId}; hash: ${streamHash}; \npeer id: ${this.peerId}` ); } @@ -72,25 +70,28 @@ export class P2PTrackerClient { connection.destroy(); } } + this._peers.clear(); + this.logger( + `destroy client; stream: ${LoggerUtils.getStreamString(this.stream)}` + ); } private onReceivePeerConnection: TrackerClientEvents["peer"] = ( peerConnection ) => { - let peerItem = this._peers.get(peerConnection.id); - + const itemId = Peer.getPeerIdFromHexString(peerConnection.id); + let peerItem = this._peers.get(itemId); if (peerItem?.peer) { peerConnection.destroy(); return; } else if (!peerItem) { peerItem = { potentialConnections: new Set() }; peerItem.potentialConnections.add(peerConnection); - const itemId = Peer.getPeerIdFromHexString(peerConnection.id); this._peers.set(itemId, peerItem); } peerConnection.on("connect", () => { - if (!peerItem) return; + if (!peerItem || peerItem.peer) return; for (const connection of peerItem.potentialConnections) { if (connection !== peerConnection) connection.destroy(); @@ -104,6 +105,7 @@ export class P2PTrackerClient { }, this.settings ); + this.logger(`connected with peer: ${peerItem.peer.id}`); this.eventHandlers.onPeerConnected(peerItem.peer); }); }; diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 4b98691d..7f0ce134 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -1,20 +1,17 @@ -import { Segment, StreamType } from "./types"; -import Debug from "debug"; +import { Segment, Settings } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Request } from "./request"; +import { Playback } from "./internal-types"; export class RequestsContainer { private readonly requests = new Map(); - private readonly logger: Debug.Debugger; constructor( - streamType: StreamType, private readonly requestProcessQueueCallback: () => void, - private readonly bandwidthApproximator: BandwidthApproximator - ) { - this.logger = Debug(`core:requests-container-${streamType}`); - this.logger.color = "LightSeaGreen"; - } + private readonly bandwidthApproximator: BandwidthApproximator, + private readonly playback: Playback, + private readonly settings: Settings + ) {} get executingHttpCount() { let count = 0; @@ -48,7 +45,9 @@ export class RequestsContainer { request = new Request( segment, this.requestProcessQueueCallback, - this.bandwidthApproximator + this.bandwidthApproximator, + this.playback, + this.settings ); this.requests.set(segment, request); } @@ -76,11 +75,13 @@ export class RequestsContainer { } isHttpRequested(segment: Segment): boolean { - return this.requests.get(segment)?.type === "http"; + const request = this.requests.get(segment); + return request?.type === "http" && request.status === "loading"; } isP2PRequested(segment: Segment): boolean { - return this.requests.get(segment)?.type === "p2p"; + const request = this.requests.get(segment); + return request?.type === "p2p" && request.status === "loading"; } isHybridLoaderRequested(segment: Segment): boolean { diff --git a/packages/p2p-media-loader-core/src/request.ts b/packages/p2p-media-loader-core/src/request.ts index 8ff65c02..77fd0b18 100644 --- a/packages/p2p-media-loader-core/src/request.ts +++ b/packages/p2p-media-loader-core/src/request.ts @@ -1,6 +1,10 @@ import { Segment, SegmentResponse } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; +import * as StreamUtils from "./utils/stream"; import * as Utils from "./utils/utils"; +import * as LoggerUtils from "./utils/logger"; +import debug from "debug"; +import { Playback } from "./internal-types"; export type EngineCallbacks = { onSuccess: (response: SegmentResponse) => void; @@ -59,14 +63,20 @@ export class Request { private progress?: LoadProgress; private notReceivingBytesTimeout: Timeout; private _abortRequestCallback?: (errorType: RequestInnerErrorType) => void; + private readonly _logger: debug.Debugger; constructor( readonly segment: Segment, private readonly requestProcessQueueCallback: () => void, - private readonly bandwidthApproximator: BandwidthApproximator + private readonly bandwidthApproximator: BandwidthApproximator, + private readonly playback: Playback, + private readonly settings: StreamUtils.PlaybackTimeWindowsSettings ) { this.id = Request.getRequestItemId(segment); this.notReceivingBytesTimeout = new Timeout(this.abortOnTimeout); + + const { type } = this.segment.stream; + this._logger = debug(`core:request-${type}`); } get status() { @@ -148,6 +158,16 @@ export class Request { this.notReceivingBytesTimeout.start(notReceivingBytesTimeoutMs); } + const statuses = StreamUtils.getSegmentPlaybackStatuses( + this.segment, + this.playback, + this.settings + ); + const statusString = LoggerUtils.getSegmentPlaybackStatusesString(statuses); + this.logger( + `${requestData.type} ${this.segment.externalId} ${statusString} started` + ); + return { firstBytesReceived: this.firstBytesReceived, addLoadedChunk: this.addLoadedChunk, @@ -214,6 +234,10 @@ export class Request { this._totalBytes = this._loadedBytes; this.resolveEngineCallbacksSuccessfully(this.finalData); + this.logger( + `${this.currentAttempt.type} ${this.segment.externalId} succeed` + ); + this.requestProcessQueueCallback(); }; private addLoadedChunk = (chunk: Uint8Array) => { @@ -238,6 +262,12 @@ export class Request { } } + private logger(message: string) { + this._logger.color = this.currentAttempt?.type === "http" ? "green" : "red"; + this._logger(message); + this._logger.color = ""; + } + static getRequestItemId(segment: Segment) { return segment.localId; } diff --git a/packages/p2p-media-loader-core/src/utils/logger.ts b/packages/p2p-media-loader-core/src/utils/logger.ts index c479f828..3133c7fc 100644 --- a/packages/p2p-media-loader-core/src/utils/logger.ts +++ b/packages/p2p-media-loader-core/src/utils/logger.ts @@ -1,5 +1,6 @@ import { Segment, Stream } from "../types"; -import { QueueItem, QueueItemStatuses } from "../internal-types"; +import { QueueItem } from "../internal-types"; +import { SegmentPlaybackStatuses } from "./stream"; export function getStreamString(stream: Stream) { return `${stream.type}-${stream.index}`; @@ -10,7 +11,9 @@ export function getSegmentString(segment: Segment) { return `(${getStreamString(segment.stream)} | ${externalId})`; } -export function getStatusesString(statuses: QueueItemStatuses): string { +export function getSegmentPlaybackStatusesString( + statuses: SegmentPlaybackStatuses +): string { const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; if (isHighDemand) return "high-demand"; if (isHttpDownloadable && isP2PDownloadable) return "http-p2p-window"; @@ -21,6 +24,6 @@ export function getStatusesString(statuses: QueueItemStatuses): string { export function getQueueItemString(item: QueueItem) { const { segment, statuses } = item; - const statusString = getStatusesString(statuses); + const statusString = getSegmentPlaybackStatusesString(statuses); return `${segment.externalId} ${statusString}`; } diff --git a/packages/p2p-media-loader-core/src/utils/queue.ts b/packages/p2p-media-loader-core/src/utils/queue.ts index 9b629596..428e3250 100644 --- a/packages/p2p-media-loader-core/src/utils/queue.ts +++ b/packages/p2p-media-loader-core/src/utils/queue.ts @@ -1,11 +1,10 @@ -import { Segment, Settings } from "../types"; +import { Segment } from "../types"; +import { Playback, QueueItem } from "../internal-types"; import { - LoadBufferRanges, - NumberRange, - Playback, - QueueItem, - QueueItemStatuses, -} from "../internal-types"; + getSegmentPlaybackStatuses, + SegmentPlaybackStatuses, + PlaybackTimeWindowsSettings, +} from "./stream"; export function generateQueue({ lastRequestedSegment, @@ -15,13 +14,9 @@ export function generateQueue({ }: { lastRequestedSegment: Readonly; playback: Readonly; - skipSegment: (segment: Segment, statuses: QueueItemStatuses) => boolean; - settings: Pick< - Settings, - "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" - >; + skipSegment: (segment: Segment, statuses: SegmentPlaybackStatuses) => boolean; + settings: PlaybackTimeWindowsSettings; }): { queue: QueueItem[]; queueSegmentIds: Set } { - const bufferRanges = getLoadBufferRanges(playback, settings); const { localId: requestedSegmentId, stream } = lastRequestedSegment; const queue: QueueItem[] = []; @@ -31,13 +26,13 @@ export function generateQueue({ const isNextNotActual = (segmentId: string) => { const next = segments.getNextTo(segmentId)?.[1]; if (!next) return true; - const statuses = getSegmentLoadStatuses(next, bufferRanges); + const statuses = getSegmentPlaybackStatuses(next, playback, settings); return isNotActualStatuses(statuses); }; let i = 0; for (const segment of segments.values(requestedSegmentId)) { - const statuses = getSegmentLoadStatuses(segment, bufferRanges); + const statuses = getSegmentPlaybackStatuses(segment, playback, settings); const isNotActual = isNotActualStatuses(statuses); if (isNotActual && (i !== 0 || isNextNotActual(requestedSegmentId))) break; i++; @@ -51,72 +46,7 @@ export function generateQueue({ return { queue, queueSegmentIds }; } -export function getLoadBufferRanges( - playback: Readonly, - settings: Pick< - Settings, - "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" - > -): LoadBufferRanges { - const { position, rate } = playback; - const { - highDemandTimeWindow, - httpDownloadTimeWindow, - p2pDownloadTimeWindow, - } = settings; - - const getRange = (position: number, rate: number, bufferLength: number) => { - return { - from: position, - to: position + rate * bufferLength, - }; - }; - return { - highDemand: getRange(position, rate, highDemandTimeWindow), - http: getRange(position, rate, httpDownloadTimeWindow), - p2p: getRange(position, rate, p2pDownloadTimeWindow), - }; -} - -export function getSegmentLoadStatuses( - segment: Readonly, - loadBufferRanges: LoadBufferRanges -): QueueItemStatuses { - const { highDemand, http, p2p } = loadBufferRanges; - const { startTime, endTime } = segment; - - const isValueInRange = (value: number, range: NumberRange) => - value >= range.from && value < range.to; - - return { - isHighDemand: - isValueInRange(startTime, highDemand) || - isValueInRange(endTime, highDemand), - isHttpDownloadable: - isValueInRange(startTime, http) || isValueInRange(endTime, http), - isP2PDownloadable: - isValueInRange(startTime, p2p) || isValueInRange(endTime, p2p), - }; -} - -function isNotActualStatuses(statuses: QueueItemStatuses) { +function isNotActualStatuses(statuses: SegmentPlaybackStatuses) { const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; return !isHighDemand && !isHttpDownloadable && !isP2PDownloadable; } - -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-core/src/utils/stream.ts b/packages/p2p-media-loader-core/src/utils/stream.ts index 39d63cba..e6c272e3 100644 --- a/packages/p2p-media-loader-core/src/utils/stream.ts +++ b/packages/p2p-media-loader-core/src/utils/stream.ts @@ -1,6 +1,17 @@ -import { Segment, Stream, StreamWithSegments } from "../types"; +import { Segment, Settings, Stream, StreamWithSegments } from "../types"; import { Playback } from "../internal-types"; +export type SegmentPlaybackStatuses = { + isHighDemand: boolean; + isHttpDownloadable: boolean; + isP2PDownloadable: boolean; +}; + +export type PlaybackTimeWindowsSettings = Pick< + Settings, + "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" +>; + const PEER_PROTOCOL_VERSION = "V1"; export function getStreamExternalId( @@ -46,6 +57,52 @@ export function getSegmentAvgDuration(stream: StreamWithSegments) { return sumDuration / size; } -export function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { - return Math.max(segment.startTime - playback.position, 0) / playback.rate; +export function isSegmentActualInPlayback( + segment: Readonly, + playback: Playback, + timeWindowsSettings: PlaybackTimeWindowsSettings +) { + const statuses = getSegmentPlaybackStatuses( + segment, + playback, + timeWindowsSettings + ); + return ( + statuses.isHighDemand || + statuses.isHttpDownloadable || + statuses.isP2PDownloadable + ); +} + +export function getSegmentPlaybackStatuses( + segment: Segment, + playback: Playback, + timeWindowsSettings: PlaybackTimeWindowsSettings +): SegmentPlaybackStatuses { + const { + highDemandTimeWindow, + httpDownloadTimeWindow, + p2pDownloadTimeWindow, + } = timeWindowsSettings; + + return { + isHighDemand: isInTimeWindow(segment, playback, highDemandTimeWindow), + isHttpDownloadable: isInTimeWindow( + segment, + playback, + httpDownloadTimeWindow + ), + isP2PDownloadable: isInTimeWindow(segment, playback, p2pDownloadTimeWindow), + }; } + +const isInTimeWindow = ( + segment: Segment, + playback: Playback, + timeWindowLength: number +) => { + const { startTime, endTime } = segment; + const { position, rate } = playback; + const rightMargin = position + timeWindowLength * rate; + return !(rightMargin < startTime || position > endTime); +}; diff --git a/packages/p2p-media-loader-shaka/src/segment-manager.ts b/packages/p2p-media-loader-shaka/src/segment-manager.ts index 856610eb..beed778e 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().toFixed(3)).toString(); + const externalId = Math.trunc(reference.getStartTime()).toString(); const segmentLocalId = Utils.getSegmentLocalIdFromReference(reference); if (!managerStream.segments.has(segmentLocalId)) { From 1ea58f088f68075e7ff18a345352774b16f13d3d Mon Sep 17 00:00:00 2001 From: igor Date: Tue, 28 Nov 2023 16:22:40 +0200 Subject: [PATCH 21/22] Remove unused code. --- .../p2p-media-loader-core/src/request-container.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts index 7f0ce134..f44c4b84 100644 --- a/packages/p2p-media-loader-core/src/request-container.ts +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -73,17 +73,6 @@ export class RequestsContainer { if (request.type === "p2p") yield request; } } - - isHttpRequested(segment: Segment): boolean { - const request = this.requests.get(segment); - return request?.type === "http" && request.status === "loading"; - } - - isP2PRequested(segment: Segment): boolean { - const request = this.requests.get(segment); - return request?.type === "p2p" && request.status === "loading"; - } - isHybridLoaderRequested(segment: Segment): boolean { return !!this.requests.get(segment)?.type; } From 349d9fc3b03ad50055984e39611d56247d45240e Mon Sep 17 00:00:00 2001 From: igor Date: Tue, 28 Nov 2023 16:24:05 +0200 Subject: [PATCH 22/22] Use function declaration instead of expression. --- packages/p2p-media-loader-core/src/utils/stream.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/p2p-media-loader-core/src/utils/stream.ts b/packages/p2p-media-loader-core/src/utils/stream.ts index e6c272e3..516e0501 100644 --- a/packages/p2p-media-loader-core/src/utils/stream.ts +++ b/packages/p2p-media-loader-core/src/utils/stream.ts @@ -96,13 +96,13 @@ export function getSegmentPlaybackStatuses( }; } -const isInTimeWindow = ( +function isInTimeWindow( segment: Segment, playback: Playback, timeWindowLength: number -) => { +) { const { startTime, endTime } = segment; const { position, rate } = playback; const rightMargin = position + timeWindowLength * rate; return !(rightMargin < startTime || position > endTime); -}; +}