
1. 从一堆静图到一条能发出去的视频中间到底隔着什么手里有一批图片想快速拼成一段带背景音乐的短视频这个需求听起来简单真动手做的时候坑比想象中多。用 Java 做这件事绕不开的一个工具就是 JavaCV——它把 FFmpeg、OpenCV 这些底层能力封装成了 Java 能直接调的接口。但很多人第一次用的时候会发现官方文档写得像天书网上搜到的代码片段要么跑不起来要么合成出来的视频音画不同步要么图片比例被拉得惨不忍睹。这篇内容就是围绕用 JavaCV 把图片合成短视频并配上音频这个具体任务把整个链路拆开讲清楚。核心关键词包括JavaCV、FFmpegFrameRecorder、图片合成视频、视频添加音频、帧率控制、音画同步。适合两类人看一类是刚接触 JavaCV、想找个能直接跑通的完整方案的开发者另一类是用过 FFmpeg 命令行、现在想把它搬到 Java 服务里做自动化处理的人。我会从环境依赖开始一路讲到图片预处理、帧写入、音频混流、参数调优最后把几个我实际踩过的坑单独拎出来说。代码部分给的是能直接抄的骨架参数部分会解释为什么这么设而不是甩一堆数字让你自己猜。整个过程不需要你懂音视频编码的底层原理但看完之后你会明白每一步在干什么出了问题知道往哪个方向查。先说结论JavaCV 做这件事的核心就是两个类——FFmpegFrameRecorder负责写视频帧和音频帧Java2DFrameConverter负责把BufferedImage转成 recorder 能吃的Frame。把这两个东西用对了剩下的就是参数和细节问题。2. 环境准备依赖引入与版本选择的那些门道2.1 Maven 依赖怎么引为什么推荐 platform 方式JavaCV 的依赖如果一个个手动引会非常痛苦因为它依赖一堆 FFmpeg 的原生库不同平台还不一样。官方推荐用javacv-platform这个聚合包它会把 Windows、Linux、Mac 各平台的 native 库都拉下来。缺点是包体大几百 MB优点是省心不会出现本地跑得好好的部署到服务器就报 UnsatisfiedLinkError这种问题。dependency groupIdorg.bytedeco/groupId artifactIdjavacv-platform/artifactId version1.5.9/version /dependency如果你明确知道部署环境是 Linux x86_64可以只引javacv加上对应的ffmpeg-platform能省不少体积。但我的建议是开发阶段先用 platform 全量包把功能跑通等上线前再根据实际运行环境做裁剪。因为 native 库缺失导致的报错信息非常不友好新手很容易卡在这里。版本选择上1.5.x 系列是目前比较稳定的。不要盲目追最新版JavaCV 的版本和 FFmpeg 版本是绑定的某些新版本在特定编码器上有行为变化。我用的 1.5.9 在 H.264 编码和 AAC 音频上表现稳定兼容性也够。2.2 一个容易被忽略的点JDK 版本与 native 库的匹配JavaCV 的 native 库对 JDK 版本有一定要求。用 JDK 8 和 JDK 17 都能跑但如果你用的是比较新的 JDK比如 21偶尔会遇到 native 库加载的兼容性提示。实测下来 JDK 11 和 17 是最稳的区间。另外如果你在 Docker 里跑基础镜像建议用带 glibc 的比如 debian 系Alpine 那种用 musl 的镜像需要额外装兼容层否则 FFmpeg 的 native 库加载会失败。还有一个细节javacv-platform会把所有平台的库都打进来最终 jar 包可能超过 500MB。如果你做的是微服务这个体积会影响部署速度。解决办法是在打包时用javacv不带 platform 指定平台的ffmpeg依赖或者用 Maven 的 classifier 机制只保留目标平台的库。3. 图片预处理为什么不能直接把原图丢进去合成3.1 尺寸不统一是合成失败的头号原因FFmpegFrameRecorder 在初始化时会设定一个固定的视频宽高之后写入的每一帧都必须是这个尺寸。如果你手里的图片尺寸五花八门直接写进去会出现两种结果要么抛异常要么画面被拉伸变形。所以第一步必须把所有图片统一到同一个尺寸。统一尺寸有两种策略拉伸和等比缩放加填充。拉伸简单但会变形人物照片会被压扁或拉长观感很差。等比缩放加填充是更专业的做法——先按比例缩放到能放进目标框然后用背景色通常是黑色或白色填充剩余区域。这样图片不会变形视觉上更舒服。public static BufferedImage normalizeImage(BufferedImage src, int targetW, int targetH) { BufferedImage canvas new BufferedImage(targetW, targetH, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g canvas.createGraphics(); // 开启抗锯齿缩放后边缘更平滑 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.setColor(Color.BLACK); g.fillRect(0, 0, targetW, targetH); double scale Math.min((double) targetW / src.getWidth(), (double) targetH / src.getHeight()); int drawW (int) (src.getWidth() * scale); int drawH (int) (src.getHeight() * scale); int offsetX (targetW - drawW) / 2; int offsetY (targetH - drawH) / 2; g.drawImage(src, offsetX, offsetY, drawW, drawH, null); g.dispose(); return canvas; }这里有个关键点BufferedImage的类型要用TYPE_3BYTE_BGR。为什么因为 FFmpeg 内部处理彩色图像时默认用的就是 BGR 排列如果你用TYPE_INT_RGB或带 alpha 通道的类型转换时可能出