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
+47 -18
View File
@@ -225,9 +225,7 @@ impl CapPortal {
proxy
.select_sources(&session, options)
.await
.map_err(|e| {
anyhow::anyhow!("Screen sharing permission denied: {e}")
})?;
.map_err(|e| anyhow::anyhow!("Screen sharing permission denied: {e}"))?;
let response = proxy
.start(&session, None, Default::default())
@@ -272,7 +270,10 @@ fn verify_secure_dir(path: &std::path::Path) -> bool {
match std::fs::symlink_metadata(path) {
Ok(meta) => {
if meta.file_type().is_symlink() {
tracing::warn!("Token parent dir is a symlink, rejecting: {}", path.display());
tracing::warn!(
"Token parent dir is a symlink, rejecting: {}",
path.display()
);
return false;
}
// Must be a directory
@@ -282,7 +283,10 @@ fn verify_secure_dir(path: &std::path::Path) -> bool {
}
// Must be owned by current user
if meta.uid() != unsafe { libc::getuid() } {
tracing::warn!("Token parent dir not owned by current user: {}", path.display());
tracing::warn!(
"Token parent dir not owned by current user: {}",
path.display()
);
return false;
}
// No group or other permissions (mode must be 0o700 exactly within the 0o777 mask)
@@ -346,7 +350,10 @@ fn load_restore_token_from(path: PathBuf) -> Option<String> {
};
if meta.file_type().is_symlink() {
tracing::warn!("Token file is a symlink, refusing to read: {}", path.display());
tracing::warn!(
"Token file is a symlink, refusing to read: {}",
path.display()
);
return None;
}
if !meta.is_file() {
@@ -369,7 +376,11 @@ fn load_restore_token_from(path: PathBuf) -> Option<String> {
let token = std::fs::read_to_string(&path).ok()?;
let trimmed = token.trim().to_string();
if trimmed.is_empty() { None } else { Some(trimmed) }
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
fn save_restore_token(token: &str) {
@@ -490,7 +501,9 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let mainloop = match pw::main_loop::MainLoopBox::new(None) {
Ok(ml) => ml,
Err(e) => {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("MainLoop::new failed: {e}"))) {
if let Err(e) =
event_tx.try_send(PwCtrlEvent::Error(format!("MainLoop::new failed: {e}")))
{
tracing::error!("MainLoop::new failed and error channel also failed: {e}");
}
return;
@@ -500,7 +513,9 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let context = match pw::context::ContextBox::new(mainloop.loop_(), None) {
Ok(c) => c,
Err(e) => {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("Context::new failed: {e}"))) {
if let Err(e) =
event_tx.try_send(PwCtrlEvent::Error(format!("Context::new failed: {e}")))
{
tracing::error!("Context::new failed and error channel also failed: {e}");
}
return;
@@ -510,7 +525,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let core = match context.connect_fd(pw_fd, None) {
Ok(c) => c,
Err(e) => {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("connect_fd failed: {e}"))) {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("connect_fd failed: {e}")))
{
tracing::error!("connect_fd failed and error channel also failed: {e}");
}
return;
@@ -534,7 +550,9 @@ fn pipewire_thread(ctx: PwThreadCtx) {
) {
Ok(s) => s,
Err(e) => {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("Stream::new failed: {e}"))) {
if let Err(e) =
event_tx.try_send(PwCtrlEvent::Error(format!("Stream::new failed: {e}")))
{
tracing::error!("Stream::new failed and error channel also failed: {e}");
}
return;
@@ -608,8 +626,10 @@ fn pipewire_thread(ctx: PwThreadCtx) {
"PipeWire format negotiated: {width}x{height}, \
drm_format={drm_format:#010x}, modifier={modifier:#x}, \
framerate={}/{}, max_framerate={}/{}",
framerate.num, framerate.denom,
max_framerate.num, max_framerate.denom,
framerate.num,
framerate.denom,
max_framerate.num,
max_framerate.denom,
);
}
})
@@ -621,7 +641,6 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let frame_tx = frame_tx.clone();
let dropped = dropped;
move |stream, _| {
let raw_buf = unsafe { stream.dequeue_raw_buffer() };
if raw_buf.is_null() {
tracing::trace!("process: null raw_buf");
@@ -742,7 +761,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
StreamFlags::AUTOCONNECT | StreamFlags::MAP_BUFFERS,
&mut params,
) {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("stream.connect failed: {e}"))) {
if let Err(e) = event_tx.try_send(PwCtrlEvent::Error(format!("stream.connect failed: {e}")))
{
tracing::error!("stream.connect failed and error channel also failed: {e}");
}
return;
@@ -918,7 +938,10 @@ mod tests {
let meta = std::fs::symlink_metadata(&new_dir).unwrap();
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o700, "created directory should be 0700, got {mode:o}");
assert_eq!(
mode, 0o700,
"created directory should be 0700, got {mode:o}"
);
}
#[test]
@@ -933,7 +956,10 @@ mod tests {
let meta = std::fs::symlink_metadata(path).unwrap();
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o700, "tightened directory should be 0700, got {mode:o}");
assert_eq!(
mode, 0o700,
"tightened directory should be 0700, got {mode:o}"
);
}
#[test]
@@ -947,7 +973,10 @@ mod tests {
let meta = std::fs::symlink_metadata(&token_path).unwrap();
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "token file should be 0600, got {mode:o}");
assert_eq!(std::fs::read_to_string(&token_path).unwrap(), "secret-token-123");
assert_eq!(
std::fs::read_to_string(&token_path).unwrap(),
"secret-token-123"
);
}
#[test]