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:
306
src/state_portal.rs
Normal file
306
src/state_portal.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
use std::mem;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use ffmpeg_next as ff;
|
||||
use ffmpeg_next::ffi;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::avhw::{self, EncState};
|
||||
use crate::cap_portal::{CapPortal, PwDmaBufFrame, PwEvent};
|
||||
use crate::fps_limit::FpsLimit;
|
||||
use crate::transform::Transform;
|
||||
|
||||
enum PortalStage {
|
||||
WaitingForFormat,
|
||||
Streaming,
|
||||
}
|
||||
|
||||
pub struct StatePortal {
|
||||
stage: PortalStage,
|
||||
enc: Option<EncState>,
|
||||
fps_limit: FpsLimit<()>,
|
||||
cap: CapPortal,
|
||||
args: Args,
|
||||
errored: bool,
|
||||
first_frame: bool,
|
||||
drm_device: PathBuf,
|
||||
}
|
||||
|
||||
impl StatePortal {
|
||||
pub fn new(args: Args) -> Result<Self> {
|
||||
let drm_device = resolve_drm_device(&args)?;
|
||||
tracing::info!("Using DRM device: {}", drm_device.display());
|
||||
|
||||
let cap = CapPortal::new(&args)?;
|
||||
|
||||
Ok(Self {
|
||||
stage: PortalStage::WaitingForFormat,
|
||||
enc: None,
|
||||
fps_limit: FpsLimit::new(args.fps),
|
||||
cap,
|
||||
args,
|
||||
errored: false,
|
||||
first_frame: true,
|
||||
drm_device,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll_and_encode(&mut self) -> Result<bool> {
|
||||
let event = match self.cap.frame_receiver().try_recv() {
|
||||
Ok(event) => event,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
|
||||
match event {
|
||||
PwEvent::Frame(frame) => {
|
||||
match self.stage {
|
||||
PortalStage::WaitingForFormat => {
|
||||
tracing::info!(
|
||||
"First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}",
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.format,
|
||||
frame.stride,
|
||||
frame.modifier
|
||||
);
|
||||
|
||||
let enc = avhw::create_encoder(
|
||||
&self.drm_device,
|
||||
self.args.output.as_ref(),
|
||||
frame.width,
|
||||
frame.height,
|
||||
self.args.fps,
|
||||
Transform::Normal,
|
||||
self.args.bitrate,
|
||||
self.args.gop_size,
|
||||
)?;
|
||||
|
||||
self.enc = Some(enc);
|
||||
self.stage = PortalStage::Streaming;
|
||||
drop(frame);
|
||||
}
|
||||
PortalStage::Streaming => {
|
||||
self.handle_pw_frame(frame)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
PwEvent::StreamEnded => {
|
||||
tracing::warn!("PipeWire stream ended");
|
||||
self.errored = true;
|
||||
}
|
||||
PwEvent::Error(e) => {
|
||||
tracing::error!("PipeWire error: {e}");
|
||||
self.errored = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> {
|
||||
// 1. FPS limiting (first frame bypasses)
|
||||
if self.first_frame {
|
||||
self.first_frame = false;
|
||||
} else {
|
||||
let now = std::time::Instant::now();
|
||||
if self.fps_limit.on_new_frame((), now).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Build DRM descriptor for DMA-BUF import
|
||||
let desc = build_drm_descriptor(&frame);
|
||||
let desc_box = Box::new(desc);
|
||||
|
||||
// 3. Allocate raw DRM_PRIME source frame using Video wrapper
|
||||
let mut raw_frame = ff::frame::Video::empty();
|
||||
unsafe {
|
||||
let raw_ptr = raw_frame.as_mut_ptr();
|
||||
(*raw_ptr).data[0] = Box::into_raw(desc_box) as *mut u8;
|
||||
(*raw_ptr).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
||||
(*raw_ptr).width = frame.width as i32;
|
||||
(*raw_ptr).height = frame.height as i32;
|
||||
}
|
||||
|
||||
// 4. Get encoder reference
|
||||
let enc = match self.enc.as_mut() {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
// Recover the Box to prevent memory leak of the descriptor
|
||||
unsafe {
|
||||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||||
if !desc_ptr.is_null() {
|
||||
let _ = Box::from_raw(desc_ptr);
|
||||
}
|
||||
}
|
||||
bail!("encoder not initialized");
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Allocate VAAPI hardware target frame
|
||||
let mut hw_frame = ff::frame::Video::empty();
|
||||
let ret = unsafe {
|
||||
ffi::av_hwframe_get_buffer(enc.frames_rgb().as_ptr(), hw_frame.as_mut_ptr(), 0)
|
||||
};
|
||||
if ret < 0 {
|
||||
// Recover the Box to prevent memory leak of the descriptor
|
||||
unsafe {
|
||||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||||
if !desc_ptr.is_null() {
|
||||
let _ = Box::from_raw(desc_ptr);
|
||||
}
|
||||
}
|
||||
bail!("av_hwframe_get_buffer failed: error {ret}");
|
||||
}
|
||||
|
||||
// 6. Import DMA-BUF into VAAPI via transfer_data
|
||||
let ret = unsafe {
|
||||
ffi::av_hwframe_transfer_data(hw_frame.as_mut_ptr(), raw_frame.as_ptr(), 0)
|
||||
};
|
||||
if ret < 0 {
|
||||
unsafe {
|
||||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||||
if !desc_ptr.is_null() {
|
||||
let _ = Box::from_raw(desc_ptr);
|
||||
}
|
||||
}
|
||||
if ret == -(ffi::EINVAL as i32) {
|
||||
bail!(
|
||||
"VAAPI does not support DMA-BUF modifier 0x{:X}",
|
||||
frame.modifier
|
||||
);
|
||||
}
|
||||
bail!("av_hwframe_transfer_data failed: error {ret}");
|
||||
}
|
||||
|
||||
// 7. Set PTS
|
||||
unsafe {
|
||||
(*hw_frame.as_mut_ptr()).pts = frame.pts;
|
||||
}
|
||||
|
||||
// 8. Encode
|
||||
enc.encode_frame(&hw_frame)?;
|
||||
|
||||
// 9. Clean up: recover the Boxed descriptor from raw_frame to prevent leak.
|
||||
// Video::drop calls av_frame_free which does NOT free data[0].
|
||||
unsafe {
|
||||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||||
if !desc_ptr.is_null() {
|
||||
let _ = Box::from_raw(desc_ptr);
|
||||
}
|
||||
}
|
||||
// raw_frame and hw_frame drop here via Video::drop → av_frame_free
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<()> {
|
||||
if let Some(enc) = &mut self.enc {
|
||||
enc.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn shutdown(&mut self) {
|
||||
if let Err(e) = self.flush() {
|
||||
tracing::error!("Flush error during shutdown: {e}");
|
||||
}
|
||||
tracing::info!("StatePortal shutdown complete");
|
||||
}
|
||||
|
||||
pub fn is_errored(&self) -> bool {
|
||||
self.errored
|
||||
}
|
||||
}
|
||||
|
||||
fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffi::AVDRMFrameDescriptor {
|
||||
let mut desc: ffi::AVDRMFrameDescriptor = unsafe { mem::zeroed() };
|
||||
|
||||
desc.nb_objects = 1;
|
||||
desc.objects[0].fd = frame.fd.as_raw_fd();
|
||||
desc.objects[0].size = 0;
|
||||
desc.objects[0].format_modifier = frame.modifier;
|
||||
|
||||
desc.nb_layers = 1;
|
||||
desc.layers[0].format = frame.format;
|
||||
desc.layers[0].nb_planes = 1;
|
||||
desc.layers[0].planes[0].object_index = 0;
|
||||
desc.layers[0].planes[0].offset = frame.offset as isize;
|
||||
desc.layers[0].planes[0].pitch = frame.stride as isize;
|
||||
|
||||
desc
|
||||
}
|
||||
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
fn resolve_drm_device(args: &Args) -> Result<PathBuf> {
|
||||
if let Some(ref drm) = args.drm_device {
|
||||
return Ok(PathBuf::from(drm));
|
||||
}
|
||||
|
||||
for render in &["/dev/dri/renderD128", "/dev/dri/renderD129"] {
|
||||
let path = PathBuf::from(render);
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
bail!("No DRM render device found. Specify --drm-device.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::fd::{FromRawFd, OwnedFd};
|
||||
|
||||
fn make_test_frame() -> PwDmaBufFrame {
|
||||
// Create a dummy fd from stderr (always valid fd 2)
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) };
|
||||
PwDmaBufFrame {
|
||||
fd,
|
||||
offset: 0,
|
||||
stride: 1920 * 4,
|
||||
modifier: 0, // DRM_FORMAT_MOD_LINEAR
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
format: 0x34325258, // XR24 little-endian
|
||||
pts: 12345,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_drm_descriptor_single_plane() {
|
||||
let frame = make_test_frame();
|
||||
let desc = build_drm_descriptor(&frame);
|
||||
|
||||
assert_eq!(desc.nb_objects, 1);
|
||||
assert_eq!(desc.objects[0].format_modifier, 0);
|
||||
assert_eq!(desc.nb_layers, 1);
|
||||
assert_eq!(desc.layers[0].format, 0x34325258);
|
||||
assert_eq!(desc.layers[0].nb_planes, 1);
|
||||
assert_eq!(desc.layers[0].planes[0].object_index, 0);
|
||||
assert_eq!(desc.layers[0].planes[0].offset, 0);
|
||||
assert_eq!(desc.layers[0].planes[0].pitch, 1920 * 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_drm_device_explicit() {
|
||||
let args = Args {
|
||||
output: "test.mp4".to_string(),
|
||||
output_name: None,
|
||||
fps: 30,
|
||||
codec: "h264".to_string(),
|
||||
hw_accel: "vaapi".to_string(),
|
||||
drm_device: Some("/dev/dri/renderD128".to_string()),
|
||||
bitrate: None,
|
||||
gop_size: None,
|
||||
verbose: false,
|
||||
backend: None,
|
||||
port: 0,
|
||||
};
|
||||
let result = resolve_drm_device(&args);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), std::path::PathBuf::from("/dev/dri/renderD128"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user