Java + FFmpeg:从"玩具"到"工业级"的音视频实战 🎬
各位 Java 老司机,大家好!👋
音视频处理听起来高大上,但很多同学的代码还停留在 Runtime.getRuntime().exec("ffmpeg -i 1.mp4 2.mp4")的幼儿园水平。😅
今天,我们从最基础的调用 开始,一路干到企业级异步调度 和硬件加速。系好安全带,我们要加速了!🚀
1. 基础篇:Java 如何"叫醒"FFmpeg 🌅
1.1 原始但危险的写法(不建议)
scss
// 极度危险!特殊字符会注入,且无法获取错误流
Runtime.getRuntime().exec("ffmpeg -i input.mp4 output.avi");
1.2 标准写法:ProcessBuilder
这是 Java 调用外部进程的标准姿势。
arduino
public class BasicFFmpeg {
public static void main(String[] args) throws Exception {
ProcessBuilder builder = new ProcessBuilder();
// Windows 需要把命令拆开,Linux/Mac 可以直接数组
builder.command("ffmpeg", "-i", "input.mp4", "-y", "output.avi");
// 关键:重定向错误流,否则进程可能卡死
builder.redirectErrorStream(true);
Process process = builder.start();
// 读取输出(非常重要!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("FFmpeg: " + line);
}
}
int exitCode = process.waitFor();
System.out.println(exitCode == 0 ? "转换成功!✅" : "转换失败!❌");
}
}
2. 进阶篇:封装工具类(工程化思维)🛠️
高级开发不会到处写 new ProcessBuilder。我们需要一个单例工具类来管理 FFmpeg 的路径和执行。
2.1 路径探测与初始化
typescript
@Component
public class FFmpegExecutor {
private String ffmpegPath;
@PostConstruct
public void init() {
String os = System.getProperty("os.name").toLowerCase();
// 优先使用系统环境变量,兜底使用配置路径
this.ffmpegPath = os.contains("win") ? "ffmpeg.exe" : "ffmpeg";
}
/**
* 执行命令并返回状态码
*/
public int execute(List<String> commands) throws Exception {
List<String> fullCmd = new ArrayList<>();
fullCmd.add(ffmpegPath);
fullCmd.addAll(commands);
ProcessBuilder pb = new ProcessBuilder(fullCmd);
Process process = pb.start();
// 异步消费流,防止缓冲区阻塞
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");
new Thread(outputGobbler).start();
new Thread(errorGobbler).start();
return process.waitFor();
}
}
// 辅助类:吃掉流
class StreamGobbler implements Runnable {
private InputStream inputStream;
private String type;
public StreamGobbler(InputStream inputStream, String type) {
this.inputStream = inputStream;
this.type = type;
}
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = br.readLine()) != null) {
// 可以接入日志系统
System.out.println(type + "> " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 实战篇:常见业务场景落地 🎯
3.1 视频截图(封面生成)
需求:截取视频第 5 秒的一帧作为封面。
typescript
public String generateCover(String videoPath, String outputDir) {
String outputImage = outputDir + "/cover.jpg";
List<String> cmd = Arrays.asList(
"-i", videoPath,
"-ss", "00:00:05", // 时间点
"-vframes", "1", // 只取一帧
"-q:v", "2", // 图片质量 (2-5 较好)
"-y", // 覆盖已存在文件
outputImage
);
execute(cmd);
return outputImage;
}
3.2 视频转码(适配 Web)
需求:手机拍的视频太大,转成网页通用的 MP4。
typescript
public void transcodeToWeb(String input, String output) {
List<String> cmd = Arrays.asList(
"-i", input,
"-c:v", "libx264", // 视频编码 H.264
"-preset", "fast", // 编码速度与压缩率平衡
"-crf", "23", // 恒定质量 (18-28 视觉无损到一般)
"-c:a", "aac", // 音频编码
"-b:a", "128k",
"-movflags", "+faststart", // 让视频元数据在前,支持边下边播
output
);
execute(cmd);
}
4. 高级篇:视频切片(HLS 流媒体)🍕
这是在线教育、直播回放的核心技术。将大视频切成无数小碎片(ts),浏览器按需加载。
4.1 切片逻辑
arduino
/**
* 将 MP4 转换为 HLS (m3u8)
*/
public void sliceToHLS(String input, String outputDir, String m3u8Name) {
// 确保目录存在
new File(outputDir).mkdirs();
List<String> cmd = Arrays.asList(
"-i", input,
"-profile:v", "baseline", // 兼容性最好
"-level", "3.0",
"-start_number", "0", // 起始切片编号
"-hls_time", "10", // 每个切片 10 秒
"-hls_list_size", "0", // 包含所有切片,不限制数量
"-f", "hls", // 格式为 HLS
outputDir + "/" + m3u8Name + ".m3u8"
);
execute(cmd);
}
结果 :你会得到 index.m3u8和 segment001.ts, segment002.ts... 前端直接用 <video>标签播放 m3u8 地址即可。
5. 专家篇:异步、进度与分布式 🚀
5.1 获取实时进度(难点!)
FFmpeg 的输出流里包含了 time=00:01:23这样的信息。我们需要解析它。
arduino
// 在 StreamGobbler 中解析
if (line.contains("time=")) {
// 提取 time=00:01:23.45
String timeStr = line.substring(line.indexOf("time=") + 5, line.indexOf("."));
// 转换成秒,对比视频总时长,计算百分比
double progress = calculateProgress(timeStr, totalDuration);
// 更新到 Redis 或数据库,前端轮询显示进度条
redisTemplate.opsForHash().put("task:progress", taskId, progress + "%");
}
5.2 架构升级:别让 Web 服务卡死
错误做法 :Controller 直接调用 execute(),用户要等 5 分钟直到转码结束。
正确做法 :异步解耦。
- Controller 接收文件,生成
taskId,返回"排队中"。 - 将任务丢进 RabbitMQ / Redis Stream。
- Worker 服务(只装 FFmpeg 的机器)消费消息,执行转码。
- 转码完成,回调 API 通知业务系统。
5.3 硬件加速(烧钱但极快)
如果你用的是云服务器(阿里云/腾讯云),开启 GPU 加速能让速度提升 5-10 倍。
arduino
// 检测 NVIDIA GPU
List<String> cmd = Arrays.asList(
"-hwaccel", "cuda", // 使用 CUDA 硬件加速
"-i", input,
"-c:v", "h264_nvenc", // 使用 NVIDIA 编码器
"-preset", "fast",
output
);
6. 总结:FFmpeg 命令速查表 📋
| 场景 | 核心参数 | 说明 |
|---|---|---|
| 提取音频 | -vn -acodec copy |
忽略视频流,直接拷贝音频 |
| 视频拼接 | -f concat -safe 0 -i list.txt |
list.txt 里写 file '1.mp4' |
| 添加水印 | -vf "movie=logo.png [watermark]; [in][watermark] overlay=10:10 [out]" |
右上角加水印 |
| 倍速播放 | -filter_complex "[0:v]setpts=0.5*PTS" -af atempo=2.0 |
视频0.5倍速(快2倍),音频2倍速 |
最后的忠告 ⚠️
FFmpeg 的命令参数有几千个,别死记硬背。学会看文档 ffmpeg -h ,以及永远不要信任用户输入的文件名 (做好路径校验,防止 ../穿越攻击)。
祝大家的视频都能秒开,音频都无损!🎉