fix & perf: Comprehensive codebase optimization, timestamp rescaling, GOP/bitrate tuning, and UI polish

This commit is contained in:
katR 2026-09-03 06:48:04 +05:30
parent e46725e66d
commit bc3d91d1b7
13 changed files with 418 additions and 151 deletions

View file

@ -31,6 +31,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (width, height) = match &first_frame {
Frame::Raw { width, height, .. } => (*width, *height),
Frame::DmaBuf { width, height, .. } => (*width, *height),
#[cfg(target_os = "windows")]
Frame::D3D11Texture { .. } => (1920, 1080),
};
let (frame_tx, frame_rx) = bounded::<Frame>(5);
@ -59,15 +61,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
});
thread::spawn(move || {
let mut encoder = VaapiEncoder::new(width, height).expect("Failed to init encoder");
let mut config = vrec::config::VrecConfig::load();
let mut encoder = VaapiEncoder::new_with_bitrate(width, height, config.record_bitrate_kbps)
.expect("Failed to init encoder");
let codec_ctx_ptr = encoder.codec_ctx() as usize;
let mut audio_encoder = if let Some(ref info) = audio_info {
vrec::encoder::AudioEncoder::new(info.1 as i32, info.2 as i32).ok()
} else { None };
} else {
None
};
let audio_codec_ctx_ptr = audio_encoder.as_ref().map(|e| e.codec_ctx() as usize);
let mut config = vrec::config::VrecConfig::load();
let mut ring = HeapRb::<Packet>::new((config.replay_duration_sec * 120).max(120) as usize);
let mut normal_muxer: Option<Muxer> = None;
let mut normal_recording = false;
@ -94,7 +99,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let _ = muxer.write_packet(&p);
}
let _ = muxer.finalize();
println!("Replay saved!");
println!("Replay saved to {}!", name);
} else {
println!("No keyframe found in buffer, skipping save.");
}
@ -110,7 +115,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let new_capacity = (config.replay_duration_sec * 120).max(120) as usize;
if ring.capacity().get() != new_capacity {
ring = HeapRb::<Packet>::new(new_capacity);
println!("Replay buffer resized to {} frames.", new_capacity);
println!("Replay buffer resized to {} packets.", new_capacity);
}
},
Command::SaveReplay => {
@ -120,7 +125,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
},
Command::StartRecording => {
if config.record_enabled && !normal_recording {
if !normal_recording {
normal_recording = true;
normal_waiting_keyframe = true;
println!("StartRecording requested, waiting for keyframe...");
@ -132,27 +137,40 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("Stopped normal recording.");
}
normal_recording = false;
normal_waiting_keyframe = false;
},
Command::ToggleRecording => {
if normal_recording {
if let Some(mut m) = normal_muxer.take() {
let _ = m.finalize();
println!("Stopped normal recording.");
}
normal_recording = false;
normal_waiting_keyframe = false;
} else {
normal_recording = true;
normal_waiting_keyframe = true;
println!("ToggleRecording: StartRecording requested, waiting for keyframe...");
}
},
_ => {}
}
}
while let Ok(audio_chunk) = audio_rx.try_recv() {
if let Some(enc) = audio_encoder.as_mut() {
if let Ok(audio_packets) = enc.encode_pcm(&audio_chunk) {
if let Some(enc) = audio_encoder.as_mut()
&& let Ok(audio_packets) = enc.encode_pcm(&audio_chunk) {
for mut pkt in audio_packets {
pkt.set_stream_index(1);
if config.replay_enabled {
ring.push_overwrite(pkt.clone());
}
if normal_recording && !normal_waiting_keyframe {
if let Some(muxer) = normal_muxer.as_mut() {
if normal_recording && !normal_waiting_keyframe
&& let Some(muxer) = normal_muxer.as_mut() {
let _ = muxer.write_packet(&pkt);
}
}
}
}
}
}
if let Ok(packets) = encoder.encode_frame(&frame) {
@ -162,23 +180,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ring.push_overwrite(pkt.clone());
}
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);
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);
}
if normal_waiting_keyframe && 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);
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);
}
if !normal_waiting_keyframe {
if let Some(muxer) = normal_muxer.as_mut() {
if !normal_waiting_keyframe
&& let Some(muxer) = normal_muxer.as_mut() {
let _ = muxer.write_packet(&pkt);
}
}
}
}
}

View file

