在现代后端开发中,视频处理是一个高频需求,无论是视频格式转换、缩略图生成,还是时长截取、水印添加,FFmpeg 都是行业内的首选工具。FFmpeg 是一套开源的跨平台音视频处理库,支持几乎所有主流的音视频格式,功能强大且灵活。本文将详细介绍如何在 Spring Boot 3 项目中集成 FFmpeg,实现基础的视频处理功能,并提供可直接复用的代码示例。
一、前置知识储备
1.1 FFmpeg 简介
FFmpeg 由 Fabrice Bellard 发起,包含三个核心组件:
ffmpeg:命令行工具,用于音视频格式转换、编解码、剪辑等操作;
ffplay:简单的音视频播放器,可用于快速预览文件;
ffprobe:用于分析音视频文件的元数据(如时长、分辨率、编码格式等)。
本文核心是通过 Spring Boot 调用 FFmpeg 命令行工具,实现视频处理功能,无需深入了解 FFmpeg 底层编解码原理,掌握基础命令即可上手。
1.2 环境要求
JDK 17+(Spring Boot 3 最低要求);
Spring Boot 3.x(本文使用 3.2.2 版本);
FFmpeg 4.x+(建议使用最新稳定版,本文使用 6.1.1 版本);
操作系统:Windows、Linux、MacOS 均可(需对应安装 FFmpeg)。
二、FFmpeg 环境安装与验证
在集成 Spring Boot 前,需先在服务器/本地环境安装 FFmpeg,并确保命令可正常执行。
2.1 Windows 系统安装
访问 FFmpeg 官方下载地址:https://ffmpeg.org/download.html,选择 Windows 版本(推荐 Full Build,包含所有功能组件);
解压下载的压缩包,将解压后的文件夹路径(如
D:\ffmpeg-6.1.1-full_build\bin)添加到系统环境变量Path中;打开命令提示符(CMD),输入
ffmpeg -version,若输出 FFmpeg 版本信息,则安装成功。
2.2 Linux 系统安装(以 CentOS 为例)
通过 YUM 包管理器快速安装:
# 安装 EPEL 源(提供额外软件包)
yum install -y epel-release
# 安装 FFmpeg
yum install -y ffmpeg
# 验证安装
ffmpeg -version若需安装最新版本,可通过源码编译或第三方仓库安装,具体参考 FFmpeg 官方文档。
2.3 MacOS 系统安装
通过 Homebrew 快速安装:
# 安装 Homebrew(若未安装)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 安装 FFmpeg
brew install ffmpeg
# 验证安装
ffmpeg -version三、Spring Boot 3 项目搭建与集成
3.1 初始化 Spring Boot 项目
通过 Spring Initializr 初始化项目,核心依赖如下:
Spring Web:提供 HTTP 接口能力;
Lombok:简化 Java 代码(可选);
Spring Boot DevTools:开发热部署(可选)。
pom.xml 核心依赖片段:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>3.2 核心工具类开发
Spring Boot 集成 FFmpeg 的核心思路是:通过 Java 调用系统命令行,执行 FFmpeg 命令。我们需要封装一个工具类,负责构建 FFmpeg 命令、执行命令、处理命令输出结果,并处理异常情况。
3.2.1 工具类核心功能
视频格式转换(如 MP4 转 WebM、FLV、m3u8);
生成视频缩略图(截取指定时间帧);
获取视频元数据(时长、分辨率、编码格式);
命令执行结果封装与异常处理。
3.2.2 完整工具类代码
package com.example.ffmpeg.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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
@Component
public class FFmpegUtil {
@Value("${app.ffmpeg.path:ffmpeg}")
private String ffmpegPath;
@Value("${app.ffmpeg.ffprobe-path:ffprobe}")
private String ffprobePath;
/**
* 视频格式转换
* @param inputPath 输入视频路径(绝对路径)
* @param outputPath 输出视频路径(绝对路径,含格式后缀)
* @param timeout 超时时间(单位:秒)
* @return 转换结果(成功/失败)
*/
public boolean convertVideoFormat(String inputPath, String outputPath, long timeout) {
// FFmpeg 转换命令:-i 输入文件 -c:v 视频编码器 -c:a 音频编码器 输出文件
// -y 表示覆盖已存在的输出文件
String[] cmd = {
ffmpegPath,
"-i", inputPath,
"-c:v", "copy", // 视频流直接复制(无重新编码,速度快)
"-c:a", "copy", // 音频流直接复制
"-y",
outputPath
};
return executeCmd(cmd, timeout);
}
/**
* 生成视频缩略图
* @param inputPath 输入视频路径
* @param outputPath 输出图片路径(支持 jpg、png 等)
* @param time 截取时间(格式:00:00:01 表示1秒处,或直接写数字表示秒)
* @return 生成结果
*/
public boolean generateVideoThumbnail(String inputPath, String outputPath, String time) {
// -vframes 1 表示只截取一帧
String[] cmd = {
ffmpegPath,
"-i", inputPath,
"-ss", time,
"-vframes", "1",
"-y",
outputPath
};
return executeCmd(cmd, 60); // 超时时间60秒
}
/**
* 获取视频元数据
* @param inputPath 输入视频路径
* @return 元数据Map(时长、宽度、高度、编码格式等)
*/
public Map<String, String> getVideoMetadata(String inputPath) {
Map<String, String> metadata = new HashMap<>();
String[] cmd = {
ffprobePath,
"-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=")) {
metadata.put("width", line.split("=")[1]);
} else if (line.startsWith("height=")) {
metadata.put("height", line.split("=")[1]);
} else if (line.startsWith("duration=")) {
metadata.put("duration", line.split("=")[1]); // 时长(秒,含小数)
} else if (line.startsWith("codec_name=")) {
metadata.put("codec", line.split("=")[1]); // 视频编码格式
}
}
process.waitFor(30, TimeUnit.SECONDS);
reader.close();
} catch (IOException | InterruptedException e) {
log.error("获取视频元数据失败", e);
}
return metadata;
}
/**
* 执行系统命令
* @param cmd 命令数组
* @param timeout 超时时间(秒)
* @return 执行结果(true:成功,false:失败)
*/
private boolean executeCmd(String[] cmd, long timeout) {
Process process = null;
try {
log.info("执行FFmpeg命令:{}", String.join(" ", cmd));
process = Runtime.getRuntime().exec(cmd);
// 异步读取命令输出(防止缓冲区阻塞)
readStream(process.getInputStream());
readStream(process.getErrorStream());
// 等待命令执行完成,设置超时
boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
if (!finished) {
log.error("FFmpeg命令执行超时,强制终止进程");
process.destroyForcibly();
return false;
}
// 命令执行状态码:0表示成功,非0表示失败
int exitCode = process.exitValue();
if (exitCode == 0) {
log.info("FFmpeg命令执行成功");
return true;
} else {
log.error("FFmpeg命令执行失败,退出码:{}", exitCode);
return false;
}
} catch (IOException | InterruptedException e) {
log.error("FFmpeg命令执行异常", e);
return false;
} finally {
if (process != null) {
process.destroy();
}
}
}
/**
* 读取流信息(防止缓冲区阻塞)
* @param inputStream 输入流
*/
private void readStream(java.io.InputStream inputStream) {
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
log.debug("FFmpeg命令输出:{}", line);
}
} catch (IOException e) {
log.error("读取FFmpeg流信息失败", e);
}
}).start();
}
}3.3 接口开发(测试用例)
开发 HTTP 接口,测试 FFmpeg 工具类的核心功能,支持文件上传后进行格式转换、生成缩略图等操作。
package com.example.ffmpeg.controller;
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.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private FFmpegUtil ffmpegUtil;
// 临时文件存储路径(实际生产环境建议使用云存储,如OSS)
private static final String TEMP_DIR = System.getProperty("user.dir") + "/temp/";
public VideoController() {
// 初始化临时文件夹
File dir = new File(TEMP_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
}
/**
* 视频上传并转换格式
* @param file 上传的视频文件
* @param targetFormat 目标格式(如 webm、flv)
* @return 转换后的文件路径
*/
@PostMapping("/convert")
public String convertVideoFormat(@RequestParam("file") MultipartFile file,
@RequestParam("targetFormat") String targetFormat) throws IOException {
// 保存上传文件到临时目录
String originalFileName = file.getOriginalFilename();
String inputFileName = UUID.randomUUID() + originalFileName.substring(originalFileName.lastIndexOf("."));
String inputPath = TEMP_DIR + inputFileName;
file.transferTo(new File(inputPath));
// 构建输出文件路径
String outputFileName = UUID.randomUUID() + "." + targetFormat;
String outputPath = TEMP_DIR + outputFileName;
// 执行格式转换
boolean result = ffmpegUtil.convertVideoFormat(inputPath, outputPath, 120);
if (result) {
return "格式转换成功,输出路径:" + outputPath;
} else {
return "格式转换失败";
}
}
/**
* 生成视频缩略图
* @param file 上传的视频文件
* @param time 截取时间(如 00:00:03)
* @return 缩略图路径
*/
@PostMapping("/thumbnail")
public String generateThumbnail(@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "00:00:01") String time) throws IOException {
// 保存上传文件到临时目录
String originalFileName = file.getOriginalFilename();
String inputFileName = UUID.randomUUID() + originalFileName.substring(originalFileName.lastIndexOf("."));
String inputPath = TEMP_DIR + inputFileName;
file.transferTo(new File(inputPath));
// 构建缩略图输出路径(jpg格式)
String outputFileName = UUID.randomUUID() + ".jpg";
String outputPath = TEMP_DIR + outputFileName;
// 生成缩略图
boolean result = ffmpegUtil.generateVideoThumbnail(inputPath, outputPath, time);
if (result) {
return "缩略图生成成功,输出路径:" + outputPath;
} else {
return "缩略图生成失败";
}
}
/**
* 获取视频元数据
* @param file 上传的视频文件
* @return 元数据Map
*/
@PostMapping("/metadata")
public Map<String, String> getVideoMetadata(@RequestParam("file") MultipartFile file) throws IOException {
// 保存上传文件到临时目录
String originalFileName = file.getOriginalFilename();
String inputFileName = UUID.randomUUID() + originalFileName.substring(originalFileName.lastIndexOf("."));
String inputPath = TEMP_DIR + inputFileName;
file.transferTo(new File(inputPath));
// 获取元数据
return ffmpegUtil.getVideoMetadata(inputPath);
}
}3.4 配置文件(application.yml)
简单配置服务器端口,可根据需求扩展文件大小限制等配置:
server:
port: 8080
# 配置文件上传大小限制
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
app:
ffmpeg:
# 如果已经将 FFmpeg 添加到系统 PATH,则可以简单地使用,如果没有,则使用完整的路径
path: ffmpeg
ffprobe-path: ffprobe四、功能测试与验证
4.1 接口测试(Postman)
4.1.1 生成视频缩略图
请求方式:POST
请求地址:
http://localhost:8080/api/video/thumbnail请求参数:form-data 格式,key 为 file(上传 MP4 视频),time 为 00:00:02
响应结果:若成功,返回缩略图保存路径,可直接打开路径下的图片验证。
4.1.2 视频格式转换
请求方式:POST
请求地址:
http://localhost:8080/api/video/convert请求参数:form-data 格式,key 为 file(上传 MP4 视频),targetFormat 为 webm
响应结果:若成功,返回转换后 WebM 文件路径,可通过播放器验证文件可用性。
4.1.3 获取视频元数据
请求方式:POST
请求地址:
http://localhost:8080/api/video/metadata请求参数:form-data 格式,key 为 file(上传 MP4 视频)
响应结果:返回 JSON 格式元数据,示例如下:
{ "width": "1920", "height": "1080", "duration": "125.34", "codec": "h264" }
4.2 常见问题排查
FFmpeg 命令执行失败,退出码非0:检查输入文件路径是否正确、输出路径是否有权限写入、FFmpeg 命令是否正确(可直接复制命令到终端执行,排查命令问题);
进程阻塞:未异步读取输入流/错误流,导致缓冲区满而阻塞,工具类中已通过异步线程读取流,避免此问题;
文件上传失败:检查 Spring 配置的文件大小限制是否满足需求,调整
max-file-size和max-request-size参数;Linux 环境下 FFmpeg 命令找不到:确认 FFmpeg 已安装且环境变量配置正确,或在工具类中指定 FFmpeg 绝对路径(如
/usr/bin/ffmpeg)。
五、生产环境优化建议
上述代码为基础实现,在生产环境中需考虑以下优化点:
5.1 异步处理
视频处理属于耗时操作,直接通过同步接口返回会导致请求超时。建议使用 Spring 的 @Async 注解或消息队列(如 RabbitMQ、Kafka)将视频处理任务异步化,返回任务ID,前端通过轮询或WebSocket查询处理结果。
5.2 分布式部署适配
分布式环境下,临时文件存储在本地会导致文件无法访问。建议使用分布式文件存储(如阿里云 OSS、MinIO)存储上传的视频和处理后的文件,工具类直接操作分布式文件路径。
5.3 命令安全校验
若允许用户传入自定义参数(如目标格式、截取时间),需进行参数校验,防止命令注入攻击。例如,限制目标格式仅支持 MP4、WebM、FLV 等安全格式,时间参数需符合格式规范。
5.4 任务监控与重试
添加任务监控日志,记录每个视频处理任务的状态、耗时、错误信息。对于处理失败的任务,实现重试机制(如使用 Spring Retry),避免因临时故障导致任务失败。
5.5 资源限制
FFmpeg 处理视频会占用大量 CPU 和内存,需限制同时执行的任务数量,避免服务器资源耗尽。可通过线程池控制并发数,或使用容器化部署(Docker)设置资源限制。
六、扩展功能
基于本文的基础框架,可扩展以下高级功能:
视频水印添加(文字水印、图片水印);
视频剪辑(截取指定时间段、合并多个视频);
音频提取与转换(从视频中提取音频,转换音频格式);
视频转码(调整分辨率、比特率,适配不同设备播放)。
这些功能仅需修改 FFmpeg 命令即可实现,例如添加水印的命令:
ffmpeg -i input.mp4 -i watermark.png -filter_complex "overlay=10:10" -y output.mp4七、总结
本文详细介绍了 Spring Boot 3 集成 FFmpeg 的完整流程,从环境搭建、工具类封装、接口开发到测试验证,提供了可直接复用的代码示例。核心思路是通过 Java 调用 FFmpeg 命令行工具,实现视频格式转换、缩略图生成、元数据获取等基础功能。在生产环境中,需结合异步处理、分布式存储、安全校验等优化点,确保系统的稳定性和可靠性。
FFmpeg 功能强大,本文仅覆盖基础应用,更多高级用法可参考 FFmpeg 官方文档,根据实际业务需求扩展功能。