diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index dcc320d..e97b1c4 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -31,6 +31,8 @@ async fn main() -> Result<(), Box> { 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::(5); @@ -59,15 +61,18 @@ async fn main() -> Result<(), Box> { }); 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::::new((config.replay_duration_sec * 120).max(120) as usize); let mut normal_muxer: Option = None; let mut normal_recording = false; @@ -94,7 +99,7 @@ async fn main() -> Result<(), Box> { 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> { let new_capacity = (config.replay_duration_sec * 120).max(120) as usize; if ring.capacity().get() != new_capacity { ring = HeapRb::::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> { } }, 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> { 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> { 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); } - } } } } diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index 1bde75d..eab6254 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -8,12 +8,40 @@ fn main() -> Result<(), Box> { 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> { 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> { 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(); } } diff --git a/src/capture/audio.rs b/src/capture/audio.rs index 4b11c54..328a08b 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -6,7 +6,7 @@ pub struct AudioCapture { } impl AudioCapture { - pub fn new(sender: Sender>) -> Result<(Self, u32, u16), Box> { + pub fn new(sender: Sender>) -> Result<(Self, u32, u16), Box> { 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()); }, diff --git a/src/capture/mock.rs b/src/capture/mock.rs index f7a0cdb..e5208bf 100644 --- a/src/capture/mock.rs +++ b/src/capture/mock.rs @@ -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> { std::thread::sleep(Duration::from_millis(16)); // ~60fps diff --git a/src/capture/wayland.rs b/src/capture/wayland.rs index bd16b2c..187d69e 100644 --- a/src/capture/wayland.rs +++ b/src/capture/wayland.rs @@ -8,8 +8,21 @@ pub struct WaylandCapture { impl WaylandCapture { pub async fn new() -> Result> { - 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)?; diff --git a/src/capture/x11.rs b/src/capture/x11.rs index 30b7b28..4a0239a 100644 --- a/src/capture/x11.rs +++ b/src/capture/x11.rs @@ -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)?; diff --git a/src/config.rs b/src/config.rs index bcf0b56..3f044f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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"); + } +} + diff --git a/src/encoder.rs b/src/encoder.rs index ed258ec..f2bf2ed 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -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::new_with_bitrate(width, height, 15_000) + } + + pub fn new_with_bitrate(width: u32, height: u32, bitrate_kbps: u32) -> Result { 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); + } } } } diff --git a/src/hotkey.rs b/src/hotkey.rs index 78b97e7..58b027f 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -3,6 +3,7 @@ use global_hotkey::hotkey::{HotKey, Modifiers, Code}; pub fn parse_hotkey(s: &str) -> Option { 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 { "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 { "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()); } } + diff --git a/src/ipc.rs b/src/ipc.rs index f57415f..c3c74e4 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -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, } impl Muxer { - pub unsafe fn new(path: &str, codec_ctx: *mut AVCodecContext, audio_codec_ctx: Option<*mut AVCodecContext>) -> Result { + /// # 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 { 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); } } } diff --git a/src/overlay.rs b/src/overlay.rs index f0c6158..22cd16e 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -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"); diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 6272946..b24925f 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -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(); }