fix(state_portal): gate handle_pw_frame on WebRTC paused state (closes #19)
Root cause (verified via code reading + Oracle design review): - webrtc_paused IS correctly initialized to true in StatePortal::new() - encode_cpu_frame DOES respect paused via early return - BUT encode_thread_loop unconditionally sent timing on every Ok(()), causing stats.record_encode_thread to tick encoded_frames even for paused-dropped frames -> phantom encoded_fps=29.7 during idle - Real waste: handle_pw_frame imports DMA-BUF + VAAPI scale + NV12 clone + crossbeam send at 60fps even when no WebRTC client is connected Fix (Option B - surgical bugfix): - Add EncodeOutcome enum (Encoded/SkippedPaused/SkippedDisconnected/ SkippedDuplicate) to encode_cpu_frame return type - encode_thread_loop only reports timing on Ok(Encoded), not on skips -> encoded_fps naturally stays at 0 during idle (also helps #20) - handle_pw_frame entry: early return on paused, skipping ALL frame processing (DMA-BUF import, VAAPI scale, NV12 clone, channel send) - MP4 mode unchanged (webrtc_paused is None, gate is no-op) - Side benefit: last_fillable_frame stays None during initial idle, so maybe_send_filler_frame also early-returns -> no filler waste during the pre-connect idle window (partial mitigation for #18) Out of scope (TODO comment added at state_portal.rs:178): - Encoder still initializes on first PipeWire frame (one-time ~50ms SwEncEncode::new_webrtc cost). Full deferral (Option A) requires splitting WebRTC signaling lifecycle from media lifecycle - deferred until startup cost becomes user-perceptible - Bitrate formula unchanged (5*W*H*fps/100) - tracked by #21 Verification (34s idle + 60s connected session): - 0 'skipping duplicate frame' events during idle (was ~30/sec before) - 0 BWE bitrate updates during idle - 0 IDR production during idle (was 180 frames into the void) - First IDR produced 178ms after connect (ForceKeyframe -> IDR in 10ms) - cargo test transform/fps_limit/backend_detect: 34 passed - SAFETY comments preserved verbatim
This commit is contained in:
+20
-6
@@ -48,6 +48,20 @@ pub struct SwEncodeTiming {
|
||||
pub output_bytes: usize,
|
||||
}
|
||||
|
||||
/// Outcome of a single `encode_cpu_frame` call. Used by the encode thread
|
||||
/// to decide whether to report timing stats (only real encodes tick encoded_fps).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodeOutcome {
|
||||
/// Frame was actually encoded and produced output bytes.
|
||||
Encoded,
|
||||
/// Frame was dropped because WebRTC is paused (no client connected).
|
||||
SkippedPaused,
|
||||
/// Frame was dropped because the encoder is in disconnected state.
|
||||
SkippedDisconnected,
|
||||
/// Frame was dropped because its Y-plane hash matched the previous frame.
|
||||
SkippedDuplicate,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AvHwDevCtx
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1116,11 +1130,11 @@ impl SwEncEncode {
|
||||
mem::take(&mut self.last_timing)
|
||||
}
|
||||
|
||||
pub fn encode_cpu_frame(&mut self, frame: &CpuNv12Frame) -> Result<()> {
|
||||
pub fn encode_cpu_frame(&mut self, frame: &CpuNv12Frame) -> Result<EncodeOutcome> {
|
||||
self.last_timing = SwEncodeTiming::default();
|
||||
|
||||
if self.webrtc_disconnected {
|
||||
return Ok(());
|
||||
return Ok(EncodeOutcome::SkippedDisconnected);
|
||||
}
|
||||
|
||||
// Must drain before the stride check: the import thread emits
|
||||
@@ -1155,7 +1169,7 @@ impl SwEncEncode {
|
||||
}
|
||||
if let Some(ref paused) = self.webrtc_paused {
|
||||
if paused.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
return Ok(EncodeOutcome::SkippedPaused);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1173,7 +1187,7 @@ impl SwEncEncode {
|
||||
if frame_index > 0 && !force_gop_frame && !force_this_frame && current_hash == self.last_frame_hash {
|
||||
tracing::debug!(frame_index, "skipping duplicate frame");
|
||||
self.last_frame_hash = current_hash;
|
||||
return Ok(());
|
||||
return Ok(EncodeOutcome::SkippedDuplicate);
|
||||
}
|
||||
self.last_frame_hash = current_hash;
|
||||
|
||||
@@ -1247,7 +1261,7 @@ impl SwEncEncode {
|
||||
output_bytes,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(EncodeOutcome::Encoded)
|
||||
}
|
||||
|
||||
fn recreate_encoder(&mut self, width: u32, height: u32) -> Result<()> {
|
||||
@@ -1470,7 +1484,7 @@ impl SwEncState {
|
||||
|
||||
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
|
||||
let cpu_frame = self.import.import_and_scale(hw_frame)?;
|
||||
self.encode.encode_cpu_frame(&cpu_frame)
|
||||
self.encode.encode_cpu_frame(&cpu_frame).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user