package org.sxkj.odm.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /** * 视频转换图片工具类 * @author zhongrj */ public class VideoConverterUtils { private static final Logger logger = LoggerFactory.getLogger(VideoConverterUtils.class); /** * 视频切图 * @param inputPath 视频路径 * @param outputPattern 图片输出路径 * @param interval 每多少帧取一个图片保存 * @throws IOException * @throws InterruptedException * @return */ public static boolean extractFrames(String inputPath, String outputPattern, int interval) throws IOException, InterruptedException { boolean flag = false; // 构建FFmpeg命令 String[] ffmpegCommand = { "ffmpeg", "-i", inputPath, // 输入文件 "-vf", "select=not(mod(n\\," + interval + "))", // 选择每interval帧中的一帧 "-vsync", "vfr", // 可变帧率处理 "-q:v", "2", // 输出图片质量(2-31,2为最高质量) "-frame_pts", "1", // 使用帧号作为输出文件名的一部分 outputPattern // 输出文件模式 }; try { // 执行FFmpeg命令 Process process = new ProcessBuilder(ffmpegCommand).start(); // 读取错误流(FFmpeg的输出通常写在错误流) BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); String line; while ((line = errorReader.readLine()) != null) { System.out.println(line); } // 等待命令执行完成 int exitCode = process.waitFor(); if (exitCode == 0) { flag = true; logger.info("视频切图完成!"); } else { flag = false; logger.info("视频切图失败,退出码: " + exitCode); } } catch (IOException | InterruptedException e) { logger.error("视频切图失败,异常消息:{}",e.getMessage()); e.printStackTrace(); throw e; } return flag; } /** * 视频切图-限制cpu,内存使用 * @param inputPath 视频路径 * @param outputPattern 图片输出路径 * @param interval 每多少帧取一个图片保存 * @throws IOException * @throws InterruptedException * @return */ public static boolean extractFramesRestriction(String inputPath, String outputPattern, int interval) throws IOException, InterruptedException { // 防止接口重复写入(重试时) if(hasJpegFileNIO(inputPath)){ return false; } boolean flag = false; // 构建FFmpeg命令 String[] ffmpegCommand = { "ffmpeg", "-loglevel", "error", // 减少日志输出量 "-threads", "1", // 限制线程数 "-hwaccel", "auto", // 启用硬件加速 "-i", inputPath, // 输入文件 "-max_muxing_queue_size", "100", // 限制复用队列大小 "-vf", "select=not(mod(n\\," + interval + "))", // 选择每interval帧中的一帧 "-vsync", "0", // 禁用帧同步缓冲 "-q:v", "3", // 输出图片质量(2-31,2为最高质量) "-frame_pts", "1", // 使用帧号作为输出文件名的一部分 "-f", "image2", // 明确指定图像格式 outputPattern // 输出文件模式 }; try { ProcessBuilder pb = new ProcessBuilder(ffmpegCommand); // 重定向错误流到临时文件,避免内存堆积 File errorLog = File.createTempFile("ffmpeg_error", ".log"); pb.redirectError(errorLog); Process process = pb.start(); // 不缓冲输出流,立即释放内存 try (InputStream ignored = process.getInputStream(); InputStream ignored2 = process.getErrorStream()) { int exitCode = process.waitFor(); if (exitCode == 0) { flag = true; logger.info("视频切图完成!"); } else { flag = false; logger.error("视频切图失败,退出码: {}。错误日志: {}", exitCode, errorLog.getAbsolutePath()); } } errorLog.delete(); // 清理临时文件 } catch (IOException | InterruptedException e) { logger.error("视频切图失败,异常消息:{}", e.getMessage()); throw e; } return flag; } /** * 判断路径下是否已经包含了 jpeg 文件 * @param inputPath * @return */ public static boolean hasJpegFileNIO(String inputPath) { try { Path dir = Paths.get(inputPath); if (!Files.isDirectory(dir)) { return false; } // 使用DirectoryStream逐项处理,不一次性加载所有文件 try (DirectoryStream stream = Files.newDirectoryStream(dir)) { for (Path path : stream) { if (Files.isRegularFile(path)) { String fileName = path.getFileName().toString().toLowerCase(); if (fileName.endsWith(".jpeg") || fileName.endsWith(".jpg")) { return true; } } } } } catch (Exception e) { e.printStackTrace(); } return false; } /** * 从视频中提取字幕为SRT文件 * @param inputPath 视频路径 * @param srtPath 输出SRT文件路径 * @return true表示成功,false表示失败 * @throws InterruptedException 进程被中断 * @throws IOException 文件操作失败 * @return */ public static boolean extractSrt(String inputPath, String srtPath) throws InterruptedException, IOException { // 1. 先检查视频是否有字幕流 if (!hasSubtitleStream(inputPath)) { logger.warn("视频文件没有字幕流: {}", inputPath); return false; } // 2. 构建FFmpeg命令 String[] ffmpegCommand = { "ffmpeg", "-i", inputPath, "-map", "0:s:0", // 提取第一条字幕流 "-c:s", "srt", // 强制转换为SRT格式 "-y", // 覆盖已存在文件 srtPath }; try { Process process = new ProcessBuilder(ffmpegCommand) .redirectErrorStream(true) // 合并错误流和标准输出 .start(); // 读取输出 try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { logger.debug("FFmpeg输出: {}", line); } } int exitCode = process.waitFor(); if (exitCode == 0) { logger.info("成功提取SRT字幕到: {}", srtPath); return true; } else { logger.error("提取SRT字幕失败,退出码: {}", exitCode); return false; } } catch (IOException | InterruptedException e) { logger.error("提取SRT字幕时出错: {}", e.getMessage()); throw e; } } /** * 检查视频是否包含字幕流 * @param inputPath * @return * @throws IOException */ private static boolean hasSubtitleStream(String inputPath) throws IOException { String[] probeCommand = { "ffprobe", "-v", "error", "-select_streams", "s", "-show_entries", "stream=codec_type", "-of", "csv=p=0", inputPath }; Process process = new ProcessBuilder(probeCommand).start(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream()))) { return reader.readLine() != null; // 有输出表示有字幕流 } } /** * 根据输入的路径获取输出路径 * @param inputPath * @return */ public static String getOutputPathByInputPath(String inputPath){ // 获取输入文件的父目录和文件名(不带扩展名) Path path = Paths.get(inputPath); String parentDir = path.getParent().toString(); String fileName = path.getFileName().toString(); String baseName = fileName.substring(0, fileName.lastIndexOf('.')); // 创建以MP4文件名命名的子目录 String outputDir = parentDir + File.separator + baseName + File.separator; // 确保输出目录存在 File file = new File(outputDir); if (!file.exists()){ // 目录不存在则创建 file.mkdirs(); } return outputDir; } /** * 根据输入的路径获取输出路径 * @param inputPath * @return */ public static String getFileBaseNamePathByInputPath(String inputPath){ // 获取输入文件的父目录和文件名(不带扩展名) Path path = Paths.get(inputPath); String fileName = path.getFileName().toString(); String baseName = fileName.substring(0, fileName.lastIndexOf('.')); return baseName; } /** * mp4 视频转 jpg 图片 * @param inputVideoPath 视频输入路径 * @param frameInterval 图片帧保持间隔,每多少帧保存一次 * @return * @throws IOException * @throws InterruptedException */ public static boolean mp4ToJpg(String inputVideoPath,int frameInterval) throws IOException, InterruptedException { // 设置 图片 保存的目录 String outputDir = getOutputPathByInputPath(inputVideoPath); String outputImagePattern = outputDir + "frame_%04d.jpg"; // 转换生成图片 boolean flag = extractFrames(inputVideoPath, outputImagePattern, frameInterval); // 返回处理结果 return flag; } /** * mp4 视频转 jpg 图片-优化版本 * @param inputVideoPath 视频输入路径 * @param frameInterval 图片帧保持间隔,每多少帧保存一次 * @return * @throws IOException * @throws InterruptedException */ public static boolean mp4ToJpgRestriction(String inputVideoPath,int frameInterval) throws IOException, InterruptedException { // 设置 图片 保存的目录 String outputDir = getOutputPathByInputPath(inputVideoPath); String outputImagePattern = outputDir + "frame_%04d.jpg"; // 转换生成图片 boolean flag = extractFramesRestriction(inputVideoPath, outputImagePattern, frameInterval); // 返回处理结果 return flag; } /** * 从 mp4 视频获取 srt 文件 * @param inputVideoPath 视频输入路径 * @return * @throws InterruptedException * @throws IOException */ public static boolean mp4ToCreateStr(String inputVideoPath) throws InterruptedException, IOException { // 设置 srt 保存的目录 String outputDir = getOutputPathByInputPath(inputVideoPath); String strPath = outputDir + "out.srt"; // 转换生成 srt 文件 boolean flag = extractSrt(inputVideoPath, strPath); // 返回结果 return flag; } // public static void main(String[] args) throws InterruptedException, IOException { // String cc = "E:\\temp\\video\\倾斜视频\\DJI_20250723102452_0001_V"; // 输入视频文件路径 //// boolean b = mp4ToJpg(inputVideoPath,30); //// boolean b1 = mp4ToCreateStr(inputVideoPath); // boolean b2 = hasJpegFileNIO(cc); // System.out.println("b2 = " + b2); // } }