feat: ffprobe 快速检测 H.264/yuv420p 视频,跳过不必要的预览转码

- 应用生成的成品视频已是 H.264/yuv420p,无需重复转码
- 超时后显式 kill ffprobe 子进程,避免僵尸进程
- 分辨率上限判断:4K 素材仍转码为 540p 代理保证预览流畅
This commit is contained in:
小鱼开发
2026-05-21 16:32:02 +08:00
parent 6def12995e
commit 44ec2dceb7
+50 -4
View File
@@ -737,6 +737,56 @@ pub async fn transcode_for_preview(app: &AppHandle, input_path: &str) -> Result<
return Err(format!("输入文件不存在: {}", input_path));
}
let path_str = input.canonicalize()
.unwrap_or(input.to_path_buf())
.to_string_lossy()
.to_string();
// 快速检测:如果已经是 H.264 + YUV420p,直接返回原始路径(避免应用自己生成的成品重复转码)
let probe_args = vec![
"-v".to_string(), "error".to_string(),
"-select_streams".to_string(), "v:0".to_string(),
"-show_entries".to_string(), "stream=codec_name,pix_fmt,width,height".to_string(),
"-of".to_string(), "json".to_string(),
path_str.clone(),
];
let probe_result = app.shell().sidecar("ffprobe")
.and_then(|s| s.args(probe_args).spawn());
if let Ok((mut rx, child)) = probe_result {
let mut stdout = String::new();
let probe_future = async {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => stdout.push_str(&String::from_utf8_lossy(&line)),
CommandEvent::Terminated(status) if status.code == Some(0) => return Ok(()),
_ => {}
}
}
Ok::<(), ()>(())
};
match tokio::time::timeout(std::time::Duration::from_secs(3), probe_future).await {
Ok(Ok(())) => {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stdout) {
if let Some(stream) = parsed.get("streams").and_then(|s| s.as_array()).and_then(|a| a.first()) {
let codec = stream.get("codec_name").and_then(|v| v.as_str()).unwrap_or("");
let pix_fmt = stream.get("pix_fmt").and_then(|v| v.as_str()).unwrap_or("");
// 应用生成的视频通常是 1080p 及以下;若用户上传了 4K H.264,仍转码为 540p 代理以保证预览流畅
let width = stream.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
let height = stream.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
if codec == "h264" && pix_fmt == "yuv420p" && width <= 1920 && height <= 1920 {
return Ok(path_str);
}
}
}
}
_ => {
// 超时或异常:强制结束 ffprobe,避免僵尸进程
let _ = child.kill();
}
}
}
// 获取文件元数据用于缓存 key
let metadata = std::fs::metadata(input_path)
.map_err(|e| format!("无法读取文件元数据: {}", e))?;
@@ -749,10 +799,6 @@ pub async fn transcode_for_preview(app: &AppHandle, input_path: &str) -> Result<
let file_size = metadata.len();
// 计算缓存路径(基于文件路径 hash + 大小 + 修改时间)
let path_str = input.canonicalize()
.unwrap_or(input.to_path_buf())
.to_string_lossy()
.to_string();
let path_hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};