Skip to content
15 changes: 15 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,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)
Expand Down Expand Up @@ -405,6 +409,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
// (undocumented)
protected levels: Array<Level> | null;
// (undocumented)
protected loadedEndOfParts(partList: Part[], targetBufferTime: number): boolean;
// (undocumented)
protected loadedmetadata: boolean;
// (undocumented)
protected loadFragment(frag: Fragment, level: Level, targetBufferTime: number): void;
Expand Down Expand Up @@ -2263,6 +2269,11 @@ export class LevelDetails {
// (undocumented)
playlistParsingError: Error | null;
// (undocumented)
preloadData?: {
frag: Fragment;
part?: Part;
};
// (undocumented)
preloadHint?: AttrList;
// (undocumented)
PTSKnown: boolean;
Expand Down Expand Up @@ -2635,6 +2646,8 @@ export interface LoaderStats {
// (undocumented)
aborted: boolean;
// (undocumented)
blockingLoad: boolean;
// (undocumented)
buffering: HlsProgressivePerformanceTiming;
// (undocumented)
bwEstimate: number;
Expand Down Expand Up @@ -2666,6 +2679,8 @@ export class LoadStats implements LoaderStats {
// (undocumented)
aborted: boolean;
// (undocumented)
blockingLoad: boolean;
// (undocumented)
buffering: HlsProgressivePerformanceTiming;
// (undocumented)
bwEstimate: number;
Expand Down
7 changes: 5 additions & 2 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ 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 && !stats.blockingLoad) {
this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start);
}
if (this.ignoreFragment(frag)) {
Expand Down Expand Up @@ -434,9 +434,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?.stats.blockingLoad
? 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(),
Expand Down
2 changes: 2 additions & 0 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
95 changes: 78 additions & 17 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +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 from '../loader/fragment-preloader';

type ResolveFragLoaded = (FragLoadedEndData) => void;
type RejectFragLoaded = (LoadError) => void;
Expand Down Expand Up @@ -95,6 +96,7 @@ export default class BaseStreamController
protected retryDate: number = 0;
protected levels: Array<Level> | null = null;
protected fragmentLoader: FragmentLoader;
protected fragmentPreloader: FragmentPreloader;
protected keyLoader: KeyLoader;
protected levelLastLoaded: Level | null = null;
protected startFragRequested: boolean = false;
Expand All @@ -115,6 +117,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;
Expand Down Expand Up @@ -153,7 +156,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();
Expand Down Expand Up @@ -363,6 +367,14 @@ export default class BaseStreamController
this.startTimeOffset = data.startTimeOffset;
}

private cachePreloadHint(details: LevelDetails): void {
const data = details.preloadData;
if (!data) {
return;
}
this.fragmentPreloader.cache(data.frag, data.part);
}

protected onHandlerDestroying() {
this.stopLoad();
if (this.transmuxer) {
Expand All @@ -379,6 +391,9 @@ export default class BaseStreamController
if (this.fragmentLoader) {
this.fragmentLoader.destroy();
}
if (this.fragmentPreloader) {
this.fragmentPreloader.destroy();
}
if (this.keyLoader) {
this.keyLoader.destroy();
}
Expand All @@ -392,6 +407,7 @@ export default class BaseStreamController
this.decrypter =
this.keyLoader =
this.fragmentLoader =
this.fragmentPreloader =
this.fragmentTracker =
null as any;
super.onHandlerDestroyed();
Expand Down Expand Up @@ -765,6 +781,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];
Expand Down Expand Up @@ -818,10 +838,15 @@ 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.cachePreloadHint(details);
}
// Fragment hint has no parts
return Promise.resolve(null);
}
Expand Down Expand Up @@ -856,23 +881,28 @@ export default class BaseStreamController
let result: Promise<PartsLoadedData | FragLoadedData | null>;
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,
/*part*/ undefined,
/*dataOnProgress*/ 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,
/*part*/ undefined,
dataOnProgress,
progressCallback,
);
result = Promise.all([loadRequest, keyLoadingPromise])
.then(([fragLoadedData]) => {
if (!dataOnProgress && fragLoadedData && progressCallback) {
progressCallback(fragLoadedData);
Expand Down Expand Up @@ -901,8 +931,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;
Expand All @@ -927,6 +956,36 @@ export default class BaseStreamController
);
}

private getCachedRequestOrLoad(
frag: Fragment,
part: Part | undefined,
dataOnProgress: boolean,
progressCallback?: FragmentLoadProgressCallback,
): Promise<FragLoadedData | PartsLoadedData> {
const request = this.fragmentPreloader.getCachedRequest(frag, part);
if (request !== undefined) {
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;
Expand Down Expand Up @@ -1332,7 +1391,7 @@ export default class BaseStreamController
return nextPart;
}

private loadedEndOfParts(
protected loadedEndOfParts(
partList: Part[],
targetBufferTime: number,
): boolean {
Expand Down Expand Up @@ -1650,6 +1709,7 @@ export default class BaseStreamController
) {
this.state = State.IDLE;
}
this.fragmentPreloader.abort();
}

protected onFragmentOrKeyLoadError(
Expand Down Expand Up @@ -1801,6 +1861,7 @@ export default class BaseStreamController
if (this.state !== State.STOPPED) {
this.state = State.IDLE;
}
this.fragmentPreloader.abort();
}

protected resetStartWhenNotLoaded(level: Level | null): void {
Expand Down
6 changes: 6 additions & 0 deletions src/controller/error-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@ export default class ErrorController
}

private getFragRetryOrSwitchAction(data: ErrorData): IErrorAction {
if (data.frag?.stats.blockingLoad) {
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
Expand Down
9 changes: 9 additions & 0 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,15 @@ export default class StreamController
let sliding = 0;
if (newDetails.live || curLevel.details?.live) {
this.checkLiveUpdate(newDetails);
if (
this.fragmentPreloader.loading &&
this.fragmentPreloader.frag?.level !== data.level
) {
this.fragmentPreloader.abort();
} else {
// reset the preloader state if we have finished loading, never loaded, or have old data
this.fragmentPreloader.revalidate(data);
}
if (newDetails.deltaUpdateFailed) {
return;
}
Expand Down
Loading