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