导读:
由于业务那边有个合成视频的需求,想做成把图片和视频混在一起带转场和bgm然后合成导出的功能,就去研究了一下音视频方面的技术,发现Android原生没有满足需求的技术,于是去学习FFmpeg的使用,总共用了大概两个星期的时间,中间遇到各种问题,好在最后都想到了解决方案,在这里记录下学习的过程,避免各位踩同样的坑
本文含以下内容:
1.FFmpeg常用命令
2.视频合成及转场的设计思路以及性能优化
3.自己在项目中遇到的比较大的问题及解决方案
4.后续优化方案
一.FFmpeg常用命令
FFmpeg官网:ffmpeg.org
里面有全部的命令参数说明,很详细
ffmpeg -i input.mp4 output.avi
FFmpeg [全局选项] {[输入文件选项] -i 输入文件路径} ... {[输出文件选项] 输出文件地址} ...
*-i 输入
ffmpeg -y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -loop 1 -i pic.png/jpg -c:v libx264 -r 25 -t 1 out.jpg
*-f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100
是添加静音音轨(后面拼接视频要用,如果只用单个视频可以不用音轨)
*-r 输出视频帧率
*-y 强制覆盖文件
*-t 输出视频时长,单位秒
生成黑色背景视频(带静音音轨)
ffmpeg -y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -r %d -i color=black -t %f -vf scale=1280:720 -vcodec mpeg4 %s
裁剪视频
ffmpeg -i input.mp4 -filter:v "crop=w:h:x:y" output.mp4
更改视频分辨率
ffmpeg -i input.mp4 -filter:v scale=1280:720 -c:a copy output.mp4
*-c:a copy 保留原音频的编码格式
ffmpeg -y -i input.mp4 -filter_complex split[a][b];[a]scale=1280:720,boxblur=30:5[a];[b]scale=1280:720:force_original_aspect_ratio=decrease[b];[a][b]overlay=(W-w)/2[blur];[blur]pad=1280:720:(ow-iw)/2:(oh-ih)/2,setdar=16/9 -vcodec libx264 -r %d -preset superfast out.mp4
*split分割输入流为两个输入流a,b,a输入流模糊并铺满屏幕,b输入流保持视频原比例盖在a输入流中间位置
ffmpeg -i input.mp4 -vf drawtext=fontfile=%s:fontcolor=white:fontsize=36:text='...':x=(w-tw)/2:y=(h-text_h)/2),drawtext=fontfile=%s:fontcolor=white:fontsize=36:text='...':x=(w-tw)/2:y=((h-text_h)/2)+(text_h-(th/4)) -y -vcodec mpeg4 output.mp4
*fontfile为字体文件,x=(w-tw)/2:y=(h-text_h)/2)
表示文字在屏幕最中央,w为视频宽度,tw为文字宽度,text_h = th同理
ffmpeg -i video.mp4 -i bgm.mp3 -filter_complex [1:a]aloop=loop=-1[out];[out][0:a]amix -ss 0 -t %f -y %s
*[1:a] 表示第二个输入流的音轨,aloop=-1循环bgm,amix混合视频和bgm的音轨
常用命令网上有一大把,根据自己的需求去找一般都能找到,但重点还是理解ffmpeg的原理,要不出了问题还是没办法定位
比如concat拼接这个命令,官网给出的原理是这样,一开始看到[0:a][3:v]这些的时候也是一脸懵逼,后来理解了这些是视频,音频的输入
理解输入流
理解filter原理
一开始的设计思路是,用fade这个渐变的命令来转场,但是效果不是特别好,第一个视频完全淡出后第二个视频才会进入,中间会有一段黑屏
,看着像在播ppt,大概是这样
于是试着做交叉淡入,大概是这样
代码如下(省略设置分辨率,视频比例,帧率,音频等):
ffmpeg -y -i a.mp4 -i b.mp4 -filter_complex [0:v]fade=t=out:st=%f:d=%f:alpha=0:color=black,setpts=PTS-STARTPTS[v0];[1:v]fade=t=in:st=0:d=%f:alpha=1,fade=t=out:st=%f:d=%f:alpha=0:color=black,setpts=PTS-STARTPTS+%f/TB[v1];[v0][v1]overlay[outv] -vcodec libx264 -map [outv] -f mp4 -r 25 -preset medium out.mp4
*st-开始渐变时间 d-持续时间 alpha-(If set to 1, fade only alpha channel, if one exists on the input. Default value is 0.官网这样解释,好像是多个输入流的时候设置为1)setpts=PTS-STARTPTS+%f/TB 第一帧开始的时间点
这样做一开始自己测试的时候好像没啥问题,但后面一接入实际操作,问题就很明显了,因为是overlay的操作,当30多个视频这样覆盖叠加的话,合成速度会变慢很多,而且性能差的手机直接报OOM错误,内存顶不住啊
于是想到了第三种方案,使用concat拼接视频而非叠加视频
先把两个视频切割成4个视频,第一个切割最后一秒,第二个切割第一秒,
然后切割出来的两个视频渐变+叠加生成一个新视频,最后把3个视频连接起来
/**
* 多个视频拼接在一起concat(交叉淡入),只拼接和加bgm,不处理其他
*/
public static String[] concatMultiVideoWithFade(List list , String bgmFilePath, String fontFilePath, String targetPath) {
final float FADE_DURATION = 0.2f;//0.2秒的fade
final int FRAM = 25;//帧率
final int bit = getFitBitRate(1280 * 720);
StringBuilder bd = new StringBuilder();
bd.append("ffmpeg ");
bd.append("-y ");
float totaltime = 0 ;
for(int i = 0 ; i < list.size() ; i++){ //; MediaBean vedio : list
MediaBean vedio = list.get(i);
//以下时间单位都是秒
if (vedio.getType() != 1){
//图片转视频,直接用视频时长
bd.append(String.format("-i %s -r %d " , codeString(vedio.getPath()) ,FRAM ));
//加入视频总时长
float addtime = (vedio.getTime()/1000f - FADE_DURATION);
totaltime += addtime > 0 ? addtime : FADE_DURATION;
}else {
//原始视频,根据需求规则截取
bd.append(String.format("-i %s -r %d " , codeString(vedio.getPath()) ,FRAM));
float addtime = (vedio.getTime()/1000f - FADE_DURATION);
totaltime += addtime > 0 ? addtime : FADE_DURATION;
}
}
//最后一个不用减
totaltime+=FADE_DURATION;
//添加bgm
if (!TextUtils.isEmpty(bgmFilePath)){
bd.append(String.format("-i %s " , bgmFilePath));
}
bd.append("-filter_complex ");
//plan A 加转场
//渐变转场
for(int i = 0 ; i < list.size() ; i++) {//; MediaBean vedio : list
MediaBean vedio = list.get(i);
float t = vedio.getTime()/1000f;
//开始渐变的时间点
float duration;
if (t > FADE_DURATION ){
duration = t - FADE_DURATION;
}else {
duration = t;
}
/*
//拆分
v0 -> [out0][fadeout0]
v1 -> [fadein0][out1][fadeout1]
v2 -> [fadein1][out2][fadeout2]
v3 -> [fadein2][out3]
//混合
[fadeout0][fadeout0] fade-> [fade0]
//拼接
[out0][fade0][out1][fade1]..[fade n-1][outn]
*/
if (i == 0){
//第一个
bd.append(String.format("[%d:v]split[out0][fadeout0];[out0]trim=end=%f[out0];[fadeout0]trim=start=%f,setpts=PTS-STARTPTS[fadeout0];", i , duration , duration ));
}else if (i == list.size()-1 ){
//最后一个
bd.append(String.format("[%d:v]split[fadein%d][out%d];", i , i-1 , i ));
bd.append(String.format("[fadein%d]trim=end=%f,fade=t=in:st=0:d=%f:alpha=1[fadein%d];" , i-1 , FADE_DURATION, FADE_DURATION , i-1 ));
bd.append(String.format("[out%d]trim=start=%f,setpts=PTS-STARTPTS[out%d];" , i , FADE_DURATION , i ));
}else {
//中间
bd.append(String.format("[%d:v]split[fadein%d][splite%d];", i , i-1 , i ));
bd.append(String.format("[splite%d]split[out%d][fadeout%d];", i , i , i ));
bd.append(String.format("[fadein%d]trim=end=%f,fade=t=in:st=0:d=%f:alpha=1[fadein%d];" , i-1 , FADE_DURATION ,FADE_DURATION , i-1 ));
bd.append(String.format("[out%d]trim=start=%f:end=%f,setpts=PTS-STARTPTS[out%d];" , i ,FADE_DURATION , duration , i ));
bd.append(String.format("[fadeout%d]trim=start=%f,setpts=PTS-STARTPTS[fadeout%d];" , i , duration , i ));
}
// currentTime += duration;
}
// 0...n-1
for(int i = 0 ; i < list.size()-1 ; i++) { //; MediaBean vedio : list
bd.append(String.format("[fadeout%d][fadein%d]overlay[fade%d];" , i , i , i));
}
//0...n
//拼接 [out0][fade0][out1][fade1]..[fade n-1][outn]
//拼接视频
for(int i = 0 ; i < list.size() ; i++) { //; MediaBean vedio : list
if (i == list.size()-1){
//最后一个
bd.append(String.format("[out%d]" , i));
}else {
bd.append(String.format("[out%d][fade%d]" , i, i ));
}
}
bd.append(String.format("concat=n=%d:v=1:a=0[outv];" , (list.size()*2) - 1 ));
float currentTime = 0;
//覆盖音频
for(int i = 0 ; i < list.size() ; i++) { //; MediaBean vedio : list
MediaBean vedio = list.get(i);
float t = vedio.getTime()/1000f;
//开始消失时间,后面的视频进入时间
float duration;
if (t > FADE_DURATION ){
duration = t - FADE_DURATION;
}else {
duration = t;
}
// adelay=1500|0|500
if (i == 0){
bd.append(String.format("[%d:a]afade=t=out:st=%f:d=%f,volume=10dB[a%d];" , i , duration , FADE_DURATION , i)); //,asetpts=PTS-STARTPTS
}else {
bd.append(String.format("[%d:a]adelay=%d|%d,afade=t=in:st=0:d=%f,afade=t=out:st=%f:d=%f,volume=10dB[a%d];" , i ,(int)(currentTime*1000) ,(int)(currentTime*1000) ,FADE_DURATION ,currentTime +duration ,FADE_DURATION ,i)); //,asetpts=PTS-STARTPTS+%f/TB
}
// bd.append(String.format("[%d:a]atrim=%f[a%d];" , i , duration - FADE_DURATION , i)); //,asetpts=PTS-STARTPTS
currentTime += duration;
}
//拼接音频
for(int i = 0 ; i < list.size() ; i++){
bd.append(String.format("[a%d]",i));
}
// bd.append(String.format("concat=n=%d:v=0:a=1[outa]" , list.size()));
bd.append(String.format("amix=inputs=%d:duration=longest[outa]",list.size() ));
//添加bgm背景音
if (!TextUtils.isEmpty(bgmFilePath)){
bd.append(String.format(";[%d:a]aloop=loop=-1:size=2e+09,afade=t=out:st=%f:d=%f[bgm];[outa][bgm]amix=inputs=2:duration=first[outbgm]" , list.size() ,currentTime-0.8 , FADE_DURATION +0.8));
// bd.append(String.format(" -vcodec libx264 -map [outv] -acodec aac -map [outbgm] -ar 22050 -ac 2 -ab 128k -r %d -preset medium -crf 18 %s" ,FRAM, targetPath));
bd.append(String.format(" -vcodec libx264 -map [outv] -acodec aac -map [outbgm] -ar 22050 -ac 2 -ab 128k -r %d -pix_fmt yuv420p -preset fast %s" ,FRAM, targetPath));
}else {
//无bgm
bd.append(String.format(" -vcodec libx264 -map [outv] -map [outa] -r %d -pix_fmt yuv420p -preset fast %s" ,FRAM, targetPath));
}
Log.d("ffmpeg--" , bd.toString());
String str = bd.toString();
String[] result = str.split(" ");
for (int i = 0;i
这样最后试了比上一种性能稍微好一点...但还是会有oom的报错,
于是把问题聚焦在输入数据过多的方向上,随着输入数据的过多,FFmpeg命令可以达到几百行... 最多一次性输入38个视频或图片,并且中间还要加字幕,裁剪,高斯模糊等效果,不崩才怪....
那只能先牺牲合成速度来提高稳定性了,然后想到了两种方法来降低对性能的要求
1.每个效果分步骤合成 -> 图片转视频 -> 生成文字水印 -> 裁剪,高斯模糊 -> 拼接,转场 -> 加bgm
2.分组合成视频,因为最多有38个视频或图片,写了一个递归,拆分6个为一组,每6个合成为一个新视频,最终再把所有新视频拼接起来,加上bgm生成最终视频,这样38个数据源最终也只用拼接7个新视频
*这样合成会降低效率,但马上要发版,为了稳定性迫不得已,最终在小米5s上成功合成38个视频和图片
*后面研究了OpenGL ES来做转场效果及合成视频,android原生来做音频混合,ffmpeg就只处理裁剪分辨率,高斯模糊和添加字幕了
三.自己在项目中遇到的比较大的问题及解决方案
1.FFmpeg源码编译及导入open-GL插件
一开始调研的时候,发现有一个open-gl的转场库能够满足需求,但需要在ffmpeg的源码里面插入转场的代码,并且编译源码,在mac环境上编译是没有问题的,但在使用Android的NDK工具(让Android调用底层的C++代码)编译FFmpeg的时候,出现了各种问题,网上普遍也没有解决方案,因为涉及到架构和汇编方面的知识,不了解,遂放弃,自己做转场效果
2.图片转视频后拼接其他视频出错
这是由于一开始图片生成的视频,没有音轨,这应该是新手经常会犯的错误,导致最后合成bgm时找不到输入音轨报错
ffmpeg中的filter过滤器很强大,但是在视频和音频处理上有比较大的差异,比如转场时,视频用的是setpts=PTS-STARTPTS+%f/TB实现延时播放,而音频要用adelay来实现延时效果..
视频拼接的时候要求各种格式都一样,比如分辨率,宽高比等,同时也要保证音频的编码,码率,比特率也要一样
5.添加文字水印必须要有字体文件路径,并且选用的第三方库没有开启这个功能...
一开始是导入了另一个库,后来发现字体文件太大而且两个库不好维护,最终用了图片水印替代
思路是先用Android原生生成一个TextView,然后利用缓存截图的原理,把Textview转成Bitmap然后再导出为图片,最后添加到视频中
6.输入源过多,会报OOM异常
分批处理,上面讲到了
这个我真的没有想到....因为用的第三方库的FFmpeg没有报任何有用的错误,于是把命令和源文件全部拉到mac环境去执行,最后发现是其中一个视频没有音轨导致合成失败
这个问题其实不是什么问题,但坑爹的是用的那个库他没有报任何信息....其实应该自己编译FFmpeg源码然后用NDK导入到android中的,但尝试几次都失败了,所以才用了别人封好的库,这就导致都在花时间找问题出在哪,甚至一度以为是性能问题然后越跑越偏,最后才想到用mac的FFmpeg来跑这个命令,把源文件拉到mac,执行起来,然后报的错是[5:a]audio 有问题,一眼就看出来是这个视频的音轨编码出错或缺失...坑啊,浪费了我大半天
经验是像这种多平台的工具或框架,在其中一个平台出了问题没有头绪,可以换个平台试试,说不定能收获点什么有用的信息
四.后续优化
1.ffmpeg专门做做图片和视频的处理,视频拼接和转场用OpenGL
2.原视频或图片的宽高比接近目标的宽高比的话不做高斯模糊处理,提高性能
**3.图片处理完导出为图片,用 **-frames:v 1 取一帧储存为图片
原文 Android音视频之FFmpeg踩坑之路 - 掘金
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