大家好,我是小悟。
第一部分:认识 FFmpeg ------ 视频界的瑞士军刀
FFmpeg 是什么?想象一下,如果你有一个朋友,他能:
- 把 MP4 变成 AVI,就像把咖啡变成奶茶
- 裁剪视频,比理发师剪头发还精准
- 提取音频,比从披萨上分离芝士还干净
- 压缩视频,比你把行李箱塞满时还高效
这个"万能朋友"就是 FFmpeg!它是一个开源的声音/影像处理工具,功能强大到能让好莱坞特效师失业(开玩笑的)。
shell
# FFmpeg 的基本心态:
# "给我一个视频,我能还你一个世界"
# 实际上它想说的是:"ffmpeg -i input.mp4 [一堆参数] output.mp4"
第二部分:整合步骤 ------ 像组装乐高一样简单
步骤1:先给项目来点"开胃菜"------ Maven依赖
xml
<!-- pom.xml -->
<dependencies>
<!-- SpringBoot 标准配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 让我们记录FFmpeg的"精彩表演" -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 视频处理时的"后悔药"------异常处理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
步骤2:配置FFmpeg ------ 像教AI用筷子
kotlin
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "ffmpeg")
@Data
public class FFmpegConfig {
/**
* FFmpeg可执行文件路径
* Windows: "C:/ffmpeg/bin/ffmpeg.exe"
* Linux/Mac: "/usr/bin/ffmpeg"
*/
private String path;
/**
* 超时时间(秒)
* 防止视频处理变成"永恒等待"
*/
private Long timeout = 3600L;
/**
* 线程数
* 多线程就像多双手,干活更快!
*/
private Integer threads = 4;
}
yaml
# application.yml
ffmpeg:
path: /usr/local/bin/ffmpeg # 你的FFmpeg安装路径
timeout: 3600 # 1小时,足够看一集电视剧了
threads: 4 # 4个线程,四核处理器的最爱
步骤3:创建FFmpeg指挥官
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class FFmpegCommander {
@Autowired
private FFmpegConfig ffmpegConfig;
/**
* 执行FFmpeg命令
* @param commands 命令参数(像给厨师递菜单)
* @return 是否成功(厨子有没有把菜做糊)
*/
public boolean execute(List<String> commands) {
List<String> fullCommand = new ArrayList<>();
fullCommand.add(ffmpegConfig.getPath());
fullCommand.addAll(commands);
log.info("FFmpeg开始干活啦!命令:{}", String.join(" ", fullCommand));
ProcessBuilder processBuilder = new ProcessBuilder(fullCommand);
processBuilder.redirectErrorStream(true); // 错误输出也给我看看
try {
Process process = processBuilder.start();
// 读取输出,防止FFmpeg"自言自语"没人听
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.debug("FFmpeg悄悄说:{}", line);
}
}
// 等待处理完成,别急着催
int exitCode = process.waitFor();
boolean success = exitCode == 0;
if (success) {
log.info("FFmpeg完美收工!");
} else {
log.error("FFmpeg罢工了!退出码:{}", exitCode);
}
return success;
} catch (Exception e) {
log.error("FFmpeg崩溃了,原因:{}", e.getMessage(), e);
return false;
}
}
/**
* 获取FFmpeg版本(验明正身)
*/
public String getVersion() {
try {
Process process = new ProcessBuilder(ffmpegConfig.getPath(), "-version").start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
return reader.readLine(); // 第一行就是版本信息
} catch (Exception e) {
return "FFmpeg可能去度假了:" + e.getMessage();
}
}
}
步骤4:创建视频处理服务 ------ 你的私人视频管家
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Slf4j
@Service
public class VideoService {
@Autowired
private FFmpegCommander ffmpegCommander;
// 临时文件存放目录(像快递的临时存放点)
private final String TEMP_DIR = System.getProperty("java.io.tmpdir") + "/video-process/";
public VideoService() {
// 确保临时目录存在
new File(TEMP_DIR).mkdirs();
}
/**
* 转换视频格式(像把中文翻译成英文)
* @param inputFile 输入文件
* @param targetFormat 目标格式(mp4, avi, mov...)
*/
public File convertFormat(MultipartFile inputFile, String targetFormat) throws IOException {
log.info("开始格式转换:{} → {}",
getFileExtension(inputFile.getOriginalFilename()),
targetFormat);
// 1. 保存上传的文件(像把食材先放到厨房)
File input = saveTempFile(inputFile);
// 2. 准备输出文件(准备好盘子)
String outputFileName = UUID.randomUUID() + "." + targetFormat;
File output = new File(TEMP_DIR + outputFileName);
// 3. 构建FFmpeg命令菜单
List<String> commands = Arrays.asList(
"-i", input.getAbsolutePath(), // 输入文件
"-threads", "4", // 用4个线程
"-preset", "fast", // 快速预设
"-c:v", "libx264", // 视频编码
"-c:a", "aac", // 音频编码
"-y", // 覆盖输出文件(别问我是否确定)
output.getAbsolutePath() // 输出文件
);
// 4. 让FFmpeg大厨开始烹饪
boolean success = ffmpegCommander.execute(commands);
// 5. 清理临时文件(洗盘子)
input.delete();
if (success && output.exists()) {
log.info("格式转换成功!文件大小:{} MB",
output.length() / (1024 * 1024));
return output;
} else {
throw new RuntimeException("转换失败,FFmpeg可能去做美甲了");
}
}
/**
* 提取视频缩略图(给视频拍证件照)
*/
public File extractThumbnail(MultipartFile videoFile, int second) throws IOException {
log.info("正在给视频拍第{}秒的证件照...", second);
File input = saveTempFile(videoFile);
String outputFileName = UUID.randomUUID() + ".jpg";
File output = new File(TEMP_DIR + outputFileName);
List<String> commands = Arrays.asList(
"-i", input.getAbsolutePath(),
"-ss", String.valueOf(second), // 跳转到指定秒数
"-vframes", "1", // 只要1帧
"-vf", "scale=320:-1", // 缩放到宽度320,高度自动
"-y",
output.getAbsolutePath()
);
boolean success = ffmpegCommander.execute(commands);
input.delete();
if (success && output.exists()) {
log.info("缩略图生成成功!");
return output;
}
throw new RuntimeException("拍照失败,视频可能害羞了");
}
/**
* 压缩视频(给视频减肥)
*/
public File compressVideo(MultipartFile videoFile, int targetBitrate) throws IOException {
log.info("开始给视频减肥,目标比特率:{}k", targetBitrate);
File input = saveTempFile(videoFile);
long originalSize = input.length();
String outputFileName = UUID.randomUUID() + "_compressed.mp4";
File output = new File(TEMP_DIR + outputFileName);
List<String> commands = Arrays.asList(
"-i", input.getAbsolutePath(),
"-threads", "4",
"-b:v", targetBitrate + "k", // 目标视频比特率
"-b:a", "128k", // 音频比特率
"-y",
output.getAbsolutePath()
);
boolean success = ffmpegCommander.execute(commands);
input.delete();
if (success && output.exists()) {
long compressedSize = output.length();
double ratio = (1.0 - (double)compressedSize/originalSize) * 100;
log.info("减肥成功!原大小:{}MB,现大小:{}MB,瘦身:{:.1f}%",
originalSize/(1024*1024),
compressedSize/(1024*1024),
ratio);
return output;
}
throw new RuntimeException("减肥失败,视频可能偷吃宵夜了");
}
/**
* 合并视频和音频(像给电影配音)
*/
public File mergeVideoAudio(MultipartFile videoFile,
MultipartFile audioFile) throws IOException {
log.info("开始给视频配音...");
File video = saveTempFile(videoFile);
File audio = saveTempFile(audioFile);
String outputFileName = UUID.randomUUID() + "_merged.mp4";
File output = new File(TEMP_DIR + outputFileName);
List<String> commands = Arrays.asList(
"-i", video.getAbsolutePath(),
"-i", audio.getAbsolutePath(),
"-c:v", "copy", // 视频流直接复制(不重新编码)
"-c:a", "aac", // 音频重新编码
"-map", "0:v:0", // 取第一个文件的视频
"-map", "1:a:0", // 取第二个文件的音频
"-shortest", // 以最短的流为准
"-y",
output.getAbsolutePath()
);
boolean success = ffmpegCommander.execute(commands);
video.delete();
audio.delete();
if (success && output.exists()) {
log.info("配音成功!新视频诞生了");
return output;
}
throw new RuntimeException("合并失败,可能视频和音频在闹离婚");
}
private File saveTempFile(MultipartFile file) throws IOException {
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path path = Paths.get(TEMP_DIR + fileName);
Files.copy(file.getInputStream(), path);
return path.toFile();
}
private String getFileExtension(String filename) {
if (filename == null) return "unknown";
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1);
}
}
步骤5:创建控制器 ------ 视频处理的接待处
less
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
@Slf4j
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private VideoService videoService;
@Autowired
private FFmpegCommander ffmpegCommander;
@GetMapping("/version")
public String getFFmpegVersion() {
String version = ffmpegCommander.getVersion();
return "{\"version\": \"" + version + "\"}";
}
@PostMapping("/convert")
public ResponseEntity<Resource> convertFormat(
@RequestParam("file") MultipartFile file,
@RequestParam("format") String format,
HttpServletResponse response) throws IOException {
log.info("收到转换请求:{} → {}", file.getOriginalFilename(), format);
File converted = videoService.convertFormat(file, format);
return buildFileResponse(converted,
"converted." + format,
MediaType.APPLICATION_OCTET_STREAM);
}
@PostMapping("/thumbnail")
public ResponseEntity<Resource> extractThumbnail(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "second", defaultValue = "5") int second) throws IOException {
File thumbnail = videoService.extractThumbnail(file, second);
return buildFileResponse(thumbnail,
"thumbnail.jpg",
MediaType.IMAGE_JPEG);
}
@PostMapping("/compress")
public ResponseEntity<Resource> compressVideo(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "bitrate", defaultValue = "1000") int bitrate) throws IOException {
File compressed = videoService.compressVideo(file, bitrate);
return buildFileResponse(compressed,
"compressed.mp4",
MediaType.APPLICATION_OCTET_STREAM);
}
@PostMapping("/merge")
public ResponseEntity<Resource> mergeVideoAudio(
@RequestParam("video") MultipartFile video,
@RequestParam("audio") MultipartFile audio) throws IOException {
File merged = videoService.mergeVideoAudio(video, audio);
return buildFileResponse(merged,
"merged.mp4",
MediaType.APPLICATION_OCTET_STREAM);
}
private ResponseEntity<Resource> buildFileResponse(File file,
String filename,
MediaType mediaType) {
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
// 文件下载完成后自动删除(深藏功与名)
file.deleteOnExit();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.contentType(mediaType)
.contentLength(file.length())
.body(resource);
}
}
步骤6:添加异常处理 ------ 给程序买份保险
typescript
import lombok.extern.slf4j.Slf4j;
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 org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
log.error("系统闹情绪了:{}", e.getMessage(), e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "服务器开小差了,可能是FFmpeg在偷懒");
response.put("error", e.getMessage());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, Object>> handleMaxSizeException() {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "文件太大了,服务器拿不动了");
response.put("suggestion", "请尝试压缩视频或上传小一点的文件");
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(response);
}
@ExceptionHandler(IOException.class)
public ResponseEntity<Map<String, Object>> handleIOException(IOException e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "文件读写出了问题,可能是磁盘在闹脾气");
response.put("error", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
第三部分:使用示例 ------ 让我们来实际操练一下
1. 启动应用程序
typescript
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class VideoProcessingApplication {
public static void main(String[] args) {
SpringApplication.run(VideoProcessingApplication.class, args);
System.out.println("视频处理服务启动成功!");
System.out.println("FFmpeg整装待发,随时准备处理你的视频");
}
}
2. 测试API
使用Postman或curl测试:
bash
# 查看FFmpeg版本
curl http://localhost:8080/api/video/version
# 转换视频格式
curl -X POST -F "file=@input.avi" -F "format=mp4" \
http://localhost:8080/api/video/convert --output output.mp4
# 提取缩略图
curl -X POST -F "file=@video.mp4" -F "second=10" \
http://localhost:8080/api/video/thumbnail --output thumbnail.jpg
# 压缩视频
curl -X POST -F "file=@large_video.mp4" -F "bitrate=500" \
http://localhost:8080/api/video/compress --output compressed.mp4
第四部分:高级技巧 ------ 让FFmpeg更懂你
1. 添加进度监听(给视频处理加个进度条)
arduino
public interface ProgressListener {
void onProgress(double percentage, String message);
void onComplete(File outputFile);
void onError(String error);
}
// 在FFmpegCommander中添加进度解析
private void parseProgress(String line, ProgressListener listener) {
// 解析FFmpeg的输出,提取进度信息
// 示例输出:frame= 123 fps=25.1 time=00:00:04.92 bitrate= 512.0kbits/s
if (line.contains("time=")) {
// 这里可以解析时间,计算进度百分比
// 实际实现需要根据视频总时长计算
}
}
2. 批量处理(一次处理多个文件)
kotlin
public List<File> batchConvert(List<MultipartFile> files, String format) {
return files.parallelStream() // 并行处理,更快!
.map(file -> {
try {
return videoService.convertFormat(file, format);
} catch (IOException e) {
log.error("转换失败:{}", file.getOriginalFilename(), e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
3. 视频信息提取(给视频做体检)
typescript
public Map<String, Object> getVideoInfo(File videoFile) {
// 使用FFprobe(FFmpeg的小伙伴)获取视频信息
List<String> commands = Arrays.asList(
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height,duration,bit_rate,codec_name",
"-of", "json",
videoFile.getAbsolutePath()
);
// 执行命令并解析JSON结果
// 返回包含分辨率、时长、码率、编码格式等信息
}
第五部分:总结与注意事项
成功整合的秘诀:
-
正确安装FFmpeg:确保系统PATH中有FFmpeg,或者配置正确的路径
bash# 检查安装 ffmpeg -version -
资源管理:
-
视频处理很吃内存,记得给JVM足够的内存
java -Xmx2g -jar your-application.jar
-
及时清理临时文件,防止磁盘被撑爆
-
-
错误处理:
- FFmpeg可能会因为各种原因失败(不支持的格式、损坏的文件等)
- 添加重试机制和详细的日志记录
-
安全性:
- 限制上传文件类型和大小
- 对用户输入进行严格验证
- 防止命令注入攻击
可能遇到的坑:
- 跨平台问题:Windows和Linux下的路径差异
- 编码问题:中文字符在命令中可能需要特殊处理
- 权限问题:确保应用有执行FFmpeg的权限
- 性能问题:大文件处理可能需要很长时间,考虑异步处理
为什么选择这个方案?
- 灵活性强:可以执行任何FFmpeg支持的操作
- 功能全面:视频处理界的"瑞士军刀"
- 社区支持好:遇到问题容易找到解决方案
- 免费开源:省钱又省心
最后:
FFmpeg就像一把强大的电锯------功能强大但需要小心使用。不要在生产环境直接运行未经验证的命令,否则可能会:
- 把服务器CPU烧得像烤红薯
- 让磁盘空间消失得比钱包里的钱还快
- 产生一堆让你怀疑人生的临时文件
但只要你按照本文的步骤,像对待一只温顺的猫一样对待FFmpeg,它就会成为你在视频处理领域最得力的助手!
祝你的视频处理之路顺畅无比,就像FFmpeg处理一个10秒的GIF一样轻松!

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海