The startup capacity probe stops black-holing constrained links #379

Merged
enricobuehler merged 1 commits from worktree-abr-probe-target-from-stream-cap into main 2026-08-23 07:38:52 +00:00
Owner

Re-implements the Rust half of #371 (see that PR for the Fire TV field report — the diagnosis there is right, this changes where the fix lands). #371 should not be merged.

Two independent defects behind the same symptom: a black screen at stream start on a constrained link, reported on a Fire TV Stick 4K Max and, earlier, on webOS.

1. The 2 Gbps probe target is more than any session can use

The probe bursts at capacity_probe_kbps (2 Gbps) on the reasoning that the target must be "deliberately far above any plausible link so the burst measures the link and not itself".

That reasoning is obsolete. pump.rs already computes stream_cap_kbps = abr::stream_ceiling_kbps(w, h, hz, codec, depth, chroma) and hands it to abr.set_stream_cap() immediately above the probe setup, and set_ceiling clamps every learned ceiling to it. The measured ceiling is delivered × 0.7. So every bit the burst measures above stream_cap_kbps / 0.7 is discarded the moment it lands — the height was paying bufferbloat for a number nothing reads.

Now derived: stream_cap_kbps × 2, capped at the old 2 Gbps.

  • ×2 is the smallest multiplier that can still prove the cap. Reaching stream_cap_kbps needs delivered ≥ cap × 1.43; the rest is margin.
  • It cannot cap anyone. A session whose mode and codec justify a high ceiling asks for a correspondingly high target on its own. 1440p120 HEVC → a 664 Mbps burst, not 2 Gbps. 4K120 10-bit → 2 Gbps, as before.
  • A mode we can't size still gets 2 Gbps. stream_ceiling_kbps returns u32::MAX for a degenerate mode; saturating_mul(2).min(2_000_000) lands on the historic default.
  • Deliberately not a cfg!(target_os = "android") special case. The constraint belongs to the session, not to the platform, and this fixes webOS and every other constrained client at the same time — with a smaller diff and no risk of capping the wired Shield / Wi-Fi 6E clients that do have the capacity.

PUNKTFUNK_ABR_PROBE_KBPS and its > 0 filter are unchanged; it still overrides the target outright.

2. A keyframe lost inside the burst was never re-requested

If the burst takes the first IDR down with it, nothing re-anchors the decoder and the client sits on black until an unrelated recovery path happens to fire.

CtrlRequest::Keyframe at probe end, matching the two existing emitters in the file and funnelling through the control task's coalescing choke point — so it cannot IDR-storm (which matters given #377).

On the guard, deliberately widened. #371 used st.frames_completed == 0 || st.media_bytes_received == 0; both are cumulative session counters, so that only ever means "no frame has ever arrived". This snapshots frames_completed at the burst's leading edge and compares against that instead. At startup the two are the same test, but this one also catches a burst that kills an already-running stream — an embedder request_probe "Test connection" mid-session — which the cumulative counter structurally could not. (media_bytes_received == 0 was redundant: it is implied by frames_completed == 0.) The block runs on every probe end including a successful one; the frame-count guard is what makes it a no-op the rest of the time, and the comment now says that rather than the old "if the startup probe fails to complete".

Tests

the_probe_target_proves_the_stream_cap_without_overshooting_it — for four real modes (720p60 HEVC → 4K120 10-bit HEVC), the derived target must both prove the cap (target × 0.7 ≥ cap) and not overshoot it (target ≤ cap × 2), plus the u32::MAX and above-2-Gbps saturation edges. The clamp itself is already pinned by abr::tests::the_stream_bound_clamps_a_learned_ceiling_only.

cargo test -p punktfunk-core --features quic --lib → 481 passed, 0 failed. Clippy and rustfmt clean.

Still owed: on-glass numbers

Neither leg of the field verification is done — no Fire TV Stick 4K Max here. Before merge:

  • a Fire TV Stick 4K Max on Wi-Fi reaches first video with no 6 s probe timeout and no send_dropped spike;
  • a high-mode session on a fast link still climbs to the same ceiling it reaches on main today (both numbers recorded here).

User-facing fact changed? Yes — PUNKTFUNK_ABR_PROBE_KBPS's default is no longer a flat 2 Gbps. docs-site/content/docs/configuration.md is updated in this PR.

