What version of Hls.js are you using?
v1.6.13 (also reproduced on latest main branch with PR #7749 applied)
What browser (including version) are you using?
Chrome 146.0.7680.178 (Official Build) (arm64)
Important: This issue started appearing after Chrome 146.0.7680.178 update. Chrome 146.0.7680.167 did not exhibit this behavior. We suspect stricter SourceBuffer quota enforcement was introduced in that Chrome update (possibly related to CVE-2026-5274 "Integer Overflow in Codecs" or CVE-2026-5272 "Heap Buffer Overflow in GPU", both security-patched and non-public).
What OS (including version) are you using?
macOS (Apple Silicon), Windows 10/11
Test stream
Live fMP4/CMAF HLS stream (~2s segments). Not publicly shareable, but reproducible with any live stream configuration described below.
Configuration
{
"autoStartLoad": true,
"startFragPrefetch": true,
"lowLatencyMode": true,
"manifestLoadingMaxRetry": 3,
"levelLoadingMaxRetry": 1,
"fragLoadingMaxRetry": 3,
"maxMaxBufferLength": 60
}
liveMaxLatencyDuration and liveMaxLatencyDurationCount are both undefined (defaults).
Steps to reproduce
- Start playing a live HLS stream (fMP4/CMAF, ~2s segments)
- Immediately pause the player
- Wait ~3–6 minutes (while paused, hls.js continues buffering forward)
- Observe in the Network tab: the same segment is requested 20+ times in rapid succession
- After exhausting retries on one segment, the player moves to the next segment and repeats the same loop
Reproduction rate: ~80% (4 out of 5 attempts across multiple machines)
Environment-specific results:
| Tester |
Chrome Version |
Reproduced? |
Notes |
| Tester A |
146.0.7680.178 |
✅ |
Mac/Windows both |
| Tester B |
146.0.7680.177 |
✅ |
Reproduced |
| Tester C |
146.0.7680.167 → .178 |
❌ → ✅ |
Only after update |
| Tester D |
146.0.7680.167 → .178 |
❌ → ✅ |
Same segment repeated 10–20x |
Expected behaviour
When QuotaExceededError occurs and back buffer eviction is not possible (e.g., all buffered data is ahead of currentTime because the player is paused near the start), hls.js should:
- Stop attempting to append/re-request the same segment indefinitely
- Either pause buffering or skip the failing segment gracefully
- Resume normal buffering when the user seeks or resumes playback
What actually happened?
hls.js enters an infinite loop re-requesting and re-appending the same segment. The loop occurs in reduceLengthAndFlushBuffer() in base-stream-controller.ts:
QuotaExceededError
→ buffer-controller triggers BUFFER_FULL_ERROR (BUFFER_APPENDED never fires)
→ stream-controller.onError → reduceLengthAndFlushBuffer()
→ reduceMaxBufferLength() → already at minimum 2s, no effect
→ fragmentTracker.removeFragment(frag) → marks fragment as NOT_LOADED
→ nextLoadPosition = frag.start ← ROOT CAUSE: resets to same position
→ resetLoadingState() → state = IDLE
→ next tick: doTickIdle() → finds NOT_LOADED fragment at frag.start → loadFragment()
→ QuotaExceededError again → infinite loop
Root cause code (base-stream-controller.ts ~L2014):
if (frag) {
this.fragmentTracker.removeFragment(frag); // fragment → NOT_LOADED
this.nextLoadPosition = frag.start; // ← resets to same position
}
this.resetLoadingState(); // state = IDLE → doTickIdle re-runs
Why this happens only when paused:
- While paused,
media.currentTime is fixed near the start
- Forward buffer accumulates continuously (no playback consumption)
- After ~190s of buffering, SourceBuffer quota is exceeded
- During playback, back buffer is naturally evicted so the quota is never reached
Console output
Full log timeline (11,767 lines total)
Normal buffering (10:19:29 – 10:22:11): Segments sn:573–667 loaded and appended successfully while paused. Buffer grows from 0s to ~190s.
Last successful append before loop:
10:22:11.492 Buffered main sn: 667 (buffer:[0.000-2.513][38.013-189.995])
First QuotaExceededError (10:22:11.575):
10:22:11.493 Loading main sn: 668 of level 4 (frag:[189.995-191.995])
10:22:11.575 [buffer-controller]: queuing "audiovideo" append sn: 668
10:22:11.575 Reduce max buffer length to 30s ← first QuotaExceededError
Stack trace:
i.reduceMaxBufferLength
i.reduceLengthAndFlushBuffer
n.onError
o.trigger (BUFFER_FULL_ERROR)
onError
i.executeNext
i.append
s.onBufferAppending ← SourceBuffer.appendBuffer() fails
Rapid reduction to minimum (10:22:37):
10:22:37.210 Reduce max buffer length to 15s (sn:668)
10:22:37.309 Reduce max buffer length to 7.5s (sn:668)
10:22:37.408 Reduce max buffer length to 3.75s (sn:668)
10:22:37.503 Reduce max buffer length to 2s (sn:668) ← minimum reached
Infinite loop at minimum (10:22:37 – 10:22:49+):
10:22:37.602 Reduce max buffer length to 2s (sn:668)
10:22:37.701 Reduce max buffer length to 2s (sn:668)
10:22:37.801 Reduce max buffer length to 2s (sn:668)
... repeats every ~100ms ...
10:22:43.100 Reduce max buffer length to 2s (sn:668 → moves to sn:669, 670, ...)
10:22:49.803 Reduce max buffer length to 2s (sn:673) ← still looping 12+ seconds later
Total: 117 "Reduce max buffer length to 2s" warnings in ~12 seconds.
Each iteration: Load segment → Parse → Append → QuotaExceededError → Remove fragment → Reset to same position → Reload same segment.
PR #7749 does not fix this case
We applied PR #7749 (cherry-picked commit dddaed8db) which adds back buffer eviction on QuotaExceededError. However, in this scenario:
media.currentTime is near the beginning (player was paused immediately after start)
- ALL buffered data is forward buffer (ahead of
currentTime)
- The eviction logic (
frag.end <= media.currentTime) finds no candidates
- Falls back to the existing
BUFFER_FULL_ERROR path → infinite loop continues
16:04:41.518 [buffer-controller]: QuotaExceededError on "audiovideo" sn: 3337 — no back buffer available to evict
16:04:41.518 [stream-controller]: Reduce max buffer length to 2s
... (sn:3337 repeats, then sn:3338, same pattern)
Workaround applied (circuit breaker)
As a temporary workaround, we added a circuit breaker in BaseStreamController that tracks consecutive BUFFER_FULL_ERROR failures per segment and stops buffering after 3 failures on the same segment:
base-stream-controller.ts — circuit breaker in reduceLengthAndFlushBuffer():
// Properties added to BaseStreamController
protected _bufferFullErrorSn: number = -1;
protected _bufferFullErrorCount: number = 0;
private static readonly BUFFER_FULL_ERROR_MAX_RETRY = 3;
// Inside reduceLengthAndFlushBuffer(), before existing logic:
if (data.details === ErrorDetails.BUFFER_FULL_ERROR && frag) {
const fragSn = frag.sn as number;
if (fragSn === this._bufferFullErrorSn) {
this._bufferFullErrorCount++;
} else {
this._bufferFullErrorSn = fragSn;
this._bufferFullErrorCount = 1;
}
if (this._bufferFullErrorCount >= BaseStreamController.BUFFER_FULL_ERROR_MAX_RETRY) {
this.warn(
`BUFFER_FULL_ERROR circuit breaker activated: sn ${fragSn} failed ` +
`${this._bufferFullErrorCount} times, stopping buffer loading. ` +
`loadPos: ${loadPos.toFixed(3)}, currentTime: ${this.media?.currentTime?.toFixed(3)}`
);
this.pauseBuffering();
this.fragmentTracker.removeFragment(frag);
this.resetLoadingState();
return false;
}
}
stream-controller.ts — recovery on startLoad() and doTickIdle():
// In startLoad(): reset circuit breaker state
this.resetBufferFullErrorState();
// In doTickIdle(): auto-recover when playback resumes
if (!this.buffering) {
if (media && !media.paused && this._bufferFullErrorCount > 0) {
this.resetBufferFullErrorState();
}
if (!this.buffering) {
return;
}
}
// Reset method:
protected resetBufferFullErrorState() {
this._bufferFullErrorSn = -1;
this._bufferFullErrorCount = 0;
if (!this.buffering) this.resumeBuffering();
}
Result with circuit breaker:
10:26:18.601 BUFFER_FULL_ERROR circuit breaker activated: sn 1629 failed 4 times,
stopping buffer loading. loadPos: 2.807, currentTime: 2.807
... (buffering stops — no more infinite loop, playlist reload continues normally)
--- User seeks → automatic recovery ---
10:26:44.691 Resetting BUFFER_FULL_ERROR circuit breaker (was sn: 2377, count: 4)
10:26:44.693 Loading main sn: 2391 of level 4
10:26:44.834 Buffered main sn: 2391 of level 4 ← normal append succeeds
This workaround complements PR #7749 — #7749 handles cases where back buffer exists, while the circuit breaker handles cases where it doesn't. The two modify different files and can be applied together.
Suggested fix
The root cause is that reduceLengthAndFlushBuffer() unconditionally resets nextLoadPosition = frag.start after removing the fragment, creating a reload loop when the append consistently fails.
Possible approaches:
- Circuit breaker (as described above): track consecutive failures per segment, pause buffering after N failures
- Advance past failing fragment: instead of
nextLoadPosition = frag.start, advance to frag.start + frag.duration when reduceMaxBufferLength() is already at minimum
- Check if reduction had effect: if
reduceMaxBufferLength() was already at minimum and couldn't reduce further, don't reset nextLoadPosition to the same fragment
Related issues / PRs
What version of Hls.js are you using?
v1.6.13 (also reproduced on latest main branch with PR #7749 applied)
What browser (including version) are you using?
Chrome 146.0.7680.178 (Official Build) (arm64)
Important: This issue started appearing after Chrome 146.0.7680.178 update. Chrome 146.0.7680.167 did not exhibit this behavior. We suspect stricter SourceBuffer quota enforcement was introduced in that Chrome update (possibly related to CVE-2026-5274 "Integer Overflow in Codecs" or CVE-2026-5272 "Heap Buffer Overflow in GPU", both security-patched and non-public).
What OS (including version) are you using?
macOS (Apple Silicon), Windows 10/11
Test stream
Live fMP4/CMAF HLS stream (~2s segments). Not publicly shareable, but reproducible with any live stream configuration described below.
Configuration
{ "autoStartLoad": true, "startFragPrefetch": true, "lowLatencyMode": true, "manifestLoadingMaxRetry": 3, "levelLoadingMaxRetry": 1, "fragLoadingMaxRetry": 3, "maxMaxBufferLength": 60 }liveMaxLatencyDurationandliveMaxLatencyDurationCountare bothundefined(defaults).Steps to reproduce
Reproduction rate: ~80% (4 out of 5 attempts across multiple machines)
Environment-specific results:
Expected behaviour
When
QuotaExceededErroroccurs and back buffer eviction is not possible (e.g., all buffered data is ahead ofcurrentTimebecause the player is paused near the start), hls.js should:What actually happened?
hls.js enters an infinite loop re-requesting and re-appending the same segment. The loop occurs in
reduceLengthAndFlushBuffer()inbase-stream-controller.ts:Root cause code (
base-stream-controller.ts~L2014):Why this happens only when paused:
media.currentTimeis fixed near the startConsole output
Full log timeline (11,767 lines total)
Normal buffering (10:19:29 – 10:22:11): Segments sn:573–667 loaded and appended successfully while paused. Buffer grows from 0s to ~190s.
Last successful append before loop:
First QuotaExceededError (10:22:11.575):
Rapid reduction to minimum (10:22:37):
Infinite loop at minimum (10:22:37 – 10:22:49+):
Total: 117 "Reduce max buffer length to 2s" warnings in ~12 seconds.
Each iteration: Load segment → Parse → Append → QuotaExceededError → Remove fragment → Reset to same position → Reload same segment.
PR #7749 does not fix this case
We applied PR #7749 (cherry-picked commit
dddaed8db) which adds back buffer eviction onQuotaExceededError. However, in this scenario:media.currentTimeis near the beginning (player was paused immediately after start)currentTime)frag.end <= media.currentTime) finds no candidatesBUFFER_FULL_ERRORpath → infinite loop continuesWorkaround applied (circuit breaker)
As a temporary workaround, we added a circuit breaker in
BaseStreamControllerthat tracks consecutiveBUFFER_FULL_ERRORfailures per segment and stops buffering after 3 failures on the same segment:base-stream-controller.ts— circuit breaker inreduceLengthAndFlushBuffer():stream-controller.ts— recovery onstartLoad()anddoTickIdle():Result with circuit breaker:
This workaround complements PR #7749 — #7749 handles cases where back buffer exists, while the circuit breaker handles cases where it doesn't. The two modify different files and can be applied together.
Suggested fix
The root cause is that
reduceLengthAndFlushBuffer()unconditionally resetsnextLoadPosition = frag.startafter removing the fragment, creating a reload loop when the append consistently fails.Possible approaches:
nextLoadPosition = frag.start, advance tofrag.start + frag.durationwhenreduceMaxBufferLength()is already at minimumreduceMaxBufferLength()was already at minimum and couldn't reduce further, don't resetnextLoadPositionto the same fragmentRelated issues / PRs