From cdd71a3f5056203085a272cc05b5f1bcbcabce16 Mon Sep 17 00:00:00 2001 From: Zaki Ahmed Date: Thu, 13 Aug 2026 23:38:08 +0200 Subject: [PATCH] Load fragment keys only after fragment selection is finalized Backports the ordering of #7874 to v1.6.x: extract key loading from the top of _doFragLoad into loadKeyFor() and call it only once fragment selection is final - after the LL-HLS part/hint swap in the parts branch, and after the loading-parts fallback in the whole-fragment path. Previously the key promise was armed with the fragment passed in, so when the parts branch advanced the load onto the fragment hint, both resolution-time context checks compared the stale pre-swap fragment against the new fragCurrent, read the mismatch as an abort, and resolved silently - leaving the stream-controller in FRAG_LOADING with no loader in flight and no reachable timeout. Video fragment loading stopped permanently on encrypted low-latency streams while the playlist kept polling. Fixes #7975 on the v1.6.x line. Co-Authored-By: Claude Fable 5 --- src/controller/base-stream-controller.ts | 49 +++++++++++++++--------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 5b5873ec5b0..cacf06068a7 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -820,27 +820,16 @@ export default class BaseStreamController frag: PartsLoadedData | FragLoadedData, ) {} - protected _doFragLoad( + private loadKeyFor( frag: Fragment, - level: Level, - targetBufferTime: number | null = null, - progressCallback?: FragmentLoadProgressCallback, - ): Promise { - this.fragCurrent = frag; - const details = level.details; - if (!this.levels || !details) { - throw new Error( - `frag load aborted, missing level${details ? '' : ' detail'}s`, - ); - } - + details: LevelDetails, + ): Promise | null { let keyLoadingPromise: Promise | null = null; if (frag.encrypted && !frag.decryptdata?.key) { this.log( `Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.playlistLabel()} ${frag.level}`, ); this.state = State.KEY_LOADING; - this.fragCurrent = frag; keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => { if (!this.fragContextChanged(keyLoadedData.frag)) { this.hls.trigger(Events.KEY_LOADED, keyLoadedData); @@ -851,10 +840,6 @@ export default class BaseStreamController } }); this.hls.trigger(Events.KEY_LOADING, { frag }); - if ((this.fragCurrent as Fragment | null) === null) { - this.log(`context changed in KEY_LOADING`); - return Promise.resolve(null); - } } else if (!frag.encrypted) { keyLoadingPromise = this.keyLoader.loadClear( frag, @@ -865,6 +850,22 @@ export default class BaseStreamController this.log(`[eme] blocking frag load until media-keys acquired`); } } + return keyLoadingPromise; + } + + protected _doFragLoad( + frag: Fragment, + level: Level, + targetBufferTime: number | null = null, + progressCallback?: FragmentLoadProgressCallback, + ): Promise { + this.fragCurrent = frag; + const details = level.details; + if (!this.levels || !details) { + throw new Error( + `frag load aborted, missing level${details ? '' : ' detail'}s`, + ); + } const fragPrevious = this.fragPrevious; if ( @@ -892,6 +893,10 @@ export default class BaseStreamController if (partIndex > -1) { const part = partList[partIndex]; frag = this.fragCurrent = part.fragment; + const keyLoadingPromise = this.loadKeyFor(frag, details); + if (this.fragContextChanged(frag)) { + return Promise.resolve(null); + } this.log( `Loading ${frag.type} sn: ${frag.sn} part: ${part.index} (${partIndex}/${partList.length - 1}) of ${this.fragInfo(frag, false, part)}) cc: ${ frag.cc @@ -962,6 +967,14 @@ export default class BaseStreamController return Promise.resolve(null); } + const keyLoadingPromise = this.loadKeyFor(frag, details); + if (this.fragContextChanged(frag)) { + this.log( + `Context changed in KEY_LOADING sn: ${frag.sn} ${frag.relurl} > ${this.fragCurrent?.relurl}`, + ); + keyLoadingPromise?.catch(() => null); + return Promise.resolve(null); + } this.log( `Loading ${frag.type} sn: ${frag.sn} of ${this.fragInfo(frag, false)}) cc: ${frag.cc} ${ '[' + details.startSN + '-' + details.endSN + ']'