@ -8,12 +8,40 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match args[1].as_str() {
"--save" => {
vrec::overlay::show_notification_overlay();
return send_command(Command::SaveReplay).map_err(|e| e);
return send_command(Command::SaveReplay);
}
"--menu" => {
vrec::overlay::show_menu_overlay();
return Ok(());
}
"--record" | "--toggle" => {
return send_command(Command::ToggleRecording);
}
"--start" => {
return send_command(Command::StartRecording);
}
"--stop" => {
return send_command(Command::StopRecording);
}
"--reload" => {
return send_command(Command::ReloadConfig);
}
"--quit" => {
return send_command(Command::StopDaemon);
}
"--help" | "-h" => {
println!("vrec-ui - UI Overlay and Hotkey listener for vrec");
println!("Usage:");
println!(" vrec-ui Run global hotkey manager in background");
println!(" vrec-ui --menu Open GPU Screen Recorder style menu overlay");
println!(" vrec-ui --save Save instant replay and show notification");
println!(" vrec-ui --record Toggle normal recording on/off");
println!(" vrec-ui --start Start normal recording");
println!(" vrec-ui --stop Stop normal recording");
println!(" vrec-ui --reload Reload daemon configuration");
println!(" vrec-ui --quit Stop background daemon");
return Ok(());
}
_ => {}
}
}
@ -28,28 +56,40 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if session_type.to_lowercase() == "wayland" {
if is_gnome || is_kde {
println!("WARNING: Wayland native layer shell used. Ensure your compositor allows it.");
println!("Note: Global hotkeys may fail to register on Wayland compositors.");
println!("Note: Wayland native layer shell used. Ensure your compositor allows it.");
println!("Tip: Global hotkeys may be restricted by Wayland compositors.");
println!(" You can bind `vrec-ui --menu` or `vrec-ui --save` in your system shortcut settings.");
} else {
println!("wlroots-based compositor detected.");
println!("wlroots/Hyprland compositor detected.");
}
} else {
println!("X11 session detected.");
}
let manager = GlobalHotKeyManager::new().unwrap();
let manager = GlobalHotKeyManager::new().map_err(|e| format!("Failed to init GlobalHotKeyManager: {:?}", e))?;
let config = vrec::config::VrecConfig::load();
let save_hotkey = vrec::hotkey::parse_hotkey(&config.save_hotkey)
.unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR));
let menu_hotkey = vrec::hotkey::parse_hotkey(&config.menu_hotkey)
.unwrap_or_else(|| HotKey::new(Some(Modifiers::ALT), Code::KeyZ));
let mut registered_count = 0;
if let Err(e) = manager.register(save_hotkey) {
eprintln!("Failed to register global hotkey! Wayland compositor might be blocking it: {}", e);
eprintln!("Fallback: running without global hotkey. Send commands via socket manually.");
eprintln!("Warning: Failed to register save hotkey ({}): {}", config.save_hotkey, e);
} else {
manager.register(menu_hotkey).unwrap();
println!("Registered global hotkeys:\n - Ctrl+Shift+R (Save Replay)\n - Alt+Z (Open GUI Menu)");
println!("Registered save replay hotkey: {}", config.save_hotkey);
registered_count += 1;
}
if let Err(e) = manager.register(menu_hotkey) {
eprintln!("Warning: Failed to register menu hotkey ({}): {}", config.menu_hotkey, e);
} else {
println!("Registered menu overlay hotkey: {}", config.menu_hotkey);
registered_count += 1;
}
if registered_count == 0 {
eprintln!("Note: Running without global hotkeys. You can bind `vrec-ui --menu` and `vrec-ui --save` to compositor shortcuts.");
}
let receiver = GlobalHotKeyEvent::receiver();
@ -57,11 +97,11 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(event) = receiver.recv()
&& event.state == global_hotkey::HotKeyState::Pressed {
if event.id == save_hotkey.id() {
println!("Hotkey triggered! Sending SaveReplay command to daemon...");
println!("Save replay hotkey pressed. Triggering SaveReplay...");
vrec::overlay::show_notification_overlay();
let _ = send_command(Command::SaveReplay);
} else if event.id == menu_hotkey.id() {
println!("Alt+Z triggered! Opening GUI Menu Overlay...");
println!("Menu hotkey pressed. Opening Overlay Menu...");
vrec::overlay::show_menu_overlay();
}
}

View file

