feat(client): the stats overlay learns whether your mic frames arrive
NativeClient::mic_stats reaches the per-second stream window: sent and dropped (queue-full + stale-shed) frame deltas join the tracing line and the Stats event, and the Detailed OSD tier renders a mic line while the uplink is live — a healthy 10 ms-frame mic reads ~100 f/s, and a drop term means the client is shedding backlog, not the network eating audio. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,12 @@ pub struct Stats {
|
||||
/// received+lost (%). The OSD renders the counter line only when nonzero.
|
||||
pub lost: u32,
|
||||
pub lost_pct: f32,
|
||||
/// Mic uplink frames this window: handed to the QUIC datagram send, and shed anywhere
|
||||
/// client-side (queue-full at the producer + the pump's stale-oldest backlog governor —
|
||||
/// see [`NativeClient::mic_stats`]). Both stay 0 while the mic is off, so the OSD
|
||||
/// renders the mic line only when the uplink is live.
|
||||
pub mic_sent: u32,
|
||||
pub mic_dropped: u32,
|
||||
/// The decode path frames actually took this window (`"vaapi"`/`"software"`, empty
|
||||
/// until the first frame) — the OSD's trailing tag; tracks a mid-session fallback.
|
||||
pub decoder: &'static str,
|
||||
@@ -434,6 +440,9 @@ fn pump(
|
||||
let mut dec_path: &'static str = "";
|
||||
// The stats window keeps its own drop cursor — the OSD shows the per-window delta.
|
||||
let mut window_dropped = connector.frames_dropped();
|
||||
// Mic uplink cursor (same per-window diffing): a healthy 10 ms-frame mic reads ~100
|
||||
// sent/s; a nonzero drop delta is the queue shedding backlog (see NativeClient::mic_stats).
|
||||
let mut window_mic = connector.mic_stats();
|
||||
let mut last_kf_req: Option<Instant> = None;
|
||||
// Freeze-until-reanchor: the shared post-loss gate ([`punktfunk_core::reanchor::ReanchorGate`]).
|
||||
// Armed on any loss signal (frame-index gap, dropped-count climb, decoder wedge/demotion), it
|
||||
@@ -789,6 +798,12 @@ fn pump(
|
||||
let (pace_p50, _) = window_percentiles(&mut pace_us_win);
|
||||
let lost = dropped.saturating_sub(window_dropped) as u32;
|
||||
window_dropped = dropped;
|
||||
let mic_now = connector.mic_stats();
|
||||
let mic_sent = mic_now.sent.saturating_sub(window_mic.sent) as u32;
|
||||
let mic_dropped = (mic_now.dropped_full + mic_now.dropped_stale)
|
||||
.saturating_sub(window_mic.dropped_full + window_mic.dropped_stale)
|
||||
as u32;
|
||||
window_mic = mic_now;
|
||||
tracing::debug!(
|
||||
fps = frames_n,
|
||||
hostnet_p50_us = hn_p50,
|
||||
@@ -800,6 +815,8 @@ fn pump(
|
||||
pace_p50_us = pace_p50,
|
||||
decode_p50_us = dec_p50,
|
||||
lost,
|
||||
mic_sent,
|
||||
mic_dropped,
|
||||
total_frames,
|
||||
"stream window"
|
||||
);
|
||||
@@ -822,6 +839,8 @@ fn pump(
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
mic_sent,
|
||||
mic_dropped,
|
||||
decoder: dec_path,
|
||||
}));
|
||||
window_start = Instant::now();
|
||||
|
||||
@@ -2017,6 +2017,14 @@ fn stats_text(
|
||||
if s.lost > 0 {
|
||||
text.push_str(&format!("\nlost {} ({:.1}%)", s.lost, s.lost_pct));
|
||||
}
|
||||
// The mic uplink line renders only while the mic is live (a healthy 10 ms-frame uplink
|
||||
// reads ~100 f/s) and only in Detailed — drops here are the client shedding backlog.
|
||||
if detailed && (s.mic_sent > 0 || s.mic_dropped > 0) {
|
||||
text.push_str(&format!("\nmic {} f/s", s.mic_sent));
|
||||
if s.mic_dropped > 0 {
|
||||
text.push_str(&format!(" · dropped {}", s.mic_dropped));
|
||||
}
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
@@ -2262,6 +2270,8 @@ mod tests {
|
||||
decode_ms: 1.8,
|
||||
lost: 3,
|
||||
lost_pct: 0.4,
|
||||
mic_sent: 0,
|
||||
mic_dropped: 0,
|
||||
decoder: "vulkan",
|
||||
},
|
||||
PresentedWindow {
|
||||
@@ -2303,6 +2313,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The mic uplink line: Detailed-only, and only while the uplink is live.
|
||||
#[test]
|
||||
fn stats_text_mic_line() {
|
||||
let (mut s, p) = sample();
|
||||
let text = |s: &Stats, v| stats_text(v, "m", s, &p, false, false, None);
|
||||
assert!(
|
||||
!text(&s, StatsVerbosity::Detailed).contains("mic"),
|
||||
"no mic line while the mic is off"
|
||||
);
|
||||
s.mic_sent = 100;
|
||||
let detailed = text(&s, StatsVerbosity::Detailed);
|
||||
assert!(detailed.contains("\nmic 100 f/s"));
|
||||
assert!(
|
||||
!detailed.contains("dropped"),
|
||||
"a healthy uplink shows no drop term"
|
||||
);
|
||||
assert!(
|
||||
!text(&s, StatsVerbosity::Normal).contains("mic"),
|
||||
"mic line is Detailed-only"
|
||||
);
|
||||
s.mic_dropped = 7;
|
||||
assert!(text(&s, StatsVerbosity::Detailed).contains("mic 100 f/s · dropped 7"));
|
||||
}
|
||||
|
||||
/// Compact omits the latency term until the presenter's first e2e window lands.
|
||||
#[test]
|
||||
fn compact_waits_for_e2e() {
|
||||
|
||||
Reference in New Issue
Block a user