性能比肩美拍秒拍的Android視頻錄制編輯特效解決方案
前言
眾所周知,Android平臺開發(fā)分為Java層和C++層,即Android SDK和Android NDK。常規(guī)產(chǎn)品功能只需要涉及到Java層即可,除非特殊需要是不需要引入NDK的。但如果是進行音視頻開發(fā)呢?
Android系統(tǒng)Java層API對音視頻的支持在MediaCodec之前,還停留在非常抽象API的級別(即只提供簡單的參數(shù)和方法,可以控制的行為少,得不到中間數(shù)據(jù),不能進行復(fù)雜功能的開發(fā),更談不上擴展)。而在MediaCodec在推出之后,也未能徹底解決問題,原因有這些:1、MediaCodec出現(xiàn)的Android版本并不低,使用則無法兼容低版本機器和系統(tǒng)版本;2、由于Android的開源和定制特性,各大廠商實現(xiàn)的MediaCodec也不盡相同,也導(dǎo)致同一段代碼A機器跑著是這個樣,B機器跑著就是另一個樣了。所以程序員童鞋們就把目光轉(zhuǎn)向了NDK,但是NDK里面谷歌并沒有提供什么關(guān)于音視頻處理的API(比如解析生成文件,編解碼幀),于是童鞋們又想著使用開源的C/C++框架,首當其沖的當然是最出名的ffmpeg、x264、mp3lame、faac這些了。問題又來了,ffmpeg最早對x86支持是最好的,arm平臺或者mips平臺支持就不這么好了(筆者調(diào)研ffmpeg2.0以后情況有所好轉(zhuǎn))。那就只能使用軟解軟編,速度跟不上是個什么體驗親們知道嗎?舉個栗子,假設(shè)要錄制640x480的視頻,音頻視頻全部使用軟編碼,x264如果純軟編碼加上手機CPU的處理性能50毫秒甚至100毫秒一幀都說不定,總之就是慢,還要算上音頻還要壓縮編碼。如果想錄制25幀率的視頻,一幀的編碼時間是不能超過40毫秒的,否則速度就跟不上了,算上其他業(yè)務(wù)功能花的時間,這個時間起碼要降到30毫秒以下,然后再使用多線程異步編碼的方式優(yōu)化一下應(yīng)該勉強能達到邊獲取畫面邊生成視頻文件。正是因為有這樣那樣的不方便,筆者才經(jīng)過幾個月的研究,找到了一個還不算太完美的解決方案供大家參考,本文將全面介紹各個環(huán)節(jié)的技術(shù)實現(xiàn)方案,最后并附上工程源碼。順便聲明一下,筆者在進行這項工作之前Android開發(fā)經(jīng)驗基本上算是1(不是0是因為以前寫過helloworld),但是C/C++,Java都已經(jīng)掌握,還在ios上使用objc開發(fā)過項目,所以我想Android也差異不大,語言不一樣,平臺不一樣,API不一樣,系統(tǒng)機制不一樣,其他應(yīng)該就一樣了。
NDK有哪些API可用?
先把NDK的include打開,普查一下到底NDK提供了哪些接口可以用。谷歌還算是有人性,其實除了linux系統(tǒng)級的API外,其實還是有一些音視頻相關(guān)的API的。
OpenSL,可以直接在C++層操作音頻采集和播放設(shè)備,進行錄音和播放聲音,從API9開始支持。
EGL,可以在C++層創(chuàng)建OpenGL的繪制環(huán)境,用于視頻圖像渲染,還可以用于一些圖像處理如裁剪、拉伸、旋轉(zhuǎn),甚至更高級的濾鏡特效處理也是可以的。另外不得不說在C++自己創(chuàng)建OpenGL的渲染環(huán)境比使用Java層的GLSurfaceView靈活性、可控性、擴展性方面直接提升好幾個數(shù)量級。而EGL在API9也已經(jīng)支持了。
OpenGL(ES), NDK在Java層提供了OpenGL接口,而在NDK層也提供了更原生的OpenGL頭文件,而要使用GLSL那就必須要有OpenGLES2.0+了,還好NDK也很早就支持了,OpenGLES2.0在API5就開始支持了,萬幸!!
OpenMAXAL,這是普查過程中發(fā)現(xiàn)的一個讓人不爽的庫,因為從它的接口定義來看它有例如以比較抽象接口方式提供的播放視頻的功能和打開攝像頭的功能。播放視頻就用不到了,后面自己編解碼自己渲染實現(xiàn),看到這個打開攝像頭的接口,心中當時是欣喜了一把的,結(jié)果是我的MX3居然告訴我該接口沒實現(xiàn)。那結(jié)果就必須從Java層傳攝像頭的數(shù)據(jù)到C++層了。不過OpenMAXIL,前者的兄弟,倒是個好東西,可惜谷歌暫時沒有開放接口。
這樣一來,圖像采集就必須從Java層打開Camera,然后獲取到數(shù)據(jù)之后通過JNI傳遞到C++層了。渲染圖像的View也要從java層創(chuàng)建SurfaceView然后傳遞句柄到C++層進而使用EGL來初始化OpenGL的渲染環(huán)境。聲音的采集和播放就和Java沒關(guān)系了,底層就可以直接處理完了。
選擇開源框架
ffmpeg: 文件解析,圖像拉伸,像素格式轉(zhuǎn)換,大多數(shù)解碼器,筆者選用的2.7.5版本,有針對ARM的不少優(yōu)化,解碼速度還算好。
x264: H264的編碼器,新的版本也對ARM有很多優(yōu)化,如果使用多線程編碼一幀640x480可以低至3-4毫秒。
mp3lame: MP3的編碼器,其實測試工程里面沒用到(測試工程使用的MP4(H264+AAC)的組合),只是習(xí)慣性強迫癥編譯了加進編碼器列表里
faac: AAC的編碼器,也是很久沒更新了,編碼速度上算是拖后腿的,所以后面才有個曲線救國的設(shè)計來解決音頻編碼的問題。
完整解決方案圖
音頻編碼慢的問題
x264和ffmpeg都下載比較新的版本,然后開啟asm,neon等優(yōu)化選項編譯之后,編解碼速度還能接受。可是FAAC的編碼速度著實還是有點慢。筆者于是乎想到個辦法,就是存儲臨時文件,在錄制的時候視頻數(shù)據(jù)直接調(diào)用x264編碼,不走ffmpeg中轉(zhuǎn)(這樣可以更靈活配置x264參數(shù),達到更快的目的),而音頻數(shù)據(jù)就直接寫入文件。這樣錄制的臨時文件其實和正兒八經(jīng)的視頻文件大小差距不大,不會造成磁卡寫入速度慢的瓶頸問題,同時還可解決編輯播放的時候拖動進度條的準確度問題,同時解決關(guān)鍵幀抽幀的問題,因為臨時文件都是自己寫的,文件里什么內(nèi)容都可以自己掌控。不得不說一個問題就是定義的抽象視頻文件讀取寫入接口Reader和Writer,而讀取寫入正式MP4文件的實現(xiàn)和讀取寫入臨時文件的實現(xiàn)都是實現(xiàn)這個Reader和Writer的,所以日后想改成直接錄制的時候就生成MP4只需要初始化的時候new另一個對象即可。還有一招來解決速度慢的問題就是多線程異步寫入,采集線程拿到數(shù)據(jù)之后丟給另一個線程來進行編碼寫入,只要編碼寫入的平均速度跟得上幀率就可以滿足需求。
引入OpenGL2D/3D引擎
當在C++層使用EGL創(chuàng)建了OpenGL的渲染環(huán)境之后,就可以使用任何C/C++編寫的基于OpenGL框架了。筆者這里引入了COCOS2D-X來給視頻加一些特效,比如序列幀,粒子效果等。COCOS2D-X本身有自己的渲染線程和OpenGL渲染環(huán)境,需要把這些代碼干掉之后,寫一部分代碼讓COCOS2D-X渲染到你自己創(chuàng)建的EGL環(huán)境上。另外COCOS2D-X的對象回收機制是模擬的Objective-C的引用計數(shù)和自動回收池方式,工程源碼中的COCOS2D-X回收機制筆者也進行了簡化修改,說實話個人覺得它的引用計數(shù)模擬的還可以,和COM差不多的原理,統(tǒng)一基類就可以實現(xiàn),但是自動回收池就不用完全照搬Objective-C了,沒必要搞回收池壓棧了,全局一個回收池就夠用了嘛。(純屬個人觀點)
主副線程模式
OpenGL的glMakeCurrent是線程敏感的,大家都知道。和OpenGL相關(guān)的所有操作都是線程敏感的,即文理加載,glsl腳本編譯鏈接,context創(chuàng)建,glDraw操作都要求在同一個線程內(nèi)。而Android平臺沒有類似iOS上自帶的MainOperationQueue的方式,所以筆者自己設(shè)計了一個主副線程模式(我自己取的名字),即主線程就是Android的UI線程,負責(zé)UI繪制的響應(yīng)按鈕Action。然后其他所有操作都交給副線程來做。也就說每一種用戶的操作的響應(yīng)函數(shù)都不直接干事,而是學(xué)習(xí)MFC的方式,post一個消息和數(shù)據(jù)到副線程。那么副線程就必然要用單線程調(diào)度消息循環(huán)和多任務(wù)的方式了,消息循環(huán)不說了,MFC的模式。單線程調(diào)度多任務(wù)可能好多童鞋沒接觸過,其實就是將傳統(tǒng)的單線程處理的任務(wù),分成很多個時間片,讓線程每次只處理一個時間片,然后緩存處理狀態(tài),到下一次輪到它的時候再繼續(xù)處理。
比如任務(wù)接口是 IMission {bool onMissionStart(); bool onMissionStep(); void onMissionStop();} 調(diào)度線程先執(zhí)行一次onMissionStart如果返回false則執(zhí)行onMissionStop結(jié)束任務(wù);如果前者返回true,則不斷的調(diào)用onMissionStep,直到返回false,再執(zhí)行onMissionStop,任務(wù)結(jié)束。具體的處理都要封裝成任務(wù)接口的實現(xiàn)類,然后丟進任務(wù)列表。
試想,這樣的設(shè)計架構(gòu)下,是不是所有的操作都在同一個線程里了,OpenGL的調(diào)用也都在同一個線程里了,還有附帶的效果就是媽媽再也不用擔(dān)心多線程并發(fā)處理到處加鎖導(dǎo)致的性能問題和bug問題了,不要懷疑它的性能,因為就算多線程到CPU那一級也變成了單線程了。redis不就是單線程的么,速度快的杠杠的。
總結(jié)一下
使用OpenSL錄音和播音
使用EGL在C++層創(chuàng)建OpenGL環(huán)境
改造COCOS2D-X,使用自己創(chuàng)建的OpenGL環(huán)境
直接使用x264而非ffmpeg中轉(zhuǎn),按最快的編碼方式配置參數(shù),一定記得開啟x264的多線程編碼。
x264和ffmpeg都要下載比較新的,并且編譯的時候使用asm,neon等選項。(筆者是在ubuntu上跨平臺編譯的)
如果錄制的時候直接編碼視頻和音頻速度跟不上就寫入臨時文件,圖像編碼,聲音直接存PCM。
除了Android主線程外,另外只開一個副線程用于調(diào)度,具體小模塊耗時的任務(wù)就單獨開線程,框架主體上只存在兩個線程,一主一副。
完整工程源碼
使用的API15開發(fā),其實是可以低到API9的。
源碼地址:http://download.csdn.net/detail/yangyk125/9416064
操作演示:http:/programs/view/PvY9MMugbRw/
渲染完生成視頻的位置:/SD卡/e4fun/video/*.mp4
需要說明一下的是:
1、com.android.video.camera.EFCameraView類 最前面兩個private字段定義當前選用的攝像頭分辨率寬度和高度,要求當前攝像頭支持這個分辨率。
2、jni/WORKER/EFRecordWorker.cpp的createRecordWorker函數(shù)內(nèi),定義當前錄制視頻的各種基本參數(shù),請根據(jù)測試機器的性能自由配置。
3、jni/WORKER/EFRecordWorker.cpp的on_create_worker函數(shù)內(nèi),有個設(shè)置setAnimationInterval調(diào)用,設(shè)置OpenGL繪制幀率,和視頻幀率是兩回事,請酌情設(shè)置。
感謝一位讀了這篇博客的網(wǎng)友,給我指出了其中可以優(yōu)化的地方
1、如果使用ffmpeg開源方案處理音視頻,那么AAC應(yīng)該使用fdk_aac而不應(yīng)該使用很久沒更新的faac。
2、glReadPixels回讀數(shù)據(jù)效率低下,筆者正在嘗試升級到gles3.0看看能不能有什么辦法快速獲取渲染結(jié)果圖像,如果您知道,請在后面留言,謝謝啦!
在Android上做音視頻處理,如果還想要更快的編解碼,如果是Java層則逃不開MediaCodec,如果是C++層,可以向下研究,比如OpenMAXIL等等。
后記:
經(jīng)過半年努力,解決了其中部分有效率問題的地方
(1)編解碼部分
編解碼部分之前文章采用的X264+FFMPEG的開源方案,而繼續(xù)學(xué)習(xí)之后,找到了android上特有的實現(xiàn)方案。
版本<4.4:x264+ffmpeg or 私有API(libstagefright.so)。
版本=4.4:jni反調(diào)android.media.MediaCodec or 或者在java層開發(fā)。
版本>4.4:NdkMediaCodec(android.media.MediaCodec 的 jni接口)。
(2)AAC更優(yōu)開源方案
AAC開源方案FDKAAC一直在更新,效率有提升,而faac早就不更新了。so…你懂的。
AAC也可以使用MediaCodec或者NdkMediaCodec
(3)OpenGL之framebuffer數(shù)據(jù)的回讀
GLES版本<3.0:使用glReadPixels 或者 EGLImageKHR(eglCreateImageKHR,glEGLImageTargetTexture2DOES)
GLES版本=3.0:Pixel Pack Buffer + glMapBufferRange。
Android版本>=4.2:還有一個android平臺化的回讀FrameBuffer的方案,那就是新建SurfaceTexture和Surface,然后新創(chuàng)建一個OpenGL Context,一比一再渲染一次,即可將FrameBuffer渲染到這個SurfaceTexture上面,surface還可以作為編碼器的輸入。這樣不僅可以快速從渲染結(jié)果傳遞數(shù)據(jù)到編碼器,還能實現(xiàn)跨線程傳遞紋理數(shù)據(jù),屬于android平臺本身提供的功能,非opengl自帶能力。之所以是4.2,是因為SurfaceTexture在4.2以后才基本完善,之前各種不穩(wěn)定。
http://www.xldzh.cn///yangyk125/article/details/50571304
文章轉(zhuǎn)載自微信公眾號碼農(nóng)突圍
聲明:本站所有文章資源內(nèi)容,如無特殊說明或標注,均為采集網(wǎng)絡(luò)資源。如若本站內(nèi)容侵犯了原著者的合法權(quán)益,可聯(lián)系本站刪除。