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,
|
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
|
// AvHwDevCtx
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1116,11 +1130,11 @@ impl SwEncEncode {
|
|||||||
mem::take(&mut self.last_timing)
|
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();
|
self.last_timing = SwEncodeTiming::default();
|
||||||
|
|
||||||
if self.webrtc_disconnected {
|
if self.webrtc_disconnected {
|
||||||
return Ok(());
|
return Ok(EncodeOutcome::SkippedDisconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must drain before the stride check: the import thread emits
|
// 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 let Some(ref paused) = self.webrtc_paused {
|
||||||
if paused.load(Ordering::Relaxed) {
|
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 {
|
if frame_index > 0 && !force_gop_frame && !force_this_frame && current_hash == self.last_frame_hash {
|
||||||
tracing::debug!(frame_index, "skipping duplicate frame");
|
tracing::debug!(frame_index, "skipping duplicate frame");
|
||||||
self.last_frame_hash = current_hash;
|
self.last_frame_hash = current_hash;
|
||||||
return Ok(());
|
return Ok(EncodeOutcome::SkippedDuplicate);
|
||||||
}
|
}
|
||||||
self.last_frame_hash = current_hash;
|
self.last_frame_hash = current_hash;
|
||||||
|
|
||||||
@@ -1247,7 +1261,7 @@ impl SwEncEncode {
|
|||||||
output_bytes,
|
output_bytes,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(EncodeOutcome::Encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recreate_encoder(&mut self, width: u32, height: u32) -> Result<()> {
|
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<()> {
|
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
|
||||||
let cpu_frame = self.import.import_and_scale(hw_frame)?;
|
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<()> {
|
pub fn flush(&mut self) -> Result<()> {
|
||||||
|
|||||||
+34
-3
@@ -9,7 +9,8 @@ use anyhow::{bail, Result}; // 错误处理工具
|
|||||||
|
|
||||||
use crate::args::Args; // 命令行参数
|
use crate::args::Args; // 命令行参数
|
||||||
use crate::avhw::{
|
use crate::avhw::{
|
||||||
self, BitrateCommand, CpuNv12Frame, ResolutionChange, SwEncEncode, SwEncImport, SwEncState,
|
self, BitrateCommand, CpuNv12Frame, EncodeOutcome, ResolutionChange, SwEncEncode, SwEncImport,
|
||||||
|
SwEncState,
|
||||||
}; // 软件编码器状态(VAAPI 导入 + H.264 编码)
|
}; // 软件编码器状态(VAAPI 导入 + H.264 编码)
|
||||||
use crate::cap_portal::{CapPortal, PwCtrlEvent, PwDmaBufFrame}; // PipeWire 屏幕采集端点
|
use crate::cap_portal::{CapPortal, PwCtrlEvent, PwDmaBufFrame}; // PipeWire 屏幕采集端点
|
||||||
use crate::stats::{FrameTimings, PipelineStats}; // 管道统计(帧计时、每秒快照)
|
use crate::stats::{FrameTimings, PipelineStats}; // 管道统计(帧计时、每秒快照)
|
||||||
@@ -176,7 +177,23 @@ impl StatePortal {
|
|||||||
|
|
||||||
match self.stage {
|
match self.stage {
|
||||||
PortalStage::WaitingForFormat => {
|
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!(
|
tracing::info!(
|
||||||
"First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}",
|
"First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}",
|
||||||
frame.width,
|
frame.width,
|
||||||
@@ -494,6 +511,15 @@ impl StatePortal {
|
|||||||
/// 通过 `av_hwframe_map` 零拷贝导入 VAAPI,然后交给 SwEncState 完成:
|
/// 通过 `av_hwframe_map` 零拷贝导入 VAAPI,然后交给 SwEncState 完成:
|
||||||
/// scale_vaapi GPU 缩放、2K NV12 回读、YUV420P 格式转换、软件 H.264 编码。
|
/// scale_vaapi GPU 缩放、2K NV12 回读、YUV420P 格式转换、软件 H.264 编码。
|
||||||
fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> {
|
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 t_import_start = Instant::now();
|
||||||
let pts = self.frames_encoded as i64;
|
let pts = self.frames_encoded as i64;
|
||||||
|
|
||||||
@@ -652,7 +678,7 @@ fn encode_thread_loop(
|
|||||||
match input_rx.recv() {
|
match input_rx.recv() {
|
||||||
Ok(frame) => {
|
Ok(frame) => {
|
||||||
match encode.encode_cpu_frame(&frame) {
|
match encode.encode_cpu_frame(&frame) {
|
||||||
Ok(()) => {
|
Ok(EncodeOutcome::Encoded) => {
|
||||||
let t = encode.take_timing();
|
let t = encode.take_timing();
|
||||||
let _ = timing_tx.try_send(EncodeThreadTiming {
|
let _ = timing_tx.try_send(EncodeThreadTiming {
|
||||||
sws_us: t.sws_us,
|
sws_us: t.sws_us,
|
||||||
@@ -660,6 +686,11 @@ fn encode_thread_loop(
|
|||||||
output_bytes: t.output_bytes,
|
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) => {
|
Err(e) => {
|
||||||
tracing::error!("Encode thread error: {e}");
|
tracing::error!("Encode thread error: {e}");
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user