前言
最近项目有需求,对图片、音频、视频进行一些简单处理,简单学习后了解到ffmpeg可以实现这些功能,这里对使用到的部分做说明。功能需要安装ffmpeg环境。
1. ffmpeg处理命令
这里介绍相关操作的ffmpeg处理命令。
1.1 图片处理
1.1.1 图片翻转
图片翻转包括水平翻转和垂直翻转。
# 原文件 source.jpg 目标文件 target.jpg
# 水平翻转
ffmpeg -i /data/image/source.jpg -vf hflip /data/image/target.jpg
# 垂直翻转
ffmpeg -i /data/image/source.jpg -vf vflip /data/image/target.jpg
1.2 音频处理
1.2.1 音频裁剪
# 从00:00:10秒开始裁剪,裁剪10秒,原文件 source.mp3 目标文件 temp1.mp3
ffmpeg -ss 00:00:10 -i source.mp3 10 temp_1.mp3
1.2.2 音频合并
方式一:
# 将temp_1.mp3和temp_2.mp3合并为target.mp3
ffmpeg -i "concat:temp_1.mp3|temp_2.mp3" -acodec copy target.mp3
方式二:
# 将temp_1.mp3和temp_2.mp3合并为target.mp3
# 注意这里的n后面的值要随着前面音频片段的个数改变而改变
ffmpeg -i temp_1.mp3 -i temp_2.mp3 -filter_complex "[0:0][1:0]concat=n=2:v=0:a=1[out]" -map "[out]" target.mp3
1.3 视频处理
1.3.1 视频提取音频
# 将source.mp4 音频提取为 target.mp3
ffmpeg -i source.mp4 -vn target.mp3
1.3.2 视频倍速
注意:
setpts这里指的是将视频变为0.5倍速,视频变为2倍速,这个值对应为0.5,4倍速对应的值为0.25。
atempo指的是音频变为0.5倍速,值和视频对应的值同理,这里如果不加atempo属性,就会导致视频倍速了,音频还是原速。
# 将source.ts 倍速变为0.5倍速
ffmpeg -i source.mp4 -filter_complex "[0:v]setpts=2.0*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" target.mp4
1.3.3 视频裁剪
这里解释一下:
-to 表示截取xx-xx时间,也可以用 00:01:00 -t 00:01:00 表示从第一分钟开始截取一分钟时长
-vcodec copy 表示视频采取复制原视频方式(不重新编码)
-acodec copy 表示音频采取复制原音频方式
-strict -2 表示关闭严格模式
-keyint_min 2 设置最小关键帧间隔为2
-g 1 将每一帧都变味关键帧
-y 覆盖文件(输出路径存在文件就覆盖原文件)
# 将source.mp4视频从00:01:00 - 00:03:00 切割出来为target.mp4
ffmpeg -i source.mp4 -ss 00:01:00 -to 00:03:00 -vcodec copy -acodec copy -strict -2 -keyint_min 2 -g 1 -y target.mp4
1.3.4 视频拼接
方式一:
这里解释一下:
-filter_complex 启用复杂滤镜图,用于拼接多个输入流。
[0:v][0:a] 取第一个视频流,取第一个音频流,后面以此类推
concat=n=3,3表示三个输入
v=1:a=1[outv][outa] 合并视频流音频流,输出视频流音频流
-map “[outv]” -map “[outa]” 选择对应的视频流音频流(刚刚合并输出的)
ffmpeg -i temp_1.mp4 -i temp_2.mp4 -i temp_3.mp4 -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]" -map "[outv]" -map "[outa]" target.mp4
方式二:
-f concat 表示使用 concat 协议
-safe 0 用于允许读取相对路径的文件
-i filelist.txt 指定包含视频文件列表的文件
-c copy 表示拷贝原始视频和音频流,而不重新编码
# 合并file.txt文件中的视频文件为output.mp4
ffmpeg -f concat -safe 0 -i file.txt -c copy output.mp4
file内容如下:
file 'temp_1.mp4'
file 'temp_2.mp4'
file 'temp_3.mp4'
1.3.5 视频添加字幕
这里需要准备一个字幕文件subtitle.srt,文件内容如下
1
00:00:00,000 --> 00:00:05,000
第一段字幕
2
00:00:05,001 --> 00:00:10,000
第二段字幕
3
00:00:10,001 --> 00:00:15,000
第三段字幕
字幕文件中,00:00:10,001 表示 时分秒毫秒
ffmpeg -i input.mp4 -vf "subtitles=subtitle.srt" -c:a copy output.mp4
1.3.6 视频添加水印
1.添加图片水印
ffmpeg -i source.mp4 -i image.jpg -filter_complex "overlay=W-w-10:H-h-10" target.mp4
2.添加文字水印
# fontsize 指定文字大小,fontfile指定字体文件,fontcolor指定字体颜色 x,y指定水印位置
ffmpeg -i source.mp4 -vf drawtext=text='文字水印:fontsize=24:fontfile=/data/font/Arial:fontcolor=white:x=10:y=10' -c:a copy target.mp4
2. java操作ffmpeg
总体操作分为两部,拼接命令 -> 执行命令,这里使用ProcessBuilder来处理执行命令。
这里拿给视频添加文字水印进行举例。代码如下:
public static void main(String[] args) throws Exception {
String textWater = "文本水印";
String videoPath = "E:\\video\\test.mp4"; // 原文件
String targetPath = "E:\\video\\water.mp4"; // 目标文件
List<String> commandList = new ArrayList<>();
commandList.add("ffmpeg");
commandList.add("-i");
commandList.add(videoPath);
commandList.add("-vf");
commandList.add("drawtext=text='" + textWater + "':fontsize=24:fontfile=/data/font/Arial:fontcolor=white:x=10:y=10");
commandList.add("-c:a");
commandList.add("copy");
commandList.add(targetPath);
StringBuilder sb = new StringBuilder();
commandList.forEach(str -> sb.append(str).append(" "));
FFmpegUtils.processCmd(commandList);
System.out.println("完成");
}
工具类代码如下:
@Slf4j
public class FFmpegUtils {
/**
* 调用ffmpeg执行对应的ffmpeg命令
* @param commandList 命令集合
*/
public static boolean processCmd(List<String> commandList) throws Exception {
ProcessBuilder processBuilder = new ProcessBuilder(commandList);
processBuilder.redirectErrorStream(true);
Process process = null;
process = processBuilder.start();
InputStream inputStream = process.getInputStream();
// 打印FFmpeg输出
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// 等待处理完成
int exitCode = process.waitFor();
process.destroy();
if (exitCode == 0) {
log.info("执行成功");
return true;
} else {
StringBuilder sb = new StringBuilder();
commandList.forEach(str -> sb.append(str).append(" "));
log.info("ffmpeg执行命令失败 , 命令 -> {}", sb);
}
return false;
}
/**
* 调用ffmpeg执行对应的ffmpeg命令(日志异步输出)
* 有时在处理部分指令的时候输出过多导致线程阻塞,就会执行失败,这里就可以将输出日志新建线程打印,防止这种情况
* @param commandList 命令集合
*/
public static boolean processCmdAsync(List<String> commandList) throws Exception {
StringBuilder sb = new StringBuilder();
commandList.forEach(str -> sb.append(str).append(" "));
log.info("ffmpeg执行命令 -> {}", sb);
ProcessBuilder processBuilder = new ProcessBuilder(commandList);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// 开线程打印避免线程阻塞
Thread outputThread = new Thread(() -> readStream(process.getInputStream()));
Thread errorThread = new Thread(() -> readStream(process.getErrorStream()));
outputThread.start();
errorThread.start();
// 等待处理完成
int exitCode = process.waitFor();
log.info("命令处理完成 code -> {}", exitCode);
outputThread.join();
errorThread.join();
if (exitCode == 0) {
log.info("执行成功");
process.destroy();
return true;
}
process.destroy();
return false;
}
/**
* 打印ffmpeg输出
* @param inputStream
*/
private static void readStream(InputStream inputStream) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里提一下我这踩的坑,当时在做音频处理的时候,对音频进行拼接,使用到的是前面我提到的方式一的音频合并,但是执行命令始终执行失败,排查以为是文件没有问题,是命令问题,但是在执行中,我打印了我的命令,我直接将命令放在项目部署的容器内也能执行,拿到宿主机也能执行,寻求同事援助,他判断原因可能是ProcessBuilder的兼容问题,不支持这种语法,然后就换了音频拼接的方式二,执行就成功了。对应音频拼接的方式一,视频拼接也能采用这种方式,但是由于之前音频的问题,所以这里我也没提到这种方式,有兴趣可以去试一下。