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:
+34
-3
@@ -9,7 +9,8 @@ use anyhow::{bail, Result}; // 错误处理工具
|
||||
|
||||
use crate::args::Args; // 命令行参数
|
||||
use crate::avhw::{
|
||||
self, BitrateCommand, CpuNv12Frame, ResolutionChange, SwEncEncode, SwEncImport, SwEncState,
|
||||
self, BitrateCommand, CpuNv12Frame, EncodeOutcome, ResolutionChange, SwEncEncode, SwEncImport,
|
||||
SwEncState,
|
||||
}; // 软件编码器状态(VAAPI 导入 + H.264 编码)
|
||||
use crate::cap_portal::{CapPortal, PwCtrlEvent, PwDmaBufFrame}; // PipeWire 屏幕采集端点
|
||||
use crate::stats::{FrameTimings, PipelineStats}; // 管道统计(帧计时、每秒快照)
|
||||
@@ -176,7 +177,23 @@ impl StatePortal {
|
||||
|
||||
match self.stage {
|
||||
PortalStage::WaitingForFormat => {
|
||||
// 首帧到达,记录 DMA-BUF 格式信息
|
||||
// TODO(#19): Currently the encoder initializes on first PipeWire frame,
|
||||
// even in WebRTC mode before any client connects. The recurring 60fps
|
||||
// idle waste (DMA-BUF import + VAAPI scale + clone + channel send) is
|
||||
// eliminated by the paused gate in handle_pw_frame (see #19 fix).
|
||||
// However, the one-time SwEncEncode::new_webrtc cost (~50ms, swscale
|
||||
// context + x264 setup + YUV frame allocation) still occurs at startup.
|
||||
//
|
||||
// If this startup cost becomes user-perceptible, upgrade to "Option A":
|
||||
// 1. Add PortalStage::AwaitingClient
|
||||
// 2. Keep WebRtcState on main thread during AwaitingClient, pump
|
||||
// handle_signaling/poll_and_feed from main loop
|
||||
// 3. On is_connected() == true, initialize encoder and move WebRtcState
|
||||
// into the WebRTC thread
|
||||
// 4. Use resolution-aware conservative default bitrate (see #21)
|
||||
//
|
||||
// Trigger condition: user reports perceivable latency or CPU spike at
|
||||
// startup. Until then, Option B is sufficient.
|
||||
tracing::info!(
|
||||
"First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}",
|
||||
frame.width,
|
||||
@@ -494,6 +511,15 @@ impl StatePortal {
|
||||
/// 通过 `av_hwframe_map` 零拷贝导入 VAAPI,然后交给 SwEncState 完成:
|
||||
/// scale_vaapi GPU 缩放、2K NV12 回读、YUV420P 格式转换、软件 H.264 编码。
|
||||
fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> {
|
||||
// #19: When WebRTC mode is paused (no client connected), skip ALL frame
|
||||
// processing — DMA-BUF import, VAAPI scale, NV12 clone, channel send, and
|
||||
// encode thread wakeup. This eliminates ~60fps of pointless work during
|
||||
// the pre-connect idle window. MP4 mode (webrtc_paused == None) is unaffected.
|
||||
if let Some(paused) = &self.webrtc_paused {
|
||||
if paused.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let t_import_start = Instant::now();
|
||||
let pts = self.frames_encoded as i64;
|
||||
|
||||
@@ -652,7 +678,7 @@ fn encode_thread_loop(
|
||||
match input_rx.recv() {
|
||||
Ok(frame) => {
|
||||
match encode.encode_cpu_frame(&frame) {
|
||||
Ok(()) => {
|
||||
Ok(EncodeOutcome::Encoded) => {
|
||||
let t = encode.take_timing();
|
||||
let _ = timing_tx.try_send(EncodeThreadTiming {
|
||||
sws_us: t.sws_us,
|
||||
@@ -660,6 +686,11 @@ fn encode_thread_loop(
|
||||
output_bytes: t.output_bytes,
|
||||
});
|
||||
}
|
||||
Ok(_) => {
|
||||
// SkippedPaused / SkippedDisconnected / SkippedDuplicate
|
||||
// Do not report timing; do not tick encoded_fps.
|
||||
// take_timing() intentionally NOT called — last_timing stays default.
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Encode thread error: {e}");
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user