refactor(video): 优化视频切割逻辑,使用concat demuxer提升性能

- 引入concat demuxer方式替代原有转码流程,提高处理效率
- 新增PROBE_SIZE常量用于控制探测大小,优化文件解析
- 重构runFfmpegForMultipleFile1方法,简化多文件处理逻辑
- 添加quickVideoCutWithConcatDemuxer方法实现无转码快速切割
- 调整ffmpeg命令参数顺序及新增选项,如-probesize、-analyzeduration等
- 在多个ffmpeg调用中统一增加-genpts标志和避免负时间戳处理
- 完善临时文件清理机制,确保执行过程中的资源回收
- 更新相关ffmpeg命令构建逻辑以适配新的处理流程
This commit is contained in:
2025-12-15 10:14:02 +08:00
parent 5bef712b1c
commit 3c838ec36e

View File

@@ -84,6 +84,8 @@ public class VideoPieceGetter {
@Autowired
private MemberRelationRepository memberRelationRepository;
public static final String PROBE_SIZE = "32M";
@Data
public static class Task {
public List<Long> faceSampleIds = new ArrayList<>();
@@ -472,63 +474,40 @@ public class VideoPieceGetter {
// 多个文件切割,用速度快的
result = runFfmpegForMultipleFile1(task);
}
// 先尝试方法1
if (result) {
return true;
}
log.warn("FFMPEG简易方法失败,尝试复杂方法转码");
// 不行再尝试方法二
return runFfmpegForMultipleFile2(task);
return result;
}
private boolean runFfmpegForMultipleFile1(FfmpegTask task) {
// 多文件,方法一:先转换成ts,然后合并切割
// 步骤一:先转换成ts,并行转换
boolean notOk = task.getFileList().stream().map(file -> {
// 使用concat demuxer(文件列表)拼接裁切
File concatListFile = null;
try {
if (file.isNeedDownload() || (!file.getName().endsWith(".ts"))) {
// 使用时间戳和线程ID确保临时文件名唯一性,避免并发冲突
String uniqueSuffix = System.currentTimeMillis() + "_" + Thread.currentThread().getId();
String tmpFile = file.getName() + "_" + uniqueSuffix + ".ts";
boolean result = convertMp4ToTs(file, tmpFile);
// 因为是并行转换,没法保证顺序,就直接存里面
if (result) {
file.setUrl(tmpFile);
} else {
// 失败了,务必删除临时文件
(new File(tmpFile)).delete();
// 创建临时文件列表
concatListFile = File.createTempFile("concat_list_", ".txt");
try (java.io.FileWriter writer = new java.io.FileWriter(concatListFile)) {
for (FileObject file : task.getFileList()) {
// concat demuxer格式:file 'path'
// 对单引号进行转义
String escapedUrl = file.getUrl().replace("'", "'\\''");
writer.write("file '" + escapedUrl + "'\n");
}
return result;
} else {
return true;
}
} catch (IOException e) {
log.warn("转码出错");
return false;
}
}).anyMatch(b -> !b);
// 转码进程中出现问题
if (notOk) {
return false;
}
// 步骤二:使用concat协议拼接裁切
boolean result;
try {
result = quickVideoCut(
"concat:" + task.getFileList().stream().map(FileObject::getUrl).collect(Collectors.joining("|")),
task.getOffsetStart(), task.getDuration(), task.getOutputFile()
// 使用concat demuxer进行裁切
return quickVideoCutWithConcatDemuxer(
concatListFile.getAbsolutePath(),
task.getOffsetStart(),
task.getDuration(),
task.getOutputFile()
);
} catch (IOException e) {
log.error("使用concat demuxer切割视频失败", e);
return false;
} finally {
// 清理临时文件
if (concatListFile != null && concatListFile.exists()) {
concatListFile.delete();
}
// 步骤三:删除临时文件
task.getFileList().stream().map(FileObject::getUrl).forEach(tmpFile -> {
File f = new File(tmpFile);
if (f.exists() && f.isFile()) {
f.delete();
}
});
return result;
}
private boolean runFfmpegForMultipleFile2(FfmpegTask task) {
@@ -589,6 +568,48 @@ public class VideoPieceGetter {
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 使用concat demuxer快速切割,不产生转码,速度快
*
* @param concatListFile concat列表文件路径
* @param offset 离输入文件开始的偏移
* @param length 输出文件时长
* @param outputFile 输出文件名称
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean quickVideoCutWithConcatDemuxer(String concatListFile, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-f");
ffmpegCmd.add("concat");
ffmpegCmd.add("-safe");
ffmpegCmd.add("0");
ffmpegCmd.add("-probesize");
ffmpegCmd.add(PROBE_SIZE);
ffmpegCmd.add("-analyzeduration");
ffmpegCmd.add("0");
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-i");
ffmpegCmd.add(concatListFile);
ffmpegCmd.add("-fflags");
ffmpegCmd.add("+genpts");
ffmpegCmd.add("-c:v");
ffmpegCmd.add("copy");
ffmpegCmd.add("-an");
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-avoid_negative_ts");
ffmpegCmd.add("make_zero");
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 快速切割,不产生转码,速度快,但可能会出现:第一帧数据不是I帧导致前面的数据无法使用
*
@@ -604,15 +625,23 @@ public class VideoPieceGetter {
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-probesize");
ffmpegCmd.add(PROBE_SIZE);
ffmpegCmd.add("-analyzeduration");
ffmpegCmd.add("0");
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-i");
ffmpegCmd.add(inputFile);
ffmpegCmd.add("-fflags");
ffmpegCmd.add("+genpts");
ffmpegCmd.add("-c:v");
ffmpegCmd.add("copy");
ffmpegCmd.add("-an");
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-avoid_negative_ts");
ffmpegCmd.add("make_zero");
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
@@ -648,12 +677,16 @@ public class VideoPieceGetter {
ffmpegCmd.add("[v]");
ffmpegCmd.add("-preset:v");
ffmpegCmd.add("fast");
ffmpegCmd.add("-fflags");
ffmpegCmd.add("+genpts");
ffmpegCmd.add("-an");
// 没有使用copy,因为使用了filter_complex
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-avoid_negative_ts");
ffmpegCmd.add("make_zero");
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
@@ -683,14 +716,14 @@ public class VideoPieceGetter {
}
try {
// 最长1分钟
boolean exited = ffmpegProcess.waitFor(1, TimeUnit.MINUTES);
boolean exited = ffmpegProcess.waitFor(90, TimeUnit.SECONDS);
if (exited) {
int code = ffmpegProcess.exitValue();
Date _endDt = new Date();
log.info("FFMPEG执行命令结束,Code:【{}】,耗费时间:【{}ms】,命令:【{}】", code, _endDt.getTime() - _startDt.getTime(), String.join(" ", ffmpegCmd));
return 0 == code;
} else {
log.error("FFMPEG执行命令没有在1分钟内退出,命令:【{}】", String.join(" ", ffmpegCmd));
log.error("FFMPEG执行命令没有在90秒内退出,命令:【{}】", String.join(" ", ffmpegCmd));
ffmpegProcess.destroy();
return false;
}