MediaCodec硬解
首先考慮使用MediaCodec硬解碼,硬解碼的代碼谷歌的文檔很詳細(xì)旁蔼,主要分為異步模式锨苏、同步模式疙教。至于解碼的輸出,如果是解碼到文件中伞租,可以提取outputBuffer后寫入文件松逊;如果是用于顯示,推薦初始化MediaCodec的時(shí)候傳入Surface:
decoder.configure(mediaFormat, surface, null, 0);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
decoder.releaseOutputBuffer(outputBufferIndex, true);
這樣Surface會(huì)與codec綁定起來肯夏,解碼后的buffer直接在底層用于顯示到Surface上经宏,無需業(yè)務(wù)層數(shù)組拷貝,效率最高驯击,同時(shí)這種情況下outputBuffer中獲取到的buffer也為null烁兰。decoder.releaseOutputBuffer是解碼器真正解碼渲染的時(shí)候。
不過有遇到某些h264流在某些手機(jī)上徊都,硬解碼丟幀的情況沪斟,調(diào)試發(fā)現(xiàn)很多幀在decoder.dequeueOutputBuffer(bufferInfo, 0);會(huì)返回-2,也就是format changed暇矫,導(dǎo)致這些幀解碼失敗主之。故思考mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);這邊將COLOR_FormatSurface換成COLOR_QCOM_FormatYUV420SemiPlanar,再將解碼出來的yuv李根,通過OpenGLES2.0或者EGL槽奕,顯示在Surface上,是否能規(guī)避這個(gè)問題房轿。經(jīng)過一番嘗試粤攒,上述出現(xiàn)問題的攝像頭和手機(jī)的配合,不管KEY_COLOR_FORMAT換成哪個(gè)囱持,依然format changed造成丟幀夯接。為了一致性的體驗(yàn),決定轉(zhuǎn)向軟解纷妆。
ffmpeg軟解
使用ffmpeg軟解不需要我們在業(yè)務(wù)代碼里拆幀盔几,只要將流一股腦的給ffmpeg就行,ffmpeg自己有av_read_frame函數(shù)可以拆幀掩幢,我們主要做好隊(duì)列工作逊拍。網(wǎng)上大部分ffmpeg軟解的代碼都是從文件讀流,直接將文件的path傳給ffmpeg最簡單粒蜈,但是要使用攝像頭這樣的buffer輸入的話顺献,需要用到avio_alloc_context、av_probe_input_buffer枯怖。
顯示部分注整,可以將Surface傳給ffmpeg,轉(zhuǎn)為ANativeWindow,解碼后的yuv通過sws_scale轉(zhuǎn)為rgb肿轨,再逐行復(fù)制到ANativeWindow里就可以顯示寿冕。
然而遇到了喜聞樂見的軟解性能問題。攝像頭碼率提到10M后椒袍,解碼成為瓶頸驼唱,每一幀的解碼需要將近30ms,加上渲染時(shí)長驹暑,導(dǎo)致幀率低于攝像頭的原始幀率30玫恳,出現(xiàn)畫面延遲,幀率不足优俘,很快解碼buffer隊(duì)列就滿了京办,隊(duì)列滿了就要丟棄老數(shù)據(jù),于是畫面就會(huì)出現(xiàn)馬賽克帆焕。這里使用三星S8加上幀率30惭婿,10M碼率的攝像頭做測試:解碼后不渲染,幀率34-38叶雹;解碼后同一個(gè)線程渲染财饥,幀率22,CPU占用始終125%左右
可見光解碼的話折晦,還是可以保證幀率的钥星,但是加上渲染就不行了。
ffmpeg軟解筋遭,解碼和渲染異步
于是嘗試將解碼線程和渲染線程獨(dú)立出來打颤,盡量榨干CPU。需要注意的是avcodec_send_packet和avcodec_receive_frame必須同步調(diào)用漓滔,就是send后必須馬上receive,等receive返回不為0后乖篷,才能繼續(xù)send响驴,否則send會(huì)失敗。
這兩個(gè)函數(shù)名設(shè)計(jì)的讓人容易產(chǎn)生誤解撕蔼,以為ffmpeg自己維護(hù)了一個(gè)幀隊(duì)列豁鲤,然后可以在兩個(gè)線程中分別send和receive,其實(shí)是錯(cuò)的鲸沮,ffmpeg應(yīng)該只維護(hù)了一個(gè)數(shù)組琳骡,數(shù)組為空取完后才可以再次send。
所以需要send和receive在同一個(gè)線程同步執(zhí)行讼溺,receive后將AVFrame放到隊(duì)列里楣号;另一個(gè)線程從隊(duì)列里取幀,進(jìn)行sws_scale后,繪制rgb到NativeWindow上炫狱。解碼+渲染藻懒,CPU占用上升到180%,性能提升到幀率29视译,但是一會(huì)兒CPU會(huì)發(fā)熱降頻嬉荆,解碼耗時(shí)增大,幀率掉到20酷含,這性能還是沒達(dá)到要求鄙早。
ffmpeg硬解
于是嘗試使用ffmpeg硬解。雖然測試過某些攝像頭在某些手機(jī)上調(diào)用MediaCodec硬解會(huì)出現(xiàn)format changed導(dǎo)致丟幀的現(xiàn)象椅亚,并且ffmpeg實(shí)際上也是使用MediaCodec實(shí)現(xiàn)的硬解蝶锋,但是本著不試一試怎么知道的精神,決定嘗試ffmpeg硬解什往。
configure需要做如下配置:
--enable-jni
--enable-mediacodec
--enable-decoder=h264_mediacodec
--enable-hwaccel=h264_mediacodec
--target-os=android(這條如果沒有扳缕,會(huì)報(bào)錯(cuò)jni not found)
由于我之前編譯過ijkplayer,有一個(gè)中間步驟是編譯ffmpeg别威,于是圖方便使用ijk的工程來編譯躯舔,發(fā)現(xiàn)加上上述configure后總是報(bào)jni not found,后來發(fā)現(xiàn)需要ijkplayer/android/contrib/tools/do-compile-ffmpeg.sh中將FF_CFG_FLAGS="$FF_CFG_FLAGS --target-os=linux"改為--target-os=android編譯好新的ffmpeg后嘗試省古,創(chuàng)建解碼器的時(shí)候粥庄,需要使用AVCodec *pCodec = avcodec_find_decoder_by_name("h264_mediacodec");代替掉AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
硬解確實(shí)解碼很快,每一幀的解碼時(shí)間縮短為1ms左右豺妓,但是發(fā)現(xiàn)畫面卡頓惜互,CPU占用依然很高,一看log琳拭,發(fā)現(xiàn)瓶頸變?yōu)閟ws_scale训堆,硬解后的yuv進(jìn)行sws_scale計(jì)算,效率非常低白嘁,使用SWS_BILINEAR算法坑鱼,一幀的scale需要68ms,很快渲染隊(duì)列就滿了絮缅。網(wǎng)上查到可以使用libyuv通過neon硬件加速替換掉sws_scale函數(shù)鲁沥,也有使用OpenGL硬件加速渲染yuv到Surface的方案。原因是軟解出來的yuv耕魄,是YUV420P画恰,sws_scale效率高,一幀只需要12ms吸奴,硬解出來的yuv是NV12允扇,sws_scale效率很低缠局。
ffmpeg多線程軟解
嘗試多線程軟解,繼續(xù)榨干CPU蔼两,解決解碼性能瓶頸甩鳄。配置多線程解碼:pCodecCtx->thread_count = 8;有個(gè)坑是設(shè)置pCodecCtx->thread_type = FF_THREAD_SLICE;后,反而多線程無效额划,注釋掉后多線程解碼生效妙啃,性能飆升,幀率直接取決于喂數(shù)據(jù)的速度
加快喂數(shù)據(jù)俊戳,能吃掉更多CPU資源
加快喂數(shù)據(jù)后的幀率直接上去了
總結(jié)
在取舍了機(jī)型一致性揖赴、性能后,最終方案:ffmpeg多線程軟解抑胎,通過sws_scale轉(zhuǎn)換yuv到rgb顯示在ANativeWindow上燥滑。
優(yōu)化空間:sws_scale替換為libyuv,或者OpenGL阿逃,進(jìn)一步降低能耗