短视频编辑:基于opengl和mediacodec的视频合成方案
短视频编辑系列,有关opengl处理文字贴纸、美颜滤镜、转场在我的前一篇文章《短视频编辑:可实时交互的播放器》
中已有介绍。
1.需求背景
近年短视频剪辑是技术热点,业内如七牛云/ 腾讯云/阿里云等短视频sdk的商用价值很大,所以其收费一般昂贵,主流商用sdk收费如下表:
商用sdk | 费用 |
---|---|
七牛云短视频sdk | 专业版12万一年 |
腾讯云短视频sdk | 企业版50万一年,动效贴纸等额外20万10个 |
阿里云短视频sdk | 专业版15万一年 |
VE视频编辑sdk(剪影团队) | 60万一年 |
为了节省成本并且根据需求灵活定制,我们打算自研短视频编辑sdk。
经过调研,众多视频编辑sdk封装ffmpeg对视频进行转码、裁剪、压缩。在音视频领域,一般大型成熟的商用sdk是跨平台的,各终端sdk公用一套由c++开发的底层引擎,针对各端硬件的不同做不同处理,例如分别对pc、android、ios提供硬编硬解的功能。我们最初也打算基于ffmpeg基础上进行二次开发,但其局限如下:
-
开发成本较大。ffmpeg媒体库较大且全部由C++编写。ios端不愿采用,其原生avFoundation框架已足够好用。
-
使用cpu进行转码,即采用软解码和软编码,相对于使用GPU的硬解码和硬编码效率低很多。如果想要支持Android硬解码和硬编码,需要二次开发使用jni调用android的mediacodec。
-
依赖包较大,会使apk多出10M以上。
由于开发成本的原因,我们在一期开发采用了android原生的mediacodec进行视频编辑转码。其已知的缺点如下:
-
api接近底层硬件,使用不够灵活,例如编码输出必须有surface、必须先预置音频视频轨道格式后才能开启合并。
-
由于Android的碎片化,机型众多版本各异,而具体编解码流程不可控,全交由各个厂商的底层硬件去实现,导致会花很多精力去视频机型兼容问题。
虽然mediacodec存在一定的弊端,但是能够支撑产品对多素材裁剪、合成视频的基本需求,也能对解码数据进行变速和重采样等处理。经过三个多月的需求洗礼和迭代,完成了大部分主流机型的兼容问题处理。实践证明,使用mediacodec编解码配和opengl 完全可以实现android 平台视频编辑功能,并且效果良好。
2.技术概述
在视频生产工具中,视频合成就是将视频、图片、音乐等原素材经过一系列的编辑,最终生成一个mp4视频文件。目前支持的编辑功能有:
-
图片和视频多素材混合拼接
-
素材裁剪
-
素材旋转
-
设置画幅比例
-
素材排序和删除某个素材
-
变速(0.5倍速-2倍速)
-
添加多个文字贴纸
-
添加美颜滤镜
-
添加素材之间转场
-
添加音乐
-
编辑草稿
在视频编辑阶段,上述编辑行为将产生一份编辑数据,其格式如下:
IMG20201122_163516.png而在视频合成阶段,将读取这份编辑数据,并针对各个功能,逐个解析参数,将各功能予以实现。
3.技术思路
视频合成涉及解码、编码、OpenGL、音频处理等相关能力,下图是视频合成的核心流程。
IMG20201029_193612.png针对mp4视频素材源文件,会使用mediaExtractor先将音视频做分离,分解为视频轨解码和音频轨解码,解码的视频帧进入opengl视频处理流程。针对图片类型的素材文件,会直接进入opengl处理流程。opengl处理包括文字贴纸、美颜滤镜、转场、设置视频比例等,opengl绘制的图像帧进入视频编码器,编码过程中通过视频帧时间戳处理裁剪和变速。
对于有声音的mp4视频素材,使用mediaExtractor分离出的音频(视频原音),需要和添加的音乐一起做混音处理。音频变速相对于视频变速较复杂,音频处理都需要将原始音频(音乐和视频原音)解码为pcm数据格式,基于pcm数据做声音模拟信号的重采样,实现混音算法和变速算法。混音采用基于音量权值的混音算法,变速采用soundTouch处理库进行声音速率的调整。
视频和音频分别经过处理之后,通过音视频合成器MediaMuxer,将音频数据和视频数据分别写入最终生成MP4文件中的音频轨道和视频轨道,这样就完成了整个视频合成流程,视频处理过程中,根据视频帧的时间戳计算进度。
4.技术实现
4.1图片与视频混合编码
为了支持图片与视频素材的混合合成,需要将图片与视频进行混合编码。其难点在于视频有解码过程,图片无解码过程,并且android mediacodec编解码接口不够灵活,视频帧解码直接输出到surface进行opengl绘制,并通过该surface进行编码,难以添加其他素材源,干预其编码过程。
我们在第一期方案中,图片合成与视频合成割裂为两个过程,是图片类型素材则走图片合成过程(含opengl处理和编码),是视频类型则走视频合成过程(含解码、opengl处理和编码)。图片和视频分别实现了一套opengl处理流程,对于文字贴纸、旋转、转场、美颜滤镜等分别对视频和图片进行处理。这种方案存在的问题有:
- 两套opengl方案维护困难,迭代和拓展极为不便。
- 图片素材和视频素材割裂处理,无法复用播放器预览时已有的opengl处理逻辑。
- 视频与图片、图片与视频素材之前的转场实现困难。
在第二期中,我们重构了视频合成器,使之既支持对视频的解码编码,也支持对图片的编码。素材是视频时才用mediaExtractor对视频进行采样,用mediaMetadataRetriever解析视频信息并处理。素材是图片时其编码循环由时间控制。
将图片嵌入到视频编码的核心实现方法是:在视频解码输出的surface中,实现bitmap图片绘制,图片纹理绘制到与视频帧一致的帧缓冲区。即在逐帧绘制方法onDrawFrame中增加bitmap绘制,替换视频帧纹理绘制。
if (imageBitmap != null) {
bitmapFilter.draw(imageBitmap);
} else {
previewFilter.draw(textureID);
}
这样就将图片嵌入到视频帧编码过程中,由于共用一个surface,可以共享一套文字贴纸、美颜滤镜、转场等opengl处理流程。
4.2opengl处理
opengl视频处理流程即使用opengl对原始视频帧进行二次处理。创建opengl渲染环境,通过SurfaceTexture的updateTexImage接口,可将视频流中最新的帧数据更新到对应的GL纹理,再操作GL纹理进行可进行美颜滤镜、添加文字贴纸等处理行为。
我们opengl处理流程总共使用了4个帧缓存fbo,其总流程如下图所示。绑定每个fbo后,首先取前个fbo的输出纹理,再执行美颜、滤镜等运算。例如绑定Beauty fbo后,使用Origin fbo绘制了原始视频帧后的输出纹理originTextureId作为美颜算法的输入,执行美颜算法,美颜算法输出纹理记为beautyTextureId,然后绑定Sticker fbo,绘制美颜输出纹理beautyTextureId,下一步绑定transition fbo,执行绘制文字贴纸算法,文字贴纸算法的输入纹理为Sticker fbo的输出纹理。其具体实现阐述如下:
IMG20201105_154920.png4.2.1设置视频比例和素材旋转
设置视频比例和素材旋转采用opengl的MVP矩阵操作实现对视频帧的缩放和旋转。将MVP矩阵传递到Vertex Shader中与顶点相乘进行矩阵变换。用于视频帧预览的Vertex Shader如下:
private static final String VERTEX_SHADER =
"uniform mat4 uMVPMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec2 aTextureCoord;\n" +
"void main() {\n" +
"gl_Position = uMVPMatrix * aPosition;\n"+
"vTextureCoord = aTextureCoord;\n"+
"}";
由首个素材尺寸和设置的视频比例确定最终生成视频的尺寸,即outResolution。inResolution为当前素材的尺寸。按最大边适应策略,缩放素材尺寸。支持90、180、270、360四个方向的旋转,MVP矩阵的计算方法如下:
float scale[] = FillMode.getScaleAspectFit(rotation.getRotation(), inputResolution.width(), inputResolution.height(), outputResolution.width(), outputResolution.height());
Matrix.scaleM(MVPMatrix, 0, scale[0] * 1, scale[1] * 1, 1);
if (rotation != Rotation.NORMAL) {
Matrix.rotateM(MVPMatrix, 0, -rotation.getRotation(), 0.f, 0.f, 1.f);
}
4.2.2美颜滤镜
4.2.3文字贴纸
4.2.2转场
4.3音频处理
音频的解码是将mp3音乐或视频原声(acc)转换为原始pcm音频流数据。因为Pcm是直接采样模拟信号的,是现在数字音频的标准格式,所以音频处理都需要首先将原始音频(音乐和视频原音)解码为pcm数据格式。在音乐解码过程中,按照解码流的时间戳,裁剪或重复拼接音乐,对齐输出视频的总时长。
对于多个不同的音频合成,必须将其采样率和通道数转换为一致后才能写入一个音频轨道,否则无法进行流畅播放。在多个有声的视频素材中,采用通道数和采样率最大的作为目标通道数和采样率(下图中对应视频1,对音乐和视频2解码后的数据进行转换通道数和重采样处理)。
音频处理总流程如下图所示:
绘图3.jpg4.3.1混音
音频混音就是将视频原音和音乐两段音频合在一起,使其能够同时播放。语音信号的叠加等价于空气中声波的叠加,如下图所示。但是把两个波形的数值直接简单相加会产生溢出。
解决这个问题业内常用的方法是:将两个音频数据采样率、通道、采样精度转化为一致后采用平均值算法。在平均值算法的基础上,加入音量权值因子进行优化,
4.3.2音频变速
音频变速相比于视频变速而言,要复杂得多。因为视频变速只需要按时间间隔跳帧即可,而如果对音频简单抽样采点会引起声音得不连续、噪声或爆破音。Soundtouch是目前音视频领域较常用的变速处理方案,是一个C++编写的开源库,为了在Android项目中使用,我们编写了jni接口,采用soundtouch库进行变速使用的api列出如下:
api | 说明 |
---|---|
setChannels(int) | 设置声道1 =单声道, 2 =立体声 |
setSampleRate(uint) | 设置采样率 |
setRate(double) | 指定播放速率,原始值为1.0 |
putSamples(const SAMPLETYPE *samples, uint nSamples) | 输入采样数据 |
receiveSamples(SAMPLETYPE *output, uint maxSamples) | 输出处理后的数据,需要循环执行 |
4.5典型兼容问题处理
在合成视频的实践过程中,遇到一些兼容问题。下面记录几个较典型的问题。
问题一:图片与有声音的视频混合合成,部分机型合成视频失败。
图片无声音,视频有声音。当第一个素材为图片时,使用mediaMuxer进行音频和视频合成,很多机型不支持一开始没有音频流写入,后来又有音频流写入的情况,报MP4写入错误。这种情况下的解决办法是:对于无音频和有音频素材混合的情况,在无音频的素材合成过程中,填充一段静默音频,并将静默音频截取为与素材最终播放时长相等。
问题二:某些oppo机型拍摄图片合成视频失败。
经测试,这些机型合成视频失败原因是:由于手机系统摄像头像素较高,拍摄出来的图片尺寸大,超过编码器支持的编码尺寸。这种情况下的解决方法是:由于在移动端通用的高清视频一般是1080p,所以使用的视频尺寸一般在1080*1920内,限制最终合成视频最大不超过这个尺寸。超过时使用opengl矩阵等比缩放。
关于编码器支持的视频尺寸,在实践过程中发现,编码尺寸除了不能过大以外,也不能过小,还有编码器编码只支持偶数尺寸的宽高。还有,有一些比较老的手机支持的硬编码尺寸只能是8的倍数。
问题三:某些机型录制的视频经合成后,画面偏移90度。
定位该问题时发现,和大部分华为等手机不同,oppo手机竖屏录制视频时获得的宽高是相反的,即发现文件系统读取的宽高和mediaExtractor解析视频轨道后得到的MediaFormat中宽高参数相反。对于这种情况,可以用mediaMetadataRetriever解析出视频素材的旋转角度。然后在opengl对旋转处理过程中加上这个素材的原始旋转角度。
5.总结和优化方向
视频生产工具上线以后,在稳定性和兼容性方面遇到过一些问题。稳定性方面,目前一些运行内存较小的设备,经过较复杂的视频编辑后(如添加多个较大的素材又添加了音乐),会因内存不足偶现闪退。兼容性方面,对于android5.0以上机型,所遇到纯粹属于硬件编解码的兼容问题较少,更多的兼容问题来自于素材的格式和尺寸大小。
基于此。目前的优化方向有两点:
-
优化编码过程中的内存耗用,并在预知内存不足时提示用户,或者处理前压缩素材大小。
-
收集各种合成失败的实例信息。根据所收集信息有针对性处理兼容问题,完善合成发方案。目前已添加线上埋点监控合成失败的机型设备信息和素材信息。