feat(core): Automatic bitrate scales to measured link capacity — probe ceiling + slow start
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 52s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 45s
apple / swift (push) Successful in 4m34s
ci / bench (push) Successful in 6m1s
flatpak / build-publish (push) Failing after 8m11s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 12m30s
windows-host / package (push) Successful in 14m50s
arch / build-publish (push) Successful in 15m1s
android / android (push) Successful in 15m47s
ci / rust (push) Successful in 17m55s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m17s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m4s
release / apple (push) Successful in 25m7s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m4s
apple / screenshots (push) Successful in 18m44s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 52s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 45s
apple / swift (push) Successful in 4m34s
ci / bench (push) Successful in 6m1s
flatpak / build-publish (push) Failing after 8m11s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 12m30s
windows-host / package (push) Successful in 14m50s
arch / build-publish (push) Successful in 15m1s
android / android (push) Successful in 15m47s
ci / rust (push) Successful in 17m55s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m17s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m4s
release / apple (push) Successful in 25m7s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m4s
apple / screenshots (push) Successful in 18m44s
The ABR ceiling was the negotiated start rate, so an 'Automatic' session was permanently boxed at the 20 Mbps default no matter the link — the most user-visible cap left after the transport work lifted the client receive ceiling to ~4.8 Gbps wire. - Startup link-capacity probe: ~2 s into an Automatic session the pump fires one speed-test burst (2 Gbps target, 800 ms) over the existing ProbeRequest machinery; delivered wire throughput x0.7 (FEC + variance headroom) becomes the controller's climb ceiling via set_ceiling(). Old hosts decline (all-zero reply) or never answer (a 6 s timeout clears the stuck probe state so LossReports resume) — the ceiling then stays negotiated, exactly the old behavior. PUNKTFUNK_ABR_PROBE=0 opts out. - Slow start: until the first congestion signal, every cooled clean window DOUBLES the rate toward the ceiling (20 Mbps -> 640 Mbps in ~10 s) instead of +6% per ~10 s (which would have taken ~10 minutes). Any congestion signal ends it for good; classic AIMD takes over. - Faster, severity-aware AIMD: a SEVERE window (unrecoverable frame, jump-to-live flush, or >=6% loss) backs off x0.7 immediately instead of waiting two windows; ordinary congestion (2-6% loss, OWD rise) keeps the two-window fuse. Additive climbs need 6 clean windows (~4.5 s, was ~10 s); the change cooldown drops 3 s -> 1.5 s. - PUNKTFUNK_VBV_FRAMES now also scales the direct-NVENC VBV (Windows + Linux, previously hardwired to 1 frame) — parity with AMF/VAAPI/QSV. Each accepted step still costs an encoder rebuild + IDR on the host; in-place rate reconfigure (NvEncReconfigureEncoder / AMF dynamic properties / Vulkan per-frame RC) is the planned follow-up that makes stepping free. Controller tests rewritten to the new policy (severity classes, slow-start climb, ceiling semantics; 144 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1952,6 +1952,22 @@ async fn worker_main(args: WorkerArgs) {
|
||||
} else {
|
||||
0
|
||||
});
|
||||
// Startup link-capacity probe (Automatic sessions): the controller's ceiling is the
|
||||
// negotiated start rate — the conservative 20 Mbps default, historically a box Automatic
|
||||
// could NEVER climb out of. One speed-test burst shortly after the stream settles
|
||||
// measures what the link actually delivers; ×0.7 (headroom for FEC overhead + variance)
|
||||
// becomes the climb ceiling and slow start does the rest. Old hosts decline (all-zero
|
||||
// reply) or never answer (timeout clears the state so LossReports resume) — either way
|
||||
// the ceiling stays negotiated, exactly the old behavior. PUNKTFUNK_ABR_PROBE=0 opts out.
|
||||
const CAPACITY_PROBE_KBPS: u32 = 2_000_000;
|
||||
const CAPACITY_PROBE_MS: u32 = 800;
|
||||
const CAPACITY_PROBE_DELAY: Duration = Duration::from_secs(2);
|
||||
const CAPACITY_PROBE_TIMEOUT: Duration = Duration::from_secs(6);
|
||||
let mut capacity_probe_at: Option<Instant> = (bitrate_kbps == 0
|
||||
&& resolved_bitrate_kbps > 0
|
||||
&& std::env::var("PUNKTFUNK_ABR_PROBE").map_or(true, |v| v != "0"))
|
||||
.then(|| Instant::now() + CAPACITY_PROBE_DELAY);
|
||||
let mut capacity_probe_deadline: Option<Instant> = None;
|
||||
let (mut owd_sum_ns, mut owd_frames) = (0i128, 0u32);
|
||||
let mut flush_in_window = false;
|
||||
// Jump-to-live state (see the guard in the loop below): the clock-based over-bound run
|
||||
@@ -2006,6 +2022,65 @@ async fn worker_main(args: WorkerArgs) {
|
||||
}
|
||||
p.active && !p.done
|
||||
};
|
||||
// Fire the startup link-capacity probe once the stream has settled (see the constants
|
||||
// above), and fold its measurement into the ABR ceiling when the result lands.
|
||||
if let Some(at) = capacity_probe_at {
|
||||
if Instant::now() >= at {
|
||||
capacity_probe_at = None;
|
||||
*pump_probe.lock().unwrap() = ProbeState {
|
||||
active: true,
|
||||
..Default::default()
|
||||
};
|
||||
if ctrl_tx
|
||||
.try_send(CtrlRequest::Probe(ProbeRequest {
|
||||
target_kbps: CAPACITY_PROBE_KBPS,
|
||||
duration_ms: CAPACITY_PROBE_MS,
|
||||
}))
|
||||
.is_ok()
|
||||
{
|
||||
capacity_probe_deadline = Some(Instant::now() + CAPACITY_PROBE_TIMEOUT);
|
||||
tracing::info!(
|
||||
target_kbps = CAPACITY_PROBE_KBPS,
|
||||
duration_ms = CAPACITY_PROBE_MS,
|
||||
"adaptive bitrate: startup link-capacity probe"
|
||||
);
|
||||
} else {
|
||||
pump_probe.lock().unwrap().active = false; // ctrl queue full — skip
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(deadline) = capacity_probe_deadline {
|
||||
let mut p = pump_probe.lock().unwrap();
|
||||
if p.done {
|
||||
capacity_probe_deadline = None;
|
||||
// An all-zero reply is a decline (old host / probe-less build) — keep the
|
||||
// negotiated ceiling. Otherwise: delivered wire kbps × 0.7.
|
||||
if p.host_duration_ms > 0 && p.delivered_bytes > 0 {
|
||||
let delivered_kbps = (p.delivered_bytes.saturating_mul(8)
|
||||
/ p.host_duration_ms.max(1) as u64)
|
||||
as u32;
|
||||
let ceiling = delivered_kbps.saturating_mul(7) / 10;
|
||||
abr.set_ceiling(ceiling);
|
||||
tracing::info!(
|
||||
delivered_kbps,
|
||||
ceiling_kbps = ceiling,
|
||||
"adaptive bitrate: link-capacity probe done — climb ceiling set"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
"adaptive bitrate: capacity probe declined — keeping negotiated ceiling"
|
||||
);
|
||||
}
|
||||
} else if Instant::now() >= deadline {
|
||||
// The host never answered (a build that ignores ProbeRequest): clear the
|
||||
// stuck-active state so LossReports resume, keep the negotiated ceiling.
|
||||
p.active = false;
|
||||
capacity_probe_deadline = None;
|
||||
tracing::info!(
|
||||
"adaptive bitrate: capacity probe timed out (old host?) — keeping negotiated ceiling"
|
||||
);
|
||||
}
|
||||
}
|
||||
if !probe_active && last_report.elapsed() >= ADAPT_REPORT_INTERVAL {
|
||||
// A no-op clock flush earlier in this window suspected a wall-clock step: fire
|
||||
// the mid-stream re-sync now (once — the 60 s periodic covers everything else).
|
||||
|
||||
Reference in New Issue
Block a user