Skip to content

feat: Configurable min-bandwidth-sample-duration-ms for hls.js EWMA estimator#1321

Open
luwes wants to merge 1 commit intomuxinc:mainfrom
luwes:feat/min-bandwidth-sample-duration
Open

feat: Configurable min-bandwidth-sample-duration-ms for hls.js EWMA estimator#1321
luwes wants to merge 1 commit intomuxinc:mainfrom
luwes:feat/min-bandwidth-sample-duration

Conversation

@luwes
Copy link
Copy Markdown
Contributor

@luwes luwes commented Apr 24, 2026

Related #1315
Test URL: https://elements-demo-vanilla-git-fork-luwes-feat-min-bandwi-4a56fa-mux.vercel.app/min-bandwidth-sample-duration-ms-test.html

Fixes an issue where very small segments at video startup are artificially lowering bandwidth calcs and engines choose lower qualities than is suitable.

Summary

Exposes the hls.js EWMA bandwidth estimator's minDelayMs_ floor as a
configurable attribute/prop on mux-video, mux-audio, and
mux-player (plus the React and Astro wrappers), so callers can tune
how short per-segment transfers are allowed to contribute to the
bandwidth estimate.

  • Attribute: min-bandwidth-sample-duration-ms
  • Property: minBandwidthSampleDurationMs
  • Default: 50 (restores the hls.js / Shaka historical value —
    previously SaneAbrController hard-coded it to 5)

Why

EwmaBandWidthEstimator#sample() clamps each fragment's download
duration up to minDelayMs_ before computing
bandwidth = 8000 × bytes / durationMs. The clamp defends against:

  1. HTTP cache hits producing sub-millisecond "downloads" that
    otherwise poison the fast EWMA with multi-Gbps samples.
  2. performance.now() resolution (historically ~1 ms, post-Spectre
    clamped to 100 µs – 2 ms) producing duration = 0Infinity.
  3. Statistical noise on very short transfers where OS scheduling / TCP
    ACK timing dominate over actual network capacity.

hls.js inherited 50 from Shaka in 2016. The assumptions behind that
number have weakened (4K/HDR ladders with small fragments on fast
connections routinely finish sub-50 ms), but some properties still
benefit from a conservative floor. Rather than pick a single magic
number globally, this PR exposes the knob so each integration can
tune it — with 0 available as an opt-in for "disable the clamp
entirely."

What's plumbed

  • playback-core / sane-abr-controller.ts:
    • MIN_DELAY_MS = 5createSaneAbrController(minDelayMs) factory.
    • DEFAULT_MIN_BANDWIDTH_SAMPLE_DURATION_MS = 50 exported.
    • SaneAbrController default export preserved (uses the default).
  • playback-core/index.ts: setupHls picks
    createSaneAbrController(minBandwidthSampleDurationMs) when the
    prop is a number, otherwise the default class.
  • playback-core/types.ts: minBandwidthSampleDurationMs?: number on
    MuxMediaPropTypes.
  • mux-video / mux-audio: new BANDWIDTH… Attributes entry +
    matching getter/setter (parses to number, undefined when unset).
  • mux-player: same getter/setter, forwards attribute to the inner
    mux-video via template.ts, includes in getProps(el) state.
  • React wrappers (mux-video-react, mux-audio-react): added to
    propTypes. mux-player-react / mux-player-astro types gain the
    optional prop. The existing camelCase → kebab-case conversion
    handles attribute emission automatically.

Docs

  • packages/mux-player/REFERENCE.md — entries in both the attribute
    and property tables.
  • packages/mux-player-react/REFERENCE.md
  • packages/mux-player-astro/REFERENCE.md
  • packages/mux-video/README.md

Tests

  • packages/playback-core/test/index.test.js — unit tests for
    createSaneAbrController (default, custom, reset re-pokes
    estimator) plus end-to-end assertion that initialize({ … })
    propagates into hls.abrController.bwEstimator.minDelayMs_.
  • packages/mux-video/test/index.test.js — attribute/prop reflection
    • setter semantics (including 0) + end-to-end propagation.
  • packages/mux-audio/test/player.test.js — attribute/prop reflection
    • setter semantics.
  • packages/mux-player/test/player.test.js — attribute/prop
    reflection + forwarding to the inner <mux-video>.

