以下是一个完整的 SpringBoot 整合 FFmpeg 的工具类:
1. 配置类
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "ffmpeg")
public class FfmpegProperties {
private String path = "ffmpeg"; // 默认使用系统PATH中的ffmpeg
private int timeout = 300; // 命令执行超时时间(秒)
private int maxThreads = 4; // 最大线程数
// getters and setters
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getMaxThreads() {
return maxThreads;
}
public void setMaxThreads(int maxThreads) {
this.maxThreads = maxThreads;
}
}
2. 完整的 FFmpeg 工具类
package com.example.util;
import com.example.config.FfmpegProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class FfmpegUtil {
private static final Logger logger = LoggerFactory.getLogger(FfmpegUtil.class);
@Autowired
private FfmpegProperties ffmpegProperties;
/**
* 执行FFmpeg命令
*/
public boolean execCmd(String[] cmd) {
return execCmd(cmd, null);
}
/**
* 执行FFmpeg命令(带输出处理)
*/
public boolean execCmd(String[] cmd, OutputHandler outputHandler) {
ProcessBuilder processBuilder = new ProcessBuilder(cmd);
processBuilder.redirectErrorStream(true); // 合并标准错误和标准输出
try {
Process process = processBuilder.start();
// 处理输出流
if (outputHandler != null) {
handleOutput(process, outputHandler);
}
// 等待进程完成
boolean finished = process.waitFor(ffmpegProperties.getTimeout(), TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
logger.error("FFmpeg命令执行超时: {}", String.join(" ", cmd));
return false;
}
int exitValue = process.exitValue();
if (exitValue == 0) {
logger.info("FFmpeg命令执行成功: {}", String.join(" ", cmd));
return true;
} else {
logger.error("FFmpeg命令执行失败,退出码: {}, 命令: {}", exitValue, String.join(" ", cmd));
return false;
}
} catch (IOException | InterruptedException e) {
logger.error("FFmpeg命令执行异常: {}", e.getMessage(), e);
return false;
}
}
/**
* 处理输出流
*/
private void handleOutput(Process process, OutputHandler outputHandler) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
outputHandler.handle(line);
}
} catch (IOException e) {
logger.error("处理FFmpeg输出流异常: {}", e.getMessage());
}
}
/**
* 音频转码为WAV格式
*/
public boolean audioToWav(String inputPath, String outputPath) {
return audioToWav(inputPath, outputPath, 44100, 2, 16);
}
/**
* 音频转码为WAV格式(自定义参数)
*/
public boolean audioToWav(String inputPath, String outputPath,
Integer sampleRate, Integer channels, Integer bitDepth) {
// 输出文件预处理
File outputFile = new File(outputPath);
prepareOutputFile(outputFile);
// 设置默认参数
if (sampleRate == null) sampleRate = 44100;
if (channels == null) channels = 2;
if (bitDepth == null) bitDepth = 16;
// 根据位深度选择PCM格式
String pcmFormat = getPcmFormat(bitDepth);
// 构建FFmpeg命令
String[] cmd = new String[]{
ffmpegProperties.getPath(),
"-i", inputPath,
"-c:a", pcmFormat,
"-ac", channels.toString(),
"-ar", sampleRate.toString(),
"-f", "wav",
"-y",
outputPath
};
logger.info("音频转码WAV: {} -> {}", inputPath, outputPath);
return execCmd(cmd);
}
/**
* 视频转码为指定编码格式
*/
public boolean videoTranscoding(String inputPath, String outputPath) {
return videoTranscoding(inputPath, outputPath, "libx264", "medium", 23);
}
/**
* 视频转码(自定义参数)
*/
public boolean videoTranscoding(String inputPath, String outputPath,
String codec, String preset, Integer crf) {
File outputFile = new File(outputPath);
prepareOutputFile(outputFile);
if (preset == null) preset = "medium";
if (crf == null) crf = 23;
String[] cmd = new String[]{
ffmpegProperties.getPath(),
"-threads", String.valueOf(ffmpegProperties.getMaxThreads()),
"-i", inputPath,
"-c:v", codec,
"-preset", preset,
"-crf", crf.toString(),
"-c:a", "aac",
"-b:a", "128k",
"-movflags", "+faststart", // 优化网络播放
"-f", "mp4",
"-y",
outputPath
};
logger.info("视频转码: {} -> {}", inputPath, outputPath);
return execCmd(cmd);
}
/**
* 提取视频中的音频
*/
public boolean extractAudio(String videoPath, String audioPath) {
File outputFile = new File(audioPath);
prepareOutputFile(outputFile);
String[] cmd = new String[]{
ffmpegProperties.getPath(),
"-i", videoPath,
"-vn", // 禁用视频流
"-acodec", "copy", // 直接复制音频流
"-y",
audioPath
};
logger.info("提取音频: {} -> {}", videoPath, audioPath);
return execCmd(cmd);
}
/**
* 获取媒体文件信息
*/
public MediaInfo getMediaInfo(String filePath) {
List<String> outputLines = new ArrayList<>();
OutputHandler handler = outputLines::add;
String[] cmd = new String[]{
ffmpegProperties.getPath(),
"-i", filePath
};
// 这个命令会失败(因为缺少输出文件),但我们可以从错误输出中获取信息
execCmd(cmd, handler);
return parseMediaInfo(outputLines, filePath);
}
/**
* 解析媒体信息
*/
private MediaInfo parseMediaInfo(List<String> outputLines, String filePath) {
MediaInfo info = new MediaInfo();
info.setFilePath(filePath);
for (String line : outputLines) {
// 解析时长
if (line.contains("Duration:")) {
String duration = line.split("Duration:")[1].split(",")[0].trim();
info.setDuration(duration);
}
// 解析视频流信息
else if (line.contains("Video:")) {
info.setVideoStream(true);
if (line.contains("h264")) info.setVideoCodec("H.264");
else if (line.contains("hevc")) info.setVideoCodec("H.265");
// 可以添加更多解析逻辑
}
// 解析音频流信息
else if (line.contains("Audio:")) {
info.setAudioStream(true);
if (line.contains("aac")) info.setAudioCodec("AAC");
else if (line.contains("mp3")) info.setAudioCodec("MP3");
}
}
return info;
}
/**
* 获取PCM格式
*/
private String getPcmFormat(int bitDepth) {
switch (bitDepth) {
case 8: return "pcm_u8";
case 24: return "pcm_s24le";
case 32: return "pcm_s32le";
default: return "pcm_s16le";
}
}
/**
* 准备输出文件
*/
private void prepareOutputFile(File outputFile) {
if (outputFile.exists()) {
outputFile.delete();
}
File parentDir = outputFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
}
/**
* 输出处理器接口
*/
public interface OutputHandler {
void handle(String line);
}
/**
* 媒体信息类
*/
public static class MediaInfo {
private String filePath;
private String duration;
private boolean videoStream;
private boolean audioStream;
private String videoCodec;
private String audioCodec;
// getters and setters
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getDuration() { return duration; }
public void setDuration(String duration) { this.duration = duration; }
public boolean isVideoStream() { return videoStream; }
public void setVideoStream(boolean videoStream) { this.videoStream = videoStream; }
public boolean isAudioStream() { return audioStream; }
public void setAudioStream(boolean audioStream) { this.audioStream = audioStream; }
public String getVideoCodec() { return videoCodec; }
public void setVideoCodec(String videoCodec) { this.videoCodec = videoCodec; }
public String getAudioCodec() { return audioCodec; }
public void setAudioCodec(String audioCodec) { this.audioCodec = audioCodec; }
@Override
public String toString() {
return String.format("MediaInfo{filePath='%s', duration='%s', videoStream=%s, audioStream=%s, videoCodec='%s', audioCodec='%s'}",
filePath, duration, videoStream, audioStream, videoCodec, audioCodec);
}
}
}
3. 应用配置文件
# application.yml
ffmpeg:
path: /usr/local/bin/ffmpeg # Windows: C:/ffmpeg/bin/ffmpeg.exe
timeout: 300 # 5分钟超时
max-threads: 4 # 最大线程数
4. 控制器示例
package com.example.controller;
import com.example.util.FfmpegUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/media")
public class MediaController {
@Autowired
private FfmpegUtil ffmpegUtil;
@PostMapping("/convert-to-wav")
public Map<String, Object> convertToWav(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
// 保存上传文件
String originalFileName = file.getOriginalFilename();
String tempDir = System.getProperty("java.io.tmpdir");
String inputPath = tempDir + File.separator + originalFileName;
String outputPath = tempDir + File.separator +
originalFileName.replaceFirst("\\.[^.]+$", "") + ".wav";
file.transferTo(new File(inputPath));
// 执行转换
boolean success = ffmpegUtil.audioToWav(inputPath, outputPath);
result.put("success", success);
result.put("outputFile", outputPath);
if (success) {
result.put("message", "转换成功");
} else {
result.put("message", "转换失败");
}
// 清理临时文件
new File(inputPath).delete();
} catch (IOException e) {
result.put("success", false);
result.put("message", "文件处理异常: " + e.getMessage());
}
return result;
}
@GetMapping("/media-info")
public FfmpegUtil.MediaInfo getMediaInfo(@RequestParam String filePath) {
return ffmpegUtil.getMediaInfo(filePath);
}
}
5. 异常处理类
package com.example.handler;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
6. 使用示例
@Service
public class MediaService {
@Autowired
private FfmpegUtil ffmpegUtil;
public void processMedia() {
// 音频转WAV
boolean success = ffmpegUtil.audioToWav(
"/path/to/input.mp3",
"/path/to/output.wav",
48000, 2, 16
);
// 获取媒体信息
FfmpegUtil.MediaInfo info = ffmpegUtil.getMediaInfo("/path/to/video.mp4");
System.out.println("视频时长: " + info.getDuration());
// 视频转码
ffmpegUtil.videoTranscoding(
"/path/to/input.avi",
"/path/to/output.mp4",
"libx264", "medium", 23
);
}
}
主要特性
- 配置化:通过配置文件管理 FFmpeg 路径和参数
- 异常处理:完善的异常处理和日志记录
- 灵活的输出处理:支持实时处理 FFmpeg 输出
- 超时控制:防止长时间运行的命令阻塞系统
- 多种媒体操作:支持音视频转码、信息提取等
- RESTful API:提供 Web 接口方便调用
这个工具类可以直接集成到 SpringBoot 项目中使用,提供了完整的媒体处理能力。
4078

被折叠的 条评论
为什么被折叠?



