feat: add KWin/KDE Plasma screen capture via xdg-desktop-portal ScreenCast + PipeWire

Add a second capture backend for compositors without wlr-screencopy
(KWin, GNOME, etc.) using the xdg-desktop-portal ScreenCast interface
and PipeWire DMA-BUF streaming.

New files:
- src/backend_detect.rs: auto-detect wlr-screencopy vs portal backend
- src/cap_portal.rs: Portal session setup + PipeWire DMA-BUF thread
- src/state_portal.rs: StatePortal encoder pipeline (DMA-BUF → VAAPI)

Changes:
- Cargo.toml: add ashpd 0.13, tokio 1, pipewire 0.9, libspa 0.9,
  crossbeam-channel 0.5
- src/args.rs: add --backend CLI flag
- src/avhw.rs: extract create_encoder() from inline State code
- src/main.rs: route to portal or wlr-screencopy based on backend
- src/state.rs: fix params.destroy() on dup failure, cleanup
  in_flight_surface on copy fail, use create_encoder()
- tests/integration_test.rs: add --backend flag tests
This commit is contained in:
dailz
2026-05-11 08:49:08 +08:00
parent 2972216a02
commit d7fbb5256c
12 changed files with 2198 additions and 79 deletions

View File

@@ -9,9 +9,12 @@ use wayland_client::Connection;
mod args;
mod avhw;
mod backend_detect;
mod cap_portal;
mod cap_wlr_screencopy;
mod fps_limit;
mod state;
mod state_portal;
mod transform;
use crate::args::Args;
@@ -40,6 +43,19 @@ fn main() -> Result<()> {
anyhow::bail!("HEVC not supported in MVP. Use --codec h264");
}
let backend = crate::backend_detect::detect_backend(&args)?;
match backend {
crate::backend_detect::CaptureBackend::WlrScreencopy => {
run_wlr_screencopy(args)
}
crate::backend_detect::CaptureBackend::PortalPipeWire => {
run_portal_pipewire(args)
}
}
}
fn run_wlr_screencopy(args: Args) -> Result<()> {
// Connect to Wayland compositor
let conn = Connection::connect_to_env()?;
let (gm, mut queue) = registry_queue_init::<State<CapWlrScreencopy>>(&conn)?;
@@ -158,3 +174,58 @@ fn main() -> Result<()> {
tracing::info!("Done");
Ok(())
}
fn run_portal_pipewire(args: Args) -> Result<()> {
use crate::state_portal::StatePortal;
tracing::info!("Using Portal/PipeWire backend (KWin/KDE/GNOME)");
let mut state = StatePortal::new(args)?;
// Set up signal handling only (no Wayland fd needed)
let mut signals = signal_hook_mio::v1_0::Signals::new(&[
signal_hook::consts::SIGINT,
signal_hook::consts::SIGTERM,
])?;
let mut poll = mio::Poll::new()?;
let mut events = mio::Events::with_capacity(8);
poll.registry().register(
&mut signals,
mio::Token(1),
mio::Interest::READABLE,
)?;
let mut running = true;
while running {
poll.poll(&mut events, Some(std::time::Duration::from_millis(10)))
.unwrap_or_else(|e| {
if e.kind() == std::io::ErrorKind::Interrupted {
return;
}
tracing::error!("poll failed: {e}");
running = false;
});
for event in &events {
if event.token() == mio::Token(1) {
tracing::info!("Received quit signal");
running = false;
}
}
// Process all available PipeWire frames
while state.poll_and_encode()? {}
if state.is_errored() {
tracing::error!("Fatal error in portal state machine, exiting");
running = false;
}
}
tracing::info!("Shutting down...");
state.shutdown();
tracing::info!("Done");
Ok(())
}