Content Table

Java 调用 FFmpeg 转换视频音频

使用 FFmpeg 的命令把一种格式的视频转换为另一种格式的视频,例如把 test.avi 转为 test.mp4 的命令为 ffmpeg -i test.avi -vcodec h264 test.mp4,Java 中可以用 ProcessBuilder 调用这个命令执行转换:

1
2
3
4
5
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", "test.avi", "-vcodec", "h264", "test.mp4");
pb.directory(new File("/Users/Biao/Desktop")); // pb 的工作目录,设置为 test.avi 所在目录
pb.start();
}

直接调用命令转换虽然很方便,但是如果视频比较大,转换需要的时间比较长时,希望能够及时的得到转换进度并反馈给客户端,就要解析命令的输出获取转换进度,这时就比较麻烦了。接下来介绍 ffmpeg-cli-wrapper 的使用,它对 FFmpeg 的命令进行了封装,简化视频转换的开发难度。

ffmpeg-cli-wrapper: A fluent interface to running FFmpeg from Java.

使用案例介绍 ffmpeg-cli-wrapper 的三种操作:

  • 视频转换
  • 音频转换
  • 提取视频中指定时间处的画面

一、安装 FFmpeg

首先当然是先安装好,FFmpeg 支持 Linux,Windows,Mac,安装步骤就不在此一一列举了,需要注意的是安装好后把 FFmpeg 的 bin 目录设置到系统环境变量中,方便调用。

二、Gradle 依赖

1
compile "net.bramp.ffmpeg:ffmpeg:0.6.2"

三、视频转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 转换任意格式的视频为 mp4 格式的视频
*
* @param src 源视频文件
* @param dest 保存 mp4 视频的文件
* @throws IOException
*/
public static void convertToMp4(File src, File dest) throws IOException {
FFmpeg ffmpeg = new FFmpeg("/usr/local/Cellar/ffmpeg/4.1/bin/ffmpeg");
FFprobe ffprobe = new FFprobe("/usr/local/Cellar/ffmpeg/4.1/bin/ffprobe");
FFmpegProbeResult in = ffprobe.probe(src.getAbsolutePath());

FFmpegBuilder builder = new FFmpegBuilder()
.overrideOutputFiles(true) // Override the output if it exists
.setInput(in)
.addOutput(dest.getAbsolutePath())
.setFormat("mp4") // Format is inferred from filename, or can be set
.setVideoCodec("libx264") // Video using x264
.setVideoFrameRate(24, 1) // At 24 frames per second
// .setVideoResolution(width, height) // At 1280x720 resolution (宽高必须都能被 2 整除)
.setAudioCodec("aac") // Using the aac codec
.setStrict(FFmpegBuilder.Strict.EXPERIMENTAL) // Allow FFmpeg to use experimental specs (ex. aac)
.done();

FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
FFmpegJob job = executor.createJob(builder, new ProgressListener() {
// 使用 FFmpegProbeResult 得到视频的长度 (单位为纳秒)
final double duration_ns = in.getFormat().duration * TimeUnit.SECONDS.toNanos(1);

@Override
public void progress(Progress progress) {
// 转换进度 [0, 100]
// [Fix] No duration for FLV, SWF file, 所以获取进度无效时都假装转换到了 99%
int percentage = (duration_ns > 0) ? (int)(progress.out_time_ns / duration_ns * 100) : 99;

// 日志中输出转换进度信息
log.debug("[{}%] status: {}, frame: {}, time: {} ms, fps: {}, speed: {}x",
percentage,
progress.status,
progress.frame,
FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS),
progress.fps.doubleValue(),
progress.speed
);
}
});

job.run();
}

四、音频转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 转换任意格式的音频为 mp3 格式的音频
*
* @param src 源音频文件
* @param dest 保存 mp3 音频的文件
* @throws IOException
*/
public static void convertAudioToMp3(File src, File dest) throws IOException {
FFmpeg ffmpeg = new FFmpeg("/usr/local/Cellar/ffmpeg/4.1/bin/ffmpeg");
FFprobe ffprobe = new FFprobe("/usr/local/Cellar/ffmpeg/4.1/bin/ffprobe");
FFmpegProbeResult in = ffprobe.probe(src.getAbsolutePath());

FFmpegBuilder builder = new FFmpegBuilder()
.overrideOutputFiles(true)
.setInput(src.getAbsolutePath())
.addOutput(dest.getAbsolutePath())
.done();

FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
FFmpegJob job = executor.createJob(builder, new ProgressListener() {
// 使用 FFmpegProbeResult 得到视频的长度 (单位为纳秒)
final double duration_ns = in.getFormat().duration * TimeUnit.SECONDS.toNanos(1);

@Override
public void progress(Progress progress) {
// 转换进度 [0, 100]
int percentage = (duration_ns > 0) ? (int)(progress.out_time_ns / duration_ns * 100) : 99;

// 日志中输出转换进度信息
log.info("[{}%] status: {}, frame: {}, time: {} ms, fps: {}, speed: {}x",
percentage,
progress.status,
progress.frame,
FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS),
progress.fps.doubleValue(),
progress.speed
);
}
});

job.run();
}

五、提取视频中指定时间处的画面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 提取视频中传入的 time 处的画面
*
* @param video 视频文件
* @throws IOException
*/
public static void extractImage(File video, File image, int time) throws IOException {
FFmpeg ffmpeg = new FFmpeg("/usr/local/Cellar/ffmpeg/4.1/bin/ffmpeg");
FFprobe ffprobe = new FFprobe("/usr/local/Cellar/ffmpeg/4.1/bin/ffprobe");
FFmpegBuilder builder = new FFmpegBuilder()
.overrideOutputFiles(true)
.setInput(video.getAbsolutePath())
.addOutput(image.getAbsolutePath())
.setFrames(1)
.setVideoFilter(String.format("select='gte(t\\,%d)'", time)) // 第 time 秒处的画面
.done();

FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
}

六、参考资料