Re-implements the Rust half of #371 (see that PR for the Fire TV field report — the diagnosis there is right, this changes where the fix lands). #371 should not be merged. Two independent defects behind the same symptom: **a black screen at stream start on a constrained link**, reported on a Fire TV Stick 4K Max and, earlier, on webOS. ## 1. The 2 Gbps probe target is more than any session can use The probe bursts at `capacity_probe_kbps` (2 Gbps) on the reasoning that the target must be *"deliberately far above any plausible link so the burst measures the link and not itself"*. That reasoning is obsolete. `pump.rs` already computes `stream_cap_kbps = abr::stream_ceiling_kbps(w, h, hz, codec, depth, chroma)` and hands it to `abr.set_stream_cap()` immediately above the probe setup, and `set_ceiling` clamps every learned ceiling to it. The measured ceiling is `delivered × 0.7`. **So every bit the burst measures above `stream_cap_kbps / 0.7` is discarded the moment it lands** — the height was paying bufferbloat for a number nothing reads. Now derived: `stream_cap_kbps × 2`, capped at the old 2 Gbps. * **×2 is the smallest multiplier that can still prove the cap.** Reaching `stream_cap_kbps` needs `delivered ≥ cap × 1.43`; the rest is margin. * **It cannot cap anyone.** A session whose mode and codec justify a high ceiling asks for a correspondingly high target on its own. 1440p120 HEVC → a 664 Mbps burst, not 2 Gbps. 4K120 10-bit → 2 Gbps, as before. * **A mode we can't size still gets 2 Gbps.** `stream_ceiling_kbps` returns `u32::MAX` for a degenerate mode; `saturating_mul(2).min(2_000_000)` lands on the historic default. * Deliberately **not** a `cfg!(target_os = "android")` special case. The constraint belongs to the session, not to the platform, and this fixes webOS and every other constrained client at the same time — with a smaller diff and no risk of capping the wired Shield / Wi-Fi 6E clients that do have the capacity. `PUNKTFUNK_ABR_PROBE_KBPS` and its `> 0` filter are unchanged; it still overrides the target outright. ## 2. A keyframe lost inside the burst was never re-requested If the burst takes the first IDR down with it, nothing re-anchors the decoder and the client sits on black until an unrelated recovery path happens to fire. `CtrlRequest::Keyframe` at probe end, matching the two existing emitters in the file and funnelling through the control task's coalescing choke point — so it cannot IDR-storm (which matters given #377). **On the guard, deliberately widened.** #371 used `st.frames_completed == 0 || st.media_bytes_received == 0`; both are *cumulative session* counters, so that only ever means "no frame has ever arrived". This snapshots `frames_completed` at the burst's leading edge and compares against that instead. At startup the two are the same test, but this one also catches a burst that kills an **already-running** stream — an embedder `request_probe` "Test connection" mid-session — which the cumulative counter structurally could not. (`media_bytes_received == 0` was redundant: it is implied by `frames_completed == 0`.) The block runs on every probe end including a successful one; the frame-count guard is what makes it a no-op the rest of the time, and the comment now says that rather than the old "if the startup probe fails to complete". ## Tests `the_probe_target_proves_the_stream_cap_without_overshooting_it` — for four real modes (720p60 HEVC → 4K120 10-bit HEVC), the derived target must both **prove** the cap (`target × 0.7 ≥ cap`) and **not overshoot** it (`target ≤ cap × 2`), plus the `u32::MAX` and above-2-Gbps saturation edges. The clamp itself is already pinned by `abr::tests::the_stream_bound_clamps_a_learned_ceiling_only`. `cargo test -p punktfunk-core --features quic --lib` → 481 passed, 0 failed. Clippy and rustfmt clean. ## Still owed: on-glass numbers Neither leg of the field verification is done — no Fire TV Stick 4K Max here. Before merge: * a Fire TV Stick 4K Max on Wi-Fi reaches first video with no 6 s probe timeout and no `send_dropped` spike; * a high-mode session on a fast link still climbs to the same ceiling it reaches on `main` today (both numbers recorded here). **User-facing fact changed?** Yes — `PUNKTFUNK_ABR_PROBE_KBPS`'s default is no longer a flat 2 Gbps. `docs-site/content/docs/configuration.md` is updated in this PR.
enricobuehler added 1 commit 2026-08-22 22:46:31 +00:00
fix(client): size the capacity probe from the session, and re-anchor if the burst eats the video
apple / swift (pull_request) Successful in 2m10s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 7m31s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 8m20s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 4m21s
ci / bun-nix (pull_request) Successful in 24s
ci / docs-drift (pull_request) Successful in 26s
ci / docs-site (pull_request) Successful in 1m17s
ci / web (pull_request) Successful in 1m23s
ci / rust-arm64 (pull_request) Successful in 1m24s
ci / rust (pull_request) Successful in 5m47s
11abff5343
The startup link-capacity probe burst at a flat 2 Gbps on the reasoning that it
must measure the link and not itself. That reasoning is obsolete: the ABR
already clamps the measured ceiling to `stream_cap_kbps` (what this session's
mode + codec could plausibly use), so every bit measured above `cap / 0.7` is
discarded the moment it lands. The height bought a number nothing reads, and
paid bufferbloat for it — a constrained Wi-Fi link can black-hole under it
(measured on webOS: a 6 s probe timeout delaying first video to 14 s, and a
"successful" probe still reporting send_dropped=20211; the same shape is now
reported on a Fire TV Stick 4K Max).

Derive the target instead: `stream_cap_kbps × 2`, capped at the old 2 Gbps.
×2 is the smallest multiplier that can still prove the cap (the ceiling is
`delivered × 0.7`, so proving it needs `delivered ≥ cap × 1.43`), so this can
never cap anyone — a session whose mode justifies a high ceiling asks for a
high target by itself, and a mode `stream_ceiling_kbps` declines to size still
gets 2 Gbps. Deliberately not a platform `cfg!`: the constraint is the
session's, not Android's, and webOS has the same bug.

Second half of the black screen: if the burst takes the first keyframe down
with it, nothing re-requests one and the client sits on black until an
unrelated recovery path happens to fire. Ask for a keyframe at probe end when
no frame completed across the burst — compared against the count snapshotted
at the burst's leading edge rather than against 0, so it also covers a
mid-session embedder speed test that kills a running stream. One request per
probe, through the control task's coalescer, so it cannot IDR-storm.

`PUNKTFUNK_ABR_PROBE_KBPS` and its `> 0` filter are unchanged.
enricobuehler merged commit 3b5c95959b into main 2026-08-23 07:38:52 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#379