package com.ruoyi.system.utils; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.ArrayList; /** * FFmpeg工具类 * 用于音频文件合并等操作 * * @author ruoyi */ public class FFmpegUtils { private static final Logger logger = LoggerFactory.getLogger(FFmpegUtils.class); /** * 查找FFmpeg路径 */ private static String findFFmpegPath() { // 首先尝试项目内部的FFmpeg String projectFFmpegPath = getProjectFFmpegPath(); if (projectFFmpegPath != null && isExecutable(projectFFmpegPath)) { logger.info("使用项目内部FFmpeg: {}", projectFFmpegPath); return projectFFmpegPath; } // 如果项目内部没有,尝试系统安装的FFmpeg String[] systemPaths = { "ffmpeg", "C:\\ffmpeg\\bin\\ffmpeg.exe", "C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe", "D:\\ffmpeg\\bin\\ffmpeg.exe", "/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg" }; for (String path : systemPaths) { if (isExecutable(path)) { logger.info("使用系统FFmpeg: {}", path); return path; } } logger.error("未找到可用的FFmpeg"); return null; } /** * 获取项目内部的FFmpeg路径 */ private static String getProjectFFmpegPath() { try { // 获取项目资源目录中的FFmpeg路径 String resourcePath = "/ffmpeg/ffmpeg.exe"; java.net.URL resourceUrl = FFmpegUtils.class.getResource(resourcePath); if (resourceUrl != null) { // 如果是jar包中的资源,需要提取到临时目录 if (resourceUrl.getProtocol().equals("jar")) { return extractFFmpegFromJar(); } else { // 如果是文件系统中的资源,直接返回路径 return new java.io.File(resourceUrl.toURI()).getAbsolutePath(); } } // 尝试从classpath中查找 String classpathPath = System.getProperty("user.dir") + "/ruoyi-system/src/main/resources/ffmpeg/ffmpeg.exe"; java.io.File classpathFile = new java.io.File(classpathPath); if (classpathFile.exists()) { return classpathFile.getAbsolutePath(); } } catch (Exception e) { logger.error("获取项目内部FFmpeg路径失败", e); } return null; } /** * 从jar包中提取FFmpeg到临时目录 */ private static String extractFFmpegFromJar() { try { // 创建临时目录 java.io.File tempDir = java.io.File.createTempFile("ffmpeg", ""); tempDir.delete(); tempDir.mkdirs(); // 提取FFmpeg String ffmpegPath = tempDir.getAbsolutePath() + "/ffmpeg.exe"; java.io.File ffmpegFile = new java.io.File(ffmpegPath); // 从jar包中复制文件 java.io.InputStream inputStream = FFmpegUtils.class.getResourceAsStream("/ffmpeg/ffmpeg.exe"); if (inputStream != null) { java.io.FileOutputStream outputStream = new java.io.FileOutputStream(ffmpegFile); byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); } inputStream.close(); outputStream.close(); // 设置可执行权限 ffmpegFile.setExecutable(true); logger.info("从jar包提取FFmpeg到: {}", ffmpegPath); return ffmpegPath; } } catch (Exception e) { logger.error("从jar包提取FFmpeg失败", e); } return null; } /** * 检查文件是否可执行 */ private static boolean isExecutable(String path) { try { ProcessBuilder pb = new ProcessBuilder(path, "-version"); Process process = pb.start(); int exitCode = process.waitFor(); return exitCode == 0; } catch (Exception e) { return false; } } /** * 合并音频文件 * * @param inputFiles 输入文件列表 * @param outputFile 输出文件路径 * @return 是否成功 */ public static boolean mergeAudioFiles(List inputFiles, String outputFile) { try { logger.info("开始合并音频文件,输入文件数量: {}, 输出文件: {}", inputFiles.size(), outputFile); // 检查输入文件 for (String inputFile : inputFiles) { if (!validateAudioFile(inputFile)) { logger.error("输入文件验证失败: {}", inputFile); return false; } } // 创建输出目录 File outputDir = new File(outputFile).getParentFile(); if (!outputDir.exists()) { outputDir.mkdirs(); } // 使用系统命令方式合并 return mergeWithSystemCommand(inputFiles, outputFile); } catch (Exception e) { logger.error("合并音频文件失败", e); return false; } } /** * 使用系统命令方式合并 */ private static boolean mergeWithSystemCommand(List inputFiles, String outputFile) throws IOException { // 创建输入文件列表 Path inputListFile = Files.createTempFile("input_list", ".txt"); try { // 写入文件列表 StringBuilder content = new StringBuilder(); for (String inputFile : inputFiles) { content.append("file '").append(inputFile).append("'\n"); logger.info("添加输入文件: {}", inputFile); } Files.write(inputListFile, content.toString().getBytes()); // 记录输入文件列表内容 logger.info("输入文件列表内容:\n{}", content.toString()); // 构建系统命令 String ffmpegPath = findFFmpegPath(); if (ffmpegPath == null) { logger.error("未找到可用的FFmpeg"); return false; } // 构建FFmpeg命令 List command = new ArrayList<>(); command.add(ffmpegPath); command.add("-f"); command.add("concat"); command.add("-safe"); command.add("0"); command.add("-i"); command.add(inputListFile.toString()); command.add("-c"); command.add("copy"); command.add(outputFile); ProcessBuilder pb = new ProcessBuilder(command); // 记录完整的FFmpeg命令 logger.info("执行FFmpeg命令: {}", String.join(" ", command)); // 设置工作目录 pb.directory(new File(System.getProperty("user.dir"))); // 合并错误和标准输出 pb.redirectErrorStream(true); // 执行命令 Process process = pb.start(); // 读取输出 java.io.BufferedReader reader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream()) ); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } int exitCode; try { exitCode = process.waitFor(); } catch (InterruptedException e) { logger.error("FFmpeg进程被中断", e); Thread.currentThread().interrupt(); return false; } if (exitCode != 0) { logger.error("FFmpeg合并失败,退出码: {}, 输出: {}", exitCode, output.toString()); // 分析错误原因 String errorOutput = output.toString(); if (errorOutput.contains("No such file or directory")) { logger.error("错误原因: 输入文件不存在或路径错误"); } else if (errorOutput.contains("Permission denied")) { logger.error("错误原因: 权限不足,无法读取输入文件或写入输出文件"); } else if (errorOutput.contains("Invalid data found")) { logger.error("错误原因: 输入文件格式无效或损坏"); } else if (errorOutput.contains("No such file or directory")) { logger.error("错误原因: 输出目录不存在或无法创建"); } else { logger.error("错误原因: 未知错误,请检查FFmpeg输出信息"); } return false; } // 验证输出文件 File outputFileObj = new File(outputFile); if (outputFileObj.exists() && outputFileObj.length() > 0) { logger.info("音频文件合并成功: {}", outputFile); return true; } else { logger.error("合并后的文件不存在或为空: {}", outputFile); return false; } } finally { // 清理临时文件 Files.deleteIfExists(inputListFile); } } /** * 检查FFmpeg是否可用 */ public static boolean isFFmpegAvailable() { return findFFmpegPath() != null; } /** * 验证音频文件格式 */ private static boolean validateAudioFile(String filePath) { try { File file = new File(filePath); if (!file.exists()) { logger.error("文件不存在: {}", filePath); return false; } if (!file.canRead()) { logger.error("文件无法读取: {}", filePath); return false; } // 检查文件扩展名 String fileName = file.getName().toLowerCase(); if (!fileName.endsWith(".mp3") && !fileName.endsWith(".wav") && !fileName.endsWith(".m4a") && !fileName.endsWith(".aac")) { logger.warn("文件格式可能不支持: {}", filePath); } // 检查文件大小 long fileSize = file.length(); if (fileSize == 0) { logger.error("文件为空: {}", filePath); return false; } logger.info("文件验证通过: {} (大小: {} bytes)", filePath, fileSize); return true; } catch (Exception e) { logger.error("验证文件失败: {}", filePath, e); return false; } } /** * 获取FFmpeg版本信息 */ public static String getFFmpegVersion() { try { String ffmpegPath = findFFmpegPath(); if (ffmpegPath != null) { ProcessBuilder pb = new ProcessBuilder(ffmpegPath, "-version"); Process process = pb.start(); java.io.BufferedReader reader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream()) ); return reader.readLine(); } } catch (Exception e) { logger.error("获取FFmpeg版本失败", e); } return "FFmpeg未安装"; } }