Android MediaCodec硬解码、ffmpeg软解码,兼顾机型一致性和性能

2,663 阅读5分钟

MediaCodec硬解

首先考虑使用MediaCodec硬解码,硬解码的代码谷歌的文档很详细,主要分为异步模式、同步模式。至于解码的输出,如果是解码到文件中,可以提取outputBuffer后写入文件;如果是用于显示,推荐初始化MediaCodec的时候传入Surface:

decoder.configure(mediaFormat, surface, null, 0);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
decoder.releaseOutputBuffer(outputBufferIndex, true);

这样Surface会与codec绑定起来,解码后的buffer直接在底层用于显示到Surface上,无需业务层数组拷贝,效率最高,同时这种情况下outputBuffer中获取到的buffer也为null。decoder.releaseOutputBuffer是解码器真正解码渲染的时候。

不过有遇到某些h264流在某些手机上,硬解码丢帧的情况,调试发现很多帧在decoder.dequeueOutputBuffer(bufferInfo, 0);会返回-2,也就是format changed,导致这些帧解码失败。故思考mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);这边将COLOR_FormatSurface换成COLOR_QCOM_FormatYUV420SemiPlanar,再将解码出来的yuv,通过OpenGLES2.0或者EGL,显示在Surface上,是否能规避这个问题。经过一番尝试,上述出现问题的摄像头和手机的配合,不管KEY_COLOR_FORMAT换成哪个,依然format changed造成丢帧。为了一致性的体验,决定转向软解。

ffmpeg软解

使用ffmpeg软解不需要我们在业务代码里拆帧,只要将流一股脑的给ffmpeg就行,ffmpeg自己有av_read_frame函数可以拆帧,我们主要做好队列工作。网上大部分ffmpeg软解的代码都是从文件读流,直接将文件的path传给ffmpeg最简单,但是要使用摄像头这样的buffer输入的话,需要用到avio_alloc_contextav_probe_input_buffer

显示部分,可以将Surface传给ffmpeg,转为ANativeWindow,解码后的yuv通过sws_scale转为rgb,再逐行复制到ANativeWindow里就可以显示。

然而遇到了喜闻乐见的软解性能问题。摄像头码率提到10M后,解码成为瓶颈,每一帧的解码需要将近30ms,加上渲染时长,导致帧率低于摄像头的原始帧率30,出现画面延迟,帧率不足,很快解码buffer队列就满了,队列满了就要丢弃老数据,于是画面就会出现马赛克。这里使用三星S8加上帧率30,10M码率的摄像头做测试:解码后不渲染,帧率34-38;解码后同一个线程渲染,帧率22,CPU占用始终125%左右

可见光解码的话,还是可以保证帧率的,但是加上渲染就不行了。

ffmpeg软解,解码和渲染异步

于是尝试将解码线程和渲染线程独立出来,尽量榨干CPU。需要注意的是avcodec_send_packetavcodec_receive_frame必须同步调用,就是send后必须马上receive,等receive返回不为0后,才能继续send,否则send会失败。

这两个函数名设计的让人容易产生误解,以为ffmpeg自己维护了一个帧队列,然后可以在两个线程中分别send和receive,其实是错的,ffmpeg应该只维护了一个数组,数组为空取完后才可以再次send。

所以需要send和receive在同一个线程同步执行,receive后将AVFrame放到队列里;另一个线程从队列里取帧,进行sws_scale后,绘制rgb到NativeWindow上。解码+渲染,CPU占用上升到180%,性能提升到帧率29,但是一会儿CPU会发热降频,解码耗时增大,帧率掉到20,这性能还是没达到要求。

ffmpeg硬解

于是尝试使用ffmpeg硬解。虽然测试过某些摄像头在某些手机上调用MediaCodec硬解会出现format changed导致丢帧的现象,并且ffmpeg实际上也是使用MediaCodec实现的硬解,但是本着不试一试怎么知道的精神,决定尝试ffmpeg硬解。

configure需要做如下配置:

--enable-jni--enable-mediacodec
--enable-decoder=h264_mediacodec
--enable-hwaccel=h264_mediacodec
--target-os=android(这条如果没有,会报错jni not found)

由于我之前编译过ijkplayer,有一个中间步骤是编译ffmpeg,于是图方便使用ijk的工程来编译,发现加上上述configure后总是报jni not found,后来发现需要do-compile-ffmpeg.sh中将FF_CFG_FLAGS="$FF_CFG_FLAGS --target-os=linux"改为--target-os=android编译好新的ffmpeg后尝试,创建解码器的时候,需要使用AVCodec *pCodec = avcodec_find_decoder_by_name("h264_mediacodec");代替掉AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

硬解确实解码很快,每一帧的解码时间缩短为1ms左右,但是发现画面卡顿,CPU占用依然很高,一看log,发现瓶颈变为sws_scale,硬解后的yuv进行sws_scale计算,效率非常低,使用SWS_BILINEAR算法,一帧的scale需要68ms,很快渲染队列就满了。网上查到可以使用libyuv通过neon硬件加速替换掉sws_scale函数,也有使用OpenGL硬件加速渲染yuv到Surface的方案。不知道为什么软解出来的yuv,sws_scale效率很高,一帧只需要12ms。

ffmpeg多线程软解

尝试多线程软解,继续榨干CPU,解决解码性能瓶颈。配置多线程解码:pCodecCtx->thread_count = 8;有个坑是设置pCodecCtx->thread_type = FF_THREAD_SLICE;后,反而多线程无效,注释掉后多线程解码生效,性能飙升,帧率直接取决于喂数据的速度

加快喂数据,能吃掉更多CPU资源

加快喂数据后的帧率直接上去了

总结

在取舍了机型一致性、性能后,最终方案:ffmpeg多线程软解,通过sws_scale转换yuv到rgb显示在ANativeWindow上。

优化空间:sws_scale替换为libyuv,进一步降低能耗