做后端开发难免碰到视频处理需求:格式转码、截缩略图、查视频参数……市面上轮子虽多,但FFmpeg依旧是最稳、兼容性最强的方案。这篇文章不讲虚的,纯实战带大家在 Spring Boot 3 里集成 FFmpeg,代码直接复制就能跑,踩过的坑也一并说明白。
一、先搞懂:FFmpeg 到底是什么?
很多同学刚接触会懵,其实不用深究底层编解码,记住三个核心工具就行:
- ffmpeg:主力命令行工具,转码、剪辑、加水印全靠它;
- ffprobe:专门读取视频元数据(时长、分辨率、编码格式),轻量高效;
- ffplay:简易播放器,本地调试用得上,服务器环境一般不用装。
咱们的集成思路很直白:Spring Boot 调用系统命令,执行 FFmpeg 指令,不用引入复杂依赖,上手快、稳定性高。
二、环境前置要求
- JDK 17+;
- Spring Boot 3.0+(本文用 3.2.5);
- FFmpeg 4.0 以上版本;
- Windows/Linux/MacOS 均可,核心是装好 FFmpeg 并配置环境变量。
三、FFmpeg 安装+环境校验
这步最容易出错,装完一定要验证命令可用性,不然代码写半天跑不起来。
Windows 安装
- 去官网下载 FFmpeg 完整包,选 Windows Full Build 版本;
- 解压到固定目录(比如 D:\ffmpeg-6.1.1\bin),把这个路径加到系统 Path 环境变量;
- 开 CMD 输入ffmpeg -version,能出版本信息就算成功。
Linux快速安装
别源码编译,太费时间,直接用 yum 安装:
# 先装扩展源yum install -y epel-release# 直接安装yum install -y ffmpeg# 校验ffmpeg -versionMacOS 安装
Homebrew 一键搞定:
brew install ffmpegffmpeg -version重点提醒:Linux 服务器部署时,一定要确认ffmpeg 命令全局可用,如果找不到命令,就写绝对路径(比如 /usr/bin/ffmpeg),后面工具类里改下常量就行。
四、Spring Boot 3 项目搭建
依赖引入
不用额外加 FFmpeg 依赖,只需要基础 Web 依赖, Lombok 可选:
<!-- Web接口 --> org.springframework.boot spring-boot-starter-web <!-- 简化代码 --> org.projectlombok lombok true <!-- 测试 --> org.springframework.boot spring-boot-starter-test test 配置文件(application.yml)
主要改文件上传大小,默认 1MB 根本不够传视频:
server: port: 8080spring: servlet: multipart: # 单个文件最大限制 max-file-size: 100MB # 请求总大小限制 max-request-size: 100MB核心工具类封装
这是整个集成的核心,封装格式转换、截缩略图、读取元数据三个常用功能,解决命令阻塞、超时等坑。
代码里加了详细注释,异步读流是关键——不这么做会导致命令执行卡死。
import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;@Slf4j@Componentpublic class FFmpegUtil { // FFmpeg命令前缀,配置了环境变量直接写ffmpeg,Linux找不到就写绝对路径 private static final String FFMPEG_CMD = "ffmpeg"; /** * 视频格式转换(无损复制流,速度超快) * @param inputPath 原视频绝对路径 * @param outputPath 输出文件绝对路径 * @param timeout 超时时间(秒) * @return 执行结果 */ public boolean convertVideo(String inputPath, String outputPath, long timeout) { // -c:v copy -c:a copy 直接复制音视频流,不重新编码,大文件也快 String[] cmd = { FFMPEG_CMD, "-i", inputPath, "-c:v", "copy", "-c:a", "copy", "-y", // 覆盖已有文件,避免交互卡住 outputPath }; return executeCmd(cmd, timeout); } /** * 截取视频缩略图 * @param inputPath 原视频路径 * @param outputPath 截图保存路径(jpg/png均可) * @param time 截取时间点(例:00:00:02 或 2) * @return 执行结果 */ public boolean generateThumb(String inputPath, String outputPath, String time) { String[] cmd = { FFMPEG_CMD, "-i", inputPath, "-ss", time, "-vframes", "1", // 只截1帧 "-y", outputPath }; return executeCmd(cmd, 60); } /** * 获取视频元数据(时长、分辨率、编码) * @param inputPath 视频路径 * @return 元数据集合 */ public Map getVideoInfo(String inputPath) { Map infoMap = new HashMap<>(); // ffprobe 命令读取视频流信息 String[] cmd = { "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,duration,codec_name", "-of", "default=noprint_wrappers=1:nokey=0", inputPath }; try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { if (line.startsWith("width=")) infoMap.put("width", line.split("=")[1]); if (line.startsWith("height=")) infoMap.put("height", line.split("=")[1]); if (line.startsWith("duration=")) infoMap.put("duration", line.split("=")[1]); if (line.startsWith("codec_name=")) infoMap.put("codec", line.split("=")[1]); } process.waitFor(30, TimeUnit.SECONDS); reader.close(); } catch (Exception e) { log.error("读取视频信息失败", e); } return infoMap; } /** * 执行系统命令(解决阻塞、超时问题) */ private boolean executeCmd(String[] cmd, long timeout) { Process process = null; try { log.info("执行FFmpeg命令:{}", String.join(" ", cmd)); process = Runtime.getRuntime().exec(cmd); // 异步读取流,必须加!否则缓冲区满会卡死进程 readStreamAsync(process.getInputStream()); readStreamAsync(process.getErrorStream()); // 等待执行完成,超时强制销毁进程 boolean finish = process.waitFor(timeout, TimeUnit.SECONDS); if (!finish) { log.error("FFmpeg命令执行超时,强制终止"); process.destroyForcibly(); return false; } // 退出码0代表执行成功 return process.exitValue() == 0; } catch (Exception e) { log.error("FFmpeg命令执行异常", e); return false; } finally { if (process != null) process.destroy(); } } /** * 异步读取命令输出流(避免阻塞) */ private void readStreamAsync(java.io.InputStream in) { new Thread(() -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { String line; while ((line = reader.readLine()) != null) { log.debug("FFmpeg输出:{}", line); } } catch (IOException e) { log.error("读取流失败", e); } }).start(); }} 接口开发
写三个简易接口,上传视频直接测试功能,临时文件存在项目根目录,生产环境记得换成云存储(OSS/MinIO)。
import com.example.ffmpeg.util.FFmpegUtil;import lombok.RequiredArgsConstructor;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import java.io.File;import java.io.IOException;import java.util.Map;import java.util.UUID;@RestController@RequestMapping("/api/video")@RequiredArgsConstructorpublic class VideoController { private final FFmpegUtil ffmpegUtil; // 临时文件目录 private static final String TEMP_PATH = System.getProperty("user.dir") + "/temp/"; // 初始化目录 static { File dir = new File(TEMP_PATH); if (!dir.exists()) dir.mkdirs(); } /** * 视频格式转换接口 */ @PostMapping("/convert") public String convert(@RequestParam("file") MultipartFile file, @RequestParam("targetFormat") String targetFormat) throws IOException { // 保存上传文件 String originalName = file.getOriginalFilename(); String inputName = UUID.randomUUID() + originalName.substring(originalName.lastIndexOf(".")); String inputPath = TEMP_PATH + inputName; file.transferTo(new File(inputPath)); // 输出文件 String outputName = UUID.randomUUID() + "." + targetFormat; String outputPath = TEMP_PATH + outputName; boolean result = ffmpegUtil.convertVideo(inputPath, outputPath, 120); return result ? "转换成功,路径:" + outputPath : "转换失败"; } /** * 生成缩略图接口 */ @PostMapping("/thumb") public String thumb(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "00:00:01") String time) throws IOException { String inputPath = TEMP_PATH + UUID.randomUUID() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); file.transferTo(new File(inputPath)); String outputPath = TEMP_PATH + UUID.randomUUID() + ".jpg"; boolean result = ffmpegUtil.generateThumb(inputPath, outputPath, time); return result ? "缩略图生成成功:" + outputPath : "生成失败"; } /** * 获取视频信息接口 */ @PostMapping("/info") public Map info(@RequestParam("file") MultipartFile file) throws IOException { String inputPath = TEMP_PATH + UUID.randomUUID() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); file.transferTo(new File(inputPath)); return ffmpegUtil.getVideoInfo(inputPath); }} 五、实测验证与常见问题、调优指南
接口测试
- 截缩略图:POST http://localhost:8080/api/video/thumb,form-data 传 file 和 time 参数;
- 格式转换:POSThttp://localhost:8080/api/video/convert,传 file 和 targetFormat(如 webm/flv);
- 查视频信息:POST http://localhost:8080/api/video/info,只传 file 即可。
常见问题
- 命令执行卡住:没加异步读流,复制工具类代码即可解决;
- Linux 找不到 ffmpeg:把工具类里 FFMPEG_CMD 改成绝对路径;
- 文件上传失败:检查 yml 里文件大小配置,别设太小;
- 权限报错:Linux 给 temp 目录开读写权限,Windows 别放系统盘受限目录。
生产环境优化
上面是本地实战代码,上线要改这几点,不然容易出问题:

- 异步化处理:视频转码耗时长,用 @Async 或消息队列(RabbitMQ/Kafka)异步执行,别同步接口硬等;
- 换掉本地存储:本地临时文件分布式环境访问不到,换成阿里云 OSS、MinIO 等云存储;
- 参数校验:限制上传文件格式、大小,防止命令注入和恶意文件;
- 控制并发:FFmpeg 耗CPU,用线程池限制同时执行的任务数;
- 日志+重试:记录任务状态,失败自动重试,方便排查问题。
六、总结
Spring Boot 集成 FFmpeg 没那么复杂,核心就是命令调用+流处理,不用引入第三方封装依赖,少一层依赖就少一层隐患。
本文代码都是实测可用的,扩展功能(加水印、剪辑、抽音频)也很简单,只需要改 FFmpeg 命令参数就行,官方文档查命令语法,直接套进工具类即可。