use anyhow::{Result, anyhow}; use serde_json::Value; use std::process::Command; use std::path::Path; use crate::data::models::material::{VideoMetadata, AudioMetadata, MaterialMetadata}; /// FFmpeg 工具集成 /// 遵循 Tauri 开发规范的基础设施层设计 pub struct FFmpegService; impl FFmpegService { /// 检查 FFmpeg 是否可用 pub fn is_available() -> bool { let ffmpeg_available = Command::new("ffmpeg") .arg("-version") .output() .map(|output| output.status.success()) .unwrap_or(false); let ffprobe_available = Command::new("ffprobe") .arg("-version") .output() .map(|output| output.status.success()) .unwrap_or(false); ffmpeg_available && ffprobe_available } /// 获取详细的FFmpeg状态信息 pub fn get_status_info() -> Result { let mut info = String::new(); // 检查 ffmpeg match Command::new("ffmpeg").arg("-version").output() { Ok(output) if output.status.success() => { let version_str = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = version_str.lines().next() { info.push_str(&format!("FFmpeg: {}\n", first_line)); } } Ok(_) => info.push_str("FFmpeg: 命令执行失败\n"), Err(e) => info.push_str(&format!("FFmpeg: 未找到 ({})\n", e)), } // 检查 ffprobe match Command::new("ffprobe").arg("-version").output() { Ok(output) if output.status.success() => { let version_str = String::from_utf8_lossy(&output.stdout); if let Some(first_line) = version_str.lines().next() { info.push_str(&format!("FFprobe: {}\n", first_line)); } } Ok(_) => info.push_str("FFprobe: 命令执行失败\n"), Err(e) => info.push_str(&format!("FFprobe: 未找到 ({})\n", e)), } Ok(info) } /// 提取视频/音频元数据 pub fn extract_metadata(file_path: &str) -> Result { if !Path::new(file_path).exists() { return Err(anyhow!("文件不存在: {}", file_path)); } let output = Command::new("ffprobe") .args([ "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", file_path ]) .output() .map_err(|e| anyhow!("执行 ffprobe 失败: {}", e))?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("ffprobe 执行失败: {}", error_msg)); } let json_str = String::from_utf8_lossy(&output.stdout); let json: Value = serde_json::from_str(&json_str) .map_err(|e| anyhow!("解析 ffprobe 输出失败: {}", e))?; Self::parse_metadata(&json) } /// 解析 ffprobe 输出的 JSON 数据 fn parse_metadata(json: &Value) -> Result { let format = json.get("format") .ok_or_else(|| anyhow!("缺少 format 信息"))?; let streams = json.get("streams") .and_then(|s| s.as_array()) .ok_or_else(|| anyhow!("缺少 streams 信息"))?; // 查找视频流 let video_stream = streams.iter() .find(|stream| stream.get("codec_type").and_then(|t| t.as_str()) == Some("video")); // 查找音频流 let audio_stream = streams.iter() .find(|stream| stream.get("codec_type").and_then(|t| t.as_str()) == Some("audio")); if let Some(video) = video_stream { // 这是一个视频文件 let duration = format.get("duration") .and_then(|d| d.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0.0); let width = video.get("width") .and_then(|w| w.as_u64()) .unwrap_or(0) as u32; let height = video.get("height") .and_then(|h| h.as_u64()) .unwrap_or(0) as u32; let fps = video.get("r_frame_rate") .and_then(|fps| fps.as_str()) .and_then(|s| Self::parse_fraction(s)) .unwrap_or(0.0); let bitrate = format.get("bit_rate") .and_then(|b| b.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0); let codec = video.get("codec_name") .and_then(|c| c.as_str()) .unwrap_or("unknown") .to_string(); let format_name = format.get("format_name") .and_then(|f| f.as_str()) .unwrap_or("unknown") .to_string(); let has_audio = audio_stream.is_some(); let audio_codec = audio_stream .and_then(|a| a.get("codec_name")) .and_then(|c| c.as_str()) .map(|s| s.to_string()); let audio_bitrate = audio_stream .and_then(|a| a.get("bit_rate")) .and_then(|b| b.as_str()) .and_then(|s| s.parse::().ok()); let audio_sample_rate = audio_stream .and_then(|a| a.get("sample_rate")) .and_then(|r| r.as_str()) .and_then(|s| s.parse::().ok()); Ok(MaterialMetadata::Video(VideoMetadata { duration, width, height, fps, bitrate, codec, format: format_name, has_audio, audio_codec, audio_bitrate, audio_sample_rate, })) } else if let Some(audio) = audio_stream { // 这是一个音频文件 let duration = format.get("duration") .and_then(|d| d.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0.0); let bitrate = format.get("bit_rate") .and_then(|b| b.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0); let codec = audio.get("codec_name") .and_then(|c| c.as_str()) .unwrap_or("unknown") .to_string(); let sample_rate = audio.get("sample_rate") .and_then(|r| r.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0); let channels = audio.get("channels") .and_then(|c| c.as_u64()) .unwrap_or(0) as u32; let format_name = format.get("format_name") .and_then(|f| f.as_str()) .unwrap_or("unknown") .to_string(); Ok(MaterialMetadata::Audio(AudioMetadata { duration, bitrate, codec, sample_rate, channels, format: format_name, })) } else { Err(anyhow!("未找到有效的视频或音频流")) } } /// 解析分数格式的帧率 (如 "30/1") fn parse_fraction(fraction_str: &str) -> Option { let parts: Vec<&str> = fraction_str.split('/').collect(); if parts.len() == 2 { if let (Ok(numerator), Ok(denominator)) = (parts[0].parse::(), parts[1].parse::()) { if denominator != 0.0 { return Some(numerator / denominator); } } } None } /// 检测视频场景变化 pub fn detect_scenes(file_path: &str, threshold: f64) -> Result> { if !Path::new(file_path).exists() { return Err(anyhow!("文件不存在: {}", file_path)); } println!("开始场景检测: {} (阈值: {})", file_path, threshold); // 首先尝试使用 ffmpeg 的 scene 滤镜 match Self::detect_scenes_with_ffmpeg(file_path, threshold) { Ok(scenes) if !scenes.is_empty() => { println!("FFmpeg场景检测成功,发现 {} 个场景: {:?}", scenes.len(), scenes); return Ok(scenes); } Err(e) => { println!("FFmpeg场景检测失败,使用备用方法: {}", e); } Ok(_) => { println!("FFmpeg场景检测返回空结果,使用备用方法"); } } // 如果FFmpeg场景检测失败,使用简单的时间间隔方法 let simple_scenes = Self::detect_scenes_simple(file_path, threshold)?; println!("备用场景检测完成,发现 {} 个场景: {:?}", simple_scenes.len(), simple_scenes); Ok(simple_scenes) } /// 使用FFmpeg进行场景检测 fn detect_scenes_with_ffmpeg(file_path: &str, threshold: f64) -> Result> { // 方法1: 使用 scene 滤镜和 showinfo let output1 = Command::new("ffmpeg") .args([ "-i", file_path, "-vf", &format!("select='gt(scene,{})',showinfo", threshold), "-f", "null", "-" ]) .stderr(std::process::Stdio::piped()) .output(); if let Ok(output) = output1 { let stderr_str = String::from_utf8_lossy(&output.stderr); let mut scene_times = Vec::new(); // 查找 showinfo 输出中的 pts_time 信息 for line in stderr_str.lines() { if line.contains("showinfo") && line.contains("pts_time:") { if let Some(pts_start) = line.find("pts_time:") { let pts_part = &line[pts_start + 9..]; if let Some(space_pos) = pts_part.find(' ') { let time_str = &pts_part[..space_pos]; if let Ok(time) = time_str.parse::() { scene_times.push(time); } } } } } if !scene_times.is_empty() { return Ok(scene_times); } } // 方法2: 使用更简单的场景检测方法 Self::detect_scenes_alternative(file_path, threshold) } /// 替代的场景检测方法 fn detect_scenes_alternative(file_path: &str, threshold: f64) -> Result> { // 使用 ffprobe 分析视频帧信息 let output = Command::new("ffprobe") .args([ "-v", "quiet", "-select_streams", "v:0", "-show_entries", "frame=pkt_pts_time,pict_type", "-of", "csv=p=0", file_path ]) .output() .map_err(|e| anyhow!("执行ffprobe帧分析失败: {}", e))?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("ffprobe帧分析失败: {}", error_msg)); } let output_str = String::from_utf8_lossy(&output.stdout); let mut scene_times = Vec::new(); let mut last_i_frame_time = 0.0; let min_scene_duration = 5.0; // 最小场景时长5秒 // 分析I帧(关键帧)作为潜在的场景切换点 for line in output_str.lines() { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { if let (Ok(time), pict_type) = (parts[0].parse::(), parts[1]) { if pict_type == "I" && time - last_i_frame_time > min_scene_duration { scene_times.push(time); last_i_frame_time = time; } } } } Ok(scene_times) } /// 简单的场景检测方法(备用) fn detect_scenes_simple(file_path: &str, threshold: f64) -> Result> { // 使用 ffprobe 获取视频时长,然后按智能间隔分割 let metadata = Self::extract_metadata(file_path)?; let duration = match metadata { MaterialMetadata::Video(video_meta) => video_meta.duration, _ => return Ok(Vec::new()), }; // 如果视频很短,不需要场景检测 if duration < 30.0 { return Ok(Vec::new()); } let mut scene_times = Vec::new(); // 根据视频时长和阈值智能确定切分策略 if duration <= 120.0 { // 2分钟以内的视频,按30秒间隔 let mut current_time = 30.0; while current_time < duration { scene_times.push(current_time); current_time += 30.0; } } else if duration <= 600.0 { // 10分钟以内的视频,按60秒间隔 let mut current_time = 60.0; while current_time < duration { scene_times.push(current_time); current_time += 60.0; } } else { // 长视频,按120秒间隔 let mut current_time = 120.0; while current_time < duration { scene_times.push(current_time); current_time += 120.0; } } // 如果阈值很低(更敏感),增加更多切点 if threshold < 0.2 && duration > 180.0 { let mut additional_times = Vec::new(); for &time in &scene_times { if time > 90.0 { additional_times.push(time - 45.0); } } scene_times.extend(additional_times); scene_times.sort_by(|a, b| a.partial_cmp(b).unwrap()); } Ok(scene_times) } /// 切分视频 pub fn split_video( input_path: &str, output_dir: &str, segments: &[(f64, f64)], // (start_time, end_time) pairs output_prefix: &str, ) -> Result> { if !Path::new(input_path).exists() { return Err(anyhow!("输入文件不存在: {}", input_path)); } std::fs::create_dir_all(output_dir) .map_err(|e| anyhow!("创建输出目录失败: {}", e))?; let mut output_files = Vec::new(); for (index, (start_time, end_time)) in segments.iter().enumerate() { let duration = end_time - start_time; let output_file = format!("{}/{}_{:03}.mp4", output_dir, output_prefix, index + 1); let output = Command::new("ffmpeg") .args([ "-i", input_path, "-ss", &start_time.to_string(), "-t", &duration.to_string(), "-c", "copy", "-avoid_negative_ts", "make_zero", "-y", // 覆盖输出文件 &output_file ]) .output() .map_err(|e| anyhow!("执行视频切分失败: {}", e))?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("视频切分失败: {}", error_msg)); } output_files.push(output_file); } Ok(output_files) } /// 获取视频缩略图 pub fn generate_thumbnail( input_path: &str, output_path: &str, timestamp: f64, width: u32, height: u32, ) -> Result<()> { if !Path::new(input_path).exists() { return Err(anyhow!("输入文件不存在: {}", input_path)); } let output = Command::new("ffmpeg") .args([ "-i", input_path, "-ss", ×tamp.to_string(), "-vframes", "1", "-vf", &format!("scale={}:{}", width, height), "-y", // 覆盖输出文件 output_path ]) .output() .map_err(|e| anyhow!("执行缩略图生成失败: {}", e))?; if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("缩略图生成失败: {}", error_msg)); } Ok(()) } /// 检查 FFmpeg 版本 pub fn get_version() -> Result { let output = Command::new("ffmpeg") .arg("-version") .output() .map_err(|e| anyhow!("获取 FFmpeg 版本失败: {}", e))?; if !output.status.success() { return Err(anyhow!("FFmpeg 不可用")); } let version_str = String::from_utf8_lossy(&output.stdout); let first_line = version_str.lines().next().unwrap_or("Unknown version"); Ok(first_line.to_string()) } }