feat: Add audio device enumeration/selection and Windows hardware encoder auto-detection (NVENC/AMF/QSV/libx264)

This commit is contained in:
katR 2026-09-03 07:59:56 +05:30
parent df9a1a0522
commit a5f7c45881
5 changed files with 263 additions and 14 deletions

View file

@ -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<dyn std::error::Error + Send + Sync>> {
let (cmd_tx, cmd_rx) = bounded::<Command>(10);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(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<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_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;

View file

@ -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<String> {
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<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
Self::new_with_device(sender, None)
}
pub fn new_with_device(
sender: Sender<Vec<f32>>,
device_name: Option<&str>,
) -> 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")?;
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();

View file

@ -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(),

View file

@ -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, String> {
Self::new_with_params(width, height, 20_000, 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 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<Vec<crate::ring::Packet>, 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,

View file

@ -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();