UNPKG

p2p-media-loader-hlsjs

Version:
168 lines (150 loc) 4.42 kB
import type { FragmentLoaderContext, HlsConfig, Loader, LoaderCallbacks, LoaderConfiguration, LoaderContext, LoaderStats, } from "hls.js"; import * as Utils from "./utils.js"; import { Core, SegmentResponse, CoreRequestError } from "p2p-media-loader-core"; const DEFAULT_DOWNLOAD_LATENCY = 10; export class FragmentLoaderBase implements Loader<FragmentLoaderContext> { context!: FragmentLoaderContext; config!: LoaderConfiguration | null; stats: LoaderStats; #callbacks!: LoaderCallbacks<FragmentLoaderContext> | null; #createDefaultLoader: () => Loader<LoaderContext>; #defaultLoader?: Loader<LoaderContext>; #core: Core; #response?: SegmentResponse; #segmentId?: string; constructor(config: HlsConfig, core: Core) { this.#core = core; this.#createDefaultLoader = () => new config.loader(config); this.stats = { aborted: false, chunkCount: 0, loading: { start: 0, first: 0, end: 0 }, buffering: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, // set total and loaded to 1 to prevent hls.js // on progress loading monitoring in AbrController total: 1, loaded: 1, bwEstimate: 0, retry: 0, }; } load( context: FragmentLoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks<LoaderContext>, ) { this.context = context; this.config = config; this.#callbacks = callbacks; const { stats } = this; const { rangeStart: start, rangeEnd: end } = context; const byteRange = Utils.getByteRange( start, end !== undefined ? end - 1 : undefined, ); this.#segmentId = Utils.getSegmentRuntimeId(context.url, byteRange); const isSegmentDownloadableByP2PCore = this.#core.isSegmentLoadable( this.#segmentId, ); if ( !this.#core.hasSegment(this.#segmentId) || !isSegmentDownloadableByP2PCore ) { this.#defaultLoader = this.#createDefaultLoader(); this.#defaultLoader.stats = this.stats; this.#defaultLoader.load(context, config, callbacks); return; } const onSuccess = (response: SegmentResponse) => { this.#response = response; const loadedBytes = this.#response.data.byteLength; stats.loading = getLoadingStat( this.#response.bandwidth, loadedBytes, performance.now(), ); stats.total = loadedBytes; stats.loaded = loadedBytes; if (callbacks.onProgress) { callbacks.onProgress( this.stats, context, this.#response.data, undefined, ); } callbacks.onSuccess( { data: this.#response.data, url: context.url }, this.stats, context, undefined, ); }; const onError = (error: unknown) => { if ( error instanceof CoreRequestError && error.type === "aborted" && this.stats.aborted ) { return; } this.#handleError(error); }; void this.#core.loadSegment(this.#segmentId, { onSuccess, onError }); } #handleError(thrownError: unknown) { const error = { code: 0, text: "" }; if ( thrownError instanceof CoreRequestError && thrownError.type === "failed" ) { // error.code = thrownError.code; error.text = thrownError.message; } else if (thrownError instanceof Error) { error.text = thrownError.message; } this.#callbacks?.onError(error, this.context, null, this.stats); } #abortInternal() { if (!this.#response && this.#segmentId) { this.stats.aborted = true; this.#core.abortSegmentLoading(this.#segmentId); } } abort() { if (this.#defaultLoader) { this.#defaultLoader.abort(); } else { this.#abortInternal(); this.#callbacks?.onAbort?.(this.stats, this.context, {}); } } destroy() { if (this.#defaultLoader) { this.#defaultLoader.destroy(); } else { if (!this.stats.aborted) this.#abortInternal(); this.#callbacks = null; this.config = null; } } } function getLoadingStat( targetBitrate: number, loadedBytes: number, loadingEndTime: number, ) { const timeForLoading = (loadedBytes * 8000) / targetBitrate; const first = loadingEndTime - timeForLoading; const start = first - DEFAULT_DOWNLOAD_LATENCY; return { start, first, end: loadingEndTime }; }