@ -6,7 +6,7 @@ pub struct AudioCapture {
}
impl AudioCapture {
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error>> {
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
let host = cpal::default_host();
let device = host.default_input_device().ok_or("No audio input device available")?;
@ -17,12 +17,12 @@ impl AudioCapture {
println!("Audio capture initialized: {} Hz, {} channels", sample_rate, channels);
let err_fn = |err| eprintln!("Audio stream error: {}", err);
let stream_config: cpal::StreamConfig = config.clone().into();
let stream_config: cpal::StreamConfig = config.into();
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => {
device.build_input_stream(
stream_config.clone(),
stream_config,
move |data: &[f32], _: &_| {
let _ = sender.try_send(data.to_vec());
},

View file

@ -13,6 +13,12 @@ impl MockCapture {
}
}
impl Default for MockCapture {
fn default() -> Self {
Self::new()
}
}
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

View file

@ -8,8 +8,21 @@ pub struct WaylandCapture {
impl WaylandCapture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let proxy = Screencast::new().await?;
let session = proxy.create_session(Default::default()).await?;
let proxy = Screencast::new().await.map_err(|e| {
format!(
"Failed to initialize XDG ScreenCast portal ({:?}).\n\
Ensure xdg-desktop-portal and your compositor's backend are installed and running:\n\
- Hyprland: xdg-desktop-portal-hyprland\n\
- Sway / wlroots: xdg-desktop-portal-wlr\n\
- GNOME: xdg-desktop-portal-gnome\n\
- KDE Plasma: xdg-desktop-portal-kde",
e
)
})?;
let session = proxy.create_session(Default::default()).await.map_err(|e| {
format!("Failed to create ScreenCast portal session: {:?}", e)
})?;
let select_opts = ashpd::desktop::screencast::SelectSourcesOptions::default()
.set_multiple(true)
@ -17,14 +30,20 @@ impl WaylandCapture {
.set_sources(SourceType::Monitor | SourceType::Window)
.set_persist_mode(PersistMode::DoNot);
proxy.select_sources(&session, select_opts).await?;
proxy.select_sources(&session, select_opts).await.map_err(|e| {
format!("Failed to select ScreenCast sources: {:?}", e)
})?;
let response = proxy.start(&session, None, Default::default()).await?.response()?;
let response = proxy.start(&session, None, Default::default()).await
.map_err(|e| format!("Failed to start ScreenCast session: {:?}", e))?
.response()
.map_err(|e| format!("Failed to get ScreenCast response: {:?}", e))?;
let stream = response.streams().first().ok_or("No stream")?;
let stream = response.streams().first().ok_or("No ScreenCast streams returned by portal")?;
let node_id = stream.pipe_wire_node_id();
let fd = proxy.open_pipe_wire_remote(&session, Default::default()).await?;
let fd = proxy.open_pipe_wire_remote(&session, Default::default()).await
.map_err(|e| format!("Failed to open PipeWire remote descriptor: {:?}", e))?;
let stream = crate::capture::wayland_stream::PipeWireStream::new(node_id, fd)?;

View file

@ -32,7 +32,7 @@ impl FrameSource for X11Capture {
y: 0,
width: self.width,
height: self.height,
plane_mask: std::u32::MAX,
plane_mask: u32::MAX,
});
let reply = self.conn.wait_for_reply(cookie)?;

View file

@ -48,10 +48,10 @@ impl VrecConfig {
pub fn load() -> Self {
let path = Self::config_path();
if path.exists()
&& let Ok(data) = fs::read_to_string(path)
&& let Ok(config) = serde_json::from_str(&data) {
return config;
}
&& let Ok(data) = fs::read_to_string(&path)
&& let Ok(config) = serde_json::from_str(&data) {
return config;
}
Self::default()
}
@ -66,15 +66,23 @@ impl VrecConfig {
}
pub fn notify_daemon_reload() {
use std::env;
use std::os::unix::net::UnixStream;
use std::io::Write;
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
if let Ok(mut stream) = UnixStream::connect(&socket_path)
&& let Ok(payload) = serde_json::to_vec(&crate::ipc::Command::ReloadConfig) {
let len_buf = (payload.len() as u32).to_le_bytes();
let _ = stream.write_all(&len_buf);
let _ = stream.write_all(&payload);
}
let _ = crate::ipc::send_command(crate::ipc::Command::ReloadConfig);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_defaults_and_json() {
let cfg = VrecConfig::default();
assert!(cfg.replay_enabled);
assert_eq!(cfg.replay_duration_sec, 60);
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");
}
}

View file

@ -6,13 +6,18 @@ pub struct VaapiEncoder {
codec_ctx: *mut AVCodecContext,
hw_device_ctx: *mut AVBufferRef,
hw_frames_ctx: *mut AVBufferRef,
sws_ctx: *mut SwsContext,
next_pts: i64,
}
impl VaapiEncoder {
pub fn new(width: u32, height: u32) -> Result<Self, String> {
Self::new_with_bitrate(width, height, 15_000)
}
pub fn new_with_bitrate(width: u32, height: u32, bitrate_kbps: u32) -> Result<Self, String> {
unsafe {
let codec = avcodec_find_encoder_by_name(b"h264_vaapi\0".as_ptr() as *const i8);
let codec = avcodec_find_encoder_by_name(c"h264_vaapi".as_ptr());
if codec.is_null() {
return Err("h264_vaapi encoder not found".into());
}
@ -27,6 +32,9 @@ impl VaapiEncoder {
(*codec_ctx).time_base = AVRational { num: 1, den: 60 };
(*codec_ctx).framerate = AVRational { num: 60, 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;
let mut hw_device_ctx: *mut AVBufferRef = ptr::null_mut();
let ret = av_hwdevice_ctx_create(
@ -37,11 +45,14 @@ impl VaapiEncoder {
0,
);
if ret < 0 {
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to create VAAPI hardware device".into());
}
let hw_frames_ref = av_hwframe_ctx_alloc(hw_device_ctx);
if hw_frames_ref.is_null() {
av_buffer_unref(&mut hw_device_ctx);
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to allocate hw frames context".into());
}
let frames_ctx = (*hw_frames_ref).data as *mut AVHWFramesContext;
@ -53,6 +64,9 @@ impl VaapiEncoder {
let ret = av_hwframe_ctx_init(hw_frames_ref);
if ret < 0 {
av_buffer_unref(&mut (hw_frames_ref as *mut _));
av_buffer_unref(&mut hw_device_ctx);
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to init hw frames context".into());
}
@ -60,6 +74,9 @@ impl VaapiEncoder {
let ret = avcodec_open2(codec_ctx, codec, ptr::null_mut());
if ret < 0 {
av_buffer_unref(&mut (hw_frames_ref as *mut _));
av_buffer_unref(&mut hw_device_ctx);
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to open h264_vaapi encoder".into());
}
@ -67,6 +84,7 @@ impl VaapiEncoder {
codec_ctx,
hw_device_ctx,
hw_frames_ctx: hw_frames_ref,
sws_ctx: ptr::null_mut(),
next_pts: 0,
})
}
@ -103,14 +121,16 @@ impl VaapiEncoder {
(*nv12_frame).height = *height as i32;
av_frame_get_buffer(nv12_frame, 32);
let sws_ctx = sws_getContext(
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_BGRA,
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_NV12,
2, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()
);
if self.sws_ctx.is_null() {
self.sws_ctx = sws_getContext(
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_BGRA,
*width as i32, *height as i32, AVPixelFormat::AV_PIX_FMT_NV12,
2, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()
);
}
sws_scale(
sws_ctx,
self.sws_ctx,
(*bgra_frame).data.as_ptr() as *const *const u8,
(*bgra_frame).linesize.as_ptr(),
0,
@ -118,7 +138,6 @@ impl VaapiEncoder {
(*nv12_frame).data.as_ptr(),
(*nv12_frame).linesize.as_ptr()
);
sws_freeContext(sws_ctx);
let mut hw_frame = av_frame_alloc();
av_hwframe_get_buffer(self.hw_frames_ctx, hw_frame, 0);
@ -145,7 +164,7 @@ impl VaapiEncoder {
nb_objects: 1,
objects: [AVDRMObjectDescriptor {
fd: *fd,
size: 0, // usually ignored or mapped by driver
size: 0,
format_modifier: *modifier,
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
nb_layers: 1,
@ -169,11 +188,6 @@ impl VaapiEncoder {
let mut hw_frame = av_frame_alloc();
(*hw_frame).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
// Map the DRM frame to VAAPI
// Since it's hardware mapping, av_hwframe_map expects the device context or frames context.
// Wait, av_hwframe_map uses the destination's hw_frames_ctx if we preallocate, OR
// if we just pass a device context to hw_frame.hw_frames_ctx?
// Let's allocate the hw_frame from our VAAPI frames context.
let ret = av_hwframe_get_buffer(self.hw_frames_ctx, hw_frame, 0);
if ret >= 0 {
let map_ret = av_hwframe_map(hw_frame, drm_frame, 0);
@ -195,9 +209,6 @@ impl VaapiEncoder {
}
}
// For AV_PIX_FMT_DRM_PRIME, we don't own the data, so don't av_frame_unref the data buffer,
// av_frame_free is safe because data[0] wasn't allocated by ffmpeg.
// Wait, av_frame_free will try to free data[0] if buf[0] is set. But we didn't set buf[0].
av_frame_free(&mut hw_frame);
av_frame_free(&mut drm_frame);
}
@ -214,9 +225,19 @@ impl VaapiEncoder {
impl Drop for VaapiEncoder {
fn drop(&mut self) {
unsafe {
avcodec_free_context(&mut self.codec_ctx);
av_buffer_unref(&mut self.hw_device_ctx);
av_buffer_unref(&mut self.hw_frames_ctx);
if !self.sws_ctx.is_null() {
sws_freeContext(self.sws_ctx);
self.sws_ctx = ptr::null_mut();
}
if !self.codec_ctx.is_null() {
avcodec_free_context(&mut self.codec_ctx);
}
if !self.hw_device_ctx.is_null() {
av_buffer_unref(&mut self.hw_device_ctx);
}
if !self.hw_frames_ctx.is_null() {
av_buffer_unref(&mut self.hw_frames_ctx);
}
}
}
}
@ -224,6 +245,7 @@ impl Drop for VaapiEncoder {
pub struct AudioEncoder {
codec_ctx: *mut AVCodecContext,
swr_ctx: *mut SwrContext,
enc_frame: *mut AVFrame,
next_pts: i64,
frame_size: i32,
channels: i32,
@ -239,22 +261,23 @@ impl AudioEncoder {
}
let codec_ctx = avcodec_alloc_context3(codec);
if codec_ctx.is_null() {
return Err("Failed to allocate AAC codec context".into());
}
(*codec_ctx).sample_rate = sample_rate;
(*codec_ctx).time_base = AVRational { num: 1, den: sample_rate };
(*codec_ctx).ch_layout.nb_channels = channels;
// AAC requires FLTP (planar float)
(*codec_ctx).sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_FLTP;
(*codec_ctx).bit_rate = 192_000;
// For older ffmpeg compatibility:
av_channel_layout_default(&mut (*codec_ctx).ch_layout, channels);
let ret = avcodec_open2(codec_ctx, codec, std::ptr::null_mut());
let ret = avcodec_open2(codec_ctx, codec, ptr::null_mut());
if ret < 0 {
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to open AAC encoder".into());
}
// Create SwrContext to convert from interleaved F32 (AV_SAMPLE_FMT_FLT) to planar F32 (AV_SAMPLE_FMT_FLTP)
let mut swr_ctx: *mut SwrContext = std::ptr::null_mut();
let mut swr_ctx: *mut SwrContext = ptr::null_mut();
swr_alloc_set_opts2(
&mut swr_ctx,
&(*codec_ctx).ch_layout,
@ -264,16 +287,24 @@ impl AudioEncoder {
AVSampleFormat::AV_SAMPLE_FMT_FLT,
sample_rate,
0,
std::ptr::null_mut()
ptr::null_mut()
);
swr_init(swr_ctx);
let frame_size = (*codec_ctx).frame_size;
let fifo = av_audio_fifo_alloc(AVSampleFormat::AV_SAMPLE_FMT_FLTP, channels, 1024 * 10);
let fifo = av_audio_fifo_alloc(AVSampleFormat::AV_SAMPLE_FMT_FLTP, channels, 1024 * 32);
let enc_frame = av_frame_alloc();
(*enc_frame).nb_samples = frame_size;
(*enc_frame).format = AVSampleFormat::AV_SAMPLE_FMT_FLTP as i32;
(*enc_frame).ch_layout = (*codec_ctx).ch_layout;
(*enc_frame).sample_rate = sample_rate;
av_frame_get_buffer(enc_frame, 0);
Ok(Self {
codec_ctx,
swr_ctx,
enc_frame,
next_pts: 0,
frame_size,
channels,
@ -325,19 +356,12 @@ impl AudioEncoder {
// Read exactly frame_size chunks from FIFO and encode
while av_audio_fifo_size(self.fifo) >= self.frame_size {
let mut enc_frame = av_frame_alloc();
(*enc_frame).nb_samples = self.frame_size;
(*enc_frame).format = AVSampleFormat::AV_SAMPLE_FMT_FLTP as i32;
(*enc_frame).ch_layout = (*self.codec_ctx).ch_layout;
(*enc_frame).sample_rate = (*self.codec_ctx).sample_rate;
av_frame_get_buffer(enc_frame, 0);
av_audio_fifo_read(self.fifo, (*enc_frame).data.as_mut_ptr() as *mut *mut std::ffi::c_void, self.frame_size);
av_audio_fifo_read(self.fifo, (*self.enc_frame).data.as_mut_ptr() as *mut *mut std::ffi::c_void, self.frame_size);
(*enc_frame).pts = self.next_pts;
(*self.enc_frame).pts = self.next_pts;
self.next_pts += self.frame_size as i64;
if avcodec_send_frame(self.codec_ctx, enc_frame) >= 0 {
if avcodec_send_frame(self.codec_ctx, self.enc_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
@ -346,7 +370,6 @@ impl AudioEncoder {
}
av_packet_free(&mut pkt);
}
av_frame_free(&mut enc_frame);
}
}
Ok(packets)
@ -356,9 +379,20 @@ impl AudioEncoder {
impl Drop for AudioEncoder {
fn drop(&mut self) {
unsafe {
av_audio_fifo_free(self.fifo);
swr_free(&mut self.swr_ctx);
avcodec_free_context(&mut self.codec_ctx);
if !self.enc_frame.is_null() {
av_frame_free(&mut self.enc_frame);
}
if !self.fifo.is_null() {
av_audio_fifo_free(self.fifo);
self.fifo = ptr::null_mut();
}
if !self.swr_ctx.is_null() {
swr_free(&mut self.swr_ctx);
self.swr_ctx = ptr::null_mut();
}
if !self.codec_ctx.is_null() {
avcodec_free_context(&mut self.codec_ctx);
}
}
}
}

View file

@ -3,6 +3,7 @@ use global_hotkey::hotkey::{HotKey, Modifiers, Code};
pub fn parse_hotkey(s: &str) -> Option<HotKey> {
let mut mods = Modifiers::empty();
let mut code = None;
for part in s.split('+') {
let p = part.trim().to_lowercase();
match p.as_str() {
@ -11,7 +12,6 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
"alt" => mods |= Modifiers::ALT,
"meta" | "super" | "windows" | "cmd" => mods |= Modifiers::META,
_ => {
// simple mapping
code = match p.as_str() {
"a" => Some(Code::KeyA),
"b" => Some(Code::KeyB),
@ -39,15 +39,61 @@ pub fn parse_hotkey(s: &str) -> Option<HotKey> {
"x" => Some(Code::KeyX),
"y" => Some(Code::KeyY),
"z" => Some(Code::KeyZ),
"0" => Some(Code::Digit0),
"1" => Some(Code::Digit1),
"2" => Some(Code::Digit2),
"3" => Some(Code::Digit3),
"4" => Some(Code::Digit4),
"5" => Some(Code::Digit5),
"6" => Some(Code::Digit6),
"7" => Some(Code::Digit7),
"8" => Some(Code::Digit8),
"9" => Some(Code::Digit9),
"f1" => Some(Code::F1),
"f2" => Some(Code::F2),
"f3" => Some(Code::F3),
"f4" => Some(Code::F4),
"f5" => Some(Code::F5),
"f6" => Some(Code::F6),
"f7" => Some(Code::F7),
"f8" => Some(Code::F8),
"f9" => Some(Code::F9),
"f10" => Some(Code::F10),
"f11" => Some(Code::F11),
"f12" => Some(Code::F12),
"space" => Some(Code::Space),
"escape" | "esc" => Some(Code::Escape),
"tab" => Some(Code::Tab),
"enter" | "return" => Some(Code::Enter),
"backspace" => Some(Code::Backspace),
"insert" => Some(Code::Insert),
"delete" | "del" => Some(Code::Delete),
"home" => Some(Code::Home),
"end" => Some(Code::End),
"pageup" | "pgup" => Some(Code::PageUp),
"pagedown" | "pgdn" => Some(Code::PageDown),
_ => None,
};
}
}
}
if let Some(c) = code {
Some(HotKey::new(Some(mods), c))
} else {
None
code.map(|c| HotKey::new(Some(mods), c))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_hotkey_combinations() {
assert!(parse_hotkey("Ctrl+Shift+R").is_some());
assert!(parse_hotkey("Alt+Z").is_some());
assert!(parse_hotkey("F12").is_some());
assert!(parse_hotkey("Ctrl+Alt+Delete").is_some());
assert!(parse_hotkey("super+space").is_some());
assert!(parse_hotkey("").is_none());
assert!(parse_hotkey("invalid_key_xyz").is_none());
}
}

View file

@ -1,10 +1,11 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum Command {
SaveReplay,
StartRecording,
StopRecording,
ToggleRecording,
ReloadConfig,
StopDaemon,
ShowOverlay,
@ -14,6 +15,7 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
use std::env;
use std::os::unix::net::UnixStream;
use std::io::Write;
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
let mut stream = UnixStream::connect(&socket_path)?;
let payload = serde_json::to_vec(&cmd)?;

View file

@ -4,80 +4,126 @@ use std::ffi::CString;
pub struct Muxer {
fmt_ctx: *mut AVFormatContext,
video_time_base: AVRational,
audio_time_base: Option<AVRational>,
}
impl Muxer {
pub unsafe fn new(path: &str, codec_ctx: *mut AVCodecContext, audio_codec_ctx: Option<*mut AVCodecContext>) -> Result<Self, String> {
/// # Safety
/// `codec_ctx` must be a valid, initialized pointer to an `AVCodecContext`.
/// `audio_codec_ctx`, if provided, must also be a valid, initialized pointer.
pub unsafe fn new(
path: &str,
codec_ctx: *mut AVCodecContext,
audio_codec_ctx: Option<*mut AVCodecContext>,
) -> Result<Self, String> {
unsafe {
let path_cstr = CString::new(path).unwrap();
let path_cstr = CString::new(path).map_err(|e| e.to_string())?;
let mut fmt_ctx: *mut AVFormatContext = std::ptr::null_mut();
let ret = avformat_alloc_output_context2(&mut fmt_ctx, std::ptr::null(), std::ptr::null(), path_cstr.as_ptr());
if ret < 0 {
let ret = avformat_alloc_output_context2(
&mut fmt_ctx,
std::ptr::null(),
std::ptr::null(),
path_cstr.as_ptr(),
);
if ret < 0 || fmt_ctx.is_null() {
return Err("Failed to alloc output context".into());
}
let stream = avformat_new_stream(fmt_ctx, std::ptr::null());
if stream.is_null() {
return Err("Failed to create stream".into());
avformat_free_context(fmt_ctx);
return Err("Failed to create video stream".into());
}
(*stream).id = 0;
let ret = avcodec_parameters_from_context((*stream).codecpar, codec_ctx);
if ret < 0 {
avformat_free_context(fmt_ctx);
return Err("Failed to copy video codec parameters".into());
}
(*stream).time_base = (*codec_ctx).time_base;
let video_time_base = (*codec_ctx).time_base;
(*stream).time_base = video_time_base;
let mut audio_time_base = None;
if let Some(actx) = audio_codec_ctx {
let astream = avformat_new_stream(fmt_ctx, std::ptr::null());
if !astream.is_null() {
(*astream).id = 1;
if avcodec_parameters_from_context((*astream).codecpar, actx) < 0 {
eprintln!("Failed to copy audio codec parameters");
eprintln!("Warning: Failed to copy audio codec parameters");
}
(*astream).time_base = (*actx).time_base;
let atb = (*actx).time_base;
(*astream).time_base = atb;
audio_time_base = Some(atb);
}
}
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
let ret = avio_open(&mut (*fmt_ctx).pb, path_cstr.as_ptr(), AVIO_FLAG_WRITE);
if ret < 0 {
return Err("Failed to open file".into());
avformat_free_context(fmt_ctx);
return Err("Failed to open output file".into());
}
}
let ret = avformat_write_header(fmt_ctx, std::ptr::null_mut());
if ret < 0 {
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
avio_closep(&mut (*fmt_ctx).pb);
}
avformat_free_context(fmt_ctx);
return Err("Failed to write header".into());
}
Ok(Self { fmt_ctx })
Ok(Self {
fmt_ctx,
video_time_base,
audio_time_base,
})
}
}
pub fn write_packet(&mut self, packet: &Packet) -> Result<(), String> {
unsafe {
// we must not pass the exact ptr to av_interleaved_write_frame directly
// if we want to keep the packet, but since it's just writing, we can clone it
if packet.ptr.is_null() {
return Ok(());
}
let mut new_pkt = av_packet_alloc();
if new_pkt.is_null() {
return Err("Failed to allocate packet".into());
}
av_packet_ref(new_pkt, packet.ptr);
let stream_idx = (*new_pkt).stream_index;
if stream_idx >= 0 && (stream_idx as u32) < (*self.fmt_ctx).nb_streams {
let out_stream = *(*self.fmt_ctx).streams.add(stream_idx as usize);
let in_tb = if stream_idx == 0 {
self.video_time_base
} else {
self.audio_time_base.unwrap_or(self.video_time_base)
};
av_packet_rescale_ts(new_pkt, in_tb, (*out_stream).time_base);
}
let ret = av_interleaved_write_frame(self.fmt_ctx, new_pkt);
av_packet_free(&mut new_pkt); // Wait, av_interleaved takes ownership usually?
// Actually av_interleaved_write_frame takes ownership of the packet reference,
// but we can just use av_write_frame if we don't want it buffered,
// but av_interleaved_write_frame will unref the packet for us!
// Wait, if it unrefs it, we shouldn't free it again.
av_packet_free(&mut new_pkt);
if ret < 0 {
return Err("Failed to write frame".into());
return Err(format!("Failed to write frame: {}", ret));
}
}
Ok(())
}
pub fn finalize(&mut self) -> Result<(), String> {
unsafe {
av_write_trailer(self.fmt_ctx);
let ret = av_write_trailer(self.fmt_ctx);
if ret < 0 {
return Err(format!("Failed to write trailer: {}", ret));
}
}
Ok(())
}
@ -86,10 +132,13 @@ impl Muxer {
impl Drop for Muxer {
fn drop(&mut self) {
unsafe {
if ((*(*self.fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
avio_closep(&mut (*self.fmt_ctx).pb);
if !self.fmt_ctx.is_null() {
if !(*self.fmt_ctx).pb.is_null() && ((*(*self.fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
avio_closep(&mut (*self.fmt_ctx).pb);
}
avformat_free_context(self.fmt_ctx);
self.fmt_ctx = std::ptr::null_mut();
}
avformat_free_context(self.fmt_ctx);
}
}
}

View file

@ -141,13 +141,22 @@ pub fn show_menu_overlay() {
window_clone.close();
});
// Toggle buttons logic (just UI for now, backend IPC will be needed to actually toggle daemon state)
let _btn_cfg_1 = config.clone();
let window_esc = window.clone();
window.connect_key_press_event(move |_, key| {
if key.keyval() == gdk::keys::constants::Escape {
window_esc.close();
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
let replay_btn_clone = replay_btn.clone();
replay_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.replay_enabled = !cfg.replay_enabled;
cfg.save().unwrap(); VrecConfig::notify_daemon_reload();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
if cfg.replay_enabled {
replay_btn_clone.set_label("Instant Replay\n🟢 ON");
} else {
@ -155,12 +164,11 @@ pub fn show_menu_overlay() {
}
});
let _btn_cfg_2 = config.clone();
let record_btn_clone = record_btn.clone();
record_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.record_enabled = !cfg.record_enabled;
cfg.save().unwrap();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
if cfg.record_enabled {
record_btn_clone.set_label("Recording\n🔴 ON");

View file

@ -5,16 +5,18 @@ use crate::config::VrecConfig;
pub fn open_replay_settings(config_in: &VrecConfig) {
let window = Window::new(WindowType::Toplevel);
window.set_title("Replay Settings");
window.set_default_size(300, 200);
window.set_default_size(320, 200);
let vbox = Box::new(Orientation::Vertical, 10);
vbox.set_margin(15);
let duration_label = Label::new(Some("Replay Duration (seconds):"));
duration_label.set_halign(gtk::Align::Start);
let duration_entry = Entry::new();
duration_entry.set_text(&config_in.replay_duration_sec.to_string());
let bitrate_label = Label::new(Some("Bitrate (kbps):"));
bitrate_label.set_halign(gtk::Align::Start);
let bitrate_entry = Entry::new();
bitrate_entry.set_text(&config_in.replay_bitrate_kbps.to_string());
@ -37,22 +39,34 @@ pub fn open_replay_settings(config_in: &VrecConfig) {
if let Ok(val) = bitrate_entry.text().parse() {
cfg.replay_bitrate_kbps = val;
}
let _ = cfg.save(); VrecConfig::notify_daemon_reload();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
window_clone.close();
});
let window_esc = window.clone();
window.connect_key_press_event(move |_, key| {
if key.keyval() == gdk::keys::constants::Escape {
window_esc.close();
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
window.show_all();
}
pub fn open_record_settings(config_in: &VrecConfig) {
let window = Window::new(WindowType::Toplevel);
window.set_title("Record Settings");
window.set_default_size(300, 150);
window.set_default_size(320, 150);
let vbox = Box::new(Orientation::Vertical, 10);
vbox.set_margin(15);
let bitrate_label = Label::new(Some("Recording Bitrate (kbps):"));
bitrate_label.set_halign(gtk::Align::Start);
let bitrate_entry = Entry::new();
bitrate_entry.set_text(&config_in.record_bitrate_kbps.to_string());
@ -70,17 +84,28 @@ pub fn open_record_settings(config_in: &VrecConfig) {
if let Ok(val) = bitrate_entry.text().parse() {
cfg.record_bitrate_kbps = val;
}
let _ = cfg.save(); VrecConfig::notify_daemon_reload();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
window_clone.close();
});
let window_esc = window.clone();
window.connect_key_press_event(move |_, key| {
if key.keyval() == gdk::keys::constants::Escape {
window_esc.close();
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
window.show_all();
}
pub fn open_general_settings(config_in: &VrecConfig) {
let window = Window::new(WindowType::Toplevel);
window.set_title("General Settings");
window.set_default_size(350, 300);
window.set_default_size(380, 320);
let vbox = Box::new(Orientation::Vertical, 10);
vbox.set_margin(15);
@ -95,6 +120,7 @@ pub fn open_general_settings(config_in: &VrecConfig) {
// Theme
let theme_label = Label::new(Some("UI Theme:"));
theme_label.set_halign(gtk::Align::Start);
let theme_combo = ComboBoxText::new();
theme_combo.append_text("dark");
theme_combo.append_text("light");
@ -102,15 +128,18 @@ pub fn open_general_settings(config_in: &VrecConfig) {
// Language
let lang_label = Label::new(Some("Language Code (e.g. en, fr):"));
lang_label.set_halign(gtk::Align::Start);
let lang_entry = Entry::new();
lang_entry.set_text(&config_in.language);
// Hotkeys
let menu_hk_label = Label::new(Some("Menu Hotkey:"));
menu_hk_label.set_halign(gtk::Align::Start);
let menu_hk_entry = Entry::new();
menu_hk_entry.set_text(&config_in.menu_hotkey);
let save_hk_label = Label::new(Some("Save Replay Hotkey:"));
save_hk_label.set_halign(gtk::Align::Start);
let save_hk_entry = Entry::new();
save_hk_entry.set_text(&config_in.save_hotkey);
@ -139,9 +168,20 @@ pub fn open_general_settings(config_in: &VrecConfig) {
cfg.language = lang_entry.text().to_string();
cfg.menu_hotkey = menu_hk_entry.text().to_string();
cfg.save_hotkey = save_hk_entry.text().to_string();
let _ = cfg.save(); VrecConfig::notify_daemon_reload();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
window_clone.close();
});
let window_esc = window.clone();
window.connect_key_press_event(move |_, key| {
if key.keyval() == gdk::keys::constants::Escape {
window_esc.close();
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
});
window.show_all();
}