第一部分:认识 FFmpeg —— 音视频处理的“全能工具集”
FFmpeg 是一套开源的音视频处理工具库与开发库,能够一站式解决音视频领域的绝大多数处理需求:
- 格式转换:轻松实现 MP4、AVI、MOV 等格式的互转,如同文件格式的“翻译官”;
- 精准剪辑:毫秒级裁剪视频片段,比专业剪辑软件更灵活;
- 音频提取:无损分离视频中的音频轨道,效率远超手动操作;
- 视频压缩:在保证画质的前提下大幅降低文件体积,节省存储和传输成本。
# FFmpeg 核心执行逻辑:
# 基础命令范式:ffmpeg -i 输入文件 [处理参数] 输出文件
# 核心思想:通过参数组合,实现任意音视频处理需求
第二部分:整合步骤 —— 标准化搭建视频处理服务
03.1 步骤1:引入核心依赖(Maven配置)
首先在 pom.xml 中添加 SpringBoot 基础依赖,为视频处理服务搭建基础运行环境:
<!-- pom.xml -->
<dependencies>
<!-- SpringBoot Web核心依赖:提供HTTP接口能力 -->
<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>
03.2 步骤2:FFmpeg配置类(参数标准化)
通过配置类统一管理 FFmpeg 的核心参数,支持多环境适配:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* FFmpeg核心配置类
* 作用:统一管理FFmpeg执行路径、超时时间、线程数等核心参数
*/
@Configuration
@ConfigurationProperties(prefix = "ffmpeg") // 绑定配置文件中ffmpeg前缀的配置项
@Data // Lombok注解:自动生成get/set方法
public class FFmpegConfig {
/**
* FFmpeg可执行文件路径
* Windows示例: "C:/ffmpeg/bin/ffmpeg.exe"
* Linux/Mac示例: "/usr/bin/ffmpeg"
*/
private String path;
/**
* 处理超时时间(秒)
* 作用:防止单个视频处理耗时过长,占用服务器资源
*/
private Long timeout = 3600L;
/**
* 处理线程数
* 作用:多线程提升处理效率,建议与CPU核心数匹配
*/
private Integer threads = 4;
}
在 application.yml 中配置具体参数:
ffmpeg:
path: /usr/local/bin/ffmpeg # 替换为实际FFmpeg安装路径
timeout: 3600 # 超时时间:1小时
threads: 4 # 处理线程数:4核CPU适配值
03.3 步骤3:FFmpeg命令执行器(核心调度层)
封装 FFmpeg 命令执行逻辑,统一处理进程启动、输出读取、结果校验:
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;
/**
* FFmpeg命令执行器
* 作用:封装FFmpeg进程启动、命令执行、结果返回的核心逻辑
*/
@Slf4j // 日志注解:简化日志输出
@Component // 交给Spring容器管理
public class FFmpegCommander {
@Autowired
private FFmpegConfig ffmpegConfig; // 注入FFmpeg配置类
/**
* 执行FFmpeg命令
* @param commands 命令参数列表(不含FFmpeg路径)
* @return 执行结果:true成功 / false失败
*/
public boolean execute(List<String> commands) {
// 组装完整命令:FFmpeg路径 + 业务参数
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();
// 读取进程输出:防止缓冲区溢出导致进程阻塞
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; // 0表示执行成功
if (success) {
log.info("FFmpeg命令执行成功!");
} else {
log.error("FFmpeg命令执行失败,退出码:{}", exitCode);
}
return success;
} catch (Exception e) {
log.error("FFmpeg进程启动/执行异常", e);
return false;
}
}
/**
* 获取FFmpeg版本信息
* 作用:校验FFmpeg是否安装成功、版本是否兼容
* @return 版本信息字符串
*/
public String getVersion() {
try {
// 执行ffmpeg -version命令
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();
}
}
}
03.4 步骤4:视频处理服务层(业务逻辑实现)
封装视频格式转换、缩略图提取、压缩、音视频合并等核心业务:
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() {
File tempDirFile = new File(TEMP_DIR);
if (!tempDirFile.exists()) {
tempDirFile.mkdirs(); // 递归创建目录
}
}
/**
* 视频格式转换
* @param inputFile 上传的视频文件
* @param targetFormat 目标格式(如mp4、avi、mov)
* @return 转换后的视频文件
* @throws IOException 文件操作异常
*/
public File convertFormat(MultipartFile inputFile, String targetFormat) throws IOException {
log.info("开始视频格式转换:原格式{} → 目标格式{}",
getFileExtension(inputFile.getOriginalFilename()),
targetFormat);
// 1. 保存上传文件到临时目录
File inputTempFile = saveTempFile(inputFile);
// 2. 生成输出文件(UUID命名避免重复)
String outputFileName = UUID.randomUUID() + "." + targetFormat;
File outputTempFile = new File(TEMP_DIR + outputFileName);
// 3. 构建FFmpeg转换命令
List<String> commands = Arrays.asList(
"-i", inputTempFile.getAbsolutePath(), // 输入文件路径
"-threads", ffmpegConfig.getThreads().toString(), // 处理线程数(从配置读取)
"-preset", "fast", // 编码预设:快速模式(平衡速度和质量)
"-c:v", "libx264", // 视频编码器:H.264(兼容性最好)
"-c:a", "aac", // 音频编码器:AAC(主流音频格式)
"-y", // 覆盖已存在的输出文件
outputTempFile.getAbsolutePath() // 输出文件路径
);
// 4. 执行转换命令
boolean success = ffmpegCommander.execute(commands);
// 5. 清理临时输入文件
inputTempFile.delete();
// 6. 校验结果并返回
if (success && outputTempFile.exists()) {
long fileSizeMB = outputTempFile.length() / (1024 * 1024);
log.info("格式转换成功,文件大小:{} MB", fileSizeMB);
return outputTempFile;
} else {
throw new RuntimeException("视频格式转换失败,请检查FFmpeg配置或文件格式");
}
}
/**
* 提取视频缩略图
* @param videoFile 视频文件
* @param second 提取帧的时间点(秒)
* @return 缩略图文件
* @throws IOException 文件操作异常
*/
public File extractThumbnail(MultipartFile videoFile, int second) throws IOException {
log.info("开始提取视频缩略图:时间点{}秒", second);
// 1. 保存临时文件
File inputTempFile = saveTempFile(videoFile);
String outputFileName = UUID.randomUUID() + ".jpg";
File outputTempFile = new File(TEMP_DIR + outputFileName);
// 2. 构建缩略图提取命令
List<String> commands = Arrays.asList(
"-i", inputTempFile.getAbsolutePath(),
"-ss", String.valueOf(second), // 跳转到指定时间点
"-vframes", "1", // 只提取1帧
"-vf", "scale=320:-1", // 缩放:宽度320,高度按比例自动计算
"-y", // 覆盖输出文件
outputTempFile.getAbsolutePath()
);
// 3. 执行命令并清理临时文件
boolean success = ffmpegCommander.execute(commands);
inputTempFile.delete();
// 4. 校验结果
if (success && outputTempFile.exists()) {
log.info("视频缩略图提取成功");
return outputTempFile;
}
throw new RuntimeException("视频缩略图提取失败");
}
/**
* 视频压缩(按比特率)
* @param videoFile 待压缩视频
* @param targetBitrate 目标视频比特率(kbps)
* @return 压缩后的视频文件
* @throws IOException 文件操作异常
*/
public File compressVideo(MultipartFile videoFile, int targetBitrate) throws IOException {
log.info("开始视频压缩:目标比特率{} kbps", targetBitrate);
// 1. 保存临时文件并记录原大小
File inputTempFile = saveTempFile(videoFile);
long originalSize = inputTempFile.length();
// 2. 生成输出文件
String outputFileName = UUID.randomUUID() + "_compressed.mp4";
File outputTempFile = new File(TEMP_DIR + outputFileName);
// 3. 构建压缩命令
List<String> commands = Arrays.asList(
"-i", inputTempFile.getAbsolutePath(),
"-threads", ffmpegConfig.getThreads().toString(),
"-b:v", targetBitrate + "k", // 视频比特率(核心压缩参数)
"-b:a", "128k", // 音频比特率(固定128kbps保证音质)
"-y",
outputTempFile.getAbsolutePath()
);
// 4. 执行命令并清理临时文件
boolean success = ffmpegCommander.execute(commands);
inputTempFile.delete();
// 5. 校验结果并计算压缩率
if (success && outputTempFile.exists()) {
long compressedSize = outputTempFile.length();
double compressionRatio = (1.0 - (double)compressedSize/originalSize) * 100;
log.info("视频压缩成功!原大小:{}MB,压缩后:{}MB,压缩率:{:.1f}%",
originalSize/(1024*1024),
compressedSize/(1024*1024),
compressionRatio);
return outputTempFile;
}
throw new RuntimeException("视频压缩失败");
}
/**
* 音视频合并(替换视频音频轨道)
* @param videoFile 视频文件
* @param audioFile 音频文件
* @return 合并后的视频文件
* @throws IOException 文件操作异常
*/
public File mergeVideoAudio(MultipartFile videoFile,
MultipartFile audioFile) throws IOException {
log.info("开始音视频合并操作");
// 1. 保存临时文件
File videoTempFile = saveTempFile(videoFile);
File audioTempFile = saveTempFile(audioFile);
// 2. 生成输出文件
String outputFileName = UUID.randomUUID() + "_merged.mp4";
File outputTempFile = new File(TEMP_DIR + outputFileName);
// 3. 构建合并命令
List<String> commands = Arrays.asList(
"-i", videoTempFile.getAbsolutePath(), // 视频源
"-i", audioTempFile.getAbsolutePath(), // 音频源
"-c:v", "copy", // 视频流直接复制(不重新编码,提升速度)
"-c:a", "aac", // 音频流重新编码(保证兼容性)
"-map", "0:v:0", // 映射:取第一个文件的视频轨道
"-map", "1:a:0", // 映射:取第二个文件的音频轨道
"-shortest", // 时长以最短的流为准
"-y",
outputTempFile.getAbsolutePath()
);
// 4. 执行命令并清理临时文件
boolean success = ffmpegCommander.execute(commands);
videoTempFile.delete();
audioTempFile.delete();
// 5. 校验结果
if (success && outputTempFile.exists()) {
log.info("音视频合并成功");
return outputTempFile;
}
throw new RuntimeException("音视频合并失败");
}
/**
* 保存上传文件到临时目录
* @param file 上传的MultipartFile文件
* @return 临时文件对象
* @throws IOException 文件写入异常
*/
private File saveTempFile(MultipartFile file) throws IOException {
// 生成唯一文件名:UUID + 原文件名(避免冲突)
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path tempPath = Paths.get(TEMP_DIR + fileName);
// 写入文件到临时目录
Files.copy(file.getInputStream(), tempPath);
return tempPath.toFile();
}
/**
* 获取文件扩展名
* @param filename 文件名
* @return 扩展名(小写)
*/
private String getFileExtension(String filename) {
if (filename == null || filename.isEmpty()) {
return "unknown";
}
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1).toLowerCase();
}
}
03.5 步骤5:控制器层(HTTP接口暴露)
封装 RESTful 接口,对外提供视频处理能力:
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;
/**
* 视频处理接口控制器
* 作用:暴露HTTP接口,接收前端请求并调用服务层处理
*/
@Slf4j
@RestController
@RequestMapping("/api/video") // 接口统一前缀
public class VideoController {
@Autowired
private VideoService videoService;
@Autowired
private FFmpegCommander ffmpegCommander;
/**
* 获取FFmpeg版本信息接口
* @return 版本信息JSON
*/
@GetMapping("/version")
public String getFFmpegVersion() {
String version = ffmpegCommander.getVersion();
return "{\"version\": \"" + version + "\"}";
}
/**
* 视频格式转换接口
* @param file 上传的视频文件
* @param format 目标格式
* @return 转换后的文件下载响应
* @throws IOException 文件操作异常
*/
@PostMapping("/convert")
public ResponseEntity<Resource> convertFormat(
@RequestParam("file") MultipartFile file,
@RequestParam("format") String format) throws IOException {
log.info("接收视频格式转换请求:文件名{},目标格式{}", file.getOriginalFilename(), format);
// 调用服务层转换方法
File convertedFile = videoService.convertFormat(file, format);
// 构建文件下载响应
return buildFileResponse(convertedFile,
"converted." + format,
MediaType.APPLICATION_OCTET_STREAM);
}
/**
* 视频缩略图提取接口
* @param file 视频文件
* @param second 提取帧的时间点(默认5秒)
* @return 缩略图文件下载响应
* @throws IOException 文件操作异常
*/
@PostMapping("/thumbnail")
public ResponseEntity<Resource> extractThumbnail(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "second", defaultValue = "5") int second) throws IOException {
File thumbnailFile = videoService.extractThumbnail(file, second);
return buildFileResponse(thumbnailFile,
"thumbnail.jpg",
MediaType.IMAGE_JPEG);
}
/**
* 视频压缩接口
* @param file 视频文件
* @param bitrate 目标比特率(默认1000kbps)
* @return 压缩后的文件下载响应
* @throws IOException 文件操作异常
*/
@PostMapping("/compress")
public ResponseEntity<Resource> compressVideo(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "bitrate", defaultValue = "1000") int bitrate) throws IOException {
File compressedFile = videoService.compressVideo(file, bitrate);
return buildFileResponse(compressedFile,
"compressed.mp4",
MediaType.APPLICATION_OCTET_STREAM);
}
/**
* 音视频合并接口
* @param video 视频文件
* @param audio 音频文件
* @return 合并后的文件下载响应
* @throws IOException 文件操作异常
*/
@PostMapping("/merge")
public ResponseEntity<Resource> mergeVideoAudio(
@RequestParam("video") MultipartFile video,
@RequestParam("audio") MultipartFile audio) throws IOException {
File mergedFile = videoService.mergeVideoAudio(video, audio);
return buildFileResponse(mergedFile,
"merged.mp4",
MediaType.APPLICATION_OCTET_STREAM);
}
/**
* 构建文件下载响应
* @param file 待下载文件
* @param filename 下载文件名
* @param mediaType 文件媒体类型
* @return ResponseEntity<Resource> 下载响应
*/
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);
}
}
03.6 步骤6:全局异常处理(统一错误反馈)
统一处理接口异常,返回标准化错误信息:
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;
/**
* 全局异常处理器
* 作用:统一捕获控制器层异常,返回标准化JSON响应
*/
@Slf4j
@RestControllerAdvice // 全局控制器异常处理
public class GlobalExceptionHandler {
/**
* 通用异常处理
* @param e 异常对象
* @return 标准化错误响应
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
log.error("系统异常", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "视频处理失败,请稍后重试");
response.put("error", e.getMessage());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
/**
* 文件大小超限异常处理
* @return 标准化错误响应
*/
@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);
}
/**
* 文件IO异常处理
* @param e IO异常对象
* @return 标准化错误响应
*/
@ExceptionHandler(IOException.class)
public ResponseEntity<Map<String, Object>> handleIOException(IOException e) {
log.error("文件IO异常", 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);
}
}
第三部分:服务启动与接口测试
04.1 应用启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 视频处理服务启动类
* 作用:启动SpringBoot应用,加载所有配置和组件
*/
@SpringBootApplication // 包含@ComponentScan、@EnableAutoConfiguration、@Configuration
public class VideoProcessingApplication {
public static void main(String[] args) {
SpringApplication.run(VideoProcessingApplication.class, args);
System.out.println("=====================================");
System.out.println("视频处理服务启动成功!");
System.out.println("FFmpeg已就绪,可通过/api/video接口调用");
System.out.println("=====================================");
}
}
04.2 接口测试示例
使用 curl 命令快速测试接口(也可使用 Postman、Apifox 等工具):
# 1. 查看FFmpeg版本
curl http://localhost:8080/api/video/version
# 2. 视频格式转换:avi → mp4
curl -X POST -F "file=@input.avi" -F "format=mp4" \
http://localhost:8080/api/video/convert --output output.mp4
# 3. 提取第10秒缩略图
curl -X POST -F "file=@video.mp4" -F "second=10" \
http://localhost:8080/api/video/thumbnail --output thumbnail.jpg
# 4. 压缩视频(目标比特率500kbps)
curl -X POST -F "file=@large_video.mp4" -F "bitrate=500" \
http://localhost:8080/api/video/compress --output compressed.mp4
第四部分:高级扩展技巧
05.1 进度监听(实时反馈处理进度)
实现进度监听接口,实时返回视频处理进度:
/**
* 视频处理进度监听接口
* 作用:实时反馈处理进度、完成状态、错误信息
*/
public interface ProgressListener {
/**
* 进度更新回调
* @param percentage 处理进度(0-100)
* @param message 进度描述
*/
void onProgress(double percentage, String message);
/**
* 处理完成回调
* @param outputFile 处理后的文件
*/
void onComplete(File outputFile);
/**
* 处理失败回调
* @param error 错误信息
*/
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=")) {
// 1. 提取time字段(已处理时长)
// 2. 结合视频总时长计算进度百分比
// 3. 调用listener.onProgress()反馈进度
}
}
05.2 批量处理(提升多文件处理效率)
并行处理多个视频文件,提升整体处理效率:
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 批量视频格式转换
* @param files 待处理文件列表
* @param format 目标格式
* @return 处理完成的文件列表
*/
public List<File> batchConvert(List<MultipartFile> files, String format) {
// 空值校验
if (CollectionUtils.isEmpty(files)) {
return List.of();
}
// 并行流处理:提升多文件处理效率
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());
}
05.3 视频信息解析(获取文件元数据)
通过 FFprobe(FFmpeg 配套工具)解析视频元数据:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 解析视频元数据
* @param videoFile 视频文件
* @return 包含分辨率、时长、码率等信息的Map
*/
public Map<String, Object> getVideoInfo(File videoFile) {
// 构建FFprobe命令(FFmpeg配套工具,专用于解析媒体信息)
List<String> commands = Arrays.asList(
ffmpegConfig.getPath().replace("ffmpeg", "ffprobe"), // 替换为ffprobe路径
"-v", "error", // 只输出错误信息
"-select_streams", "v:0", // 只选择视频流
"-show_entries", "stream=width,height,duration,bit_rate,codec_name", // 要提取的字段
"-of", "json", // 输出格式:JSON
videoFile.getAbsolutePath()
);
// 执行命令并读取输出
String jsonOutput = executeCommand(commands); // 需实现命令执行并返回字符串的方法
// 解析JSON并封装结果
JSONObject jsonObject = JSON.parseObject(jsonOutput);
JSONObject stream = jsonObject.getJSONArray("streams").getJSONObject(0);
Map<String, Object> videoInfo = new HashMap<>();
videoInfo.put("width", stream.getInteger("width")); // 宽度
videoInfo.put("height", stream.getInteger("height")); // 高度
videoInfo.put("duration", stream.getDouble("duration")); // 时长(秒)
videoInfo.put("bitRate", stream.getLong("bit_rate")); // 比特率(bps)
videoInfo.put("codec", stream.getString("codec_name")); // 编码格式
return videoInfo;
}
第五部分:生产环境注意事项
06.1 环境配置要点
-
FFmpeg环境校验
确保服务器已安装 FFmpeg,且配置路径正确:
# 检查FFmpeg是否安装
ffmpeg -version
# 检查FFprobe是否安装(用于解析视频信息)
ffprobe -version
-
JVM资源配置
视频处理消耗内存,需调整 JVM 参数:
# 启动脚本示例:分配2G堆内存
java -Xmx2g -jar video-processing.jar
-
临时文件清理
定时清理临时目录,避免磁盘空间耗尽:
# Linux定时任务示例:每天凌晨清理7天前的临时文件
0 0 * * * find /tmp/video-process/ -mtime +7 -delete
06.2 常见问题与解决方案
| 问题类型 |
常见原因 |
解决方案 |
| 跨平台路径异常 |
Windows/Linux路径分隔符不同 |
使用 File.separator 或Paths类统一处理路径 |
| 编码兼容问题 |
部分格式编码不支持 |
统一使用libx264(视频)+aac(音频)编码 |
| 权限不足 |
FFmpeg执行权限/临时目录写入权限不足 |
给FFmpeg可执行文件添加执行权限(chmod +x),给临时目录添加写入权限 |
| 处理超时 |
大文件处理耗时过长 |
调整超时配置,采用异步处理(如MQ+异步线程) |
06.3 安全防护措施
- 限制文件上传大小(在
application.yml 中配置):
spring:
servlet:
multipart:
max-file-size: 100MB # 单个文件最大100MB
max-request-size: 200MB # 单次请求最大200MB
- 校验文件类型:仅允许上传视频/音频格式(如 mp4、avi、mp3 等);
- 防止命令注入:对用户输入的参数进行严格校验,避免拼接恶意命令;
- 异步处理:大文件处理采用异步模式,避免阻塞 HTTP 请求。
总结
通过 SpringBoot 整合 FFmpeg,可以快速搭建一套功能完善的视频处理服务,覆盖格式转换、缩略图提取、压缩、音视频合并等核心场景。此方案基于成熟的 Java 生态,实现成本低,可扩展性强。
该方案的核心优势:
- 灵活性:支持 FFmpeg 所有原生命令,可扩展任意视频处理能力;
- 标准化:基于 SpringBoot 生态,符合企业级开发规范;
- 可维护性:分层设计(配置层→执行层→服务层→接口层),便于扩展和维护;
- 低成本:基于开源工具,无商业授权成本。
在生产环境中,需重点关注资源占用、异常处理、安全防护三大核心点,确保服务稳定运行。对于更复杂的生产级需求,可以参考在云栈社区分享的分布式架构与持续集成部署方案,构建更健壮的视频处理中台。