Android 视频抽帧
要对移动端的抽帧,对于 iOS 来说,有 AVFoundation 这样一个神奇的库,开箱即用,已经支持了抽帧并且效率非常的高。而 Android 就不那么乐观了,Android 自带的 MediaMetadataRetriever
也能实现抽帧并将帧数据转化为 Bitmap,但效率非常低,平均抽取一帧需要 200ms-300ms,这当然满足不了我们的需求。无独有偶,Android 还提供了另一个类 MediaCodec
-用于对音视频进行编解码的类,它通过访问底层的 codec 来实现编解码的功能,我们能对解码的数据进行定制化处理,本文也主要讲解利用 MediaCodec 进行抽帧。
一、MediaCodec
为什么选择 MediaCodec? 项目的前期做了比较多的调研,在 Android 平台上除了 MediaCodec
还可以实现抽帧的方案有:MediaMetadataRetriever
、OpenCV
、FFmpeg
,对于前两者实现效率非常的低,获取成本也比较大,对于 FFmpeg
方案有进行了一定的尝试,ffmpeg是软解码抽帧(当然ffmpeg也可以 ffmpeg+mediaCodec 进行硬解码),在设置 AVCodecContext->thread_count=8
速度提升了很多个档次,但对于 CPU 的使用率非常的高,消耗资源比较严重,不利于手机的流畅度,这里不再赘述。
如上图所示 ffmpeg 软解 CPU 的使用率,维持在80%左右。
MediaCodec 实现抽帧主要是参考 bigflake 网站提供的抽帧 Demo:
ExtractMpegFramesTest
主要方案流程如下图所示:
方案使用 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 抽帧最大的优点就是能够使用硬件进行解码,降低 CPU 的使用率,并且整个帧数据可以存在于 GPU 上,算法侧也能直接拿取数据进行进行识别,能比较好的提升 “抽帧-识别” 的效率。但由于硬解码在不同的硬件上表现的性能有一定的差异,以及在不同的视频与FFmpeg上也有存在不同的性能差异,各有优劣,所以在后续的方案上,针对于不同的视频可能会采取不同的方案进行抽帧。
参考: