You've already forked FrameTour-BE
refactor(video): 优化视频切割逻辑,使用concat demuxer提升性能
- 引入concat demuxer方式替代原有转码流程,提高处理效率 - 新增PROBE_SIZE常量用于控制探测大小,优化文件解析 - 重构runFfmpegForMultipleFile1方法,简化多文件处理逻辑 - 添加quickVideoCutWithConcatDemuxer方法实现无转码快速切割 - 调整ffmpeg命令参数顺序及新增选项,如-probesize、-analyzeduration等 - 在多个ffmpeg调用中统一增加-genpts标志和避免负时间戳处理 - 完善临时文件清理机制,确保执行过程中的资源回收 - 更新相关ffmpeg命令构建逻辑以适配新的处理流程
This commit is contained in:
@@ -84,6 +84,8 @@ public class VideoPieceGetter {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MemberRelationRepository memberRelationRepository;
|
private MemberRelationRepository memberRelationRepository;
|
||||||
|
|
||||||
|
public static final String PROBE_SIZE = "32M";
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Task {
|
public static class Task {
|
||||||
public List<Long> faceSampleIds = new ArrayList<>();
|
public List<Long> faceSampleIds = new ArrayList<>();
|
||||||
@@ -472,63 +474,40 @@ public class VideoPieceGetter {
|
|||||||
// 多个文件切割,用速度快的
|
// 多个文件切割,用速度快的
|
||||||
result = runFfmpegForMultipleFile1(task);
|
result = runFfmpegForMultipleFile1(task);
|
||||||
}
|
}
|
||||||
// 先尝试方法1
|
return result;
|
||||||
if (result) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
log.warn("FFMPEG简易方法失败,尝试复杂方法转码");
|
|
||||||
// 不行再尝试方法二
|
|
||||||
return runFfmpegForMultipleFile2(task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean runFfmpegForMultipleFile1(FfmpegTask task) {
|
private boolean runFfmpegForMultipleFile1(FfmpegTask task) {
|
||||||
// 多文件,方法一:先转换成ts,然后合并切割
|
// 使用concat demuxer(文件列表)拼接裁切
|
||||||
// 步骤一:先转换成ts,并行转换
|
File concatListFile = null;
|
||||||
boolean notOk = task.getFileList().stream().map(file -> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("转码出错");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}).anyMatch(b -> !b);
|
|
||||||
// 转码进程中出现问题
|
|
||||||
if (notOk) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 步骤二:使用concat协议拼接裁切
|
|
||||||
boolean result;
|
|
||||||
try {
|
try {
|
||||||
result = quickVideoCut(
|
// 创建临时文件列表
|
||||||
"concat:" + task.getFileList().stream().map(FileObject::getUrl).collect(Collectors.joining("|")),
|
concatListFile = File.createTempFile("concat_list_", ".txt");
|
||||||
task.getOffsetStart(), task.getDuration(), task.getOutputFile()
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用concat demuxer进行裁切
|
||||||
|
return quickVideoCutWithConcatDemuxer(
|
||||||
|
concatListFile.getAbsolutePath(),
|
||||||
|
task.getOffsetStart(),
|
||||||
|
task.getDuration(),
|
||||||
|
task.getOutputFile()
|
||||||
);
|
);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
log.error("使用concat demuxer切割视频失败", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
} finally {
|
||||||
// 步骤三:删除临时文件
|
// 清理临时文件
|
||||||
task.getFileList().stream().map(FileObject::getUrl).forEach(tmpFile -> {
|
if (concatListFile != null && concatListFile.exists()) {
|
||||||
File f = new File(tmpFile);
|
concatListFile.delete();
|
||||||
if (f.exists() && f.isFile()) {
|
|
||||||
f.delete();
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean runFfmpegForMultipleFile2(FfmpegTask task) {
|
private boolean runFfmpegForMultipleFile2(FfmpegTask task) {
|
||||||
@@ -589,6 +568,48 @@ public class VideoPieceGetter {
|
|||||||
return handleFfmpegProcess(ffmpegCmd);
|
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帧导致前面的数据无法使用
|
* 快速切割,不产生转码,速度快,但可能会出现:第一帧数据不是I帧导致前面的数据无法使用
|
||||||
*
|
*
|
||||||
@@ -604,15 +625,23 @@ public class VideoPieceGetter {
|
|||||||
ffmpegCmd.add("ffmpeg");
|
ffmpegCmd.add("ffmpeg");
|
||||||
ffmpegCmd.add("-hide_banner");
|
ffmpegCmd.add("-hide_banner");
|
||||||
ffmpegCmd.add("-y");
|
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("-i");
|
||||||
ffmpegCmd.add(inputFile);
|
ffmpegCmd.add(inputFile);
|
||||||
|
ffmpegCmd.add("-fflags");
|
||||||
|
ffmpegCmd.add("+genpts");
|
||||||
ffmpegCmd.add("-c:v");
|
ffmpegCmd.add("-c:v");
|
||||||
ffmpegCmd.add("copy");
|
ffmpegCmd.add("copy");
|
||||||
ffmpegCmd.add("-an");
|
ffmpegCmd.add("-an");
|
||||||
ffmpegCmd.add("-ss");
|
|
||||||
ffmpegCmd.add(offset.toPlainString());
|
|
||||||
ffmpegCmd.add("-t");
|
ffmpegCmd.add("-t");
|
||||||
ffmpegCmd.add(length.toPlainString());
|
ffmpegCmd.add(length.toPlainString());
|
||||||
|
ffmpegCmd.add("-avoid_negative_ts");
|
||||||
|
ffmpegCmd.add("make_zero");
|
||||||
ffmpegCmd.add("-f");
|
ffmpegCmd.add("-f");
|
||||||
ffmpegCmd.add("mp4");
|
ffmpegCmd.add("mp4");
|
||||||
ffmpegCmd.add(outputFile);
|
ffmpegCmd.add(outputFile);
|
||||||
@@ -648,12 +677,16 @@ public class VideoPieceGetter {
|
|||||||
ffmpegCmd.add("[v]");
|
ffmpegCmd.add("[v]");
|
||||||
ffmpegCmd.add("-preset:v");
|
ffmpegCmd.add("-preset:v");
|
||||||
ffmpegCmd.add("fast");
|
ffmpegCmd.add("fast");
|
||||||
|
ffmpegCmd.add("-fflags");
|
||||||
|
ffmpegCmd.add("+genpts");
|
||||||
ffmpegCmd.add("-an");
|
ffmpegCmd.add("-an");
|
||||||
// 没有使用copy,因为使用了filter_complex
|
// 没有使用copy,因为使用了filter_complex
|
||||||
ffmpegCmd.add("-ss");
|
ffmpegCmd.add("-ss");
|
||||||
ffmpegCmd.add(offset.toPlainString());
|
ffmpegCmd.add(offset.toPlainString());
|
||||||
ffmpegCmd.add("-t");
|
ffmpegCmd.add("-t");
|
||||||
ffmpegCmd.add(length.toPlainString());
|
ffmpegCmd.add(length.toPlainString());
|
||||||
|
ffmpegCmd.add("-avoid_negative_ts");
|
||||||
|
ffmpegCmd.add("make_zero");
|
||||||
ffmpegCmd.add("-f");
|
ffmpegCmd.add("-f");
|
||||||
ffmpegCmd.add("mp4");
|
ffmpegCmd.add("mp4");
|
||||||
ffmpegCmd.add(outputFile);
|
ffmpegCmd.add(outputFile);
|
||||||
@@ -683,14 +716,14 @@ public class VideoPieceGetter {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// 最长1分钟
|
// 最长1分钟
|
||||||
boolean exited = ffmpegProcess.waitFor(1, TimeUnit.MINUTES);
|
boolean exited = ffmpegProcess.waitFor(90, TimeUnit.SECONDS);
|
||||||
if (exited) {
|
if (exited) {
|
||||||
int code = ffmpegProcess.exitValue();
|
int code = ffmpegProcess.exitValue();
|
||||||
Date _endDt = new Date();
|
Date _endDt = new Date();
|
||||||
log.info("FFMPEG执行命令结束,Code:【{}】,耗费时间:【{}ms】,命令:【{}】", code, _endDt.getTime() - _startDt.getTime(), String.join(" ", ffmpegCmd));
|
log.info("FFMPEG执行命令结束,Code:【{}】,耗费时间:【{}ms】,命令:【{}】", code, _endDt.getTime() - _startDt.getTime(), String.join(" ", ffmpegCmd));
|
||||||
return 0 == code;
|
return 0 == code;
|
||||||
} else {
|
} else {
|
||||||
log.error("FFMPEG执行命令没有在1分钟内退出,命令:【{}】", String.join(" ", ffmpegCmd));
|
log.error("FFMPEG执行命令没有在90秒内退出,命令:【{}】", String.join(" ", ffmpegCmd));
|
||||||
ffmpegProcess.destroy();
|
ffmpegProcess.destroy();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user