perf(core): Implement pure VRAM zero-copy for Windows, MIT-SHM for X11, High-Quality VBR rate control, and output directory management
This commit is contained in:
parent
0d73a17815
commit
df9a1a0522
7 changed files with 216 additions and 77 deletions
|
|
@ -99,7 +99,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let mut config = vrec::config::VrecConfig::load();
|
||||
replay_state_clone.store(config.replay_enabled, Ordering::SeqCst);
|
||||
|
||||
let mut encoder = VaapiEncoder::new_with_bitrate(width, height, config.record_bitrate_kbps)
|
||||
let mut encoder = VaapiEncoder::new_with_params(width, height, config.record_bitrate_kbps, config.fps)
|
||||
.expect("Failed to init encoder");
|
||||
let codec_ctx_ptr = encoder.codec_ctx() as usize;
|
||||
|
||||
|
|
@ -118,8 +118,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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 filename = format!("replay_{}.mp4", ts);
|
||||
let full_path = vrec::config::VrecConfig::load().resolve_save_path(&filename);
|
||||
println!("Saving replay to {}...", full_path);
|
||||
let mut start_idx = 0;
|
||||
for (i, p) in drain.iter().enumerate() {
|
||||
if p.is_keyframe() {
|
||||
|
|
@ -131,12 +132,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
if start_idx < drain.len() {
|
||||
let codec_ctx = codec_ctx_ptr as *mut ffmpeg_next::ffi::AVCodecContext;
|
||||
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
|
||||
let mut muxer = unsafe { Muxer::new(&name, codec_ctx, audio_codec_ctx).unwrap() };
|
||||
let mut muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).unwrap() };
|
||||
for p in drain.into_iter().skip(start_idx) {
|
||||
let _ = muxer.write_packet(&p);
|
||||
}
|
||||
let _ = muxer.finalize();
|
||||
println!("Replay saved to {}!", name);
|
||||
println!("Replay saved to {}!", full_path);
|
||||
} else {
|
||||
println!("No keyframe found in buffer, skipping save.");
|
||||
}
|
||||
|
|
@ -242,10 +243,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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);
|
||||
let filename = format!("record_{}.mp4", ts);
|
||||
let full_path = config.resolve_save_path(&filename);
|
||||
let audio_codec_ctx = audio_codec_ctx_ptr.map(|p| p as *mut ffmpeg_next::ffi::AVCodecContext);
|
||||
normal_muxer = unsafe { Muxer::new(&name, codec_ctx, audio_codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", name);
|
||||
normal_muxer = unsafe { Muxer::new(&full_path, codec_ctx, audio_codec_ctx).ok() };
|
||||
println!("Started normal recording to {}", full_path);
|
||||
}
|
||||
|
||||
if !normal_waiting_keyframe
|
||||
|
|
|
|||
|
|
@ -15,11 +15,14 @@ pub struct WindowsCapture {
|
|||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
duplication: IDXGIOutputDuplication,
|
||||
staging_texture: ID3D11Texture2D,
|
||||
gpu_texture: ID3D11Texture2D,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe impl Send for WindowsCapture {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl WindowsCapture {
|
||||
pub fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
|
@ -60,31 +63,32 @@ impl WindowsCapture {
|
|||
// Initialize Desktop Duplication
|
||||
let duplication = output1.DuplicateOutput(&device)?;
|
||||
|
||||
// Allocate a staging texture for CPU mapping fallback
|
||||
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||
// Allocate a dedicated VRAM-resident GPU texture for 100% zero-copy capture
|
||||
// (Frame stays on GPU die, zero PCIe bus readback, 0% CPU consumption)
|
||||
let gpu_desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: width,
|
||||
Height: height,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
|
||||
Usage: D3D11_USAGE_STAGING,
|
||||
BindFlags: 0,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
||||
Usage: D3D11_USAGE_DEFAULT,
|
||||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||
CPUAccessFlags: 0,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
|
||||
let mut staging_texture: Option<ID3D11Texture2D> = None;
|
||||
device.CreateTexture2D(&staging_desc, None, Some(&mut staging_texture))?;
|
||||
let staging_texture = staging_texture.ok_or("Failed to create staging texture")?;
|
||||
let mut gpu_texture: Option<ID3D11Texture2D> = None;
|
||||
device.CreateTexture2D(&gpu_desc, None, Some(&mut gpu_texture))?;
|
||||
let gpu_texture = gpu_texture.ok_or("Failed to create GPU VRAM texture")?;
|
||||
|
||||
println!("Windows DXGI Desktop Duplication active: {}x{}", width, height);
|
||||
println!("Windows DXGI Hardware Capture active (Pure GPU Zero-Copy): {}x{}", width, height);
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
context,
|
||||
duplication,
|
||||
staging_texture,
|
||||
gpu_texture,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
|
@ -116,37 +120,25 @@ impl FrameSource for WindowsCapture {
|
|||
if let Some(resource) = desktop_resource {
|
||||
let texture: ID3D11Texture2D = resource.cast()?;
|
||||
|
||||
// Copy to CPU staging texture
|
||||
self.context.CopyResource(&self.staging_texture, &texture);
|
||||
// Copy directly on the GPU from the desktop buffer to our persistent VRAM texture
|
||||
self.context.CopyResource(&self.gpu_texture, &texture);
|
||||
|
||||
// Immediately release the desktop frame back to the DWM compositor
|
||||
let _ = self.duplication.ReleaseFrame();
|
||||
|
||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||
self.context.Map(&self.staging_texture, 0, D3D11_MAP_READ, 0, Some(&mut mapped))?;
|
||||
|
||||
let row_pitch = mapped.RowPitch as usize;
|
||||
let mut data = vec![0u8; (self.width * self.height * 4) as usize];
|
||||
let src_ptr = mapped.pData as *const u8;
|
||||
|
||||
for y in 0..(self.height as usize) {
|
||||
let src_row = std::slice::from_raw_parts(src_ptr.add(y * row_pitch), (self.width * 4) as usize);
|
||||
let dst_offset = y * (self.width * 4) as usize;
|
||||
data[dst_offset..dst_offset + (self.width * 4) as usize].copy_from_slice(src_row);
|
||||
}
|
||||
|
||||
self.context.Unmap(&self.staging_texture, 0);
|
||||
|
||||
return Ok(Frame::Raw {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
stride: self.width * 4,
|
||||
data,
|
||||
// Pass the VRAM texture handle directly to the hardware encoder (Zero CPU copy!)
|
||||
return Ok(Frame::D3D11Texture {
|
||||
handle: self.gpu_texture.as_raw() as usize,
|
||||
});
|
||||
}
|
||||
let _ = self.duplication.ReleaseFrame();
|
||||
}
|
||||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
||||
// Display is idle (no new pixels), yield current texture without spinning CPU
|
||||
std::thread::sleep(std::time::Duration::from_millis(8));
|
||||
continue;
|
||||
return Ok(Frame::D3D11Texture {
|
||||
handle: self.gpu_texture.as_raw() as usize,
|
||||
});
|
||||
}
|
||||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
|
||||
println!("DXGI access lost (display mode or fullscreen switch), reacquiring...");
|
||||
|
|
@ -160,11 +152,8 @@ impl FrameSource for WindowsCapture {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(Frame::Raw {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
stride: self.width * 4,
|
||||
data: vec![0u8; (self.width * self.height * 4) as usize],
|
||||
Ok(Frame::D3D11Texture {
|
||||
handle: self.gpu_texture.as_raw() as usize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use xcb::{x, Connection};
|
||||
use xcb::{x, shm, Connection};
|
||||
use crate::capture::{Frame, FrameSource};
|
||||
|
||||
pub struct X11Capture {
|
||||
|
|
@ -6,25 +6,99 @@ pub struct X11Capture {
|
|||
root: x::Window,
|
||||
width: u16,
|
||||
height: u16,
|
||||
shm_seg: Option<shm::Seg>,
|
||||
shm_addr: *mut u8,
|
||||
shm_size: usize,
|
||||
shmid: i32,
|
||||
}
|
||||
|
||||
unsafe impl Send for X11Capture {}
|
||||
|
||||
impl X11Capture {
|
||||
pub fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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")?;
|
||||
let (root, width, height) = {
|
||||
let setup = conn.get_setup();
|
||||
let screen = setup.roots().nth(screen_num as usize).ok_or("No screen found")?;
|
||||
(screen.root(), screen.width_in_pixels(), screen.height_in_pixels())
|
||||
};
|
||||
|
||||
let size = (width as usize) * (height as usize) * 4;
|
||||
|
||||
// Try initializing MIT-SHM for zero-socket high performance
|
||||
let (shm_seg, shm_addr, shmid) = unsafe {
|
||||
let id = libc::shmget(libc::IPC_PRIVATE, size, libc::IPC_CREAT | 0o777);
|
||||
if id >= 0 {
|
||||
let addr = libc::shmat(id, std::ptr::null(), 0) as *mut u8;
|
||||
if addr != (-1isize as *mut u8) {
|
||||
let seg: shm::Seg = conn.generate_id();
|
||||
let attach_cookie = conn.send_request_checked(&shm::Attach {
|
||||
shmseg: seg,
|
||||
shmid: id as u32,
|
||||
read_only: false,
|
||||
});
|
||||
if conn.check_request(attach_cookie).is_ok() {
|
||||
println!("X11 MIT-SHM shared memory capture initialized (zero socket overhead): {}x{}", width, height);
|
||||
(Some(seg), addr, id)
|
||||
} else {
|
||||
libc::shmdt(addr as *const _);
|
||||
libc::shmctl(id, libc::IPC_RMID, std::ptr::null_mut());
|
||||
(None, std::ptr::null_mut(), -1)
|
||||
}
|
||||
} else {
|
||||
libc::shmctl(id, libc::IPC_RMID, std::ptr::null_mut());
|
||||
(None, std::ptr::null_mut(), -1)
|
||||
}
|
||||
} else {
|
||||
(None, std::ptr::null_mut(), -1)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
root: screen.root(),
|
||||
width: screen.width_in_pixels(),
|
||||
height: screen.height_in_pixels(),
|
||||
conn,
|
||||
root,
|
||||
width,
|
||||
height,
|
||||
shm_seg,
|
||||
shm_addr,
|
||||
shm_size: size,
|
||||
shmid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameSource for X11Capture {
|
||||
fn next_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let stride = (self.width as u32) * 4;
|
||||
|
||||
if let Some(seg) = self.shm_seg && !self.shm_addr.is_null() {
|
||||
let cookie = self.conn.send_request(&shm::GetImage {
|
||||
drawable: x::Drawable::Window(self.root),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
plane_mask: u32::MAX,
|
||||
format: x::ImageFormat::ZPixmap as u8,
|
||||
shmseg: seg,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
self.conn.wait_for_reply(cookie)?;
|
||||
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts(self.shm_addr, self.shm_size).to_vec()
|
||||
};
|
||||
|
||||
return Ok(Frame::Raw {
|
||||
width: self.width as u32,
|
||||
height: self.height as u32,
|
||||
stride,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to standard GetImage if SHM is not available
|
||||
let cookie = self.conn.send_request(&x::GetImage {
|
||||
format: x::ImageFormat::ZPixmap,
|
||||
drawable: x::Drawable::Window(self.root),
|
||||
|
|
@ -38,8 +112,6 @@ impl FrameSource for X11Capture {
|
|||
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,
|
||||
|
|
@ -48,3 +120,21 @@ impl FrameSource for X11Capture {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Capture {
|
||||
fn drop(&mut self) {
|
||||
if let Some(seg) = self.shm_seg {
|
||||
let _ = self.conn.send_request(&shm::Detach { shmseg: seg });
|
||||
}
|
||||
if !self.shm_addr.is_null() && self.shm_addr != (-1isize as *mut u8) {
|
||||
unsafe {
|
||||
libc::shmdt(self.shm_addr as *const _);
|
||||
}
|
||||
}
|
||||
if self.shmid >= 0 {
|
||||
unsafe {
|
||||
libc::shmctl(self.shmid, libc::IPC_RMID, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ pub struct VrecConfig {
|
|||
|
||||
pub record_enabled: bool,
|
||||
pub record_bitrate_kbps: u32,
|
||||
pub fps: u32,
|
||||
pub output_directory: String,
|
||||
|
||||
pub autostart: bool,
|
||||
pub language: String,
|
||||
|
|
@ -23,10 +25,12 @@ impl Default for VrecConfig {
|
|||
Self {
|
||||
replay_enabled: true,
|
||||
replay_duration_sec: 60,
|
||||
replay_bitrate_kbps: 10000,
|
||||
replay_bitrate_kbps: 18000,
|
||||
|
||||
record_enabled: false,
|
||||
record_bitrate_kbps: 15000,
|
||||
record_bitrate_kbps: 20000,
|
||||
fps: 60,
|
||||
output_directory: Self::default_output_directory(),
|
||||
|
||||
autostart: false,
|
||||
language: "en".to_string(),
|
||||
|
|
@ -38,6 +42,12 @@ impl Default for VrecConfig {
|
|||
}
|
||||
|
||||
impl VrecConfig {
|
||||
pub fn default_output_directory() -> String {
|
||||
let mut p = dirs::video_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
p.push("vrec");
|
||||
p.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn config_path() -> PathBuf {
|
||||
let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
path.push("vrec");
|
||||
|
|
@ -55,11 +65,18 @@ impl VrecConfig {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
pub fn resolve_save_path(&self, filename: &str) -> String {
|
||||
let p = std::path::Path::new(&self.output_directory);
|
||||
let _ = fs::create_dir_all(p);
|
||||
p.join(filename).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), std::io::Error> {
|
||||
let path = Self::config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let _ = fs::create_dir_all(&self.output_directory);
|
||||
let data = serde_json::to_string_pretty(self)?;
|
||||
fs::write(path, data)?;
|
||||
let _ = self.update_autostart();
|
||||
|
|
@ -101,10 +118,11 @@ mod tests {
|
|||
let cfg = VrecConfig::default();
|
||||
assert!(cfg.replay_enabled);
|
||||
assert_eq!(cfg.replay_duration_sec, 60);
|
||||
assert_eq!(cfg.fps, 60);
|
||||
assert!(cfg.output_directory.contains("vrec"));
|
||||
let json = serde_json::to_string(&cfg).unwrap();
|
||||
let parsed: VrecConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.menu_hotkey, "Alt+Z");
|
||||
assert_eq!(parsed.save_hotkey, "Ctrl+Shift+R");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,15 @@ pub struct VaapiEncoder {
|
|||
|
||||
impl VaapiEncoder {
|
||||
pub fn new(width: u32, height: u32) -> Result<Self, String> {
|
||||
Self::new_with_bitrate(width, height, 15_000)
|
||||
Self::new_with_params(width, height, 18_000, 60)
|
||||
}
|
||||
|
||||
pub fn new_with_bitrate(width: u32, height: u32, bitrate_kbps: u32) -> Result<Self, String> {
|
||||
Self::new_with_params(width, height, bitrate_kbps, 60)
|
||||
}
|
||||
|
||||
pub fn new_with_params(width: u32, height: u32, bitrate_kbps: u32, fps: u32) -> Result<Self, String> {
|
||||
let fps = fps.clamp(20, 144) as i32;
|
||||
unsafe {
|
||||
let codec = avcodec_find_encoder_by_name(c"h264_vaapi".as_ptr());
|
||||
if codec.is_null() {
|
||||
|
|
@ -29,12 +34,20 @@ impl VaapiEncoder {
|
|||
|
||||
(*codec_ctx).width = width as i32;
|
||||
(*codec_ctx).height = height as i32;
|
||||
(*codec_ctx).time_base = AVRational { num: 1, den: 60 };
|
||||
(*codec_ctx).framerate = AVRational { num: 60, den: 1 };
|
||||
(*codec_ctx).time_base = AVRational { num: 1, den: fps };
|
||||
(*codec_ctx).framerate = AVRational { num: fps, den: 1 };
|
||||
(*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI;
|
||||
(*codec_ctx).gop_size = 60; // Emit IDR keyframe every 60 frames (1 second)
|
||||
(*codec_ctx).max_b_frames = 0; // Low latency, no B-frame reordering
|
||||
(*codec_ctx).bit_rate = (bitrate_kbps as i64) * 1000;
|
||||
(*codec_ctx).gop_size = fps; // Emit IDR keyframe every second
|
||||
(*codec_ctx).max_b_frames = 0; // Zero latency, no frame reordering
|
||||
|
||||
// High Quality & Clean VBR Rate Control
|
||||
let rate = (bitrate_kbps as i64) * 1000;
|
||||
(*codec_ctx).bit_rate = rate;
|
||||
(*codec_ctx).rc_max_rate = rate * 3 / 2; // Allow burst for fast motion scenes
|
||||
(*codec_ctx).rc_buffer_size = (rate / 2) as i32;
|
||||
(*codec_ctx).qmin = 16;
|
||||
(*codec_ctx).qmax = 28; // Prevent compression blockiness
|
||||
(*codec_ctx).profile = FF_PROFILE_H264_HIGH;
|
||||
|
||||
let mut hw_device_ctx: *mut AVBufferRef = ptr::null_mut();
|
||||
let ret = av_hwdevice_ctx_create(
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ pub fn show_menu_overlay() {
|
|||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(820)
|
||||
.default_height(220)
|
||||
.default_width(860)
|
||||
.default_height(340)
|
||||
.build();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -281,16 +281,35 @@ pub fn show_menu_overlay() {
|
|||
let menu_hk_entry = Entry::new();
|
||||
menu_hk_entry.set_text(&config.menu_hotkey);
|
||||
|
||||
// Framerate Selector (Crucial for low-end PCs: 30 FPS cuts GPU load by 50%)
|
||||
let fps_lbl = Label::new(Some("Framerate (FPS):"));
|
||||
fps_lbl.set_halign(gtk::Align::Start);
|
||||
let fps_combo = ComboBoxText::new();
|
||||
fps_combo.append(Some("30"), "30 FPS (Low-End PC)");
|
||||
fps_combo.append(Some("60"), "60 FPS (Smooth / Gaming)");
|
||||
fps_combo.append(Some("120"), "120 FPS (High-End)");
|
||||
fps_combo.set_active_id(Some(&config.fps.to_string()));
|
||||
|
||||
// Save Folder
|
||||
let dir_lbl = Label::new(Some("Save Folder:"));
|
||||
dir_lbl.set_halign(gtk::Align::Start);
|
||||
let dir_entry = Entry::new();
|
||||
dir_entry.set_text(&config.output_directory);
|
||||
|
||||
settings_grid.attach(&dur_lbl, 0, 0, 1, 1);
|
||||
settings_grid.attach(&dur_combo, 1, 0, 1, 1);
|
||||
settings_grid.attach(&bit_lbl, 0, 1, 1, 1);
|
||||
settings_grid.attach(&bit_combo, 1, 1, 1, 1);
|
||||
settings_grid.attach(&auto_lbl, 0, 2, 1, 1);
|
||||
settings_grid.attach(&auto_switch, 1, 2, 1, 1);
|
||||
settings_grid.attach(&save_hk_lbl, 0, 3, 1, 1);
|
||||
settings_grid.attach(&save_hk_entry, 1, 3, 1, 1);
|
||||
settings_grid.attach(&menu_hk_lbl, 0, 4, 1, 1);
|
||||
settings_grid.attach(&menu_hk_entry, 1, 4, 1, 1);
|
||||
settings_grid.attach(&fps_lbl, 0, 2, 1, 1);
|
||||
settings_grid.attach(&fps_combo, 1, 2, 1, 1);
|
||||
settings_grid.attach(&dir_lbl, 0, 3, 1, 1);
|
||||
settings_grid.attach(&dir_entry, 1, 3, 1, 1);
|
||||
settings_grid.attach(&auto_lbl, 0, 4, 1, 1);
|
||||
settings_grid.attach(&auto_switch, 1, 4, 1, 1);
|
||||
settings_grid.attach(&save_hk_lbl, 0, 5, 1, 1);
|
||||
settings_grid.attach(&save_hk_entry, 1, 5, 1, 1);
|
||||
settings_grid.attach(&menu_hk_lbl, 0, 6, 1, 1);
|
||||
settings_grid.attach(&menu_hk_entry, 1, 6, 1, 1);
|
||||
|
||||
let save_settings_btn = Button::with_label("💾 Apply & Save Settings");
|
||||
save_settings_btn.style_context().add_class("apply-btn");
|
||||
|
|
@ -449,6 +468,14 @@ pub fn show_menu_overlay() {
|
|||
cfg.record_bitrate_kbps = bit;
|
||||
cfg.replay_bitrate_kbps = bit;
|
||||
}
|
||||
if let Some(fps_str) = fps_combo.active_id()
|
||||
&& let Ok(fps) = fps_str.parse() {
|
||||
cfg.fps = fps;
|
||||
}
|
||||
let dir_val = dir_entry.text().to_string();
|
||||
if !dir_val.trim().is_empty() {
|
||||
cfg.output_directory = dir_val;
|
||||
}
|
||||
cfg.autostart = auto_switch.is_active();
|
||||
cfg.save_hotkey = save_hk_entry.text().to_string();
|
||||
cfg.menu_hotkey = menu_hk_entry.text().to_string();
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ cargo build --release --quiet
|
|||
|
||||
SOCKET_PATH="${XDG_RUNTIME_DIR:-/tmp}/vrec.sock"
|
||||
rm -f "$SOCKET_PATH"
|
||||
rm -f test_record_*.mp4 test_replay_*.mp4
|
||||
rm -f record_*.mp4 replay_*.mp4
|
||||
|
||||
SAVE_DIR=$(python3 -c 'import json, os; print(json.load(open(os.path.expanduser("~/.config/vrec/config.json")))["output_directory"])' 2>/dev/null || echo "$HOME/Videos/vrec")
|
||||
mkdir -p "$SAVE_DIR"
|
||||
|
||||
echo "[2/6] Starting vrec-daemon with mock capture..."
|
||||
target/release/vrec-daemon --mock > daemon_test.log 2>&1 &
|
||||
DAEMON_PID=$!
|
||||
|
||||
# Ensure daemon is killed on exit
|
||||
cleanup() {
|
||||
if kill -0 $DAEMON_PID 2>/dev/null; then
|
||||
target/release/vrec-ui --quit 2>/dev/null || kill -9 $DAEMON_PID 2>/dev/null || true
|
||||
|
|
@ -69,10 +71,8 @@ echo "============================================="
|
|||
echo " Verifying Generated Recordings with ffprobe"
|
||||
echo "============================================="
|
||||
|
||||
MP4_FILES=(record_*.mp4 replay_*.mp4)
|
||||
FOUND_ANY=0
|
||||
|
||||
for f in "${MP4_FILES[@]}"; do
|
||||
for f in $(find "$SAVE_DIR" ./vrec . -maxdepth 2 \( -name "record_*.mp4" -o -name "replay_*.mp4" \) 2>/dev/null); do
|
||||
if [ -f "$f" ]; then
|
||||
FOUND_ANY=1
|
||||
echo "--> Testing file: $f ($(ls -lh "$f" | awk '{print $5}'))"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue