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:
parent
704e6d8fff
commit
864d1b42a9
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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, // 默认使用精确模式
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
// 初始化应用状态
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue