性能比肩美拍秒拍的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開始支持。

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

EGL,可以在C++層創建OpenGL的繪製環境,用於視頻圖像渲染,還可以用於一些圖像處理如裁剪、拉伸、旋轉,甚至更高級的濾鏡特效處理也是可以的。另外不得不說在C++自己創建OpenGL的渲染環境比使用Java層的GLSurfaceView靈活性、可控性、擴展性方面直接提升好幾個數量級。而EGL在API9也已經支持了。

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

OpenGL(ES), NDK在Java層提供了OpenGL接口,而在NDK層也提供了更原生的OpenGL頭文件,而要使用GLSL那就必須要有OpenGLES2.0+了,還好NDK也很早就支持了,OpenGLES2.0在API5就開始支持了,萬幸!!

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

OpenMAXAL,這是普查過程中發現的一個讓人不爽的庫,因為從它的接口定義來看它有例如以比較抽象接口方式提供的播放視頻的功能和打開攝像頭的功能。播放視頻就用不到了,後面自己編解碼自己渲染實現,看到這個打開攝像頭的接口,心中當時是欣喜了一把的,結果是我的MX3居然告訴我該接口沒實現。那結果就必須從Java層傳攝像頭的數據到C++層了。不過OpenMAXIL,前者的兄弟,倒是個好東西,可惜谷歌暫時沒有開放接口。

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

這樣一來,圖像採集就必須從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的編碼器,也是很久沒更新了,編碼速度上算是拖後腿的,所以後面才有個曲線救國的設計來解決音頻編碼的問題。

完整解決方案圖

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

音頻編碼慢的問題

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/

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

渲染完生成視頻的位置:/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

性能比肩美拍秒拍的Android視頻錄製編輯特效解決方案

歡迎關注我的微信公眾號「碼農突圍」,分享Python、Java、大數據、機器學習、人工智能等技術,關注碼農技術提升•職場突圍•思維躍遷,20萬+碼農成長充電第一站,陪有夢想的你一起成長。


分享到:


相關文章: