|
@@ -50,6 +50,21 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
|
|
|
// FFmpeg 命令路径(从配置文件读取)
|
|
// FFmpeg 命令路径(从配置文件读取)
|
|
|
@Value("${ffmpeg.path}")
|
|
@Value("${ffmpeg.path}")
|
|
|
private String FFMPEG_PATH;
|
|
private String FFMPEG_PATH;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否启用Docker模式
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${ffmpeg.docker-enabled:false}")
|
|
|
|
|
+ private boolean dockerEnabled;
|
|
|
|
|
+ // Docker镜像名称
|
|
|
|
|
+ @Value("${ffmpeg.docker-image:jrottenberg/ffmpeg}")
|
|
|
|
|
+ private String dockerImage;
|
|
|
|
|
+ // Docker容器内工作目录
|
|
|
|
|
+ @Value("${ffmpeg.container-work-dir:/data}")
|
|
|
|
|
+ private String containerWorkDir;
|
|
|
|
|
+ // 宿主机路径映射(Docker volume映射的源路径)
|
|
|
|
|
+ @Value("${ffmpeg.host-volume-path}")
|
|
|
|
|
+ private String hostVolumePath;
|
|
|
// 截取时间点(秒)
|
|
// 截取时间点(秒)
|
|
|
private static final int CAPTURE_TIME_SECOND = 1;
|
|
private static final int CAPTURE_TIME_SECOND = 1;
|
|
|
// 命令执行超时时间(秒)
|
|
// 命令执行超时时间(秒)
|
|
@@ -258,6 +273,44 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
|
|
|
return thumbnailPathStr;
|
|
return thumbnailPathStr;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将宿主机路径转换为Docker容器内路径
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 例如:宿主机路径 /usr/local/java/nightFragrance/uploadPath/upload/2026/02/03/video.mp4
|
|
|
|
|
+ * 转换为容器内路径 /data/upload/2026/02/03/video.mp4
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param hostPath 宿主机上的绝对路径
|
|
|
|
|
+ * @return Docker容器内的对应路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private String convertToContainerPath(String hostPath) {
|
|
|
|
|
+ if (hostPath == null || hostPath.isEmpty()) {
|
|
|
|
|
+ return hostPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 规范化路径分隔符(Windows风格转Linux风格)
|
|
|
|
|
+ String normalizedPath = hostPath.replace("\\", "/");
|
|
|
|
|
+ // 移除可能存在的重复斜杠
|
|
|
|
|
+ normalizedPath = normalizedPath.replaceAll("/+", "/");
|
|
|
|
|
+
|
|
|
|
|
+ //添加指定的前缀
|
|
|
|
|
+ normalizedPath = "/usr/local/java" + normalizedPath;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取配置的宿主机映射路径(规范化)
|
|
|
|
|
+ String normalizedVolumePath = hostVolumePath.replace("\\", "/").replaceAll("/+", "/");
|
|
|
|
|
+ if (!normalizedVolumePath.endsWith("/")) {
|
|
|
|
|
+ normalizedVolumePath += "/";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果路径以宿主机映射路径开头,则替换为容器工作目录
|
|
|
|
|
+ if (normalizedPath.startsWith(normalizedVolumePath)) {
|
|
|
|
|
+ String relativePath = normalizedPath.substring(normalizedVolumePath.length());
|
|
|
|
|
+ return containerWorkDir + "/" + relativePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果路径不匹配,直接返回原路径
|
|
|
|
|
+ log.warn("路径 {} 不在Docker卷映射路径 {} 下,使用原路径", hostPath, hostVolumePath);
|
|
|
|
|
+ return normalizedPath;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取不带扩展名的文件名
|
|
* 获取不带扩展名的文件名
|
|
|
*
|
|
*
|
|
@@ -275,40 +328,77 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 执行 FFmpeg 命令截取封面
|
|
* 执行 FFmpeg 命令截取封面
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 支持两种模式:
|
|
|
|
|
+ * 1. 本地模式:直接调用本地FFmpeg可执行文件
|
|
|
|
|
+ * 2. Docker模式:通过docker run命令调用容器内的FFmpeg
|
|
|
|
|
+ * </p>
|
|
|
*
|
|
*
|
|
|
- * @param videoPath 视频文件路径
|
|
|
|
|
- * @param thumbnailPath 封面图片保存路径
|
|
|
|
|
|
|
+ * @param videoPath 视频文件路径(宿主机路径)
|
|
|
|
|
+ * @param thumbnailPath 封面图片保存路径(宿主机路径)
|
|
|
*/
|
|
*/
|
|
|
private void executeFfmpegCommand(String videoPath, String thumbnailPath) throws IOException, InterruptedException, VideoThumbnailService.VideoProcessException {
|
|
private void executeFfmpegCommand(String videoPath, String thumbnailPath) throws IOException, InterruptedException, VideoThumbnailService.VideoProcessException {
|
|
|
- log.info("开始执行FFmpeg命令,视频文件路径: {}, 封面图片保存路径: {}", videoPath, thumbnailPath);
|
|
|
|
|
- // 构建 FFmpeg 命令
|
|
|
|
|
- // -ss: 截取时间点
|
|
|
|
|
- // -i: 输入文件
|
|
|
|
|
- // -vframes 1: 只截取一帧
|
|
|
|
|
- // -q:v 2: 图片质量(1-31,数字越小质量越高)
|
|
|
|
|
- // -y: 覆盖输出文件
|
|
|
|
|
|
|
+ log.info("开始执行FFmpeg命令,Docker模式: {}, 视频文件路径: {}, 封面图片保存路径: {}", dockerEnabled, videoPath, thumbnailPath);
|
|
|
List<String> command = new ArrayList<>();
|
|
List<String> command = new ArrayList<>();
|
|
|
- command.add(FFMPEG_PATH);
|
|
|
|
|
- command.add("-ss");
|
|
|
|
|
- command.add(String.valueOf(CAPTURE_TIME_SECOND));
|
|
|
|
|
- command.add("-i");
|
|
|
|
|
- command.add(videoPath);
|
|
|
|
|
- command.add("-vframes");
|
|
|
|
|
- command.add("1");
|
|
|
|
|
- command.add("-q:v");
|
|
|
|
|
- command.add("2");
|
|
|
|
|
- command.add("-y");
|
|
|
|
|
- command.add(thumbnailPath);
|
|
|
|
|
- log.info("执行FFmpeg命令: {}", String.join(" ", command));
|
|
|
|
|
|
|
+ if (dockerEnabled) {
|
|
|
|
|
+ // Docker模式:构建 docker run 命令
|
|
|
|
|
+ // 命令格式:docker run --rm -v /host/path:/data -w /data jrottenberg/ffmpeg -ss 1 -i /data/input.mp4 -vframes 1 -q:v 2 -y /data/output.jpg
|
|
|
|
|
+ command.add("docker");
|
|
|
|
|
+ command.add("run");
|
|
|
|
|
+ command.add("--rm");
|
|
|
|
|
|
|
|
|
|
+ // 添加volume映射:将宿主机路径映射到容器内
|
|
|
|
|
+ command.add("-v");
|
|
|
|
|
+ // 规范化路径(处理Windows风格的路径分隔符)
|
|
|
|
|
+ String normalizedVolumePath = hostVolumePath.replace("\\", "/");
|
|
|
|
|
+ if (!normalizedVolumePath.startsWith("/")) {
|
|
|
|
|
+ // Windows路径需要特殊处理,转换为 /host 形式或使用完整路径
|
|
|
|
|
+ normalizedVolumePath = "/" + normalizedVolumePath.replace(":", "").replace("\\", "/");
|
|
|
|
|
+ }
|
|
|
|
|
+ String volumeMapping = normalizedVolumePath + ":" + containerWorkDir;
|
|
|
|
|
+ command.add(volumeMapping);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置工作目录
|
|
|
|
|
+ command.add("-w");
|
|
|
|
|
+ command.add(containerWorkDir);
|
|
|
|
|
+
|
|
|
|
|
+ // Docker镜像名称
|
|
|
|
|
+ command.add(dockerImage);
|
|
|
|
|
+
|
|
|
|
|
+ // 将宿主机路径转换为容器内路径
|
|
|
|
|
+ String containerVideoPath = convertToContainerPath(videoPath);
|
|
|
|
|
+ String containerThumbnailPath = convertToContainerPath(thumbnailPath);
|
|
|
|
|
+
|
|
|
|
|
+ // FFmpeg参数
|
|
|
|
|
+ command.add("-ss");
|
|
|
|
|
+ command.add(String.valueOf(CAPTURE_TIME_SECOND));
|
|
|
|
|
+ command.add("-i");
|
|
|
|
|
+ command.add(containerVideoPath);
|
|
|
|
|
+ command.add("-vframes");
|
|
|
|
|
+ command.add("1");
|
|
|
|
|
+ command.add("-q:v");
|
|
|
|
|
+ command.add("2");
|
|
|
|
|
+ command.add("-y");
|
|
|
|
|
+ command.add(containerThumbnailPath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 本地模式:直接调用FFmpeg可执行文件
|
|
|
|
|
+ command.add(FFMPEG_PATH);
|
|
|
|
|
+ command.add("-ss");
|
|
|
|
|
+ command.add(String.valueOf(CAPTURE_TIME_SECOND));
|
|
|
|
|
+ command.add("-i");
|
|
|
|
|
+ command.add(videoPath);
|
|
|
|
|
+ command.add("-vframes");
|
|
|
|
|
+ command.add("1");
|
|
|
|
|
+ command.add("-q:v");
|
|
|
|
|
+ command.add("2");
|
|
|
|
|
+ command.add("-y");
|
|
|
|
|
+ command.add(thumbnailPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("执行命令: {}", String.join(" ", command));
|
|
|
// 执行命令
|
|
// 执行命令
|
|
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
|
|
- Map<String, String> env = processBuilder.environment();
|
|
|
|
|
- env.put("LD_LIBRARY_PATH", "/usr/local/ffmpeg-8.0/lib");
|
|
|
|
|
- env.put("PATH", env.get("PATH") + ":/usr/local/ffmpeg-8.0/bin");
|
|
|
|
|
processBuilder.redirectErrorStream(true);
|
|
processBuilder.redirectErrorStream(true);
|
|
|
Process process = processBuilder.start();
|
|
Process process = processBuilder.start();
|
|
|
-
|
|
|
|
|
// 读取命令输出(用于日志和调试)
|
|
// 读取命令输出(用于日志和调试)
|
|
|
StringBuilder output = new StringBuilder();
|
|
StringBuilder output = new StringBuilder();
|
|
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|