feat: 修复FFmpeg场景检测功能并添加调试工具

主要修复:
- 重构场景检测算法,使用正确的FFmpeg命令
- 添加备用的简单场景检测方法
- 改进FFmpeg可用性检查,同时检查ffmpeg和ffprobe
- 添加详细的FFmpeg状态信息获取功能

新增功能:
- FFmpegDebugPanel调试面板组件
- test_scene_detection测试命令用于调试
- get_ffmpeg_status命令获取详细状态
- 项目详情页面添加调试工具选项卡

技术改进:
- 更可靠的场景检测实现,支持降级到时间间隔方法
- 完善的错误处理和日志记录
- 用户友好的调试界面
- 实时测试和诊断工具

这个版本应该能够正确处理场景检测,即使在FFmpeg配置有问题的情况下也能提供备用方案。
This commit is contained in:
imeepos
2025-07-13 21:04:46 +08:00
parent 954c8a6a1c
commit dbcd98118c
7 changed files with 410 additions and 61 deletions

View File

@@ -283,16 +283,22 @@ impl MaterialService {
// 2. 场景检测(如果是视频且启用了场景检测)
if matches!(material.material_type, MaterialType::Video) && config.enable_scene_detection {
println!("开始视频场景检测: {}", material.original_path);
match Self::detect_video_scenes(&material.original_path, config.scene_detection_threshold) {
Ok(scene_detection) => {
println!("场景检测成功,发现 {} 个场景", scene_detection.scenes.len());
material.set_scene_detection(scene_detection);
repository.update(&material)?;
}
Err(e) => {
// 场景检测失败不应该导致整个处理失败
println!("场景检测失败: {}", e);
eprintln!("场景检测失败: {}", e);
}
}
} else {
println!("跳过场景检测 - 视频类型: {}, 启用检测: {}",
matches!(material.material_type, MaterialType::Video),
config.enable_scene_detection);
}
// 3. 检查是否需要切分视频

View File

@@ -11,11 +11,50 @@ pub struct FFmpegService;
impl FFmpegService {
/// 检查 FFmpeg 是否可用
pub fn is_available() -> bool {
Command::new("ffprobe")
let ffmpeg_available = Command::new("ffmpeg")
.arg("-version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
.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<String> {
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)
}
/// 提取视频/音频元数据
@@ -193,34 +232,87 @@ impl FFmpegService {
return Err(anyhow!("文件不存在: {}", file_path));
}
let output = Command::new("ffprobe")
// 首先尝试使用 ffmpeg 的 scene 滤镜
match Self::detect_scenes_with_ffmpeg(file_path, threshold) {
Ok(scenes) if !scenes.is_empty() => return Ok(scenes),
Err(e) => {
eprintln!("FFmpeg场景检测失败使用备用方法: {}", e);
}
_ => {}
}
// 如果FFmpeg场景检测失败使用简单的时间间隔方法
Self::detect_scenes_simple(file_path, threshold)
}
/// 使用FFmpeg进行场景检测
fn detect_scenes_with_ffmpeg(file_path: &str, threshold: f64) -> Result<Vec<f64>> {
let output = Command::new("ffmpeg")
.args([
"-f", "lavfi",
"-i", &format!("movie={}:s=v:0[in];[in]select=gt(scene\\,{}),showinfo[out]", file_path, threshold),
"-show_entries", "frame=pkt_pts_time",
"-of", "csv=p=0",
"-v", "quiet"
"-i", file_path,
"-vf", &format!("select='gt(scene,{})',showinfo", threshold),
"-f", "null",
"-"
])
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| anyhow!("执行场景检测失败: {}", e))?;
.map_err(|e| anyhow!("执行FFmpeg场景检测失败: {}", e))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("场景检测失败: {}", error_msg));
return Err(anyhow!("FFmpeg场景检测命令失败: {}", error_msg));
}
let output_str = String::from_utf8_lossy(&output.stdout);
// 解析 stderr 中的 showinfo 输出
let stderr_str = String::from_utf8_lossy(&output.stderr);
let mut scene_times = Vec::new();
for line in output_str.lines() {
if let Ok(time) = line.trim().parse::<f64>() {
scene_times.push(time);
// 查找 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::<f64>() {
scene_times.push(time);
}
}
}
}
}
Ok(scene_times)
}
/// 简单的场景检测方法(备用)
fn detect_scenes_simple(file_path: &str, threshold: f64) -> Result<Vec<f64>> {
// 使用 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 < 60.0 {
return Ok(Vec::new());
}
// 按照阈值相关的间隔创建场景切点
let interval = (60.0 / threshold).max(30.0).min(300.0); // 30秒到5分钟之间
let mut scene_times = Vec::new();
let mut current_time = interval;
while current_time < duration {
scene_times.push(current_time);
current_time += interval;
}
Ok(scene_times)
}
/// 切分视频
pub fn split_video(
input_path: &str,

View File

@@ -53,9 +53,11 @@ pub fn run() {
commands::material_commands::get_file_info,
commands::material_commands::check_ffmpeg_available,
commands::material_commands::get_ffmpeg_version,
commands::material_commands::get_ffmpeg_status,
commands::material_commands::extract_file_metadata,
commands::material_commands::detect_video_scenes,
commands::material_commands::generate_video_thumbnail
commands::material_commands::generate_video_thumbnail,
commands::material_commands::test_scene_detection
])
.setup(|app| {
// 初始化应用状态

View File

@@ -271,6 +271,12 @@ pub async fn get_ffmpeg_version() -> Result<String, String> {
FFmpegService::get_version().map_err(|e| e.to_string())
}
/// 获取 FFmpeg 状态信息命令
#[command]
pub async fn get_ffmpeg_status() -> Result<String, String> {
FFmpegService::get_status_info().map_err(|e| e.to_string())
}
/// 提取文件元数据命令
#[command]
pub async fn extract_file_metadata(file_path: String) -> Result<MaterialMetadata, String> {
@@ -298,3 +304,40 @@ pub async fn generate_video_thumbnail(
FFmpegService::generate_thumbnail(&input_path, &output_path, timestamp, width, height)
.map_err(|e| e.to_string())
}
/// 测试场景检测命令(用于调试)
#[command]
pub async fn test_scene_detection(file_path: String) -> Result<String, String> {
// 首先检查FFmpeg状态
let ffmpeg_status = FFmpegService::get_status_info().unwrap_or_else(|_| "无法获取FFmpeg状态".to_string());
let mut result = format!("FFmpeg状态:\n{}\n", ffmpeg_status);
// 检查文件是否存在
if !std::path::Path::new(&file_path).exists() {
return Ok(format!("{}文件不存在: {}", result, file_path));
}
result.push_str(&format!("测试文件: {}\n", file_path));
// 尝试提取元数据
match FFmpegService::extract_metadata(&file_path) {
Ok(metadata) => {
result.push_str(&format!("元数据提取成功: {:?}\n", metadata));
}
Err(e) => {
result.push_str(&format!("元数据提取失败: {}\n", e));
}
}
// 尝试场景检测
match FFmpegService::detect_scenes(&file_path, 0.3) {
Ok(scenes) => {
result.push_str(&format!("场景检测成功,发现 {} 个场景切点: {:?}\n", scenes.len(), scenes));
}
Err(e) => {
result.push_str(&format!("场景检测失败: {}\n", e));
}
}
Ok(result)
}