feat(stats): populate frame_age metric for WebRTC path (partial #20)
Add quantitative capture-to-send latency measurement so we can diagnose remaining latency sources after #24/#25 PTS fixes. Previously frame_age_p95 was always 0.0ms because the WebRTC code path never propagated capture timestamps, even though stats.rs already supported the metric. The infrastructure existed but was disconnected. Changes: - avhw.rs: Add capture_time: Instant field to CpuNv12Frame (set when PipeWire delivers frame) and EncodedH264Frame (propagated through encode thread via new last_capture_time side-channel on SwEncEncode). - state_portal.rs: Change sent_gap channel type from Sender<f64> to Sender<(f64, Option<f64>)> so WebRTC thread can send pre-computed age_ms = capture_time.elapsed() at the exact send moment (not at stats drain time, which would inflate the measurement by ~1s). - stats.rs: record_send_from_thread now accepts Option<f64> age_ms and pushes to frame_age_ms Vec when Some. After this commit: - stats: log lines show real frame_age_p95 / frame_age_max in ms - Expected range: 5-30ms (import + scale + encode + channel send) - If much higher: server pipeline has queueing issue - If low but user still sees latency: confirms bottleneck is network or browser-side (jitter buffer, decode queue) Scope notes: - Only Portal/PipeWire path is instrumented. wlr-screencopy path uses different code path (EncState, not SwEncState) and will continue to report frame_age=0.0ms. Adding wlr instrumentation is separate scope. - This is diagnostic only — does NOT change user-visible behavior. No encoding, sending, or stats output format changes. Tests: - cargo build --release: 0 new warnings (19 baseline preserved) - cargo test: 96 lib + 3 integration, 0 failed - SAFETY comments preserved verbatim - 3 files changed, +39/-10 lines Refs #20.
This commit is contained in:
+18
@@ -725,6 +725,8 @@ pub struct EncodedH264Frame {
|
||||
/// PTS in encoder time_base units (1/fps seconds), normalized so first frame = 0.
|
||||
/// Derived from real capture time, NOT frame counter.
|
||||
pub pts_ticks: i64,
|
||||
/// Wall-clock capture time, propagated from CpuNv12Frame for frame_age stat.
|
||||
pub capture_time: std::time::Instant,
|
||||
}
|
||||
|
||||
pub enum FrameOutput {
|
||||
@@ -740,6 +742,9 @@ pub struct CpuNv12Frame {
|
||||
pub y_stride: usize,
|
||||
pub uv_stride: usize,
|
||||
pub pts: i64,
|
||||
/// Wall-clock time when this frame was captured (PipeWire delivery).
|
||||
/// Used for frame_age stat: time from capture to WebRTC send.
|
||||
pub capture_time: std::time::Instant,
|
||||
}
|
||||
|
||||
pub struct SwEncImport {
|
||||
@@ -987,6 +992,7 @@ impl SwEncImport {
|
||||
y_stride,
|
||||
uv_stride,
|
||||
pts,
|
||||
capture_time: std::time::Instant::now(),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1020,6 +1026,10 @@ pub struct SwEncEncode {
|
||||
/// every `encode_cpu_frame` call (even on early returns) so stale values
|
||||
/// from a previous frame can never leak out.
|
||||
last_timing: SwEncodeTiming,
|
||||
/// Capture time of the frame currently being encoded. Saved from the
|
||||
/// input `CpuNv12Frame` so `drain_encoder` can propagate it into the
|
||||
/// emitted `EncodedH264Frame` for the frame_age stat (issue #20).
|
||||
last_capture_time: Option<Instant>,
|
||||
}
|
||||
|
||||
const FNV1A_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
|
||||
@@ -1090,6 +1100,7 @@ impl SwEncEncode {
|
||||
gop_size,
|
||||
force_keyframe_pending: false,
|
||||
last_timing: SwEncodeTiming::default(),
|
||||
last_capture_time: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1130,6 +1141,7 @@ impl SwEncEncode {
|
||||
gop_size,
|
||||
force_keyframe_pending: false,
|
||||
last_timing: SwEncodeTiming::default(),
|
||||
last_capture_time: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1154,6 +1166,9 @@ impl SwEncEncode {
|
||||
|
||||
pub fn encode_cpu_frame(&mut self, frame: &CpuNv12Frame) -> Result<EncodeOutcome> {
|
||||
self.last_timing = SwEncodeTiming::default();
|
||||
// Save capture_time so drain_encoder can propagate it into the
|
||||
// EncodedH264Frame emitted via the WebRTC channel (issue #20).
|
||||
self.last_capture_time = Some(frame.capture_time);
|
||||
|
||||
if self.webrtc_disconnected {
|
||||
return Ok(EncodeOutcome::SkippedDisconnected);
|
||||
@@ -1417,6 +1432,9 @@ impl SwEncEncode {
|
||||
match tx.try_send(EncodedH264Frame {
|
||||
data: data.to_vec(),
|
||||
pts_ticks,
|
||||
capture_time: self
|
||||
.last_capture_time
|
||||
.unwrap_or_else(Instant::now),
|
||||
}) {
|
||||
Ok(()) => {}
|
||||
Err(crossbeam_channel::TrySendError::Full(frame)) => {
|
||||
|
||||
Reference in New Issue
Block a user