feat: Add audio device enumeration/selection and Windows hardware encoder auto-detection (NVENC/AMF/QSV/libx264)
This commit is contained in:
parent
df9a1a0522
commit
a5f7c45881
5 changed files with 263 additions and 14 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
194
src/encoder.rs
194
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, 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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue