chore: Add mock screen capture driver for headless testing
This commit is contained in:
parent
82904833f0
commit
587b164457
14 changed files with 350 additions and 9 deletions
BIN
record_1788315688.mp4
Normal file
BIN
record_1788315688.mp4
Normal file
Binary file not shown.
BIN
record_1788315823.mp4
Normal file
BIN
record_1788315823.mp4
Normal file
Binary file not shown.
BIN
record_1788315881.mp4
Normal file
BIN
record_1788315881.mp4
Normal file
Binary file not shown.
BIN
replay_1788315659.mp4
Normal file
BIN
replay_1788315659.mp4
Normal file
Binary file not shown.
60
scratch/muxer_fix.patch
Normal file
60
scratch/muxer_fix.patch
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
--- src/bin/vrec-daemon.rs
|
||||
+++ src/bin/vrec-daemon.rs
|
||||
@@ -62,6 +62,8 @@
|
||||
let mut config = vrec::config::VrecConfig::load();
|
||||
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 60).max(60) as usize);
|
||||
let mut normal_muxer: Option<Muxer> = None;
|
||||
+ let mut normal_recording = false;
|
||||
+ let mut normal_waiting_keyframe = false;
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(drain) = mux_rx.recv() {
|
||||
@@ -108,16 +110,14 @@
|
||||
}
|
||||
},
|
||||
Command::StartRecording => {
|
||||
- if config.record_enabled && normal_muxer.is_none() {
|
||||
- let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
- let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
- let name = format!("record_{}.mp4", ts);
|
||||
- normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() };
|
||||
- println!("Started normal recording to {}", name);
|
||||
+ if config.record_enabled && !normal_recording {
|
||||
+ normal_recording = true;
|
||||
+ normal_waiting_keyframe = true;
|
||||
+ println!("StartRecording requested, waiting for keyframe...");
|
||||
}
|
||||
},
|
||||
Command::StopRecording => {
|
||||
- if let Some(mut m) = normal_muxer.take() {
|
||||
+ if let Some(mut m) = normal_muxer.take() {
|
||||
let _ = m.finalize();
|
||||
println!("Stopped normal recording.");
|
||||
}
|
||||
+ normal_recording = false;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
@@ -129,7 +129,19 @@
|
||||
ring.push_overwrite(pkt.clone());
|
||||
}
|
||||
- if let Some(muxer) = normal_muxer.as_mut() {
|
||||
- let _ = muxer.write_packet(&pkt);
|
||||
+ if normal_recording {
|
||||
+ if normal_waiting_keyframe {
|
||||
+ if pkt.is_keyframe() {
|
||||
+ normal_waiting_keyframe = false;
|
||||
+ let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
+ let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
+ let name = format!("record_{}.mp4", ts);
|
||||
+ normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() };
|
||||
+ println!("Started normal recording to {}", name);
|
||||
+ }
|
||||
+ }
|
||||
+ if !normal_waiting_keyframe {
|
||||
+ if let Some(muxer) = normal_muxer.as_mut() {
|
||||
+ let _ = muxer.write_packet(&pkt);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
14
send_ipc.py
Normal file
14
send_ipc.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import socket
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
|
||||
def send(cmd):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock")
|
||||
payload = json.dumps(cmd).encode('utf-8')
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
|
||||
import sys
|
||||
send(sys.argv[1])
|
||||
|
|
@ -16,7 +16,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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" {
|
||||
let mut source: Box<dyn FrameSource> = if std::env::args().any(|a| a == "--mock") {
|
||||
println!("Initializing MOCK capture...");
|
||||
Box::new(vrec::capture::mock::MockCapture::new())
|
||||
} else if session_type.to_lowercase() == "wayland" {
|
||||
println!("Initializing Wayland capture...");
|
||||
Box::new(vrec::capture::wayland::WaylandCapture::new().await?)
|
||||
} else {
|
||||
|
|
@ -60,6 +63,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let mut config = vrec::config::VrecConfig::load();
|
||||
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 60).max(60) as usize);
|
||||
let mut normal_muxer: Option<Muxer> = None;
|
||||
let mut normal_recording = false;
|
||||
let mut normal_waiting_keyframe = false;
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(drain) = mux_rx.recv() {
|
||||
|
|
@ -107,12 +112,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
}
|
||||
},
|
||||
Command::StartRecording => {
|
||||
if config.record_enabled && normal_muxer.is_none() {
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let name = format!("record_{}.mp4", ts);
|
||||
normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", name);
|
||||
if config.record_enabled && !normal_recording {
|
||||
normal_recording = true;
|
||||
normal_waiting_keyframe = true;
|
||||
println!("StartRecording requested, waiting for keyframe...");
|
||||
}
|
||||
},
|
||||
Command::StopRecording => {
|
||||
|
|
@ -120,6 +123,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let _ = m.finalize();
|
||||
println!("Stopped normal recording.");
|
||||
}
|
||||
normal_recording = false;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -130,8 +134,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
if config.replay_enabled {
|
||||
ring.push_overwrite(pkt.clone());
|
||||
}
|
||||
if let Some(muxer) = normal_muxer.as_mut() {
|
||||
let _ = muxer.write_packet(&pkt);
|
||||
if normal_recording {
|
||||
if normal_waiting_keyframe {
|
||||
if pkt.is_keyframe() {
|
||||
normal_waiting_keyframe = false;
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let name = format!("record_{}.mp4", ts);
|
||||
normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", name);
|
||||
}
|
||||
}
|
||||
|
||||
if !normal_waiting_keyframe {
|
||||
if let Some(muxer) = normal_muxer.as_mut() {
|
||||
let _ = muxer.write_packet(&pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
176
src/bin/vrec-daemon.rs.orig
Normal file
176
src/bin/vrec-daemon.rs.orig
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
use vrec::capture::{Frame, FrameSource};
|
||||
use vrec::encoder::VaapiEncoder;
|
||||
use vrec::ring::Packet;
|
||||
use vrec::muxer::Muxer;
|
||||
use vrec::ipc::Command;
|
||||
use ringbuf::HeapRb;
|
||||
use ringbuf::traits::{RingBuffer, Consumer, Observer};
|
||||
use crossbeam_channel::bounded;
|
||||
use std::thread;
|
||||
use std::env;
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::io::Read;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
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 std::env::args().any(|a| a == "--mock") {
|
||||
println!("Initializing MOCK capture...");
|
||||
Box::new(vrec::capture::mock::MockCapture::new())
|
||||
} else if session_type.to_lowercase() == "wayland" {
|
||||
println!("Initializing Wayland capture...");
|
||||
Box::new(vrec::capture::wayland::WaylandCapture::new().await?)
|
||||
} else {
|
||||
println!("Initializing X11 capture...");
|
||||
Box::new(vrec::capture::x11::X11Capture::new()?)
|
||||
};
|
||||
|
||||
let first_frame = source.next_frame()?;
|
||||
let (width, height) = match &first_frame {
|
||||
Frame::Raw { width, height, .. } => (*width, *height),
|
||||
Frame::DmaBuf { width, height, .. } => (*width, *height),
|
||||
};
|
||||
|
||||
let (frame_tx, frame_rx) = bounded::<Frame>(5);
|
||||
let (cmd_tx, cmd_rx) = bounded::<Command>(10);
|
||||
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
|
||||
|
||||
let _ = frame_tx.send(first_frame);
|
||||
|
||||
let capture_tx = frame_tx.clone();
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
match source.next_frame() {
|
||||
Ok(frame) => {
|
||||
if capture_tx.send(frame).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Capture error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder");
|
||||
let codec_ctx_ptr = encoder.codec_ctx() as usize;
|
||||
|
||||
let mut config = vrec::config::VrecConfig::load();
|
||||
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 60).max(60) as usize);
|
||||
let mut normal_muxer: Option<Muxer> = None;
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(drain) = mux_rx.recv() {
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let name = format!("replay_{}.mp4", ts);
|
||||
println!("Saving replay to {}...", name);
|
||||
let mut start_idx = 0;
|
||||
for (i, p) in drain.iter().enumerate() {
|
||||
if p.is_keyframe() {
|
||||
start_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if start_idx < drain.len() {
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let mut muxer = unsafe { Muxer::new(&name, codec_ctx).unwrap() };
|
||||
for p in drain.into_iter().skip(start_idx) {
|
||||
let _ = muxer.write_packet(&p);
|
||||
}
|
||||
let _ = muxer.finalize();
|
||||
println!("Replay saved!");
|
||||
} else {
|
||||
println!("No keyframe found in buffer, skipping save.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Ok(frame) = frame_rx.recv() {
|
||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||
match cmd {
|
||||
Command::ReloadConfig => {
|
||||
config = vrec::config::VrecConfig::load();
|
||||
println!("Daemon config reloaded!");
|
||||
let new_capacity = (config.replay_duration_sec * 60).max(60) as usize;
|
||||
if ring.capacity().get() != new_capacity {
|
||||
ring = HeapRb::<Packet>::new(new_capacity);
|
||||
println!("Replay buffer resized to {} frames.", new_capacity);
|
||||
}
|
||||
},
|
||||
Command::SaveReplay => {
|
||||
if config.replay_enabled {
|
||||
let drain = ring.iter().cloned().collect::<Vec<_>>();
|
||||
let _ = mux_tx.try_send(drain);
|
||||
}
|
||||
},
|
||||
Command::StartRecording => {
|
||||
if config.record_enabled && normal_muxer.is_none() {
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let name = format!("record_{}.mp4", ts);
|
||||
normal_muxer = unsafe { Muxer::new(&name, codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", name);
|
||||
}
|
||||
},
|
||||
Command::StopRecording => {
|
||||
if let Some(mut m) = normal_muxer.take() {
|
||||
let _ = m.finalize();
|
||||
println!("Stopped normal recording.");
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(packets) = encoder.encode_frame(&frame) {
|
||||
for pkt in packets {
|
||||
if config.replay_enabled {
|
||||
ring.push_overwrite(pkt.clone());
|
||||
}
|
||||
if let Some(muxer) = normal_muxer.as_mut() {
|
||||
let _ = muxer.write_packet(&pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
println!("Daemon listening on IPC socket: {}", socket_path);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(mut stream) => {
|
||||
let mut len_buf = [0u8; 4];
|
||||
if stream.read_exact(&mut len_buf).is_ok() {
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
let mut payload = vec![0u8; len];
|
||||
if stream.read_exact(&mut payload).is_ok()
|
||||
&& let Ok(cmd) = serde_json::from_slice::<Command>(&payload) {
|
||||
match cmd {
|
||||
Command::StopDaemon => {
|
||||
break;
|
||||
},
|
||||
other => {
|
||||
let _ = cmd_tx.try_send(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Socket error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
src/capture/mock.rs
Normal file
39
src/capture/mock.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use super::{Frame, FrameSource};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct MockCapture {
|
||||
width: u32,
|
||||
height: u32,
|
||||
frame_count: u64,
|
||||
}
|
||||
|
||||
impl MockCapture {
|
||||
pub fn new() -> Self {
|
||||
Self { width: 1920, height: 1080, frame_count: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameSource for MockCapture {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
std::thread::sleep(Duration::from_millis(16)); // ~60fps
|
||||
let mut data = vec![0u8; (self.width * self.height * 4) as usize];
|
||||
|
||||
// Fill with a changing color
|
||||
let color = (self.frame_count % 255) as u8;
|
||||
for chunk in data.chunks_exact_mut(4) {
|
||||
chunk[0] = color; // B
|
||||
chunk[1] = color; // G
|
||||
chunk[2] = 255; // R
|
||||
chunk[3] = 255; // A
|
||||
}
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
Ok(Frame::Raw {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
stride: self.width * 4,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -34,3 +34,4 @@ pub enum Frame {
|
|||
pub trait FrameSource: Send {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
pub mod mock;
|
||||
|
|
|
|||
11
test_ipc.py
Normal file
11
test_ipc.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import socket
|
||||
import json
|
||||
import struct
|
||||
import os
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock")
|
||||
payload = b'"SaveReplay"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent SaveReplay")
|
||||
7
test_record.py
Normal file
7
test_record.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import socket, struct, os
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock")
|
||||
payload = b'"StartRecording"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent StartRecording")
|
||||
7
test_reload.py
Normal file
7
test_reload.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import socket, struct, os
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock")
|
||||
payload = b'"ReloadConfig"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent ReloadConfig")
|
||||
7
test_stop.py
Normal file
7
test_stop.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import socket, struct, os
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/vrec.sock")
|
||||
payload = b'"StopRecording"'
|
||||
sock.sendall(struct.pack('<I', len(payload)))
|
||||
sock.sendall(payload)
|
||||
print("Sent StopRecording")
|
||||
Loading…
Add table
Reference in a new issue