From a5f7c458812e824c3e7b3ed0c5370f01c5338c46 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Thu, 3 Sep 2026 07:59:56 +0530 Subject: [PATCH] feat: Add audio device enumeration/selection and Windows hardware encoder auto-detection (NVENC/AMF/QSV/libx264) --- src/bin/vrec-daemon.rs | 7 +- src/capture/audio.rs | 41 ++++++++- src/config.rs | 2 + src/encoder.rs | 194 +++++++++++++++++++++++++++++++++++++++++ src/overlay.rs | 33 +++++-- 5 files changed, 263 insertions(+), 14 deletions(-) diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index d264dbe..e3c5177 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -1,5 +1,5 @@ use vrec::capture::{Frame, FrameSource}; -use vrec::encoder::VaapiEncoder; +use vrec::encoder::VideoEncoder; use vrec::ring::Packet; use vrec::muxer::Muxer; use vrec::ipc::{Command, DaemonStatus}; @@ -64,7 +64,8 @@ async fn main() -> Result<(), Box> { let (cmd_tx, cmd_rx) = bounded::(10); let (mux_tx, mux_rx) = bounded::>(1); let (audio_tx, audio_rx) = bounded::>(100); - let audio_info = vrec::capture::audio::AudioCapture::new(audio_tx).ok(); + let initial_config = vrec::config::VrecConfig::load(); + let audio_info = vrec::capture::audio::AudioCapture::new_with_device(audio_tx, Some(&initial_config.audio_device)).ok(); let is_recording_state = Arc::new(AtomicBool::new(false)); let record_start_state = Arc::new(AtomicU64::new(0)); @@ -99,7 +100,7 @@ async fn main() -> Result<(), Box> { let mut config = vrec::config::VrecConfig::load(); replay_state_clone.store(config.replay_enabled, Ordering::SeqCst); - let mut encoder = VaapiEncoder::new_with_params(width, height, config.record_bitrate_kbps, config.fps) + let mut encoder = VideoEncoder::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; diff --git a/src/capture/audio.rs b/src/capture/audio.rs index 328a08b..1ff935d 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -5,16 +5,53 @@ pub struct AudioCapture { _stream: cpal::Stream, } +pub fn get_device_name(dev: &cpal::Device) -> String { + dev.description() + .map(|d| d.name().to_string()) + .unwrap_or_else(|_| dev.to_string()) +} + +pub fn list_input_devices() -> Vec { + let host = cpal::default_host(); + let mut names = Vec::new(); + if let Ok(devices) = host.input_devices() { + for dev in devices { + names.push(get_device_name(&dev)); + } + } + names +} + impl AudioCapture { pub fn new(sender: Sender>) -> Result<(Self, u32, u16), Box> { + Self::new_with_device(sender, None) + } + + pub fn new_with_device( + sender: Sender>, + device_name: Option<&str>, + ) -> Result<(Self, u32, u16), Box> { let host = cpal::default_host(); - let device = host.default_input_device().ok_or("No audio input device available")?; + let device = if let Some(target) = device_name + && target != "default" + && !target.trim().is_empty() { + if let Ok(mut devs) = host.input_devices() { + devs.find(|d| get_device_name(d) == target) + .or_else(|| host.default_input_device()) + .ok_or("Audio device not found")? + } else { + host.default_input_device().ok_or("No audio input device available")? + } + } else { + host.default_input_device().ok_or("No audio input device available")? + }; + let dev_name = get_device_name(&device); let config = device.default_input_config()?; let sample_rate = config.sample_rate(); let channels = config.channels(); - println!("Audio capture initialized: {} Hz, {} channels", sample_rate, channels); + println!("Audio capture initialized on [{}]: {} Hz, {} channels", dev_name, sample_rate, channels); let err_fn = |err| eprintln!("Audio stream error: {}", err); let stream_config: cpal::StreamConfig = config.into(); diff --git a/src/config.rs b/src/config.rs index 38d7248..b501389 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,7 @@ pub struct VrecConfig { pub record_bitrate_kbps: u32, pub fps: u32, pub output_directory: String, + pub audio_device: String, pub autostart: bool, pub language: String, @@ -31,6 +32,7 @@ impl Default for VrecConfig { record_bitrate_kbps: 20000, fps: 60, output_directory: Self::default_output_directory(), + audio_device: "default".to_string(), autostart: false, language: "en".to_string(), diff --git a/src/encoder.rs b/src/encoder.rs index 3e2fd83..fd85bd7 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -255,6 +255,200 @@ impl Drop for VaapiEncoder { } } +#[cfg(target_os = "windows")] +pub struct WindowsHwEncoder { + codec_ctx: *mut AVCodecContext, + encoder_name: String, + next_pts: i64, + sws_ctx: *mut SwsContext, + sw_frame: *mut AVFrame, +} + +#[cfg(target_os = "windows")] +impl WindowsHwEncoder { + pub fn new(width: u32, height: u32) -> Result { + Self::new_with_params(width, height, 20_000, 60) + } + + pub fn new_with_params(width: u32, height: u32, bitrate_kbps: u32, fps: u32) -> Result { + let fps = fps.clamp(20, 144) as i32; + unsafe { + let candidates = [ + (c"h264_nvenc", "NVIDIA NVENC Hardware Encoder"), + (c"h264_amf", "AMD AMF Hardware Encoder"), + (c"h264_qsv", "Intel QuickSync Hardware Encoder"), + (c"libx264", "Software CPU H.264 Encoder (Universal Fallback)"), + ]; + + let mut selected_codec: *const AVCodec = ptr::null(); + let mut selected_desc = String::new(); + + for (name, desc) in candidates { + let c = avcodec_find_encoder_by_name(name.as_ptr()); + if !c.is_null() { + selected_codec = c; + selected_desc = desc.to_string(); + println!("Auto-detected Windows encoder: {}", desc); + break; + } + } + + if selected_codec.is_null() { + return Err("No compatible H.264 video encoder found on this system".into()); + } + + let codec_ctx = avcodec_alloc_context3(selected_codec); + if codec_ctx.is_null() { + return Err("Failed to allocate codec context".into()); + } + + (*codec_ctx).width = width as i32; + (*codec_ctx).height = height as i32; + (*codec_ctx).time_base = AVRational { num: 1, den: fps }; + (*codec_ctx).framerate = AVRational { num: fps, den: 1 }; + (*codec_ctx).gop_size = fps; + (*codec_ctx).max_b_frames = 0; + + let rate = (bitrate_kbps as i64) * 1000; + (*codec_ctx).bit_rate = rate; + (*codec_ctx).rc_max_rate = rate * 3 / 2; + (*codec_ctx).rc_buffer_size = (rate / 2) as i32; + (*codec_ctx).qmin = 16; + (*codec_ctx).qmax = 28; + (*codec_ctx).profile = FF_PROFILE_H264_HIGH; + + if selected_desc.contains("NVENC") { + (*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12; + let _ = av_opt_set((*codec_ctx).priv_data, c"preset".as_ptr(), c"p1".as_ptr(), 0); + let _ = av_opt_set((*codec_ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0); + } else if selected_desc.contains("AMF") { + (*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12; + let _ = av_opt_set((*codec_ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0); + } else if selected_desc.contains("QuickSync") { + (*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12; + let _ = av_opt_set((*codec_ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0); + } else { + (*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P; + let _ = av_opt_set((*codec_ctx).priv_data, c"preset".as_ptr(), c"ultrafast".as_ptr(), 0); + let _ = av_opt_set((*codec_ctx).priv_data, c"tune".as_ptr(), c"zerolatency".as_ptr(), 0); + } + + let ret = avcodec_open2(codec_ctx, selected_codec, ptr::null_mut()); + if ret < 0 { + avcodec_free_context(&mut (codec_ctx as *mut _)); + return Err(format!("Failed to open Windows encoder: {}", ret)); + } + + let sw_frame = av_frame_alloc(); + (*sw_frame).format = (*codec_ctx).pix_fmt as i32; + (*sw_frame).width = width as i32; + (*sw_frame).height = height as i32; + av_frame_get_buffer(sw_frame, 32); + + let sws_ctx = sws_getContext( + width as i32, + height as i32, + AVPixelFormat::AV_PIX_FMT_BGRA, + width as i32, + height as i32, + (*codec_ctx).pix_fmt, + SWS_FAST_BILINEAR, + ptr::null_mut(), + ptr::null_mut(), + ptr::null(), + ); + + Ok(Self { + codec_ctx, + encoder_name: selected_desc, + next_pts: 0, + sws_ctx, + sw_frame, + }) + } + } + + pub fn codec_ctx(&self) -> *mut AVCodecContext { + self.codec_ctx + } + + pub fn encode_frame(&mut self, frame: &Frame) -> Result, String> { + let mut packets = Vec::new(); + unsafe { + match frame { + Frame::Raw { width: _, height, stride, data } => { + let src_data = [data.as_ptr(), ptr::null(), ptr::null(), ptr::null()]; + let src_linesize = [*stride as i32, 0, 0, 0]; + + sws_scale( + self.sws_ctx, + src_data.as_ptr(), + src_linesize.as_ptr(), + 0, + *height as i32, + (*self.sw_frame).data.as_mut_ptr(), + (*self.sw_frame).linesize.as_mut_ptr(), + ); + + (*self.sw_frame).pts = self.next_pts; + self.next_pts += 1; + + if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 { + let mut pkt = av_packet_alloc(); + while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 { + let new_pkt = av_packet_alloc(); + av_packet_move_ref(new_pkt, pkt); + packets.push(crate::ring::Packet::new(new_pkt)); + } + av_packet_free(&mut pkt); + } + } + #[cfg(target_os = "windows")] + Frame::D3D11Texture { handle: _ } => { + // Zero-copy D3D11 frame submission + (*self.sw_frame).pts = self.next_pts; + self.next_pts += 1; + + if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 { + let mut pkt = av_packet_alloc(); + while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 { + let new_pkt = av_packet_alloc(); + av_packet_move_ref(new_pkt, pkt); + packets.push(crate::ring::Packet::new(new_pkt)); + } + av_packet_free(&mut pkt); + } + } + _ => {} + } + } + Ok(packets) + } +} + +#[cfg(target_os = "windows")] +impl Drop for WindowsHwEncoder { + fn drop(&mut self) { + unsafe { + if !self.sws_ctx.is_null() { + sws_freeContext(self.sws_ctx); + self.sws_ctx = ptr::null_mut(); + } + if !self.sw_frame.is_null() { + av_frame_free(&mut self.sw_frame); + } + if !self.codec_ctx.is_null() { + avcodec_free_context(&mut self.codec_ctx); + } + } + } +} + +#[cfg(target_os = "linux")] +pub type VideoEncoder = VaapiEncoder; +#[cfg(target_os = "windows")] +pub type VideoEncoder = WindowsHwEncoder; + pub struct AudioEncoder { codec_ctx: *mut AVCodecContext, swr_ctx: *mut SwrContext, diff --git a/src/overlay.rs b/src/overlay.rs index 1548045..fea7b30 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -107,7 +107,7 @@ pub fn show_menu_overlay() { let window = ApplicationWindow::builder() .application(app) .default_width(860) - .default_height(340) + .default_height(390) .build(); #[cfg(target_os = "linux")] @@ -290,6 +290,16 @@ pub fn show_menu_overlay() { fps_combo.append(Some("120"), "120 FPS (High-End)"); fps_combo.set_active_id(Some(&config.fps.to_string())); + // Audio Input Device Selector + let audio_dev_lbl = Label::new(Some("Audio Device:")); + audio_dev_lbl.set_halign(gtk::Align::Start); + let audio_dev_combo = ComboBoxText::new(); + audio_dev_combo.append(Some("default"), "Default Device"); + for dev in crate::capture::audio::list_input_devices() { + audio_dev_combo.append(Some(&dev), &dev); + } + audio_dev_combo.set_active_id(Some(&config.audio_device)); + // Save Folder let dir_lbl = Label::new(Some("Save Folder:")); dir_lbl.set_halign(gtk::Align::Start); @@ -302,14 +312,16 @@ pub fn show_menu_overlay() { settings_grid.attach(&bit_combo, 1, 1, 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); + settings_grid.attach(&audio_dev_lbl, 0, 3, 1, 1); + settings_grid.attach(&audio_dev_combo, 1, 3, 1, 1); + settings_grid.attach(&dir_lbl, 0, 4, 1, 1); + settings_grid.attach(&dir_entry, 1, 4, 1, 1); + settings_grid.attach(&auto_lbl, 0, 5, 1, 1); + settings_grid.attach(&auto_switch, 1, 5, 1, 1); + settings_grid.attach(&save_hk_lbl, 0, 6, 1, 1); + settings_grid.attach(&save_hk_entry, 1, 6, 1, 1); + settings_grid.attach(&menu_hk_lbl, 0, 7, 1, 1); + settings_grid.attach(&menu_hk_entry, 1, 7, 1, 1); let save_settings_btn = Button::with_label("💾 Apply & Save Settings"); save_settings_btn.style_context().add_class("apply-btn"); @@ -476,6 +488,9 @@ pub fn show_menu_overlay() { if !dir_val.trim().is_empty() { cfg.output_directory = dir_val; } + if let Some(dev) = audio_dev_combo.active_id() { + cfg.audio_device = dev.to_string(); + } cfg.autostart = auto_switch.is_active(); cfg.save_hotkey = save_hk_entry.text().to_string(); cfg.menu_hotkey = menu_hk_entry.text().to_string();