From cdf6c9320e1d64fe09bfad65fcbbd21a9f24b189 Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Fri, 24 Mar 2023 11:36:22 -0700 Subject: [PATCH 1/9] Add support for EXT-X-PRELOAD-HINT via state machine --- api-extractor/report/hls.js.api.md | 17 +- src/controller/abr-controller.ts | 10 +- src/controller/audio-stream-controller.ts | 2 + src/controller/base-stream-controller.ts | 98 +++++++++-- src/controller/error-controller.ts | 6 + src/controller/stream-controller.ts | 10 ++ src/loader/fragment-preloader.ts | 191 ++++++++++++++++++++++ src/loader/fragment.ts | 5 + src/loader/level-details.ts | 4 +- src/loader/m3u8-parser.ts | 68 ++++++++ tests/unit/loader/playlist-loader.ts | 20 +++ 11 files changed, 410 insertions(+), 21 deletions(-) create mode 100644 src/loader/fragment-preloader.ts diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index e51390cb018..942388b949e 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -294,6 +294,8 @@ export class BaseSegment { // (undocumented) elementaryStreams: ElementaryStreams; // (undocumented) + isPreload?: boolean; + // (undocumented) relurl?: string; // (undocumented) setByteRange(value: string, previous?: BaseSegment): void; @@ -350,6 +352,10 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // // (undocumented) protected fragmentLoader: FragmentLoader; + // Warning: (ae-forgotten-export) The symbol "FragmentPreloader" needs to be exported by the entry point hls.d.ts + // + // (undocumented) + protected fragmentPreloader: FragmentPreloader; // (undocumented) protected fragmentTracker: FragmentTracker; // (undocumented) @@ -405,6 +411,10 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) protected levels: Array | null; // (undocumented) + protected loadAndCachePreloadHint(details: LevelDetails): void; + // (undocumented) + protected loadedEndOfParts(partList: Part[], targetBufferTime: number): boolean; + // (undocumented) protected loadedmetadata: boolean; // (undocumented) protected loadFragment(frag: Fragment, level: Level, targetBufferTime: number): void; @@ -2263,6 +2273,11 @@ export class LevelDetails { // (undocumented) playlistParsingError: Error | null; // (undocumented) + preloadData?: { + frag: Fragment; + part?: Part; + }; + // (undocumented) preloadHint?: AttrList; // (undocumented) PTSKnown: boolean; @@ -3054,7 +3069,7 @@ export type ParsedMultivariantPlaylist = { // // @public export class Part extends BaseSegment { - constructor(partAttrs: AttrList, frag: MediaFragment, baseurl: string, index: number, previous?: Part); + constructor(partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part, isPreload?: boolean); // (undocumented) readonly duration: number; // (undocumented) diff --git a/src/controller/abr-controller.ts b/src/controller/abr-controller.ts index 9c9b3b64345..53ad7b5a645 100644 --- a/src/controller/abr-controller.ts +++ b/src/controller/abr-controller.ts @@ -378,7 +378,10 @@ class AbrController extends Logger implements AbrComponentAPI { { frag, part }: FragLoadedData, ) { const stats = part ? part.stats : frag.stats; - if (frag.type === PlaylistLevelType.MAIN) { + if ( + frag.type === PlaylistLevelType.MAIN && + !(part?.isPreload || frag.isPreload) + ) { this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start); } if (this.ignoreFragment(frag)) { @@ -434,9 +437,12 @@ class AbrController extends Logger implements AbrComponentAPI { // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing; // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch // is used. If we used buffering in that case, our BW estimate sample will be very large. + const loadStart = part?.isPreload + ? stats.loading.first + : stats.loading.start; const processingMs = stats.parsing.end - - stats.loading.start - + loadStart - Math.min( stats.loading.first - stats.loading.start, this.bwEstimator.getEstimateTTFB(), diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index a5fb144daa7..49766f2b96e 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -536,6 +536,8 @@ class AudioStreamController let sliding = 0; if (newDetails.live || track.details?.live) { this.checkLiveUpdate(newDetails); + // reset the preloader state to IDLE if we have finished loading, never loaded, or have old data + this.fragmentPreloader.revalidate(data); const mainDetails = this.mainDetails; if (newDetails.deltaUpdateFailed || !mainDetails) { return; diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 20e407e546a..8801620a566 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -52,6 +52,9 @@ import type { HlsConfig } from '../config'; import type { NetworkComponentAPI } from '../types/component-api'; import type { SourceBufferName } from '../types/buffer'; import type { RationalTimestamp } from '../utils/timescale-conversion'; +import FragmentPreloader, { + FragPreloadRequestState, +} from '../loader/fragment-preloader'; type ResolveFragLoaded = (FragLoadedEndData) => void; type RejectFragLoaded = (LoadError) => void; @@ -95,6 +98,7 @@ export default class BaseStreamController protected retryDate: number = 0; protected levels: Array | null = null; protected fragmentLoader: FragmentLoader; + protected fragmentPreloader: FragmentPreloader; protected keyLoader: KeyLoader; protected levelLastLoaded: Level | null = null; protected startFragRequested: boolean = false; @@ -115,6 +119,7 @@ export default class BaseStreamController this.playlistType = playlistType; this.hls = hls; this.fragmentLoader = new FragmentLoader(hls.config); + this.fragmentPreloader = new FragmentPreloader(hls.config, logPrefix); this.keyLoader = keyLoader; this.fragmentTracker = fragmentTracker; this.config = hls.config; @@ -153,7 +158,8 @@ export default class BaseStreamController return; } this.fragmentLoader.abort(); - this.keyLoader.abort(this.playlistType); + this.fragmentPreloader.abort(); + this.keyLoader.abort(); const frag = this.fragCurrent; if (frag?.loader) { frag.abortRequests(); @@ -363,6 +369,14 @@ export default class BaseStreamController this.startTimeOffset = data.startTimeOffset; } + protected loadAndCachePreloadHint(details: LevelDetails): void { + const data = details.preloadData; + if (!data) { + return; + } + this.fragmentPreloader.preload(data.frag, data.part); + } + protected onHandlerDestroying() { this.stopLoad(); if (this.transmuxer) { @@ -379,6 +393,9 @@ export default class BaseStreamController if (this.fragmentLoader) { this.fragmentLoader.destroy(); } + if (this.fragmentPreloader) { + this.fragmentPreloader.destroy(); + } if (this.keyLoader) { this.keyLoader.destroy(); } @@ -392,6 +409,7 @@ export default class BaseStreamController this.decrypter = this.keyLoader = this.fragmentLoader = + this.fragmentPreloader = this.fragmentTracker = null as any; super.onHandlerDestroyed(); @@ -765,6 +783,10 @@ export default class BaseStreamController if (targetBufferTime > frag.end && details.fragmentHint) { frag = details.fragmentHint; } + const loadedEndOfParts = this.loadedEndOfParts( + partList, + targetBufferTime, + ); const partIndex = this.getNextPart(partList, frag, targetBufferTime); if (partIndex > -1) { const part = partList[partIndex]; @@ -818,10 +840,16 @@ export default class BaseStreamController ); } return result; - } else if ( - !frag.url || - this.loadedEndOfParts(partList, targetBufferTime) - ) { + } else if (!frag.url || loadedEndOfParts) { + if ( + loadedEndOfParts && + this.hls.lowLatencyMode && + details?.live && + details.canBlockReload && + this.fragmentPreloader.state === FragPreloadRequestState.IDLE + ) { + this.loadAndCachePreloadHint(details); + } // Fragment hint has no parts return Promise.resolve(null); } @@ -856,23 +884,28 @@ export default class BaseStreamController let result: Promise; if (dataOnProgress && keyLoadingPromise) { result = keyLoadingPromise - .then((keyLoadedData) => { + .then((keyLoadedData: void | KeyLoadedData) => { if (!keyLoadedData || this.fragContextChanged(keyLoadedData?.frag)) { return null; } - return this.fragmentLoader.load(frag, progressCallback); + return this.getCachedRequestOrLoad( + frag, + null, + true, + progressCallback, + ); }) .catch((error) => this.handleFragLoadError(error)); } else { // load unencrypted fragment data with progress event, // or handle fragment result after key and fragment are finished loading - result = Promise.all([ - this.fragmentLoader.load( - frag, - dataOnProgress ? progressCallback : undefined, - ), - keyLoadingPromise, - ]) + const loadRequest = this.getCachedRequestOrLoad( + frag, + null, + dataOnProgress, + progressCallback, + ); + result = Promise.all([loadRequest, keyLoadingPromise]) .then(([fragLoadedData]) => { if (!dataOnProgress && fragLoadedData && progressCallback) { progressCallback(fragLoadedData); @@ -901,8 +934,7 @@ export default class BaseStreamController const partsLoaded: FragLoadedData[] = []; const initialPartList = level.details?.partList; const loadPart = (part: Part) => { - this.fragmentLoader - .loadPart(frag, part, progressCallback) + this.getCachedRequestOrLoad(frag, part, true, progressCallback) .then((partLoadedData: FragLoadedData) => { partsLoaded[part.index] = partLoadedData; const loadedPart = partLoadedData.part as Part; @@ -927,6 +959,36 @@ export default class BaseStreamController ); } + private getCachedRequestOrLoad( + frag: Fragment, + part: Part | null, + dataOnProgress: boolean, + progressCallback?: FragmentLoadProgressCallback, + ): Promise { + const request = this.fragmentPreloader.getCachedRequest(frag, part); + if (request !== null) { + return request.then((data) => { + if (progressCallback) { + progressCallback(data); + } + return data; + }); + } + + if (part) { + return this.fragmentLoader.loadPart( + frag, + part, + progressCallback ?? (() => {}), + ); + } + + return this.fragmentLoader.load( + frag, + dataOnProgress ? progressCallback : undefined, + ); + } + private handleFragLoadError(error: LoadError | Error) { if ('data' in error) { const data = error.data; @@ -1332,7 +1394,7 @@ export default class BaseStreamController return nextPart; } - private loadedEndOfParts( + protected loadedEndOfParts( partList: Part[], targetBufferTime: number, ): boolean { @@ -1650,6 +1712,7 @@ export default class BaseStreamController ) { this.state = State.IDLE; } + this.fragmentPreloader.abort(); } protected onFragmentOrKeyLoadError( @@ -1801,6 +1864,7 @@ export default class BaseStreamController if (this.state !== State.STOPPED) { this.state = State.IDLE; } + this.fragmentPreloader.abort(); } protected resetStartWhenNotLoaded(level: Level | null): void { diff --git a/src/controller/error-controller.ts b/src/controller/error-controller.ts index aca730efba2..c38147ceb40 100644 --- a/src/controller/error-controller.ts +++ b/src/controller/error-controller.ts @@ -276,6 +276,12 @@ export default class ErrorController } private getFragRetryOrSwitchAction(data: ErrorData): IErrorAction { + if (data.frag?.isPreload) { + return { + action: NetworkErrorAction.DoNothing, + flags: ErrorActionFlags.None, + }; + } const hls = this.hls; // Share fragment error count accross media options (main, audio, subs) // This allows for level based rendition switching when media option assets fail diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index d2f5bc2a700..c9b054ac2b6 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -40,6 +40,7 @@ import type { ManifestParsedData, MediaAttachedData, } from '../types/events'; +import { FragPreloadRequestState } from '../loader/fragment-preloader'; const TICK_INTERVAL = 100; // how often to tick in ms @@ -652,6 +653,15 @@ export default class StreamController let sliding = 0; if (newDetails.live || curLevel.details?.live) { this.checkLiveUpdate(newDetails); + if ( + this.fragmentPreloader.state === FragPreloadRequestState.LOADING && + this.fragmentPreloader.frag?.level !== data.level + ) { + this.fragmentPreloader.abort(); + } else { + // reset the preloader state to IDLE if we have finished loading, never loaded, or have old data + this.fragmentPreloader.revalidate(data); + } if (newDetails.deltaUpdateFailed) { return; } diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts new file mode 100644 index 00000000000..a1b4ae775e3 --- /dev/null +++ b/src/loader/fragment-preloader.ts @@ -0,0 +1,191 @@ +import FragmentLoader from './fragment-loader'; +import { Fragment, Part } from './fragment'; + +import { + FragLoadedData, + LevelLoadedData, + PartsLoadedData, + TrackLoadedData, +} from '../types/events'; +import { HlsConfig } from '../hls'; +import { logger } from '../utils/logger'; + +export const enum FragPreloadRequestState { + IDLE, + LOADING, +} + +type FragPreloadRequest = { + frag: Fragment; + part: Part | null; + loadPromise: Promise; +}; + +type FragPreloadRequestInfo = { + info: FragPreloadRequest | null; + state: FragPreloadRequestState; +}; + +export default class FragmentPreloader extends FragmentLoader { + private storage: FragPreloadRequestInfo = { + info: null, + state: FragPreloadRequestState.IDLE, + }; + protected log: (msg: any) => void; + + constructor(config: HlsConfig, logPrefix: string) { + super(config); + this.log = logger.log.bind(logger, `${logPrefix}>preloader:`); + } + + private getPreloadStateStr() { + switch (this.storage.state) { + case FragPreloadRequestState.IDLE: + return 'IDLE'; + case FragPreloadRequestState.LOADING: + return 'LOADING'; + } + } + + public haveMatchingRequest(frag: Fragment, part: Part | null): boolean { + const request = this.storage.info; + return ( + request !== null && + request.frag.sn === frag.sn && + request.part?.index === part?.index + ); + } + + public preload(frag: Fragment, part: Part | undefined): void { + // We might have a stale request preloaded + const { info, state } = this.storage; + if (info && state !== FragPreloadRequestState.IDLE) { + return; + } + + this.log( + `[${this.getPreloadStateStr()}] create request for [${frag.type}] ${ + frag.sn + }:${part?.index}`, + ); + + const noop = () => {}; + const loadPromise = + part !== undefined + ? this.loadPart(frag, part, noop) + : this.load(frag, noop); + + const request = { + frag, + part: part ?? null, + loadPromise, + }; + + this.storage = { + info: request, + state: FragPreloadRequestState.LOADING, + }; + } + + public getCachedRequest( + frag: Fragment, + part: Part | null, + ): Promise | null { + const request = this.storage.info; + if (request) { + this.log( + `[${this.getPreloadStateStr()}] check cache for [${frag.type}] ${ + frag.sn + }:${part?.index} / preloadInfo=${request?.frag?.sn}/${request?.part + ?.index}`, + ); + } + if ( + this.storage.state !== FragPreloadRequestState.IDLE && + request && + this.haveMatchingRequest(frag, part) + ) { + // Do we need to merge the preload frag into the frag/part? + return request.loadPromise.then((data) => { + mergeFragData(frag, part, data); + this.reset(); + return data; + }); + } + + if (request && this.storage.state !== FragPreloadRequestState.IDLE) { + const { frag: preloadFrag, part: preloadPart } = request; + const haveOldSn = preloadFrag.sn < frag.sn; + const haveOldPart = + preloadPart !== null && + part !== null && + !haveOldSn && + preloadPart.index < part.index; + + if (haveOldSn || haveOldPart) { + this.reset(); + } + } + + return null; + } + + public revalidate(data: LevelLoadedData | TrackLoadedData) { + const partList = data.details.partList ?? []; + if (partList.length === 0) { + this.abort(); + return; + } + } + + public get state() { + return this.storage.state; + } + + public get frag() { + if (this.storage.info) { + return this.storage.info.frag; + } + return null; + } + + public reset() { + this.storage = { + info: null, + state: FragPreloadRequestState.IDLE, + }; + } + + abort(): void { + super.abort(); + this.reset(); + } + + destroy(): void { + this.reset(); + super.destroy(); + } +} + +function mergeFragData( + frag: Fragment, + part: Part | null, + data: FragLoadedData, +) { + const loadedFrag = data.frag; + const loadedPart = data.part; + + frag.isPreload = true; + if (frag.stats.loaded === 0) { + frag.stats = loadedFrag.stats; + } else { + const fragStats = frag.stats; + const loadStats = loadedFrag.stats; + fragStats.loading.end = loadStats.loading.end; + } + + if (part && loadedPart) { + part.isPreload = true; + part.stats = loadedPart.stats; + } +} diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 007a32a25d6..c631f41a738 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -43,6 +43,7 @@ export class BaseSegment { [ElementaryStreamTypes.VIDEO]: null, [ElementaryStreamTypes.AUDIOVIDEO]: null, }; + public isPreload?: boolean; constructor(baseurl: string) { this.baseurl = baseurl; @@ -288,6 +289,7 @@ export class Part extends BaseSegment { baseurl: string, index: number, previous?: Part, + isPreload?: boolean, ) { super(baseurl); this.duration = partAttrs.decimalFloatingPoint('DURATION'); @@ -303,6 +305,9 @@ export class Part extends BaseSegment { if (previous) { this.fragOffset = previous.fragOffset + previous.duration; } + if (isPreload) { + this.isPreload = isPreload; + } } get start(): number { diff --git a/src/loader/level-details.ts b/src/loader/level-details.ts index ecb855ce468..8d168e2880b 100644 --- a/src/loader/level-details.ts +++ b/src/loader/level-details.ts @@ -44,6 +44,7 @@ export class LevelDetails { public holdBack: number = 0; public partTarget: number = 0; public preloadHint?: AttrList; + public preloadData?: { frag: Fragment; part?: Part }; public renditionReports?: AttrList[]; public tuneInGoal: number = 0; public deltaUpdateFailed?: boolean; @@ -120,7 +121,8 @@ export class LevelDetails { get partEnd(): number { if (this.partList?.length) { - return this.partList[this.partList.length - 1].end; + const lastPart = this.partList[this.partList.length - 1]; + return lastPart.end; } return this.fragmentEnd; } diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 65043dab63d..7a16fa4deb8 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -690,6 +690,74 @@ export default class M3U8Parser { level.endCC = discontinuityCounter; + const preloadHintAttrs = level.preloadHint; + if (preloadHintAttrs) { + let byteRange: string | undefined; + if ( + preloadHintAttrs['BYTERANGE-START'] || + preloadHintAttrs['BYTERANGE-LENGTH'] + ) { + const byteRangeStartOffset = preloadHintAttrs['BYTERANGE-START'] | 0; + const byteRangeLength = + preloadHintAttrs['BYTERANGE-LENGTH'] | (2 ** 53 - 1); + byteRange = `${byteRangeLength}@${byteRangeStartOffset}`; + } + const preloadType = preloadHintAttrs.TYPE; + if (preloadType === 'PART' && level.partList) { + const lastPart = level.partList[level.partList.length - 1]; + const lastPartSn = lastPart.fragment.sn; + const lastPartPublished = lastPartSn === lastFragment?.sn; + const partIndex = lastPartPublished ? 0 : lastPart.index + 1; + + let preloadFrag = lastPart.fragment; + // Need to construct fake fragment for this part since the fragment this part belongs to + // is not published either. + if (lastPartPublished && level.fragmentHint) { + preloadFrag = level.fragmentHint; + } + const partAttrs = new AttrList({ + DURATION: level.partTarget, + URI: preloadHintAttrs.URI, + BYTERANGE: byteRange, + }); + const preloadPart = new Part( + partAttrs, + preloadFrag, + baseurl, + partIndex, + lastPartPublished ? undefined : lastPart, + true, + ); + level.preloadData = { + frag: preloadFrag, + part: preloadPart, + }; + } else if (preloadType === 'MAP') { + const preloadFrag = new Fragment(type, baseurl); + const mapAttrs = new AttrList({ + URI: preloadHintAttrs.URI, + BYTERANGE: byteRange, + }); + // Initial segment tag is before segment duration tag + setInitSegment(preloadFrag, mapAttrs, id, levelkeys); + level.preloadData = { + frag: preloadFrag, + }; + } + } + + /** + * Backfill any missing PDT values + * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after + * one or more Media Segment URIs, the client SHOULD extrapolate + * backward from that tag (using EXTINF durations and/or media + * timestamps) to associate dates with those segments." + * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs + * computed. + */ + if (firstPdtIndex > 0) { + backfillProgramDateTimes(fragments, firstPdtIndex); + } return level; } } diff --git a/tests/unit/loader/playlist-loader.ts b/tests/unit/loader/playlist-loader.ts index 28c4263a4b3..4370da0702e 100644 --- a/tests/unit/loader/playlist-loader.ts +++ b/tests/unit/loader/playlist-loader.ts @@ -1587,6 +1587,26 @@ fileSequence1151226.ts`, }); }); + it('Creates preload hinted part', function () { + const details = M3U8Parser.parseLevelPlaylist( + playlist, + 'http://dummy.url.com/playlist.m3u8', + 0, + PlaylistLevelType.MAIN, + 0, + null, + ); + expect(details.preloadData).to.be.an('object'); + const frag = details.preloadData?.frag; + const part = details.preloadData?.part; + + expect(frag?.sn).to.equal(1151234); + + expect(part).to.be.an('object'); + expect(part?.fragment.sn).to.equal(1151234); + expect(part?.index).to.equal(0); + }); + it('Parses EXT-X-RENDITION-REPORT', function () { const details = M3U8Parser.parseLevelPlaylist( playlist, From 86386375206c18ff737ff2e8e7464c947a1a1e27 Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Mon, 15 Apr 2024 14:11:35 -0700 Subject: [PATCH 2/9] Run prettier --- src/loader/fragment-preloader.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index a1b4ae775e3..7aeec8c6471 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -96,8 +96,9 @@ export default class FragmentPreloader extends FragmentLoader { this.log( `[${this.getPreloadStateStr()}] check cache for [${frag.type}] ${ frag.sn - }:${part?.index} / preloadInfo=${request?.frag?.sn}/${request?.part - ?.index}`, + }:${part?.index} / preloadInfo=${request?.frag?.sn}/${ + request?.part?.index + }`, ); } if ( From defcac18d44e898d7e496d4e111fe2d7723b4853 Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Sat, 4 May 2024 13:39:34 -0700 Subject: [PATCH 3/9] Clean up fragment preloader --- src/controller/base-stream-controller.ts | 16 ++-- src/controller/stream-controller.ts | 3 +- src/loader/fragment-preloader.ts | 102 ++++++++++++----------- 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 8801620a566..598159b9015 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -52,9 +52,7 @@ import type { HlsConfig } from '../config'; import type { NetworkComponentAPI } from '../types/component-api'; import type { SourceBufferName } from '../types/buffer'; import type { RationalTimestamp } from '../utils/timescale-conversion'; -import FragmentPreloader, { - FragPreloadRequestState, -} from '../loader/fragment-preloader'; +import FragmentPreloader from '../loader/fragment-preloader'; type ResolveFragLoaded = (FragLoadedEndData) => void; type RejectFragLoaded = (LoadError) => void; @@ -846,7 +844,7 @@ export default class BaseStreamController this.hls.lowLatencyMode && details?.live && details.canBlockReload && - this.fragmentPreloader.state === FragPreloadRequestState.IDLE + !this.fragmentPreloader.loading ) { this.loadAndCachePreloadHint(details); } @@ -890,8 +888,8 @@ export default class BaseStreamController } return this.getCachedRequestOrLoad( frag, - null, - true, + /*part*/ undefined, + /*dataOnProgress*/ true, progressCallback, ); }) @@ -901,7 +899,7 @@ export default class BaseStreamController // or handle fragment result after key and fragment are finished loading const loadRequest = this.getCachedRequestOrLoad( frag, - null, + /*part*/ undefined, dataOnProgress, progressCallback, ); @@ -961,12 +959,12 @@ export default class BaseStreamController private getCachedRequestOrLoad( frag: Fragment, - part: Part | null, + part: Part | undefined, dataOnProgress: boolean, progressCallback?: FragmentLoadProgressCallback, ): Promise { const request = this.fragmentPreloader.getCachedRequest(frag, part); - if (request !== null) { + if (request !== undefined) { return request.then((data) => { if (progressCallback) { progressCallback(data); diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index c9b054ac2b6..977acd4717b 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -40,7 +40,6 @@ import type { ManifestParsedData, MediaAttachedData, } from '../types/events'; -import { FragPreloadRequestState } from '../loader/fragment-preloader'; const TICK_INTERVAL = 100; // how often to tick in ms @@ -654,7 +653,7 @@ export default class StreamController if (newDetails.live || curLevel.details?.live) { this.checkLiveUpdate(newDetails); if ( - this.fragmentPreloader.state === FragPreloadRequestState.LOADING && + this.fragmentPreloader.loading && this.fragmentPreloader.frag?.level !== data.level ) { this.fragmentPreloader.abort(); diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index 7aeec8c6471..17658dee3dd 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -10,26 +10,26 @@ import { import { HlsConfig } from '../hls'; import { logger } from '../utils/logger'; -export const enum FragPreloadRequestState { +export const enum FragRequestState { IDLE, LOADING, } type FragPreloadRequest = { frag: Fragment; - part: Part | null; + part: Part | undefined; loadPromise: Promise; }; -type FragPreloadRequestInfo = { - info: FragPreloadRequest | null; - state: FragPreloadRequestState; +type FragPreloaderStorage = { + request: FragPreloadRequest | undefined; + state: FragRequestState; }; export default class FragmentPreloader extends FragmentLoader { - private storage: FragPreloadRequestInfo = { - info: null, - state: FragPreloadRequestState.IDLE, + private storage: FragPreloaderStorage = { + request: undefined, + state: FragRequestState.IDLE, }; protected log: (msg: any) => void; @@ -40,27 +40,32 @@ export default class FragmentPreloader extends FragmentLoader { private getPreloadStateStr() { switch (this.storage.state) { - case FragPreloadRequestState.IDLE: - return 'IDLE'; - case FragPreloadRequestState.LOADING: + case FragRequestState.IDLE: + return 'IDLE '; + case FragRequestState.LOADING: return 'LOADING'; } } - public haveMatchingRequest(frag: Fragment, part: Part | null): boolean { - const request = this.storage.info; + public has(frag: Fragment, part: Part | undefined): boolean { + const { request } = this.storage; return ( - request !== null && + request !== undefined && request.frag.sn === frag.sn && request.part?.index === part?.index ); } + public get loading(): boolean { + const { request, state } = this.storage; + return request !== undefined && state !== FragRequestState.IDLE; + } + public preload(frag: Fragment, part: Part | undefined): void { - // We might have a stale request preloaded - const { info, state } = this.storage; - if (info && state !== FragPreloadRequestState.IDLE) { + if (this.has(frag, part)) { return; + } else { + this.abort(); } this.log( @@ -69,7 +74,6 @@ export default class FragmentPreloader extends FragmentLoader { }:${part?.index}`, ); - const noop = () => {}; const loadPromise = part !== undefined ? this.loadPart(frag, part, noop) @@ -77,49 +81,45 @@ export default class FragmentPreloader extends FragmentLoader { const request = { frag, - part: part ?? null, + part, loadPromise, }; this.storage = { - info: request, - state: FragPreloadRequestState.LOADING, + request: request, + state: FragRequestState.LOADING, }; } public getCachedRequest( frag: Fragment, - part: Part | null, - ): Promise | null { - const request = this.storage.info; - if (request) { - this.log( - `[${this.getPreloadStateStr()}] check cache for [${frag.type}] ${ - frag.sn - }:${part?.index} / preloadInfo=${request?.frag?.sn}/${ - request?.part?.index - }`, - ); + part: Part | undefined, + ): Promise | undefined { + const request = this.storage.request; + + if (!request) { + return undefined; } - if ( - this.storage.state !== FragPreloadRequestState.IDLE && - request && - this.haveMatchingRequest(frag, part) - ) { - // Do we need to merge the preload frag into the frag/part? + + const cacheHit = this.has(frag, part); + + this.log( + `[${this.getPreloadStateStr()}] check cache for [${frag.type}] ${ + frag.sn + }:${part?.index ?? ''} / have: ${request.frag.sn}:${request.part?.index ?? ''} hit=${cacheHit}`, + ); + if (cacheHit) { return request.loadPromise.then((data) => { mergeFragData(frag, part, data); this.reset(); return data; }); - } - - if (request && this.storage.state !== FragPreloadRequestState.IDLE) { + } else if (this.loading) { const { frag: preloadFrag, part: preloadPart } = request; const haveOldSn = preloadFrag.sn < frag.sn; const haveOldPart = - preloadPart !== null && - part !== null && + preloadPart !== undefined && + part !== undefined && !haveOldSn && preloadPart.index < part.index; @@ -128,7 +128,7 @@ export default class FragmentPreloader extends FragmentLoader { } } - return null; + return undefined; } public revalidate(data: LevelLoadedData | TrackLoadedData) { @@ -144,16 +144,16 @@ export default class FragmentPreloader extends FragmentLoader { } public get frag() { - if (this.storage.info) { - return this.storage.info.frag; + if (this.storage.request) { + return this.storage.request.frag; } - return null; + return undefined; } public reset() { this.storage = { - info: null, - state: FragPreloadRequestState.IDLE, + request: undefined, + state: FragRequestState.IDLE, }; } @@ -168,9 +168,11 @@ export default class FragmentPreloader extends FragmentLoader { } } +function noop() {} + function mergeFragData( frag: Fragment, - part: Part | null, + part: Part | undefined, data: FragLoadedData, ) { const loadedFrag = data.frag; From bd909201649a84e1dc4c69027858910e916944e1 Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Sat, 4 May 2024 13:48:44 -0700 Subject: [PATCH 4/9] Simpler renaming --- api-extractor/report/hls.js.api.md | 4 ++-- src/controller/base-stream-controller.ts | 9 ++++----- src/controller/stream-controller.ts | 2 +- src/loader/fragment-preloader.ts | 8 ++++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 942388b949e..135e8037645 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -323,6 +323,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) protected buffering: boolean; // (undocumented) + protected cachePreloadHint(details: LevelDetails): void; + // (undocumented) protected checkLiveUpdate(details: LevelDetails): void; // (undocumented) protected clearTrackerIfNeeded(frag: Fragment): void; @@ -411,8 +413,6 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) protected levels: Array | null; // (undocumented) - protected loadAndCachePreloadHint(details: LevelDetails): void; - // (undocumented) protected loadedEndOfParts(partList: Part[], targetBufferTime: number): boolean; // (undocumented) protected loadedmetadata: boolean; diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 598159b9015..9a1321935af 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -367,12 +367,12 @@ export default class BaseStreamController this.startTimeOffset = data.startTimeOffset; } - protected loadAndCachePreloadHint(details: LevelDetails): void { + protected cachePreloadHint(details: LevelDetails): void { const data = details.preloadData; if (!data) { return; } - this.fragmentPreloader.preload(data.frag, data.part); + this.fragmentPreloader.cache(data.frag, data.part); } protected onHandlerDestroying() { @@ -843,10 +843,9 @@ export default class BaseStreamController loadedEndOfParts && this.hls.lowLatencyMode && details?.live && - details.canBlockReload && - !this.fragmentPreloader.loading + details.canBlockReload ) { - this.loadAndCachePreloadHint(details); + this.cachePreloadHint(details); } // Fragment hint has no parts return Promise.resolve(null); diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 977acd4717b..95e5658fcdc 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -658,7 +658,7 @@ export default class StreamController ) { this.fragmentPreloader.abort(); } else { - // reset the preloader state to IDLE if we have finished loading, never loaded, or have old data + // reset the preloader state if we have finished loading, never loaded, or have old data this.fragmentPreloader.revalidate(data); } if (newDetails.deltaUpdateFailed) { diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index 17658dee3dd..41d51618488 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -38,7 +38,7 @@ export default class FragmentPreloader extends FragmentLoader { this.log = logger.log.bind(logger, `${logPrefix}>preloader:`); } - private getPreloadStateStr() { + private getStateString() { switch (this.storage.state) { case FragRequestState.IDLE: return 'IDLE '; @@ -61,7 +61,7 @@ export default class FragmentPreloader extends FragmentLoader { return request !== undefined && state !== FragRequestState.IDLE; } - public preload(frag: Fragment, part: Part | undefined): void { + public cache(frag: Fragment, part: Part | undefined): void { if (this.has(frag, part)) { return; } else { @@ -69,7 +69,7 @@ export default class FragmentPreloader extends FragmentLoader { } this.log( - `[${this.getPreloadStateStr()}] create request for [${frag.type}] ${ + `[${this.getStateString()}] create request for [${frag.type}] ${ frag.sn }:${part?.index}`, ); @@ -104,7 +104,7 @@ export default class FragmentPreloader extends FragmentLoader { const cacheHit = this.has(frag, part); this.log( - `[${this.getPreloadStateStr()}] check cache for [${frag.type}] ${ + `[${this.getStateString()}] check cache for [${frag.type}] ${ frag.sn }:${part?.index ?? ''} / have: ${request.frag.sn}:${request.part?.index ?? ''} hit=${cacheHit}`, ); From 2f80070ac48c7e7c350e6870a2f3b77cfa7274e4 Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Fri, 28 Jun 2024 10:28:18 -0700 Subject: [PATCH 5/9] Add detection of open-ended byterange requests and ignore them for now. Add skeleton for fetch loader support of these requests. --- src/loader/fragment-preloader.ts | 51 +++++++++++++++++++++++++++----- src/loader/m3u8-parser.ts | 12 +++++--- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index 41d51618488..f117d89a4cd 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -9,6 +9,7 @@ import { } from '../types/events'; import { HlsConfig } from '../hls'; import { logger } from '../utils/logger'; +import FetchLoader from '../utils/fetch-loader'; export const enum FragRequestState { IDLE, @@ -32,6 +33,7 @@ export default class FragmentPreloader extends FragmentLoader { state: FragRequestState.IDLE, }; protected log: (msg: any) => void; + private fetchLoader?: FetchLoader | undefined; constructor(config: HlsConfig, logPrefix: string) { super(config); @@ -49,10 +51,38 @@ export default class FragmentPreloader extends FragmentLoader { public has(frag: Fragment, part: Part | undefined): boolean { const { request } = this.storage; + + if (request === undefined || request.frag.sn !== frag.sn) { + return false; + } + + const requestPart = request.part; + // frag preload hint only + if (requestPart === undefined && part === undefined) { + return true; + } + + // mismatched part / frag + if (requestPart === undefined || part === undefined) { + return false; + } + + if (requestPart.index === part.index) { + return true; + } + + // Return true if byterange into requested part AND fetch loader exists return ( - request !== undefined && - request.frag.sn === frag.sn && - request.part?.index === part?.index + this.fetchLoader !== undefined && + // request is byterange + requestPart.byteRangeStartOffset !== undefined && + requestPart.byteRangeEndOffset !== undefined && + // part is byterange + part.byteRangeStartOffset !== undefined && + part.byteRangeEndOffset !== undefined && + // part byterange contained within request range + requestPart.byteRangeStartOffset <= part.byteRangeStartOffset && + requestPart.byteRangeEndOffset >= part.byteRangeEndOffset ); } @@ -74,10 +104,17 @@ export default class FragmentPreloader extends FragmentLoader { }:${part?.index}`, ); - const loadPromise = - part !== undefined - ? this.loadPart(frag, part, noop) - : this.load(frag, noop); + let loadPromise; + if (part !== undefined) { + // TODO: Use fetch loader to progressively load open-ended byterange requests + if (part.byteRangeEndOffset === 2 ** 53 - 1) { + return; + } else { + loadPromise = this.loadPart(frag, part, noop); + } + } else { + loadPromise = this.load(frag, noop); + } const request = { frag, diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 7a16fa4deb8..25e9d7fc226 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -697,10 +697,14 @@ export default class M3U8Parser { preloadHintAttrs['BYTERANGE-START'] || preloadHintAttrs['BYTERANGE-LENGTH'] ) { - const byteRangeStartOffset = preloadHintAttrs['BYTERANGE-START'] | 0; - const byteRangeLength = - preloadHintAttrs['BYTERANGE-LENGTH'] | (2 ** 53 - 1); - byteRange = `${byteRangeLength}@${byteRangeStartOffset}`; + const byteRangeStartOffset = preloadHintAttrs['BYTERANGE-START']; + let byteRangeLength = preloadHintAttrs['BYTERANGE-LENGTH']; + if (byteRangeLength <= 0) { + byteRangeLength = 2 ** 53 - 1; + } + if (isFinite(byteRangeLength) && isFinite(byteRangeStartOffset)) { + byteRange = `${byteRangeLength}@${byteRangeStartOffset}`; + } } const preloadType = preloadHintAttrs.TYPE; if (preloadType === 'PART' && level.partList) { From b3e230ee4cec398171c4820c188eca325ef89f9c Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Fri, 28 Jun 2024 13:49:53 -0700 Subject: [PATCH 6/9] Fixup max safe integer check for open ended byteranges with nonzero start --- src/loader/fragment-preloader.ts | 14 +++++++------- src/loader/fragment.ts | 8 +++++++- src/loader/m3u8-parser.ts | 11 ++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index f117d89a4cd..3e4fbb5239e 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -98,16 +98,10 @@ export default class FragmentPreloader extends FragmentLoader { this.abort(); } - this.log( - `[${this.getStateString()}] create request for [${frag.type}] ${ - frag.sn - }:${part?.index}`, - ); - let loadPromise; if (part !== undefined) { // TODO: Use fetch loader to progressively load open-ended byterange requests - if (part.byteRangeEndOffset === 2 ** 53 - 1) { + if (part?.byteRangeEndOffset === Number.MAX_SAFE_INTEGER) { return; } else { loadPromise = this.loadPart(frag, part, noop); @@ -116,6 +110,12 @@ export default class FragmentPreloader extends FragmentLoader { loadPromise = this.load(frag, noop); } + this.log( + `[${this.getStateString()}] create request for [${frag.type}] ${ + frag.sn + }:${part?.index}`, + ); + const request = { frag, part, diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index c631f41a738..bb8186f84df 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -58,7 +58,13 @@ export class BaseSegment { } else { start = parseInt(params[1]); } - this._byteRange = [start, parseInt(params[0]) + start]; + const bytelength = parseInt(params[0]); + const offsetEnd = + Number.isSafeInteger(bytelength) && bytelength !== Number.MAX_SAFE_INTEGER + ? start + bytelength + : Number.MAX_SAFE_INTEGER; + + this._byteRange = [start, offsetEnd]; } get byteRange(): [number, number] | [] { diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 25e9d7fc226..70244520966 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -698,11 +698,12 @@ export default class M3U8Parser { preloadHintAttrs['BYTERANGE-LENGTH'] ) { const byteRangeStartOffset = preloadHintAttrs['BYTERANGE-START']; - let byteRangeLength = preloadHintAttrs['BYTERANGE-LENGTH']; - if (byteRangeLength <= 0) { - byteRangeLength = 2 ** 53 - 1; - } - if (isFinite(byteRangeLength) && isFinite(byteRangeStartOffset)) { + + if (isFinite(byteRangeStartOffset)) { + let byteRangeLength = preloadHintAttrs['BYTERANGE-LENGTH']; + if (!isFinite(byteRangeLength) || byteRangeLength <= 0) { + byteRangeLength = Number.MAX_SAFE_INTEGER; + } byteRange = `${byteRangeLength}@${byteRangeStartOffset}`; } } From 6779eef273b2bafc4b2e98f1620e38d0a280826c Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Fri, 30 Aug 2024 09:32:32 -0700 Subject: [PATCH 7/9] Explicitly type load promise, make stream-controller:cachePreloadHint private --- api-extractor/report/hls.js.api.md | 2 -- src/controller/base-stream-controller.ts | 2 +- src/loader/fragment-preloader.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 135e8037645..981721f5681 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -323,8 +323,6 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) protected buffering: boolean; // (undocumented) - protected cachePreloadHint(details: LevelDetails): void; - // (undocumented) protected checkLiveUpdate(details: LevelDetails): void; // (undocumented) protected clearTrackerIfNeeded(frag: Fragment): void; diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 9a1321935af..6371e8e28f0 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -367,7 +367,7 @@ export default class BaseStreamController this.startTimeOffset = data.startTimeOffset; } - protected cachePreloadHint(details: LevelDetails): void { + private cachePreloadHint(details: LevelDetails): void { const data = details.preloadData; if (!data) { return; diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index 3e4fbb5239e..40f2d4cb2d2 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -98,7 +98,7 @@ export default class FragmentPreloader extends FragmentLoader { this.abort(); } - let loadPromise; + let loadPromise: Promise; if (part !== undefined) { // TODO: Use fetch loader to progressively load open-ended byterange requests if (part?.byteRangeEndOffset === Number.MAX_SAFE_INTEGER) { From d18760f36e195387bbabd91e349785ef192deb4e Mon Sep 17 00:00:00 2001 From: Evan Burton Date: Fri, 30 Aug 2024 09:46:35 -0700 Subject: [PATCH 8/9] Remove isPreload property from frag/part and add blockingLoad property to stats --- api-extractor/report/hls.js.api.md | 4 +--- src/controller/abr-controller.ts | 7 ++----- src/controller/error-controller.ts | 2 +- src/loader/fragment-preloader.ts | 5 +++-- src/loader/fragment.ts | 5 ----- src/loader/load-stats.ts | 1 + src/loader/m3u8-parser.ts | 1 - src/types/loader.ts | 1 + 8 files changed, 9 insertions(+), 17 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 981721f5681..7fc7b667c98 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -294,8 +294,6 @@ export class BaseSegment { // (undocumented) elementaryStreams: ElementaryStreams; // (undocumented) - isPreload?: boolean; - // (undocumented) relurl?: string; // (undocumented) setByteRange(value: string, previous?: BaseSegment): void; @@ -3067,7 +3065,7 @@ export type ParsedMultivariantPlaylist = { // // @public export class Part extends BaseSegment { - constructor(partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part, isPreload?: boolean); + constructor(partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part); // (undocumented) readonly duration: number; // (undocumented) diff --git a/src/controller/abr-controller.ts b/src/controller/abr-controller.ts index 53ad7b5a645..31649faba4f 100644 --- a/src/controller/abr-controller.ts +++ b/src/controller/abr-controller.ts @@ -378,10 +378,7 @@ class AbrController extends Logger implements AbrComponentAPI { { frag, part }: FragLoadedData, ) { const stats = part ? part.stats : frag.stats; - if ( - frag.type === PlaylistLevelType.MAIN && - !(part?.isPreload || frag.isPreload) - ) { + if (frag.type === PlaylistLevelType.MAIN && !stats.blockingLoad) { this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start); } if (this.ignoreFragment(frag)) { @@ -437,7 +434,7 @@ class AbrController extends Logger implements AbrComponentAPI { // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing; // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch // is used. If we used buffering in that case, our BW estimate sample will be very large. - const loadStart = part?.isPreload + const loadStart = part?.stats.blockingLoad ? stats.loading.first : stats.loading.start; const processingMs = diff --git a/src/controller/error-controller.ts b/src/controller/error-controller.ts index c38147ceb40..8131f2e38c2 100644 --- a/src/controller/error-controller.ts +++ b/src/controller/error-controller.ts @@ -276,7 +276,7 @@ export default class ErrorController } private getFragRetryOrSwitchAction(data: ErrorData): IErrorAction { - if (data.frag?.isPreload) { + if (data.frag?.stats.blockingLoad) { return { action: NetworkErrorAction.DoNothing, flags: ErrorActionFlags.None, diff --git a/src/loader/fragment-preloader.ts b/src/loader/fragment-preloader.ts index 40f2d4cb2d2..077cb323e47 100644 --- a/src/loader/fragment-preloader.ts +++ b/src/loader/fragment-preloader.ts @@ -215,7 +215,6 @@ function mergeFragData( const loadedFrag = data.frag; const loadedPart = data.part; - frag.isPreload = true; if (frag.stats.loaded === 0) { frag.stats = loadedFrag.stats; } else { @@ -225,7 +224,9 @@ function mergeFragData( } if (part && loadedPart) { - part.isPreload = true; part.stats = loadedPart.stats; + part.stats.blockingLoad = true; } + + frag.stats.blockingLoad = true; } diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index bb8186f84df..979552ba36a 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -43,7 +43,6 @@ export class BaseSegment { [ElementaryStreamTypes.VIDEO]: null, [ElementaryStreamTypes.AUDIOVIDEO]: null, }; - public isPreload?: boolean; constructor(baseurl: string) { this.baseurl = baseurl; @@ -295,7 +294,6 @@ export class Part extends BaseSegment { baseurl: string, index: number, previous?: Part, - isPreload?: boolean, ) { super(baseurl); this.duration = partAttrs.decimalFloatingPoint('DURATION'); @@ -311,9 +309,6 @@ export class Part extends BaseSegment { if (previous) { this.fragOffset = previous.fragOffset + previous.duration; } - if (isPreload) { - this.isPreload = isPreload; - } } get start(): number { diff --git a/src/loader/load-stats.ts b/src/loader/load-stats.ts index 0bb0e6adb54..937d32f3178 100644 --- a/src/loader/load-stats.ts +++ b/src/loader/load-stats.ts @@ -14,4 +14,5 @@ export class LoadStats implements LoaderStats { loading: HlsProgressivePerformanceTiming = { start: 0, first: 0, end: 0 }; parsing: HlsPerformanceTiming = { start: 0, end: 0 }; buffering: HlsProgressivePerformanceTiming = { start: 0, first: 0, end: 0 }; + blockingLoad = false; } diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 70244520966..feb041e155a 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -731,7 +731,6 @@ export default class M3U8Parser { baseurl, partIndex, lastPartPublished ? undefined : lastPart, - true, ); level.preloadData = { frag: preloadFrag, diff --git a/src/types/loader.ts b/src/types/loader.ts index fc563688b23..843be86358e 100644 --- a/src/types/loader.ts +++ b/src/types/loader.ts @@ -79,6 +79,7 @@ export interface LoaderStats { loading: HlsProgressivePerformanceTiming; parsing: HlsPerformanceTiming; buffering: HlsProgressivePerformanceTiming; + blockingLoad: boolean; } export interface HlsPerformanceTiming { From 884838f9b46c2b46a73c828c77849886d0f45652 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 30 Aug 2024 11:58:37 -0700 Subject: [PATCH 9/9] Update api-extractor report --- api-extractor/report/hls.js.api.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 7fc7b667c98..537d0c16c14 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -2646,6 +2646,8 @@ export interface LoaderStats { // (undocumented) aborted: boolean; // (undocumented) + blockingLoad: boolean; + // (undocumented) buffering: HlsProgressivePerformanceTiming; // (undocumented) bwEstimate: number; @@ -2677,6 +2679,8 @@ export class LoadStats implements LoaderStats { // (undocumented) aborted: boolean; // (undocumented) + blockingLoad: boolean; + // (undocumented) buffering: HlsProgressivePerformanceTiming; // (undocumented) bwEstimate: number; @@ -3065,7 +3069,7 @@ export type ParsedMultivariantPlaylist = { // // @public export class Part extends BaseSegment { - constructor(partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part); + constructor(partAttrs: AttrList, frag: MediaFragment, baseurl: string, index: number, previous?: Part); // (undocumented) readonly duration: number; // (undocumented)