feat: 修复FFmpeg场景检测功能并添加调试工具
主要修复: - 重构场景检测算法,使用正确的FFmpeg命令 - 添加备用的简单场景检测方法 - 改进FFmpeg可用性检查,同时检查ffmpeg和ffprobe - 添加详细的FFmpeg状态信息获取功能 新增功能: - FFmpegDebugPanel调试面板组件 - test_scene_detection测试命令用于调试 - get_ffmpeg_status命令获取详细状态 - 项目详情页面添加调试工具选项卡 技术改进: - 更可靠的场景检测实现,支持降级到时间间隔方法 - 完善的错误处理和日志记录 - 用户友好的调试界面 - 实时测试和诊断工具 这个版本应该能够正确处理场景检测,即使在FFmpeg配置有问题的情况下也能提供备用方案。
This commit is contained in:
@@ -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. 检查是否需要切分视频
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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| {
|
||||
// 初始化应用状态
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user