diff --git a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java index 74557e59..993bad6a 100644 --- a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java +++ b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java @@ -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 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 -> { - 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; + // 使用concat demuxer(文件列表)拼接裁切 + File concatListFile = null; try { - result = quickVideoCut( - "concat:" + task.getFileList().stream().map(FileObject::getUrl).collect(Collectors.joining("|")), - task.getOffsetStart(), task.getDuration(), task.getOutputFile() + // 创建临时文件列表 + 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"); + } + } + + // 使用concat demuxer进行裁切 + return quickVideoCutWithConcatDemuxer( + concatListFile.getAbsolutePath(), + task.getOffsetStart(), + task.getDuration(), + task.getOutputFile() ); } catch (IOException e) { + log.error("使用concat demuxer切割视频失败", e); return false; - } - // 步骤三:删除临时文件 - task.getFileList().stream().map(FileObject::getUrl).forEach(tmpFile -> { - File f = new File(tmpFile); - if (f.exists() && f.isFile()) { - f.delete(); + } finally { + // 清理临时文件 + if (concatListFile != null && concatListFile.exists()) { + concatListFile.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 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; }