要对移动端的抽帧,对于 iOS 来说,有 AVFoundation 这样一个神奇的库,开箱即用,已经支持了抽帧并且效率非常的高。而 Android 就不那么乐观了,Android 自带的 MediaMetadataRetriever 也能实现抽帧并将帧数据转化为 Bitmap,但效率非常低,平均抽取一帧需要 200ms-300ms,这当然满足不了我们的需求。无独有偶,Android 还提供了另一个类 MediaCodec-用于对音视频进行编解码的类,它通过访问底层的 codec 来实现编解码的功能,我们能对解码的数据进行定制化处理,本文也主要讲解利用 MediaCodec 进行抽帧。

一、MediaCodec

为什么选择 MediaCodec? 项目的前期做了比较多的调研,在 Android 平台上除了 MediaCodec 还可以实现抽帧的方案有:MediaMetadataRetrieverOpenCVFFmpeg,对于前两者实现效率非常的低,获取成本也比较大,对于 FFmpeg方案有进行了一定的尝试,ffmpeg是软解码抽帧(当然ffmpeg也可以 ffmpeg+mediaCodec 进行硬解码),在设置 AVCodecContext->thread_count=8 速度提升了很多个档次,但对于 CPU 的使用率非常的高,消耗资源比较严重,不利于手机的流畅度,这里不再赘述。

FFmpeg

如上图所示 ffmpeg 软解 CPU 的使用率,维持在80%左右。

MediaCodec 实现抽帧主要是参考 bigflake 网站提供的抽帧 Demo:
ExtractMpegFramesTest

主要方案流程如下图所示:

enter image description here

方案使用 MediaExtractor 获取 Codec-specific Data(对于H.264来说,”csd-0”和”csd-1”分别对应sps和pps;对于AAC来说,”csd-0”对应ADTS)发送给 MediaCodec 进行解码,将解码后的数据存放在 Surface,由于不需要将解码后的帧进行播放展示,我们进行离屏渲染(Pbuffer),通过 glReadPixels() 将 GPU 渲染完存在显存数据,回传内存。获取到对应帧 Buffer 数据之后,再利用Bitmap.copyPixelsFromBuffer 创建 Android 平台 Bitmap 对象。

但整个方案尝试下来之后发现:使用 glReadPixels 将显存数据回传,以及保存 Bitmap 是比较耗时以及消耗内存的操作。

那么我们可以将数据不进行回传也不保存为 Bitmap,而直接使用 GPU 上的数据进行识别么?

二、GPU Buffer 生成流程

在创建GPU Buffer之前我们需要简单介绍一下 SurfaceTexture,SurfaceTexture 是离屏渲染,内部包含了一个BufferQueue,可以把 Surface 生成的图像流,转换为纹理,供进一步加工使用。那么 SurfaceTexture 与前面的 MediaCodec 结合起来

我们的目的是为了将 GPU 上的图片 buffer 传递给算法侧进行识别,来自 SurfaceTexture 只支持外部 GLES 纹理GL_TEXTURE_EXTERNAL_OES,而算法一般都是基于 OpenGL使用 GL_TEXTURE_2D , 所以需要客户端这边做一个转换工作。

外部纹理 GL_TEXTURE_EXTERNAL_OES 的主要优势是它们能够直接从 BufferQueue 数据进行渲染。

整体流程如下图所示:

在拿到了 GPU Buffer 之后,就可以与算法愉快的进行相关的识别了,并且使用硬解抽帧之后对于 CPU 的使用率降到了15%左右。

MediaCodec

三、总结

使用 MediaCodec 抽帧最大的优点就是能够使用硬件进行解码,降低 CPU 的使用率,并且整个帧数据可以存在于 GPU 上,算法侧也能直接拿取数据进行进行识别,能比较好的提升 “抽帧-识别” 的效率。但由于硬解码在不同的硬件上表现的性能有一定的差异,以及在不同的视频与FFmpeg上也有存在不同的性能差异,各有优劣,所以在后续的方案上,针对于不同的视频可能会采取不同的方案进行抽帧。

参考:

1.ExtractMpegFramesTest.java

2.SurfaceTexture

3.Android Opengl OES 纹理渲染到 GL_TEXTURE_2D