From 44ec2dceb775003dec1c1b4d2d5fb45b0e86c76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=BC=80=E5=8F=91?= Date: Thu, 21 May 2026 16:32:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ffprobe=20=E5=BF=AB=E9=80=9F=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=20H.264/yuv420p=20=E8=A7=86=E9=A2=91=EF=BC=8C?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E8=BD=AC=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 应用生成的成品视频已是 H.264/yuv420p,无需重复转码 - 超时后显式 kill ffprobe 子进程,避免僵尸进程 - 分辨率上限判断:4K 素材仍转码为 540p 代理保证预览流畅 --- tauri-app/src-tauri/src/ffmpeg_cmd.rs | 54 +++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/tauri-app/src-tauri/src/ffmpeg_cmd.rs b/tauri-app/src-tauri/src/ffmpeg_cmd.rs index 3a2d39c..70b3ab4 100644 --- a/tauri-app/src-tauri/src/ffmpeg_cmd.rs +++ b/tauri-app/src-tauri/src/ffmpeg_cmd.rs @@ -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::(&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};