What version of HLS.js are you using?
v1.7.1 (also reproduced on v1.7.2-0.canary.11984 via the dev demo). Not reproducible on v1.6.19.
What browser (including version) are you using?
Chrome 139.0.7258 (also Chromium via Playwright)
What OS (including version) are you using?
macOS 26
Test stream
https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8
Configuration
{
"capLevelToPlayerSize": true
}
Additional player setup steps
The media element must have a stable layout size (explicit CSS width/height) before attachMedia. This is the common production case (fixed player, poster). The default demo page masks the bug because its video element resizes when media dimensions arrive, which happens to re-fire the ResizeObserver.
Checklist
Steps to reproduce
- Open https://hlsjs.video-dev.org/demo/ and run this in the console:
window.hls.destroy();
const video = document.querySelector('video');
video.style.width = '640px'; video.style.height = '360px'; // stable layout, like a real player
setTimeout(() => {
const h = new Hls({ capLevelToPlayerSize: true, debug: true });
window.h = h;
h.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
h.attachMedia(video);
video.muted = true;
setTimeout(() => console.log('autoLevelCapping after 4s:', h.autoLevelCapping), 4000);
}, 300);
- Wait a few seconds, then check
h.autoLevelCapping.
Expected behaviour
A 640x360 player should be capped (level 2, 848x480 on this stream), as in v1.6.x.
What actually happened?
h.autoLevelCapping stays -1 for the whole session; the 1920x1080 level plays. CapLevelController's internal autoLevelCapping is still Infinity (its constructor value), i.e. detectPlayerSize() never ran with levels loaded.
Calling it manually proves the machinery itself is fine — the trigger is what's missing:
h.capLevelController.detectPlayerSize();
h.autoLevelCapping; // 2, immediately
Cause (from reading cap-level-controller.ts, introduced by #7108):
onMediaAttaching calls observe(), creating the ResizeObserver before the manifest is loaded.
- The observer's single initial callback fires on the next rendering frame →
detectPlayerSize() runs with levels.length === 0 and does nothing.
MANIFEST_PARSED / BUFFER_CODECS → startCapping() → if (this.timer || this.observer || ...) return; — the observer already exists, so it early-returns before reaching its detectPlayerSize() call.
onLevelsUpdated can't recover either: Number.isFinite(this.autoLevelCapping) is false (still Infinity) and this.observer !== undefined.
Nothing re-triggers detection unless the element happens to resize after the manifest is parsed. In v1.6.x the setInterval in startCapping recovered within 1s regardless.
If the manifest loads within one rendering frame of attachMedia (e.g. cached), the initial observer callback lands after MANIFEST_PARSED and everything works — which makes this intermittent on fast networks and consistent on real ones.
Console output
(debug: true — no capping log for the entire session; only after the manual call:)
[log] > Setting autoLevelCapping to 2: 480p@836280 for media 640x360
[log] > set autoLevelCapping:2
What version of HLS.js are you using?
v1.7.1 (also reproduced on v1.7.2-0.canary.11984 via the dev demo). Not reproducible on v1.6.19.
What browser (including version) are you using?
Chrome 139.0.7258 (also Chromium via Playwright)
What OS (including version) are you using?
macOS 26
Test stream
https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8
Configuration
Additional player setup steps
The media element must have a stable layout size (explicit CSS width/height) before
attachMedia. This is the common production case (fixed player, poster). The default demo page masks the bug because its video element resizes when media dimensions arrive, which happens to re-fire the ResizeObserver.Checklist
Steps to reproduce
h.autoLevelCapping.Expected behaviour
A 640x360 player should be capped (level 2, 848x480 on this stream), as in v1.6.x.
What actually happened?
h.autoLevelCappingstays-1for the whole session; the 1920x1080 level plays.CapLevelController's internalautoLevelCappingis stillInfinity(its constructor value), i.e.detectPlayerSize()never ran with levels loaded.Calling it manually proves the machinery itself is fine — the trigger is what's missing:
Cause (from reading
cap-level-controller.ts, introduced by #7108):onMediaAttachingcallsobserve(), creating theResizeObserverbefore the manifest is loaded.detectPlayerSize()runs withlevels.length === 0and does nothing.MANIFEST_PARSED/BUFFER_CODECS→startCapping()→if (this.timer || this.observer || ...) return;— the observer already exists, so it early-returns before reaching itsdetectPlayerSize()call.onLevelsUpdatedcan't recover either:Number.isFinite(this.autoLevelCapping)is false (stillInfinity) andthis.observer !== undefined.Nothing re-triggers detection unless the element happens to resize after the manifest is parsed. In v1.6.x the
setIntervalinstartCappingrecovered within 1s regardless.If the manifest loads within one rendering frame of
attachMedia(e.g. cached), the initial observer callback lands afterMANIFEST_PARSEDand everything works — which makes this intermittent on fast networks and consistent on real ones.Console output