Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Implement support for LL-HLS #EXT-X-PRELOAD-HINT part loading #6356

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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
Loading