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};