diff --git a/crates/punktfunk-host/src/native/control.rs b/crates/punktfunk-host/src/native/control.rs index 0c59e7a8..2ce0e7d0 100644 --- a/crates/punktfunk-host/src/native/control.rs +++ b/crates/punktfunk-host/src/native/control.rs @@ -22,10 +22,10 @@ pub(super) async fn run( live_reconfig_ok: bool, adaptive_fec: bool, session_bitrate_kbps: u32, - /// Encoder-truth bridge (data plane → here, §ABR overdrive): the encoder's live applied rate, - /// its discovered codec-level ceiling (0 = unknown), and the "encode can't hold cadence" - /// flag. Read at `SetBitrate`-resolve time so the ack — the base the client's controller - /// climbs from — never promises a rate the encoder won't run at. + // Encoder-truth bridge (data plane → here, §ABR overdrive): the encoder's live applied rate, + // its discovered codec-level ceiling (0 = unknown), and the "encode can't hold cadence" + // flag. Read at `SetBitrate`-resolve time so the ack — the base the client's controller + // climbs from — never promises a rate the encoder won't run at. live_bitrate: Arc, encoder_ceiling_kbps: Arc, cadence_degraded: Arc, diff --git a/crates/punktfunk-host/src/native/stream.rs b/crates/punktfunk-host/src/native/stream.rs index 17bdfb30..a84f4e0f 100644 --- a/crates/punktfunk-host/src/native/stream.rs +++ b/crates/punktfunk-host/src/native/stream.rs @@ -1453,6 +1453,10 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option ceiling { + tracing::info!( + requested_kbps = *k, + ceiling_kbps = ceiling, + "bitrate request clamped to the known encoder ceiling" + ); + *k = ceiling; + } + } if let Some(new_kbps) = want_kbps.filter(|&k| k != bitrate_kbps) { if enc.reconfigure_bitrate(new_kbps as u64 * 1000) { + // Adopt the encoder's post-clamp truth, not the request: it feeds the send + // pacer, the console/mgmt view and the control task's acks, and a short apply + // teaches the ceiling used above. + let applied_kbps = enc + .applied_bitrate_bps() + .map(|b| (b / 1000) as u32) + .filter(|&k| k > 0) + .unwrap_or(new_kbps); tracing::info!( from_kbps = bitrate_kbps, - to_kbps = new_kbps, + to_kbps = applied_kbps, + requested_kbps = new_kbps, "encoder bitrate reconfigured in place (adaptive bitrate — no IDR)" ); - bitrate_kbps = new_kbps; - live_bitrate.store(new_kbps, Ordering::Relaxed); + if applied_kbps < new_kbps { + encoder_ceiling_kbps.store(applied_kbps, Ordering::Relaxed); + } + if applied_kbps < bitrate_kbps { + // Down-step: the behind-cadence backlog was scored against the old, + // heavier rate — clean slate so it can't feed a false escalation. + behind_score = 0; + } + bitrate_kbps = applied_kbps; + live_bitrate.store(applied_kbps, Ordering::Relaxed); // Same encoder, same stream: the in-flight AUs and the wire-index prediction // stay valid — no inflight forfeit, no IDR-cooldown anchor. } else { @@ -1719,9 +1757,17 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option { + // The fresh encoder may have clamped to its codec-level ceiling — + // adopt (and record) ITS rate, not the request; see the in-place arm. + let applied_kbps = new_enc + .applied_bitrate_bps() + .map(|b| (b / 1000) as u32) + .filter(|&k| k > 0) + .unwrap_or(new_kbps); tracing::info!( from_kbps = bitrate_kbps, - to_kbps = new_kbps, + to_kbps = applied_kbps, + requested_kbps = new_kbps, "encoder rebuilt at new bitrate (adaptive bitrate)" ); if let Some(c) = plan.wire_chunk { @@ -1731,8 +1777,11 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option { tracing::warn!(error = %format!("{e:#}"), to_kbps = new_kbps, @@ -2573,6 +2627,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option= DEPTH_DEGRADE, Ordering::Relaxed); if behind_score >= DEPTH_ESCALATE { if cur_depth < max_depth { cur_depth = max_depth;