视频中磨皮、美颜功能已成为刚需,那么如何在 Android 短视频中实现 720P 磨皮美颜录制?本篇文章中,网易云信资深开发工程师将向大家介绍具体的操作方法。相关阅读推荐《短视频技术详解:Android 端的短视频开发技术》
《如何快速实现移动端短视频功能?》
在 Android 上要实现一个录制功能,需要有几个方面的知识储备:自定义相机的开发、视频数据格式的了解、编码相关知识以及视频合成技术,同时如果需要美颜、磨皮等滤镜操作还需要一定的 openGL 的知识。如果有需要深入音视频方面开发的同学建议先了解下上述的基本知识点。
既然要实现 720P、30 帧,同时又要对视频数据进行滤镜处理的录制,那我们首先就要确定一个正确的实现方案。如果方案选错了,那即使实现了录制功能,但性能达不到 30 帧或是 CPU 消耗太大手机发烫那就不好了。
视频的编码录制主要是软编和硬编两种方案:软编即采用 CPU 对相机采集的原始数据进行编码后再和音频一起合并成一个 MP4 等格式的文件。
优点是技术相对成熟,网上开源的编码以及合成库很多,实现相对较快,同时兼容性比较好。
缺点是 CPU 暂用率高,性能差的手机无法达到 720P 的 30 帧,同时引用了大量的第三方库,导致包很大。
软编的具体实现方案如下图所示,流程相对清晰简单:
硬编即采用手机提供的硬编接口,利用硬件芯片直接进行编码合成。
优点是速度快、效率高、CPU 占用极少,即使长时间高清录制也不会发烫,同时由于使用系统 API,库相对较小。
缺点是某些奇葩机型需要处理兼容性问题,同时 Android 上的硬编跟 Surface 以及 openGL 关系比较密切,网上相关知识较少,需要自己摸索踩坑。
硬编的主要流程如下图所示,可以看到所有的数据,从采集、编码、显示以及合成都在 GPU 里面进行流转。
结合上面分析的两种方案我们可以看到,在 Android 这类移动平台上,使用第二种硬编的方式是比较合适的。由于短视频的本地录制不像直播等场景对带宽的要求比较大,需要动态调节编码器码率帧率的情况,本地录制可以将编码器的码率设置的比较高,也不需要动态改变分辨率。因此采用硬件编码的方式既可以省 CPU 的性能又可以实现 720P 的高效编码。
确定了方案之后,我们就着重讲一下硬编方案的各个步骤的实现方式。
自定义相机的开发我们知道根据 Android 的系统 Camera API,可以通过 setPreviewDisplay 接口给 Camera 设置一个 SurfaceView 的 SurfaceHolder 就可以让 Camera 采集的数据显示到 SurfaceView 上了。这个接口的好处是系统帮我们处理了相机采集的各种角度同时进行了绘制,如果只是简单的录制可以这么使用,但我们需要对相机采集的数据进行滤镜处理,那这个接口就不合适了。
因此我们需要用到另外一个接口 setPreviewTexture:
通过给 Camera 设置一个 SurfaceTexture,可以将 Camera 采集的数据先映射到这个 SurfaceTexture 上,然后我们根据创建这个 SurfaceTexture 的 TextureID 来获取 GPU 上的 Camera 数据
滤镜以及本地绘制我们通过 SurfaceTexture 绑定的 TextureID 可以获取到 Camera 采集到 GPU 上的视频数据。然后可以将 TextureID 送给一些第三方滤镜库进行美颜滤镜或是自己编写 Shader 进行磨皮和美白。自己编写 Shader 需要 opengl 以及图像算法方面的知识,通常需要专门的开发人员,这里就不做详细的展开了(当然最简单的就是接入网易云短视频 SDK 了,里面实现了磨皮、美颜和多款滤镜)。
本地绘制主要靠 openGL 进行绘制,我们需要先在 Camera 的采集回调线程上创建一个 EGLContext 以及 EGLDisplay 和 EGLSurface,其中 EGLContext 是 openGL 在该线程上的上下文,EGLDisplay 是一块 GPU 中的虚拟显示区,主要用于缓存 GPU 上的视频数据,EGLSurface 为具体显示的 View 到 openGL 上的映射,是真正绘制到 View 上的工具。当接受到 Camera 采集回调的一帧数据后,我们先通过 SurfaceTexture.updateTexImage() 方法,将 Camera 采集的数据映射到 SurfaceTexture。然后根据 glsl 语言将 TextureID 对应的数据绘制到 EGLDisplay 上,这里需要注意的是,Camera 采集是有角度的,横竖屏下角度不同,可以通过 SurfaceTexture 的 getTransformMatrix 方法获取角度矩阵,然后把矩阵传给 EGLDisplay 进行旋转。EGLDisplay 旋转绘制完成后通过 eglSwapBuffers 方法就可以将 EGLDisplay 上的数据拷贝到 EGLSurface 上进行显示了。Android 系统中的 GLSurfaceView 最后就是通过 eglSwapBuffers 将数据显示到我们看到的屏幕上的。
硬件编码 Android 上的硬件编码主要靠 MediaCodeC API 实现的,下面是 MediaCodeC 比较经典的一张数据处理图。
从图中我们看到,MediaCodeC 主要处理流程就是:
创建并配置一个 MediaCodec 对象循环直到完成:
如果输入缓冲区就绪,读取一个输入块,并复制到输入缓冲区中
如果输出缓冲区就绪,复制输出缓冲区的数据
释放 MediaCodec 对象
从 Android 的官方文档我们看到,MediaCodeC 支持 ByteBuffers 和 Surface 两种输入方式,文档也指明了 Surface 方式可以提高编码效率,而且我们上面的 Camera 数据也是采集到的 SurfaceTexture,因此我们这里使用 Surface 方式作为输入源。
我们在上面显示部分提到 EGLSurface 是作为真正输出显示的模块,MediaCodec 也是。我们先通过 MediaCodec 创建一个 Surface,然后将这个 Surface 绑定到一个 EGLSurface,当 Camera 采集的数据回调时,我们只要重复一次绘制模块的操作,将 Camera 采集到 SurfaceTexture 上的数据 swapBuffers 到 EGLSurface 上就可以了。然后循环 MediaCodec 输出缓冲区,MediaCodec 就会将编码后的数据返回给我们了。这样做的好处就是将显示和编码完全分离了,即使我们没有 UI View 的情况下也可以进行编码,比如在不同 Activity 之间切换也不会影响我们的正常编码。
视频合成 Android 上视频合成主要通过 MediaMuxer API 实现。MediaMuxer 类相对比较简单,特别是配合 MediaCodec 使用。我们只需要通过 addTrack 来添加视频和音频通道接口。AddTrack 接口需要传入一个 MediaFormat 对象,MediaFormat 即媒体格式类,用于描述媒体的格式参数,如视频帧率、音频采样率等。还好我们使用了 MediaCodeC,MediaCodeC 会返回 MediaFormat 给我们,如果是使用软编然后用 MediaMuxer 进行合并的话,这里有一个比较大的坑,如果手动创建 MediaFormat 对象的话,一定要记得设置 ”csd-0″ 和 ”csd-1″ 这两个参数:其中 ”csd-0″ 和 ”csd-1″ 对应的是视频的 sps 和 pps,对于 AAC 音频的话,对应的是 ADTS。不设置的话会崩溃的。设置完这些之后,只要编码器来一帧数据,我们送到 MediaMuxer 中可以出来 MP4 了。
最后大体的流程图就是:
以上就是在 Android 短视频中实现 720P 磨皮美颜录制的分享。
另外,想要获取更多产品干货、技术干货,记得关注网易云信博客。