fix: check encoders list (#218)

This commit is contained in:
Xinrea
2025-11-02 13:52:01 +08:00
committed by GitHub
parent 183eb063bb
commit 6127c67cd3
2 changed files with 115 additions and 47 deletions

View File

@@ -1,4 +1,4 @@
use std::{collections::HashSet, process::Stdio};
use std::{collections::HashSet, process::Stdio, sync::OnceLock};
use tokio::io::AsyncReadExt;
@@ -14,6 +14,9 @@ const TARGET_ENCODERS: [&str; 7] = [
"h264_v4l2m2m",
];
// 缓存选中的编码器,避免重复检查
static ENCODER_CACHE: OnceLock<String> = OnceLock::new();
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[cfg(target_os = "windows")]
@@ -98,46 +101,130 @@ pub async fn list_supported_hwaccels() -> Result<Vec<String>, String> {
Ok(hwaccels)
}
/// 依据优先级从支持列表中挑选推荐的硬件编码器。
/// 测试指定的编码器是否在当前硬件上真正可用
///
/// 当前优先级顺序:`h264_nvenc` > `h264_videotoolbox` > `h264_qsv` > `h264_amf` > `h264_mf` > `h264_vaapi` > `h264_v4l2m2m`。
pub fn select_preferred_hwaccel(supported: &[String]) -> Option<&'static str> {
const PRIORITY: [&str; 7] = [
"h264_nvenc",
"h264_videotoolbox",
"h264_qsv",
"h264_amf",
"h264_mf",
"h264_vaapi",
"h264_v4l2m2m",
];
/// 通过尝试对测试流进行编码来验证编码器可用性
async fn test_encoder_availability(encoder: &str) -> bool {
let mut command = tokio::process::Command::new(ffmpeg_path());
PRIORITY
.iter()
.find(|candidate| {
supported
.iter()
.any(|value| value.eq_ignore_ascii_case(candidate))
})
.copied()
#[cfg(target_os = "windows")]
command.creation_flags(CREATE_NO_WINDOW);
// 使用合成输入源 (testsrc2) 测试编码器
// -t 0.1 只编码0.1秒,-frames:v 3 只编码3帧快速测试
// -f null 丢弃输出,不需要实际文件
let child = command
.arg("-hide_banner")
.arg("-loglevel")
.arg("error")
.arg("-f")
.arg("lavfi")
.arg("-i")
.arg("testsrc2=duration=0.1:size=320x240:rate=1")
.arg("-c:v")
.arg(encoder)
.arg("-frames:v")
.arg("3")
.arg("-f")
.arg("null")
.arg("-")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
match child {
Ok(process) => {
let output = process.wait_with_output().await;
match output {
Ok(output) => {
// 如果退出码为0说明编码器可用
if output.status.success() {
log::debug!("Encoder {encoder} is available");
true
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
log::debug!("Encoder {encoder} failed: {stderr}");
false
}
}
Err(err) => {
log::debug!("Encoder {encoder} test error: {err}");
false
}
}
}
Err(err) => {
log::debug!("Failed to spawn ffmpeg process to test {encoder}: {err}");
false
}
}
}
/// Get the preferred hardware encoder for x264
///
/// Returns the preferred hardware encoder for x264, or "libx264" if no hardware acceleration is available.
/// This function not only checks if the encoder is compiled into ffmpeg, but also verifies it's actually
/// usable on the current hardware.
///
/// The result is cached to avoid repeated checks during the program's lifetime.
pub async fn get_x264_encoder() -> &'static str {
let mut encoder = "libx264";
match list_supported_hwaccels().await {
// 先检查缓存,如果已存在直接返回
if let Some(encoder) = ENCODER_CACHE.get() {
return encoder.as_str();
}
// 执行硬件编码器检测和验证
let encoder = match list_supported_hwaccels().await {
Ok(hwaccels) => {
if let Some(arg) = select_preferred_hwaccel(&hwaccels) {
encoder = arg;
// 按优先级顺序测试每个硬件编码器
const PRIORITY: [&str; 7] = [
"h264_nvenc",
"h264_videotoolbox",
"h264_qsv",
"h264_amf",
"h264_mf",
"h264_vaapi",
"h264_v4l2m2m",
];
let mut selected = None;
for &candidate in &PRIORITY {
// 检查编码器是否在支持列表中
if hwaccels
.iter()
.any(|value| value.eq_ignore_ascii_case(candidate))
{
// 测试编码器在实际硬件上是否可用
if test_encoder_availability(candidate).await {
log::info!("Found available hardware encoder: {candidate}");
selected = Some(candidate.to_string());
break;
} else {
log::debug!("Hardware encoder {candidate} is compiled in but not usable");
}
}
}
selected.unwrap_or_else(|| {
log::info!("No usable hardware encoder found, falling back to libx264");
"libx264".to_string()
})
}
Err(err) => {
log::warn!("Failed to query hardware encoders: {err}");
"libx264".to_string()
}
};
log::info!("Selected x264 encoder: {}", encoder);
// 存入缓存,如果设置成功则从缓存返回,否则返回刚才得到的值
// 注意set() 可能被其他线程抢先,但每个线程都会得到相同的 encoder 值
match ENCODER_CACHE.set(encoder.clone()) {
Ok(_) => ENCODER_CACHE.get().unwrap().as_str(),
Err(_) => {
// 其他线程已经设置了,返回缓存的值
ENCODER_CACHE.get().unwrap().as_str()
}
}
log::info!("Selected x264 encoder: {encoder}");
encoder
}

View File

@@ -1315,25 +1315,6 @@ mod tests {
}
}
#[test]
fn test_select_preferred_hwaccel() {
let cases = vec![
(vec!["h264_nvenc", "h264_vaapi"], Some("h264_nvenc")),
(
vec!["h264_videotoolbox", "h264_qsv"],
Some("h264_videotoolbox"),
),
(vec!["h264_vaapi"], Some("h264_vaapi")),
(vec!["h264_v4l2m2m"], Some("h264_v4l2m2m")),
(vec!["libx264"], None),
];
for (inputs, expected) in cases {
let inputs = inputs.into_iter().map(String::from).collect::<Vec<_>>();
assert_eq!(super::hwaccel::select_preferred_hwaccel(&inputs), expected);
}
}
// 测试字幕生成错误处理
#[tokio::test]
async fn test_generate_video_subtitle_errors() {