feat(portal): BWE-driven resolution adaptation + duplicate frame skipping

WebRTC client bandwidth estimate now drives both encoder bitrate and
resolution tier selection, replacing the previous static-target encoder.

- webrtc.rs: enable str0m BWE (seeded at 5 Mbps), surface
  EgressBitrateEstimate + KeyframeRequest events, expose
  get_bwe_estimate() / set_need_keyframe()
- state_portal.rs: wire bitrate/resolution channels between the WebRTC
  thread and the encode thread; tier ladder [1440p, 1080p, 720p] with
  downscale at 60% budget and upscale hysteresis (120% sustained 10s)
- avhw.rs: SwEncImport::poll_resolution_commands() rebuilds the import
  filter graph on UpdateResolution; SwEncEncode::recreate_encoder()
  rebuilds sws/enc_video/yuv_frame atomically; hash_sampled_y_plane()
  skips duplicate frames; VBV x264opts cap IDR bursts; H.264 level 4.0
  (muxer) / 4.2 (WebRTC)
- state.rs: sync wlr-screencopy GOP to fps*2 max 20 for parity
- fix: drain bitrate_rx + resolution_rx BEFORE the stride check in
  encode_cpu_frame() so the new (smaller-stride) frame produced after
  a resolution change does not hit the stale (larger) enc_width and
  crash the encode thread
- WebRTC GOP widened to fps*2 max 20 (was fps/2 max 10)
This commit is contained in:
dailz
2026-06-13 22:46:33 +08:00
parent 503e4dbc22
commit 3e60258627
9 changed files with 905 additions and 169 deletions
+32 -14
View File
@@ -539,7 +539,10 @@ 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: {}", std::io::Error::last_os_error());
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;
@@ -641,7 +644,11 @@ impl<S: CaptureSource> State<S> {
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");
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;
}
@@ -672,7 +679,9 @@ impl<S: CaptureSource> State<S> {
}
pub fn poll_webrtc(&mut self) -> Result<()> {
let Some(ref mut wrtc) = self.webrtc else { return Ok(()) };
let Some(ref mut wrtc) = self.webrtc else {
return Ok(());
};
wrtc.handle_signaling()?;
wrtc.poll_and_feed()?;
@@ -697,7 +706,8 @@ impl<S: CaptureSource> State<S> {
continue;
}
count += 1;
if let Err(e) = wrtc.write_h264_frame(&data, self.webrtc_frames_sent, self.args.fps) {
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);
@@ -709,10 +719,8 @@ impl<S: CaptureSource> State<S> {
}
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),
);
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}");
}
@@ -752,9 +760,12 @@ impl<S: CaptureSource> State<S> {
.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100);
let enc = if let Some(ref tx) = self.webrtc_tx {
let (enc_w, enc_h) =
transpose_if_transform_transposed(output_info.transform, width as i32, height as i32);
let actual_gop_size = self.args.gop_size.unwrap_or((fps / 2).max(10));
let (enc_w, enc_h) = transpose_if_transform_transposed(
output_info.transform,
width as i32,
height as i32,
);
let actual_gop_size = self.args.gop_size.unwrap_or((fps * 2).max(20));
match SwEncState::new_webrtc(
&drm_path,
width,
@@ -765,7 +776,10 @@ impl<S: CaptureSource> State<S> {
bitrate,
actual_gop_size,
tx.clone(),
self.webrtc_paused.as_ref().expect("webrtc_paused must exist when webrtc_tx exists").clone(),
self.webrtc_paused
.as_ref()
.expect("webrtc_paused must exist when webrtc_tx exists")
.clone(),
) {
Ok(enc) => StreamingEncoder::WebRtc(enc),
Err(e) => {
@@ -775,7 +789,11 @@ impl<S: CaptureSource> State<S> {
}
}
} else {
let output_path = self.args.output.as_deref().expect("output required for MP4 mode");
let output_path = self
.args
.output
.as_deref()
.expect("output required for MP4 mode");
match crate::avhw::create_encoder(
&drm_path,
Path::new(output_path),
@@ -1035,7 +1053,7 @@ impl<S: CaptureSource> Dispatch<WlRegistry, GlobalListContents> for State<S> {
qhandle: &QueueHandle<State<S>>,
) {
use wayland_client::protocol::wl_registry::Event as RegistryEvent;
match event {
RegistryEvent::Global {
name,