视频录制由视频预览、伴奏播放、视频录制/暂停、倍速/半速录制四部分组成。
(1)视频预览
在Android&IOS平台我们可以使用自己的Camera+OpenGL ES进行视频的预览,这里的预览过程一般就是从相机获取纹理ID并进行诸如美颜、贴纸等特效的处理,之后再绘制到相应的View上。
(2)伴奏播放
伴奏播放是指将一个伴奏文件以m4a或mp3形式下载到本地,随后对文件进行解码。最常见的解码方案是FFmpeg。由于经过解码获取的PCM或WAV文件可被直接读取,伴奏播放的同时我们可将解码后的伴奏PCM文件写入磁盘中以方便后续编辑阶段更高效率的使用。
(3)视频录制暂停
开始录制之后系统将预览的视频帧编码并写入文件中,这相当于为录制开辟一条旁路。当得到一张图像后系统会将其送入编码器,此时这里的纹理ID保持不变并可再让其绘制到我们的View上。如果用户点击暂停则停止编码,当用户继续则将视频帧进行再次编码。暂停时也可以当作一段视频的录制结束,后期再做拼接处理。
(4)倍速/半速录制
对短视频APP来说这一步骤十分重要,常用的方法是抽帧或者插帧操作。例如用户录制一段打篮球的视频并希望在扣篮或上篮时放慢动作,首先需要以正常速度录制而后再拉长视频以实现慢放效果。倍速与半速录制的关键是背景声处理,这里需要对背景声进行变速不变调处理。在FFmpeg的AVfilter模块中的Audiofilter里面有一个被称为Tempo的滤波器,而在SOX中也有tempo.c可实现类似功能,当然SoundTouch也可以实现。
视频录制器分为三部分:输入、处理和输出。
输入就是通过摄像头和麦克风这类采集设备去做音频和画面的采集。处理则是针对采集到的画面和声音进行处理,比如大家熟知的美颜、混响等等。最终输出其实会分为几部分,首先是预览,比如用手机录制视频的时候,在手机屏幕上会有预览画面;第二部分是编码,在安卓平台是采用硬件编码+软件编码的处理,而iOS平台的兼容性较好,所以只采用硬件编码就可以达到要求;最后是将音视频数据封装成一个容器——FLV或MP4,再进行IO输出, IO输出有可能是磁盘——录播场景,也有可能向流媒体服务器推流——直播场景。
音频架构设计:
对于安卓平台,要有一个MP3的Decoder,它可以通过MAD、Lame或者FFmpeg等开源库来实现,最终通过AudioTrack 的API或者OpenSL ES的API来播放,同时我们把播放PCM数据放到PCM队列中。而在采集过程,我们一般使用Audio Recoder或OpenSL ES来采集人声,采集到的人声也会放在一个PCM队列中。在一般架构设计中,队列一般承担生产者和消费者中间解耦的角色,因此可以看到Input和Output就是上面两个队列的生产者,而Consumer线程中的Encoder就是消费者——从队列中取出PCM数据进行编码。
对于iOS平台同样,我们使用的AUGraph,它底层使用的是AudioUnit,其中RemoteIO类型的AudioUnit可以采集人声,AudioFilePlayer类型的AudioUnit可以播放伴奏。然后通过Mixer类型的AudioUnit将人声和伴奏混合之后入队,后面Consumer线程中的Encoder从队列中取出PCM数据进行编码。
安卓平台通过Camera采集视频,在Output中首先是通过EGL Display来回显预览界面,其次编码则是采用MediaCodec硬件编码和Libx264软件编码相结合的实现方式(由于安卓平台硬件编码有可能出现兼容性问题)。而在iOS平台,直接使用Camera采集,然后通过GLImageView来进行渲染,GLImageView的实现方式是继承自UIView,然后在LayerClass中返回CAEAGLLayer,然后构造出OpenGL环境用来渲染纹理,最终再用VideoToolbox进行编码输出。编码后的数据会放到H.264队列中,那么这里的生产者就编码器,消费者实际上是Consumer模块,它把H.264队列中数据Mux后再进行IO操作(输出到磁盘成为mp4文件或者输出到流媒体服务器)。
在视频编辑模块中,我们需要一个集成时间轴的可正常预览视频的标准视频播放器从而让用户更精确地控制每一帧使用什么特效;视频特效方面可以添加贴纸或者实现灵魂出窍,井格,九宫格等特效;音频特效方面我们可以添加背景音乐、视频静音、添加声音特效等;也可进行快放慢放处理。
(1)特效时间Model
当我们在视频编辑阶段点击保存按纽时,特效时间Model就已建立了,视频编辑阶段会把视频特效作用的起始时间和终止时间记录到Model中,在视频保存阶段的对应时间段内进行特效应用。
(2)特效处理
无论音频特效还是视频特效都会按照特效时间Model进行对应的特效处理,处理后的音频PCM或视频帧会被送至编码器(这里的编码器一般为H.264或AAC),一般我们会选择硬件编码器从而实现高效编解码处理过程。
(3)编解码器
解码器解码而成的原始数据可被解析为纹理ID和PCM,随后这些数据会被交给Processor进行处理, 最终交给编码器并编码为H264与AAC数据。
(4)封装格式IO
最终我们会将H264与AAC封装成MP4并存储到本地文件中,整体就是视频保存模块。
视频处理系统:
跨平台的视频处理系统实际可以说是跨平台的复杂滤镜系统,它所应用的场景主要会有实现美颜、实现瘦脸这种单帧图片的处理,也有如雨天、老照片等主题效果,以及贴纸效果这几种。为了达到效果,我们通过OpenGL ES来实现,如果用软件(CPU中计算)做视频处理是非常消耗性能的,尤其在移动端是无法接受的。因此它的输入是纹理ID和时间戳,时间戳主要用于主题和贴纸场景的处理,而输出则是处理完毕的纹理ID。
这里特别介绍下GPUImage框架(以iOS平台作为讲解),它的整个流程分为Input、Processor和Output。首先通过GPUImageVideoCamera采集画面;然后转化为纹理ID就可以通过模糊、混合、边缘检测、饱和度等一系列处理进行优化;最终Output中使用GPUImageView把处理完的视频帧渲染到屏幕上,而对于录制则提供了GPUImageMovieWriter,它可以将纹理ID硬件编码到本地文件。除了视频录制过程,它对视频播放器和离线处理场景提供了GPUImageMovie作为Input的实现。
对于搭建跨平台的视频处理系统,我们需要搭建两个客户端的OpenGL环境,安卓平台使用EGL来提供上下文环境与窗口管理,iOS使用EAGL来提供上下文环境与窗口管理,然后我们抽象出统一接口服务于两个平台。
这是结构图,左边是根据平台搭建的环境——Platform OpenGL Environment,右边是视频处理系统—VideoEffectProcessor。整个过程为:首先通过Camera或者Decoder采集或者解码出视频帧纹理,将纹理ID交给VideoEffectProcessor完成视频处理功能,而这里面可能需要很多支持,比如集成一些第三方库解析XML、解析Json、libpng等等,同时我们也要暴露一些可以动态添加和删除Filter的功能。当处理完成后会输出一个Output TexId做渲染,最终呈现到界面上,或者给到Encoder做离线保存。
OpenGL视频处理演示:
镜像:
镜像,先生成一个16:9的屏幕比例的画布,将它分割为四部分,每部分画一个相同的视频帧,因为屏幕被分割为4部分,我们的物体坐标在渲染时就不能设定为全屏的。在OpenGL中物体坐标,左下角为(-1,-1),右上角为(1,1),这样我们就可以分别计算出4部分的物体坐标。
确认好物体坐标后,我们接下来就要确认画什么?也就是将视频帧以什么样的方式画在物体坐标上,这时就需要控制纹理坐标,我们可以看到OpenGL的纹理坐标定义:从左下角(0,0)到右上角(1,1),实际画的时候左上角是我们完整的纹理,右上角我们需要做镜像处理,左下角需要做横向翻转,右下角则是针对右上角视频帧做横向翻转,这样就可以实现简单的镜像效果。
这个特效就是人影有一个向外扩散的效果,同样它的节奏也是非常重要的。那么它的实现过程如下:首先我们每隔15帧拷贝一帧作为“灵魂”并且按照比例放大,这里特别需要提到的是SRT(Scale/Rotate/Translate),基于这三个的组合我们可以写一个TransformEffect,它可以利用通用的SRT矩阵变化纹理。
在得到放大后的“灵魂”(拷贝帧),我们就需要考虑把“灵魂”和“肉体”(原本视频帧)混合起来,这里需要用到GLES的一个内嵌Mix函数将两个纹理进行mix即可。那么同理,我们还可以实现眩晕、影随的效果:眩晕是将每一帧向两侧做位移再与本帧进行mix,而影随则是将之前的帧缓存下来,以一定的间隔和当前帧做mix。