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:
+47
-18
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user