Interactive example page

New examples/vanilla-ts-esm/public/min-bandwidth-sample-duration-ms-test.html:

  • Uses preload="none" to keep Play an explicit user action.
  • Two-column layout with a draggable splitter (width persisted in
    localStorage, keyboard-resizable via /).
  • Live dashboard: configured floor, current EWMA estimate, first/
    current rendition, segment count, count of clamped samples.
  • Per-segment table showing real vs. effective (clamped) ms with
    orange highlighting when the clamp kicked in.
  • Available-renditions strip that highlights:
    • blue = active
    • green outline = first-segment pick
    • red = lowest rendition ever played
    • strikethrough + hatched = excluded by CapLevelController
      (i.e. above hls.autoLevelCapping)
  • Checkbox to disable CapLevelController by setting
    capRenditionToPlayerSize = false (which is only settable via the
    JS property, not the attribute — the test page sets it
    synchronously after appendChild so it's in place before the
    microtask-deferred load()).

Backwards compatibility

  • When unset, minBandwidthSampleDurationMs defaults to 50 — same
    as upstream hls.js. Existing consumers see no behavior change.
  • The original SaneAbrController default export is preserved for any
    direct importers (wired to the default).

Test plan

  • npm test passes for playback-core, mux-video, mux-audio,
    mux-player.
  • Manual: open the test page, hit Start/Play, confirm
    renditions strip populates, dashboard updates, and the
    minDelayMs_ dashboard cell reflects the input.
  • Manual: set field to 5 and verify the configured floor
    dashboard cell shows 5 ms and some segments get flagged as
    clamped when the player is on a fast network.
  • Manual: toggle Disable CapLevelController and confirm the
    strikethrough/hatched badges all disappear.
  • Manual: drag the column splitter and verify the player cap
    indicator updates live (cap recomputes on size change).

Made with Cursor


Note

Medium Risk
Touches playback/ABR initialization and reaches into hls.js internals (bwEstimator.minDelayMs_), which can affect initial bitrate selection and buffering behavior across all MSE HLS playback paths.

Overview
Adds a new min-bandwidth-sample-duration-ms attribute / minBandwidthSampleDurationMs prop across mux-video, mux-audio, and mux-player (plus React and Astro wrappers) to let callers configure the hls.js EWMA bandwidth estimator’s minimum sample-duration floor.

In playback-core, introduces createSaneAbrController(minDelayMs) and wires setupHls() to use it when the prop is provided (otherwise defaulting to the upstream 50ms behavior), with new unit/integration tests covering attribute reflection/forwarding and estimator configuration.

Updates docs to describe the new knob and adds an interactive example page (min-bandwidth-sample-duration-ms-test.html) to visualize how the floor impacts bandwidth estimates and rendition selection.

Reviewed by Cursor Bugbot for commit 8927147. Bugbot is set up for automated code reviews on this repo. Configure here.

…igurable attribute

Adds `min-bandwidth-sample-duration-ms` attribute and
`minBandwidthSampleDurationMs` property on mux-video, mux-audio, and
mux-player (plus the React and Astro wrappers) so callers can tune how
short per-segment transfers are allowed to contribute to hls.js's EWMA
bandwidth estimate.

Previously SaneAbrController hard-coded the estimator's `minDelayMs_`
floor to 5 ms. This PR replaces the constant with
`createSaneAbrController(minDelayMs)` and restores the hls.js/Shaka
historical default of 50 ms as the out-of-box value (preserving
upstream behavior for everyone). Callers that want more aggressive
short-transfer sampling can opt in with e.g.
`min-bandwidth-sample-duration-ms="5"` or disable the clamp entirely
with `"0"`.

Also includes an interactive test page for observing and tuning the
behavior at runtime.

Made-with: Cursor
@luwes luwes requested a review from a team as a code owner April 24, 2026 03:23
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

@luwes is attempting to deploy a commit to the Mux Team on Vercel.

A member of the Team first needs to authorize it.

@snyk-io
Copy link
Copy Markdown

snyk-io Bot commented Apr 24, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
elements-demo-astro Ready Ready Preview, Comment Apr 24, 2026 3:26am
elements-demo-vanilla Ready Ready Preview, Comment Apr 24, 2026 3:26am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant