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

Live reload throttling per level details schedule #6879

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ export type BaseData = {
export class BasePlaylistController extends Logger implements NetworkComponentAPI {
constructor(hls: Hls, logPrefix: string);
// (undocumented)
protected canLoad: boolean;
// (undocumented)
protected checkRetry(errorEvent: ErrorData): boolean;
// (undocumented)
destroy(): void;
Expand All @@ -319,7 +321,7 @@ export class BasePlaylistController extends Logger implements NetworkComponentAP
// (undocumented)
protected playlistLoaded(index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails): void;
// (undocumented)
protected scheduleLoading(levelOrTrack: Level | MediaPlaylist, deliveryDirectives?: HlsUrlParameters): void;
protected scheduleLoading(levelOrTrack: Level | MediaPlaylist, deliveryDirectives?: HlsUrlParameters, updatedDetails?: LevelDetails): void;
// (undocumented)
protected shouldLoadPlaylist(playlist: Level | MediaPlaylist | null | undefined): playlist is Level | MediaPlaylist;
// (undocumented)
Expand Down Expand Up @@ -520,7 +522,7 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
// (undocumented)
protected resetTransmuxer(): void;
// (undocumented)
protected resetWhenMissingContext(chunkMeta: ChunkMetadata): void;
protected resetWhenMissingContext(chunkMeta: ChunkMetadata | Fragment): void;
// (undocumented)
resumeBuffering(): void;
// (undocumented)
Expand Down Expand Up @@ -2012,6 +2014,7 @@ class Hls implements HlsEventEmitter {
// (undocumented)
listeners<E extends keyof HlsListeners>(event: E): HlsListeners[E][];
get liveSyncPosition(): number | null;
get loadingEnabled(): boolean;
get loadLevel(): number;
// Warning: (ae-setter-with-docs) The doc comment for the property "loadLevel" must appear on the getter, not the setter.
set loadLevel(newLevel: number);
Expand Down Expand Up @@ -3158,6 +3161,8 @@ export class LevelDetails {
// (undocumented)
m3u8: string;
// (undocumented)
get maxPartIndex(): number;
// (undocumented)
misses: number;
// (undocumented)
get partEnd(): number;
Expand Down Expand Up @@ -3258,6 +3263,8 @@ export interface LevelLoadedData {
networkDetails: any;
// (undocumented)
stats: LoaderStats;
// (undocumented)
withoutMultiVariant?: boolean;
}

// Warning: (ae-missing-release-tag) "LevelLoadingData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
4 changes: 2 additions & 2 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ class AbrController extends Logger implements AbrComponentAPI {
if (levels.length === 1) {
return 0;
}
const level: Level | undefined = levels[selectionBaseLevel];
const level = levels[selectionBaseLevel] as Level | undefined;
const live = !!this.hls.latestLevelDetails?.live;
const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1;
let currentCodecSet: string | undefined;
Expand Down Expand Up @@ -920,7 +920,7 @@ class AbrController extends Logger implements AbrComponentAPI {
)} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${
levels[levelsSkipped[0]].codecs
}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${
level.codecs
currentCodecSet
}" ${currentVideoRange}`,
);
}
Expand Down
83 changes: 39 additions & 44 deletions src/controller/base-playlist-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export default class BasePlaylistController
implements NetworkComponentAPI
{
protected hls: Hls;
protected canLoad: boolean = false;
private timer: number = -1;
private canLoad: boolean = false;

constructor(hls: Hls, logPrefix: string) {
super(logPrefix, hls.logger);
Expand Down Expand Up @@ -170,6 +170,25 @@ export default class BasePlaylistController
if (details.live || previousDetails?.live) {
const levelOrTrack = 'levelInfo' in data ? data.levelInfo : data.track;
details.reloaded(previousDetails);
// Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
if (previousDetails && details.fragments.length > 0) {
mergeDetails(previousDetails, details);
}
if (details.requestScheduled === -1) {
details.requestScheduled = stats.loading.start;
}
const bufferInfo = this.hls.mainForwardBufferInfo;
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
const reloadInterval = computeReloadInterval(
details,
distanceToLiveEdgeMs,
);
if (details.requestScheduled + reloadInterval < now) {
details.requestScheduled = now;
} else {
details.requestScheduled += reloadInterval;
}
this.log(
`live playlist ${index} ${
details.advanced
Expand All @@ -179,10 +198,6 @@ export default class BasePlaylistController
: 'MISSED'
}`,
);
// Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
if (previousDetails && details.fragments.length > 0) {
mergeDetails(previousDetails, details);
}
if (!this.canLoad || !details.live) {
return;
}
Expand All @@ -196,12 +211,16 @@ export default class BasePlaylistController
const endSn = details.endSN;
const lastPartIndex = details.lastPartIndex;
const hasParts = lastPartIndex !== -1;
const lastPart = lastPartSn === endSn;
// When low latency mode is disabled, we'll skip part requests once the last part index is found
const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex;
const atLastPartOfSegment = lastPartSn === endSn;
if (hasParts) {
msn = lastPart ? endSn + 1 : lastPartSn;
part = lastPart ? nextSnStartIndex : lastPartIndex + 1;
// When low latency mode is disabled, request the last part of the next segment
if (atLastPartOfSegment) {
msn = endSn + 1;
part = lowLatencyMode ? 0 : lastPartIndex;
} else {
msn = lastPartSn;
part = lowLatencyMode ? lastPartIndex + 1 : details.maxPartIndex;
}
} else {
msn = endSn + 1;
}
Expand Down Expand Up @@ -258,7 +277,8 @@ export default class BasePlaylistController
msn,
part,
);
if (lowLatencyMode || !lastPart) {
if (lowLatencyMode || !atLastPartOfSegment) {
details.requestScheduled = now;
this.loadingPlaylist(levelOrTrack, deliveryDirectives);
return;
}
Expand All @@ -270,25 +290,12 @@ export default class BasePlaylistController
part,
);
}
if (details.requestScheduled === -1) {
details.requestScheduled = stats.loading.start;
}
if (deliveryDirectives && msn !== undefined && details.canBlockReload) {
details.requestScheduled -= details.partTarget * 1000 || 1000;
}
const bufferInfo = this.hls.mainForwardBufferInfo;
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
const reloadInterval = computeReloadInterval(
details,
distanceToLiveEdgeMs,
);
if (details.requestScheduled + reloadInterval < now) {
details.requestScheduled = now;
} else {
details.requestScheduled += reloadInterval;
details.requestScheduled =
stats.loading.first +
Math.max(reloadInterval - elapsed * 2, reloadInterval / 2);
}
this.scheduleLoading(levelOrTrack, deliveryDirectives);
this.scheduleLoading(levelOrTrack, deliveryDirectives, details);
} else {
this.clearTimer();
}
Expand All @@ -297,8 +304,9 @@ export default class BasePlaylistController
protected scheduleLoading(
levelOrTrack: Level | MediaPlaylist,
deliveryDirectives?: HlsUrlParameters,
updatedDetails?: LevelDetails,
) {
const details = levelOrTrack.details;
const details = updatedDetails || levelOrTrack.details;
if (!details) {
this.loadingPlaylist(levelOrTrack, deliveryDirectives);
return;
Expand All @@ -316,22 +324,8 @@ export default class BasePlaylistController
estimatedTimeUntilUpdate,
)} ms`,
);
// this.log(
// `live reload ${details.updated ? 'REFRESHED' : 'MISSED'}
// reload in ${estimatedTimeUntilUpdate / 1000}
// round trip ${(stats.loading.end - stats.loading.start) / 1000}
// diff ${
// (reloadInterval -
// (estimatedTimeUntilUpdate +
// stats.loading.end -
// stats.loading.start)) /
// 1000
// }
// reload interval ${reloadInterval / 1000}
// target duration ${details.targetduration}
// distance to edge ${distanceToLiveEdgeMs / 1000}`
// );

this.clearTimer();
this.timer = self.setTimeout(
() => this.loadingPlaylist(levelOrTrack, deliveryDirectives),
estimatedTimeUntilUpdate,
Expand Down Expand Up @@ -379,6 +373,7 @@ export default class BasePlaylistController
} else {
const delay = getRetryDelay(retryConfig, retryCount);
// Schedule level/track reload
this.clearTimer();
this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
this.warn(
`Retrying playlist loading ${retryCount + 1}/${
Expand Down
2 changes: 1 addition & 1 deletion src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1871,7 +1871,7 @@ export default class BaseStreamController
}
}

protected resetWhenMissingContext(chunkMeta: ChunkMetadata) {
protected resetWhenMissingContext(chunkMeta: ChunkMetadata | Fragment) {
this.warn(
`The loading context changed while buffering fragment ${chunkMeta.sn} of ${this.playlistLabel()} ${chunkMeta.level}. This chunk will not be buffered.`,
);
Expand Down
23 changes: 18 additions & 5 deletions src/controller/interstitials-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
player.media?.play();
}
} else if (scheduledItem !== null) {
this.resumePrimary(scheduledItem, index);
this.resumePrimary(scheduledItem, index, currentItem);
if (this.shouldPlay) {
this.hls.media?.play();
}
Expand Down Expand Up @@ -1144,12 +1144,16 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
private resumePrimary(
scheduledItem: InterstitialSchedulePrimaryItem,
index: number,
fromItem: InterstitialScheduleItem | null,
) {
this.playingItem = scheduledItem;
this.playingAsset = null;
this.waitingItem = null;

this.bufferedToItem(scheduledItem);
if (!fromItem) {
return;
}

this.log(`resuming ${segmentToString(scheduledItem)}`);

Expand Down Expand Up @@ -1231,13 +1235,22 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
} else {
this.transferMediaTo(hls, media);
if (skipSeekToStartPosition) {
hls.startLoad(timelinePos, skipSeekToStartPosition);
this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
}
}
if (!skipSeekToStartPosition) {
// Set primary position to resume time
this.timelinePos = timelinePos;
hls.startLoad(timelinePos, skipSeekToStartPosition);
this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
}
}

private startLoadingPrimaryAt(
timelinePos: number,
skipSeekToStartPosition?: boolean,
) {
if (this.hls.loadingEnabled) {
this.hls.startLoad(timelinePos, skipSeekToStartPosition);
}
}

Expand Down Expand Up @@ -1688,7 +1701,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
private preloadPrimary(item: InterstitialSchedulePrimaryItem) {
const index = this.findItemIndex(item);
const timelinePos = this.getPrimaryResumption(item, index);
this.hls.startLoad(timelinePos);
this.startLoadingPrimaryAt(timelinePos);
}

private bufferedToEvent(
Expand Down Expand Up @@ -1869,7 +1882,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
}
const playerConfig: Partial<HlsConfig> = {
...userConfig,
// autoStartLoad: false,
autoStartLoad: true,
startFragPrefetch: true,
primarySessionId: primary.sessionId,
assetPlayerId: assetItem.identifier,
Expand Down
27 changes: 13 additions & 14 deletions src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,18 +380,6 @@ export default class LevelController extends BasePlaylistController {
altAudio: !audioOnly && audioTracks.some((t) => !!t.url),
};
this.hls.trigger(Events.MANIFEST_PARSED, edata);

// Initiate loading after all controllers have received MANIFEST_PARSED
const {
config: { autoStartLoad, startPosition },
forceStartLoad,
} = this.hls;
if (autoStartLoad || forceStartLoad) {
this.log(
`${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`,
);
this.hls.startLoad(startPosition);
}
}

get levels(): Level[] | null {
Expand Down Expand Up @@ -606,8 +594,8 @@ export default class LevelController extends BasePlaylistController {
return;
}

// only process level loaded events matching with expected level
if (curLevel === this.currentLevel) {
// only process level loaded events matching with expected level or prior to switch when media playlist is loaded directly
if (curLevel === this.currentLevel || data.withoutMultiVariant) {
// reset level load error counter on successful level loaded only if there is no issues with fragments
if (curLevel.fragmentError === 0) {
curLevel.loadError = 0;
Expand Down Expand Up @@ -676,6 +664,9 @@ export default class LevelController extends BasePlaylistController {
}

removeLevel(levelIndex: number) {
if (this._levels.length === 1) {
return;
}
const levels = this._levels.filter((level, index) => {
if (index !== levelIndex) {
return true;
Expand All @@ -697,6 +688,14 @@ export default class LevelController extends BasePlaylistController {
if (this.currentLevelIndex > -1 && this.currentLevel?.details) {
this.currentLevelIndex = this.currentLevel.details.fragments[0].level;
}
if (this.manualLevelIndex > -1) {
this.manualLevelIndex = this.currentLevelIndex;
}
const maxLevel = levels.length - 1;
this._firstLevel = Math.min(this._firstLevel, maxLevel);
if (this._startLevel) {
this._startLevel = Math.min(this._startLevel, maxLevel);
}
this.hls.trigger(Events.LEVELS_UPDATED, { levels });
}

Expand Down
3 changes: 3 additions & 0 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,9 @@ export default class StreamController
) {
if (this.level > -1 && this.fragCurrent) {
this.level = this.fragCurrent.level;
if (this.level === -1) {
this.resetWhenMissingContext(this.fragCurrent);
}
}
this.levels = data.levels;
}
Expand Down
Loading
Loading