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_context
、av_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_packet
和avcodec_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,后来发现需要ijkplayer/android/contrib/tools/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,是YUV420P
,sws_scale
效率高,一帧只需要12ms,硬解出来的yuv是NV12
,sws_scale
效率很低。
ffmpeg多线程软解
尝试多线程软解,继续榨干CPU,解决解码性能瓶颈。配置多线程解码:pCodecCtx->thread_count = 8;
有个坑是设置pCodecCtx->thread_type = FF_THREAD_SLICE;
后,反而多线程无效,注释掉后多线程解码生效,性能飙升,帧率直接取决于喂数据的速度
加快喂数据,能吃掉更多CPU资源
加快喂数据后的帧率直接上去了
总结
在取舍了机型一致性、性能后,最终方案:ffmpeg多线程软解,通过sws_scale
转换yuv到rgb显示在ANativeWindow
上。
优化空间:sws_scale
替换为libyuv,或者OpenGL,进一步降低能耗