349 lines
12 KiB
Java
349 lines
12 KiB
Java
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<String> 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<String> 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<String> 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未安装";
|
||
}
|
||
} |