Phase 1: Basic capture core and module layout
This commit is contained in:
commit
78b50a2fde
8 changed files with 1745 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
1528
Cargo.lock
generated
Normal file
1528
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal 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
28
src/capture/mod.rs
Normal 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
42
src/capture/wayland.rs
Normal 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
50
src/capture/x11.rs
Normal 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
26
src/debug.rs
Normal 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
57
src/main.rs
Normal 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(())
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue