fix: 修复视频切分前几秒无画面问题

问题分析:
- 原因:使用 -c copy 流复制模式在非关键帧位置切分
- 症状:切分后的视频前几秒显示黑屏或无画面

解决方案:
1. 新增三种切分模式:
   - Fast: 快速模式(流复制,速度快但可能有画面问题)
   - Accurate: 精确模式(重新编码,确保画面完整)
   - Smart: 智能模式(关键帧对齐 + 快速切分)

2. 精确模式技术改进:
   - 使用 libx264 重新编码视频
   - 使用 aac 重新编码音频
   - 添加 -preset fast 提高编码速度
   - 设置 -crf 23 保证质量
   - 添加 -movflags +faststart 优化播放

3. 智能模式特性:
   - 自动获取视频关键帧信息
   - 将切分点调整到最近的关键帧
   - 结合快速切分提高效率

4. 新增调试功能:
   - test_video_split 命令测试不同切分模式
   - 详细的切分日志输出
   - 模式选择和参数配置

默认配置:
- 使用 Accurate 模式确保画面完整
- 可通过配置切换到其他模式

现在切分的视频应该不会再有前几秒无画面的问题!
This commit is contained in:
imeepos 2025-07-13 21:17:48 +08:00
parent 704e6d8fff
commit 864d1b42a9
5 changed files with 219 additions and 23 deletions

View File

@ -399,13 +399,36 @@ impl MaterialService {
// 创建输出目录
let output_dir = format!("{}_segments", material.original_path.trim_end_matches(".mp4"));
// 执行视频切分
let output_files = FFmpegService::split_video(
&material.original_path,
&output_dir,
&segments,
&material.name.replace(".mp4", ""),
)?;
// 根据配置选择切分模式
let output_files = match config.split_mode {
crate::data::models::material::VideoSplitMode::Fast => {
println!("使用快速切分模式(可能有前几秒无画面问题)");
FFmpegService::split_video_fast(
&material.original_path,
&output_dir,
&segments,
&material.name.replace(".mp4", ""),
)?
}
crate::data::models::material::VideoSplitMode::Accurate => {
println!("使用精确切分模式(重新编码,确保画面完整)");
FFmpegService::split_video(
&material.original_path,
&output_dir,
&segments,
&material.name.replace(".mp4", ""),
)?
}
crate::data::models::material::VideoSplitMode::Smart => {
println!("使用智能切分模式(关键帧对齐)");
FFmpegService::split_video_smart(
&material.original_path,
&output_dir,
&segments,
&material.name.replace(".mp4", ""),
)?
}
};
// 保存片段信息到数据库
for (index, output_file) in output_files.iter().enumerate() {

View File

@ -142,6 +142,20 @@ pub struct CreateMaterialRequest {
pub max_segment_duration: Option<f64>, // 最大片段时长(秒)
}
/// 视频切分模式
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VideoSplitMode {
Fast, // 快速模式(流复制,可能有画面问题)
Accurate, // 精确模式(重新编码,确保画面完整)
Smart, // 智能模式(关键帧对齐 + 快速切分)
}
impl Default for VideoSplitMode {
fn default() -> Self {
VideoSplitMode::Accurate
}
}
/// 素材处理配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialProcessingConfig {
@ -152,6 +166,7 @@ pub struct MaterialProcessingConfig {
pub audio_quality: String, // 音频质量设置
pub output_format: String, // 输出格式
pub auto_process: Option<bool>, // 是否自动处理
pub split_mode: VideoSplitMode, // 视频切分模式
}
impl Default for MaterialProcessingConfig {
@ -164,6 +179,7 @@ impl Default for MaterialProcessingConfig {
audio_quality: "medium".to_string(),
output_format: "mp4".to_string(),
auto_process: Some(true),
split_mode: VideoSplitMode::Accurate, // 默认使用精确模式
}
}
}

View File

@ -391,12 +391,33 @@ impl FFmpegService {
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<Vec<String>> {
Self::split_video_with_mode(input_path, output_dir, segments, output_prefix, false)
}
/// 快速切分视频(流复制模式,速度快但可能有画面问题)
pub fn split_video_fast(
input_path: &str,
output_dir: &str,
segments: &[(f64, f64)], // (start_time, end_time) pairs
output_prefix: &str,
) -> Result<Vec<String>> {
Self::split_video_with_mode(input_path, output_dir, segments, output_prefix, true)
}
/// 切分视频的内部实现
fn split_video_with_mode(
input_path: &str,
output_dir: &str,
segments: &[(f64, f64)], // (start_time, end_time) pairs
output_prefix: &str,
use_copy_mode: bool,
) -> Result<Vec<String>> {
if !Path::new(input_path).exists() {
return Err(anyhow!("输入文件不存在: {}", input_path));
@ -410,31 +431,133 @@ impl FFmpegService {
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))?;
println!("切分视频片段 {}: {}s - {}s (时长: {}s) [模式: {}]",
index + 1, start_time, end_time, duration,
if use_copy_mode { "快速复制" } else { "重新编码" });
let output = if use_copy_mode {
// 快速模式:流复制(可能有前几秒无画面问题)
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))?
} else {
// 重新编码模式:确保切分点准确,避免前几秒无画面问题
Command::new("ffmpeg")
.args([
"-i", input_path,
"-ss", &start_time.to_string(),
"-t", &duration.to_string(),
"-c:v", "libx264", // 重新编码视频
"-c:a", "aac", // 重新编码音频
"-preset", "fast", // 编码速度设置
"-crf", "23", // 质量设置0-51越小质量越好
"-avoid_negative_ts", "make_zero",
"-movflags", "+faststart", // 优化网络播放
"-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));
return Err(anyhow!("视频切分失败 (片段 {}): {}", index + 1, error_msg));
}
println!("片段 {} 切分完成: {}", index + 1, output_file);
output_files.push(output_file);
}
Ok(output_files)
}
/// 智能切分视频(寻找最近的关键帧)
pub fn split_video_smart(
input_path: &str,
output_dir: &str,
segments: &[(f64, f64)], // (start_time, end_time) pairs
output_prefix: &str,
) -> Result<Vec<String>> {
// 首先获取关键帧信息
let keyframes = Self::get_keyframes(input_path)?;
// 调整切分点到最近的关键帧
let adjusted_segments: Vec<(f64, f64)> = segments.iter().map(|(start, end)| {
let adjusted_start = Self::find_nearest_keyframe(&keyframes, *start);
let adjusted_end = Self::find_nearest_keyframe(&keyframes, *end);
(adjusted_start, adjusted_end)
}).collect();
println!("原始切分点: {:?}", segments);
println!("调整后切分点: {:?}", adjusted_segments);
// 使用调整后的切分点进行快速切分
Self::split_video_fast(input_path, output_dir, &adjusted_segments, output_prefix)
}
/// 获取视频的关键帧时间点
fn get_keyframes(input_path: &str) -> Result<Vec<f64>> {
let output = Command::new("ffprobe")
.args([
"-v", "quiet",
"-select_streams", "v:0",
"-show_entries", "frame=pkt_pts_time",
"-of", "csv=p=0",
"-skip_frame", "nokey", // 只显示关键帧
input_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));
}
let output_str = String::from_utf8_lossy(&output.stdout);
let mut keyframes = Vec::new();
for line in output_str.lines() {
if let Ok(time) = line.trim().parse::<f64>() {
keyframes.push(time);
}
}
keyframes.sort_by(|a, b| a.partial_cmp(b).unwrap());
Ok(keyframes)
}
/// 寻找最近的关键帧
fn find_nearest_keyframe(keyframes: &[f64], target_time: f64) -> f64 {
if keyframes.is_empty() {
return target_time;
}
let mut nearest = keyframes[0];
let mut min_distance = (target_time - keyframes[0]).abs();
for &keyframe in keyframes {
let distance = (target_time - keyframe).abs();
if distance < min_distance {
min_distance = distance;
nearest = keyframe;
}
}
nearest
}
/// 获取视频缩略图
pub fn generate_thumbnail(
input_path: &str,

View File

@ -58,7 +58,8 @@ pub fn run() {
commands::material_commands::detect_video_scenes,
commands::material_commands::generate_video_thumbnail,
commands::material_commands::test_scene_detection,
commands::material_commands::get_material_segments
commands::material_commands::get_material_segments,
commands::material_commands::test_video_split
])
.setup(|app| {
// 初始化应用状态

View File

@ -357,3 +357,36 @@ pub async fn get_material_segments(
repository.get_segments(&material_id)
.map_err(|e| e.to_string())
}
/// 测试视频切分命令(用于调试不同切分模式)
#[command]
pub async fn test_video_split(
input_path: String,
output_dir: String,
start_time: f64,
end_time: f64,
mode: String,
) -> Result<String, String> {
use crate::infrastructure::ffmpeg::FFmpegService;
let segments = vec![(start_time, end_time)];
let output_prefix = "test_segment";
let result = match mode.as_str() {
"fast" => {
FFmpegService::split_video_fast(&input_path, &output_dir, &segments, output_prefix)
}
"accurate" => {
FFmpegService::split_video(&input_path, &output_dir, &segments, output_prefix)
}
"smart" => {
FFmpegService::split_video_smart(&input_path, &output_dir, &segments, output_prefix)
}
_ => return Err("无效的切分模式,支持: fast, accurate, smart".to_string()),
};
match result {
Ok(files) => Ok(format!("切分成功,生成文件: {:?}", files)),
Err(e) => Err(format!("切分失败: {}", e)),
}
}