fix(webrtc): SO_SNDBUF 2MB + VBV rate limiting + stats integration

P0 - UDP send buffer: set SO_SNDBUF=2MB to prevent EAGAIN on large IDR
frames (218KB/256KB keyframes caused 18+ EAGAIN bursts). Actual Linux
buffer 4096KB confirmed.

P1 - VBV rate limiting: cap rc_max_rate=bitrate and rc_buffer_size=
bitrate/4 for WebRTC encode path, preventing oversized IDR frames.

Stats: integrate PipelineStats into cap_portal (dropped_count), state.rs
(wlroots path), webrtc.rs (browser getStats enhancement + stats panel).
This commit is contained in:
dailz
2026-06-07 16:55:07 +08:00
parent 029fe13e37
commit aae030f309
3 changed files with 231 additions and 39 deletions

View File

@@ -46,6 +46,7 @@ use crate::args::Args;
use crate::avhw::{AvHwDevCtx, EncState, SwEncState};
use crate::cap_wlr_screencopy::CapWlrScreencopy;
use crate::fps_limit::FpsLimit;
use crate::stats::{FrameTimings, PipelineStats};
use crate::transform::{transpose_if_transform_transposed, Transform};
use crate::webrtc::WebRtcState;
@@ -213,6 +214,9 @@ pub struct State<S: CaptureSource> {
pub stage: EncConstructionStage<S>,
pub in_flight_surface: InFlightSurface<S>,
pub starting_timestamp: Option<i64>,
pub stats_start_time: Option<Instant>,
pub stats_last_time: Option<Instant>,
pub stats_frames: u64,
pub first_frame: bool,
pub args: Args,
pub errored: bool,
@@ -226,6 +230,7 @@ pub struct State<S: CaptureSource> {
webrtc_rx: Option<crossbeam_channel::Receiver<Vec<u8>>>,
webrtc_frames_sent: u64,
webrtc_paused: Option<Arc<AtomicBool>>,
stats: PipelineStats,
}
// ---------------------------------------------------------------------------
@@ -302,6 +307,9 @@ impl<S: CaptureSource> State<S> {
},
in_flight_surface: InFlightSurface::None,
starting_timestamp: None,
stats_start_time: None,
stats_last_time: None,
stats_frames: 0,
first_frame: true,
fps_limit: FpsLimit::new(fps),
args,
@@ -315,6 +323,7 @@ impl<S: CaptureSource> State<S> {
webrtc_rx,
webrtc_frames_sent: 0,
webrtc_paused,
stats: PipelineStats::new(),
};
// registry_queue_init consumes registry events internally during its
@@ -492,7 +501,7 @@ impl<S: CaptureSource> State<S> {
// is a freshly allocated empty Video frame.
let ret = unsafe { ffi::av_hwframe_get_buffer(frames_rgb_ctx, surface.as_mut_ptr(), 0) };
if ret < 0 {
tracing::error!("av_hwframe_get_buffer failed: error {}", ret);
tracing::error!("av_hwframe_get_buffer failed: {}", crate::avhw::ff_err(ret));
self.errored = true;
return;
}
@@ -505,7 +514,7 @@ impl<S: CaptureSource> State<S> {
}
let ret = unsafe { ffi::av_hwframe_map(map_frame.as_mut_ptr(), surface.as_ptr(), 0) };
if ret < 0 {
tracing::error!("av_hwframe_map failed: error {}", ret);
tracing::error!("av_hwframe_map failed: {}", crate::avhw::ff_err(ret));
self.errored = true;
return;
}
@@ -530,7 +539,7 @@ impl<S: CaptureSource> State<S> {
// takes ownership of the fd, and the original fd is owned by map_frame.
let fd_dup = unsafe { libc::dup(obj.fd) };
if fd_dup < 0 {
tracing::error!("failed to dup dma-buf fd");
tracing::error!("failed to dup dma-buf fd: {}", std::io::Error::last_os_error());
// wayland-client does not auto-destroy params on Drop.
params.destroy();
self.errored = true;
@@ -574,6 +583,8 @@ impl<S: CaptureSource> State<S> {
where
S::Frame: Default,
{
self.stats.record_capture();
let (mut surface, _drm_map, frame, buffer) =
match mem::replace(&mut self.in_flight_surface, InFlightSurface::None) {
InFlightSurface::CopyQueued {
@@ -614,10 +625,29 @@ impl<S: CaptureSource> State<S> {
.is_some()
};
if should_encode {
let encode_start = Instant::now();
if let Err(e) = enc.encode_frame(&surface) {
tracing::error!("encode_frame failed: {}", e);
self.errored = true;
}
let encode_elapsed = encode_start.elapsed().as_micros() as u64;
self.stats.record_encode(&FrameTimings {
total_us: encode_elapsed,
..Default::default()
});
}
self.stats_frames += 1;
if let Some(last) = self.stats_last_time {
if last.elapsed() >= std::time::Duration::from_secs(10) {
let delta = self.stats_frames;
let fps = delta as f64 / last.elapsed().as_secs_f64();
tracing::info!(frames = self.stats_frames, fps = format!("{fps:.1}"), "encoding stats");
self.stats_last_time = Some(std::time::Instant::now());
self.stats_frames = 0;
}
} else {
self.stats_start_time = Some(std::time::Instant::now());
self.stats_last_time = Some(std::time::Instant::now());
}
}
@@ -670,13 +700,23 @@ impl<S: CaptureSource> State<S> {
if let Err(e) = wrtc.write_h264_frame(&data, self.webrtc_frames_sent, self.args.fps) {
tracing::debug!("WebRTC write frame error: {e}");
}
self.stats.record_send(0.0, None);
self.webrtc_frames_sent = self.webrtc_frames_sent.saturating_add(1);
}
if count > 0 {
tracing::info!("WebRTC forwarded {count} frames from channel");
tracing::debug!("WebRTC forwarded {count} frames from channel");
}
}
if self.args.stats && self.stats.should_snapshot() {
self.stats.set_queue_depths(
0,
self.webrtc_rx.as_ref().map(|r| r.len()).unwrap_or(0),
);
let snap = self.stats.snapshot_and_reset();
tracing::info!("stats: {snap}");
}
Ok(())
}
@@ -995,8 +1035,7 @@ impl<S: CaptureSource> Dispatch<WlRegistry, GlobalListContents> for State<S> {
qhandle: &QueueHandle<State<S>>,
) {
use wayland_client::protocol::wl_registry::Event as RegistryEvent;
tracing::debug!("Dispatch<WlRegistry>::event fired: {:?}", event);
match event {
RegistryEvent::Global {
name,