Skip to content

Commit b3be549

Browse files
authored
Live reload throttling per level details schedule (#6879)
Live reload throttling per level details schedule - Improve blocking requests with parts and low-latency mode disabled - Defer autostart until after manifest loasded and level loaded when media playlist is source Fixes #6858 Interstitial startup fixes - Added `hls.loadingEnabled` getter - Do not call startLoad when stopped and do not resume primary on first item Fixes #6839 Break start/stop load function synchronously if loading state was changed or instance destroyed - Fix segment loading edge cases following `removeLevel(<currentLevelIndex>)` Addresses functional test failure: https://github.com/video-dev/hls.js/actions/runs/12124731664/job/33809171310?pr=6879
1 parent 537ca4a commit b3be549

15 files changed

+186
-89
lines changed

api-extractor/report/hls.js.api.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ export type BaseData = {
305305
export class BasePlaylistController extends Logger implements NetworkComponentAPI {
306306
constructor(hls: Hls, logPrefix: string);
307307
// (undocumented)
308+
protected canLoad: boolean;
309+
// (undocumented)
308310
protected checkRetry(errorEvent: ErrorData): boolean;
309311
// (undocumented)
310312
destroy(): void;
@@ -319,7 +321,7 @@ export class BasePlaylistController extends Logger implements NetworkComponentAP
319321
// (undocumented)
320322
protected playlistLoaded(index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails): void;
321323
// (undocumented)
322-
protected scheduleLoading(levelOrTrack: Level | MediaPlaylist, deliveryDirectives?: HlsUrlParameters): void;
324+
protected scheduleLoading(levelOrTrack: Level | MediaPlaylist, deliveryDirectives?: HlsUrlParameters, updatedDetails?: LevelDetails): void;
323325
// (undocumented)
324326
protected shouldLoadPlaylist(playlist: Level | MediaPlaylist | null | undefined): playlist is Level | MediaPlaylist;
325327
// (undocumented)
@@ -520,7 +522,7 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
520522
// (undocumented)
521523
protected resetTransmuxer(): void;
522524
// (undocumented)
523-
protected resetWhenMissingContext(chunkMeta: ChunkMetadata): void;
525+
protected resetWhenMissingContext(chunkMeta: ChunkMetadata | Fragment): void;
524526
// (undocumented)
525527
resumeBuffering(): void;
526528
// (undocumented)
@@ -2012,6 +2014,7 @@ class Hls implements HlsEventEmitter {
20122014
// (undocumented)
20132015
listeners<E extends keyof HlsListeners>(event: E): HlsListeners[E][];
20142016
get liveSyncPosition(): number | null;
2017+
get loadingEnabled(): boolean;
20152018
get loadLevel(): number;
20162019
// Warning: (ae-setter-with-docs) The doc comment for the property "loadLevel" must appear on the getter, not the setter.
20172020
set loadLevel(newLevel: number);
@@ -3158,6 +3161,8 @@ export class LevelDetails {
31583161
// (undocumented)
31593162
m3u8: string;
31603163
// (undocumented)
3164+
get maxPartIndex(): number;
3165+
// (undocumented)
31613166
misses: number;
31623167
// (undocumented)
31633168
get partEnd(): number;
@@ -3258,6 +3263,8 @@ export interface LevelLoadedData {
32583263
networkDetails: any;
32593264
// (undocumented)
32603265
stats: LoaderStats;
3266+
// (undocumented)
3267+
withoutMultiVariant?: boolean;
32613268
}
32623269

32633270
// 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)

src/controller/abr-controller.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ class AbrController extends Logger implements AbrComponentAPI {
718718
if (levels.length === 1) {
719719
return 0;
720720
}
721-
const level: Level | undefined = levels[selectionBaseLevel];
721+
const level = levels[selectionBaseLevel] as Level | undefined;
722722
const live = !!this.hls.latestLevelDetails?.live;
723723
const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1;
724724
let currentCodecSet: string | undefined;
@@ -920,7 +920,7 @@ class AbrController extends Logger implements AbrComponentAPI {
920920
)} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${
921921
levels[levelsSkipped[0]].codecs
922922
}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${
923-
level.codecs
923+
currentCodecSet
924924
}" ${currentVideoRange}`,
925925
);
926926
}

src/controller/base-playlist-controller.ts

+39-44
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export default class BasePlaylistController
2424
implements NetworkComponentAPI
2525
{
2626
protected hls: Hls;
27+
protected canLoad: boolean = false;
2728
private timer: number = -1;
28-
private canLoad: boolean = false;
2929

3030
constructor(hls: Hls, logPrefix: string) {
3131
super(logPrefix, hls.logger);
@@ -170,6 +170,25 @@ export default class BasePlaylistController
170170
if (details.live || previousDetails?.live) {
171171
const levelOrTrack = 'levelInfo' in data ? data.levelInfo : data.track;
172172
details.reloaded(previousDetails);
173+
// Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
174+
if (previousDetails && details.fragments.length > 0) {
175+
mergeDetails(previousDetails, details);
176+
}
177+
if (details.requestScheduled === -1) {
178+
details.requestScheduled = stats.loading.start;
179+
}
180+
const bufferInfo = this.hls.mainForwardBufferInfo;
181+
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
182+
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
183+
const reloadInterval = computeReloadInterval(
184+
details,
185+
distanceToLiveEdgeMs,
186+
);
187+
if (details.requestScheduled + reloadInterval < now) {
188+
details.requestScheduled = now;
189+
} else {
190+
details.requestScheduled += reloadInterval;
191+
}
173192
this.log(
174193
`live playlist ${index} ${
175194
details.advanced
@@ -179,10 +198,6 @@ export default class BasePlaylistController
179198
: 'MISSED'
180199
}`,
181200
);
182-
// Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
183-
if (previousDetails && details.fragments.length > 0) {
184-
mergeDetails(previousDetails, details);
185-
}
186201
if (!this.canLoad || !details.live) {
187202
return;
188203
}
@@ -196,12 +211,16 @@ export default class BasePlaylistController
196211
const endSn = details.endSN;
197212
const lastPartIndex = details.lastPartIndex;
198213
const hasParts = lastPartIndex !== -1;
199-
const lastPart = lastPartSn === endSn;
200-
// When low latency mode is disabled, we'll skip part requests once the last part index is found
201-
const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex;
214+
const atLastPartOfSegment = lastPartSn === endSn;
202215
if (hasParts) {
203-
msn = lastPart ? endSn + 1 : lastPartSn;
204-
part = lastPart ? nextSnStartIndex : lastPartIndex + 1;
216+
// When low latency mode is disabled, request the last part of the next segment
217+
if (atLastPartOfSegment) {
218+
msn = endSn + 1;
219+
part = lowLatencyMode ? 0 : lastPartIndex;
220+
} else {
221+
msn = lastPartSn;
222+
part = lowLatencyMode ? lastPartIndex + 1 : details.maxPartIndex;
223+
}
205224
} else {
206225
msn = endSn + 1;
207226
}
@@ -258,7 +277,8 @@ export default class BasePlaylistController
258277
msn,
259278
part,
260279
);
261-
if (lowLatencyMode || !lastPart) {
280+
if (lowLatencyMode || !atLastPartOfSegment) {
281+
details.requestScheduled = now;
262282
this.loadingPlaylist(levelOrTrack, deliveryDirectives);
263283
return;
264284
}
@@ -270,25 +290,12 @@ export default class BasePlaylistController
270290
part,
271291
);
272292
}
273-
if (details.requestScheduled === -1) {
274-
details.requestScheduled = stats.loading.start;
275-
}
276293
if (deliveryDirectives && msn !== undefined && details.canBlockReload) {
277-
details.requestScheduled -= details.partTarget * 1000 || 1000;
278-
}
279-
const bufferInfo = this.hls.mainForwardBufferInfo;
280-
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
281-
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
282-
const reloadInterval = computeReloadInterval(
283-
details,
284-
distanceToLiveEdgeMs,
285-
);
286-
if (details.requestScheduled + reloadInterval < now) {
287-
details.requestScheduled = now;
288-
} else {
289-
details.requestScheduled += reloadInterval;
294+
details.requestScheduled =
295+
stats.loading.first +
296+
Math.max(reloadInterval - elapsed * 2, reloadInterval / 2);
290297
}
291-
this.scheduleLoading(levelOrTrack, deliveryDirectives);
298+
this.scheduleLoading(levelOrTrack, deliveryDirectives, details);
292299
} else {
293300
this.clearTimer();
294301
}
@@ -297,8 +304,9 @@ export default class BasePlaylistController
297304
protected scheduleLoading(
298305
levelOrTrack: Level | MediaPlaylist,
299306
deliveryDirectives?: HlsUrlParameters,
307+
updatedDetails?: LevelDetails,
300308
) {
301-
const details = levelOrTrack.details;
309+
const details = updatedDetails || levelOrTrack.details;
302310
if (!details) {
303311
this.loadingPlaylist(levelOrTrack, deliveryDirectives);
304312
return;
@@ -316,22 +324,8 @@ export default class BasePlaylistController
316324
estimatedTimeUntilUpdate,
317325
)} ms`,
318326
);
319-
// this.log(
320-
// `live reload ${details.updated ? 'REFRESHED' : 'MISSED'}
321-
// reload in ${estimatedTimeUntilUpdate / 1000}
322-
// round trip ${(stats.loading.end - stats.loading.start) / 1000}
323-
// diff ${
324-
// (reloadInterval -
325-
// (estimatedTimeUntilUpdate +
326-
// stats.loading.end -
327-
// stats.loading.start)) /
328-
// 1000
329-
// }
330-
// reload interval ${reloadInterval / 1000}
331-
// target duration ${details.targetduration}
332-
// distance to edge ${distanceToLiveEdgeMs / 1000}`
333-
// );
334327

328+
this.clearTimer();
335329
this.timer = self.setTimeout(
336330
() => this.loadingPlaylist(levelOrTrack, deliveryDirectives),
337331
estimatedTimeUntilUpdate,
@@ -379,6 +373,7 @@ export default class BasePlaylistController
379373
} else {
380374
const delay = getRetryDelay(retryConfig, retryCount);
381375
// Schedule level/track reload
376+
this.clearTimer();
382377
this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
383378
this.warn(
384379
`Retrying playlist loading ${retryCount + 1}/${

src/controller/base-stream-controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1871,7 +1871,7 @@ export default class BaseStreamController
18711871
}
18721872
}
18731873

1874-
protected resetWhenMissingContext(chunkMeta: ChunkMetadata) {
1874+
protected resetWhenMissingContext(chunkMeta: ChunkMetadata | Fragment) {
18751875
this.warn(
18761876
`The loading context changed while buffering fragment ${chunkMeta.sn} of ${this.playlistLabel()} ${chunkMeta.level}. This chunk will not be buffered.`,
18771877
);

src/controller/interstitials-controller.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
11141114
player.media?.play();
11151115
}
11161116
} else if (scheduledItem !== null) {
1117-
this.resumePrimary(scheduledItem, index);
1117+
this.resumePrimary(scheduledItem, index, currentItem);
11181118
if (this.shouldPlay) {
11191119
this.hls.media?.play();
11201120
}
@@ -1144,12 +1144,16 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
11441144
private resumePrimary(
11451145
scheduledItem: InterstitialSchedulePrimaryItem,
11461146
index: number,
1147+
fromItem: InterstitialScheduleItem | null,
11471148
) {
11481149
this.playingItem = scheduledItem;
11491150
this.playingAsset = null;
11501151
this.waitingItem = null;
11511152

11521153
this.bufferedToItem(scheduledItem);
1154+
if (!fromItem) {
1155+
return;
1156+
}
11531157

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

@@ -1231,13 +1235,22 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
12311235
} else {
12321236
this.transferMediaTo(hls, media);
12331237
if (skipSeekToStartPosition) {
1234-
hls.startLoad(timelinePos, skipSeekToStartPosition);
1238+
this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
12351239
}
12361240
}
12371241
if (!skipSeekToStartPosition) {
12381242
// Set primary position to resume time
12391243
this.timelinePos = timelinePos;
1240-
hls.startLoad(timelinePos, skipSeekToStartPosition);
1244+
this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
1245+
}
1246+
}
1247+
1248+
private startLoadingPrimaryAt(
1249+
timelinePos: number,
1250+
skipSeekToStartPosition?: boolean,
1251+
) {
1252+
if (this.hls.loadingEnabled) {
1253+
this.hls.startLoad(timelinePos, skipSeekToStartPosition);
12411254
}
12421255
}
12431256

@@ -1688,7 +1701,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
16881701
private preloadPrimary(item: InterstitialSchedulePrimaryItem) {
16891702
const index = this.findItemIndex(item);
16901703
const timelinePos = this.getPrimaryResumption(item, index);
1691-
this.hls.startLoad(timelinePos);
1704+
this.startLoadingPrimaryAt(timelinePos);
16921705
}
16931706

16941707
private bufferedToEvent(
@@ -1869,7 +1882,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
18691882
}
18701883
const playerConfig: Partial<HlsConfig> = {
18711884
...userConfig,
1872-
// autoStartLoad: false,
1885+
autoStartLoad: true,
18731886
startFragPrefetch: true,
18741887
primarySessionId: primary.sessionId,
18751888
assetPlayerId: assetItem.identifier,

src/controller/level-controller.ts

+13-14
Original file line numberDiff line numberDiff line change
@@ -380,18 +380,6 @@ export default class LevelController extends BasePlaylistController {
380380
altAudio: !audioOnly && audioTracks.some((t) => !!t.url),
381381
};
382382
this.hls.trigger(Events.MANIFEST_PARSED, edata);
383-
384-
// Initiate loading after all controllers have received MANIFEST_PARSED
385-
const {
386-
config: { autoStartLoad, startPosition },
387-
forceStartLoad,
388-
} = this.hls;
389-
if (autoStartLoad || forceStartLoad) {
390-
this.log(
391-
`${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`,
392-
);
393-
this.hls.startLoad(startPosition);
394-
}
395383
}
396384

397385
get levels(): Level[] | null {
@@ -606,8 +594,8 @@ export default class LevelController extends BasePlaylistController {
606594
return;
607595
}
608596

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

678666
removeLevel(levelIndex: number) {
667+
if (this._levels.length === 1) {
668+
return;
669+
}
679670
const levels = this._levels.filter((level, index) => {
680671
if (index !== levelIndex) {
681672
return true;
@@ -697,6 +688,14 @@ export default class LevelController extends BasePlaylistController {
697688
if (this.currentLevelIndex > -1 && this.currentLevel?.details) {
698689
this.currentLevelIndex = this.currentLevel.details.fragments[0].level;
699690
}
691+
if (this.manualLevelIndex > -1) {
692+
this.manualLevelIndex = this.currentLevelIndex;
693+
}
694+
const maxLevel = levels.length - 1;
695+
this._firstLevel = Math.min(this._firstLevel, maxLevel);
696+
if (this._startLevel) {
697+
this._startLevel = Math.min(this._startLevel, maxLevel);
698+
}
700699
this.hls.trigger(Events.LEVELS_UPDATED, { levels });
701700
}
702701

src/controller/stream-controller.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,9 @@ export default class StreamController
10951095
) {
10961096
if (this.level > -1 && this.fragCurrent) {
10971097
this.level = this.fragCurrent.level;
1098+
if (this.level === -1) {
1099+
this.resetWhenMissingContext(this.fragCurrent);
1100+
}
10981101
}
10991102
this.levels = data.levels;
11001103
}

0 commit comments

Comments
 (0)