Phase 1: Basic capture core and module layout

This commit is contained in:
katR 2026-09-01 16:55:44 +05:30
commit 78b50a2fde
8 changed files with 1745 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1528
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "vrec"
version = "0.1.0"
edition = "2024"
[dependencies]
ashpd = "0.13.13"
libc = "0.2.189"
memmap2 = "0.9.11"
minifb = "0.28.0"
pipewire = "0.10.1"
tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread"] }
xcb = { version = "1.7.1", features = ["shm", "xfixes"] }

28
src/capture/mod.rs Normal file
View file

@ -0,0 +1,28 @@
use std::os::unix::io::RawFd;
pub mod wayland;
pub mod x11;
#[derive(Debug)]
pub enum Frame {
/// Zero-copy DMA-BUF file descriptor. Used by Wayland/PipeWire.
DmaBuf {
width: u32,
height: u32,
format: u32,
fd: RawFd,
stride: u32,
offset: u32,
},
/// Raw memory buffer. Used by X11 fallback.
Raw {
width: u32,
height: u32,
stride: u32,
data: Vec<u8>,
},
}
pub trait FrameSource {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>>;
}

42
src/capture/wayland.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::capture::{Frame, FrameSource};
use ashpd::desktop::screencast::{CursorMode, PersistMode, Screencast, SourceType};
use ashpd::WindowIdentifier;
use std::os::unix::io::RawFd;
pub struct WaylandCapture {
fd: RawFd,
node_id: u32,
}
impl WaylandCapture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let proxy = Screencast::new().await?;
let session = proxy.create_session().await?;
proxy.select_sources(
&session,
CursorMode::Hidden,
SourceType::Monitor | SourceType::Window,
true,
None,
PersistMode::DoNot,
).await?;
let response = proxy.start(&session, &WindowIdentifier::default()).await?;
// This is as far as we go with ashpd, next we need pipewire
let stream = response.streams().first().ok_or("No stream")?;
let node_id = stream.pipe_wire_node_id();
let fd = proxy.open_pipe_wire_remote(&session).await?;
Ok(Self {
fd: fd.into_raw_fd(),
node_id,
})
}
}
impl FrameSource for WaylandCapture {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
Err("Wayland/Pipewire frame extraction not fully implemented yet".into())
}
}

50
src/capture/x11.rs Normal file
View file

@ -0,0 +1,50 @@
use xcb::{x, Connection};
use crate::capture::{Frame, FrameSource};
pub struct X11Capture {
conn: Connection,
root: x::Window,
width: u16,
height: u16,
}
impl X11Capture {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let (conn, screen_num) = Connection::connect(None)?;
let setup = conn.get_setup();
let screen = setup.roots().nth(screen_num as usize).ok_or("No screen found")?;
Ok(Self {
root: screen.root(),
width: screen.width_in_pixels(),
height: screen.height_in_pixels(),
conn,
})
}
}
impl FrameSource for X11Capture {
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
let cookie = self.conn.send_request(&x::GetImage {
format: x::ImageFormat::ZPixmap,
drawable: x::Drawable::Window(self.root),
x: 0,
y: 0,
width: self.width,
height: self.height,
plane_mask: std::u32::MAX,
});
let reply = self.conn.wait_for_reply(cookie)?;
let data = reply.data().to_vec();
let stride = (self.width as u32) * 4;
Ok(Frame::Raw {
width: self.width as u32,
height: self.height as u32,
stride,
data,
})
}
}

26
src/debug.rs Normal file
View file

@ -0,0 +1,26 @@
use minifb::{Window, WindowOptions};
pub struct DebugWindow {
window: Window,
}
impl DebugWindow {
pub fn new(width: usize, height: usize) -> Result<Self, Box<dyn std::error::Error>> {
let window = Window::new(
"vrec debug capture",
width,
height,
WindowOptions::default(),
)?;
Ok(Self { window })
}
pub fn update(&mut self, buffer: &[u32], width: usize, height: usize) -> Result<(), Box<dyn std::error::Error>> {
self.window.update_with_buffer(buffer, width, height)?;
Ok(())
}
pub fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(minifb::Key::Escape)
}
}

57
src/main.rs Normal file
View file

@ -0,0 +1,57 @@
pub mod capture;
pub mod debug;
use capture::{Frame, FrameSource};
use debug::DebugWindow;
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string());
println!("Detected session type: {}", session_type);
let mut source: Box<dyn FrameSource> = if session_type.to_lowercase() == "wayland" {
println!("Initializing Wayland capture...");
Box::new(capture::wayland::WaylandCapture::new().await?)
} else {
println!("Initializing X11 capture...");
Box::new(capture::x11::X11Capture::new()?)
};
let mut window: Option<DebugWindow> = None;
println!("Starting capture loop...");
loop {
match source.next_frame() {
Ok(Frame::Raw { width, height, data, stride }) => {
if window.is_none() {
window = Some(DebugWindow::new(width as usize, height as usize)?);
}
if let Some(win) = &mut window {
if !win.is_open() {
break;
}
// For minifb, we need to cast &[u8] to &[u32].
// Assuming BGRA or similar 4-byte format.
let u32_data: Vec<u32> = data
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
win.update(&u32_data, width as usize, height as usize)?;
}
}
Ok(Frame::DmaBuf { .. }) => {
println!("Wayland/PipeWire DMA-BUF received. Phase 1 debug viewer not fully implemented for DMA-BUFs yet.");
break;
}
Err(e) => {
eprintln!("Capture error: {}", e);
break;
}
}
}
Ok(())
}