WebRTC-Android 原始碼導讀(四):VideoCRE 與記憶體抖動優化
前面三篇中,我們依次分析了 WebRTC Android 的視訊採集、視訊渲染和視訊硬編碼,Live Streaming 視訊的前段就已經全了。WebRTC 是個寶,初窺這部分程式碼時就被它的 Capturer 類的設計驚豔到了,仔細品鑑後越發佩服起來,裡面簡直填了太多坑了,如此寶貝,如不能為我所用,豈非一大憾事!而前三篇的解讀,正是為了今天能將其剝離出來所做的鋪墊,現在就有請我們今天的主角——VideoCRE, Video Capture, Render and Encode——閃亮登場。
VideoCRE 結構
我們當然可以直接使用 Capturer/Renderer/Encoder,但如果能將它們進行一定的封裝,讓基本的需求實現起來更加簡單,豈不妙哉。
下面介紹一下 VideoCRE 的結構:
- 視訊資料由
VideoCapturer
採集,例如Camera1Capturer
; VideoCapturer
、SurfaceTextureHelper
等由VideoSource
類管理;VideoCapturer
採集到的資料會回撥給VideoCapturer.CapturerObserver
,VideoSink
實現了該介面;VideoSink
會把資料傳送給多個VideoRenderer.Callbacks
,例如SurfaceViewRenderer
負責預覽,HwAvcEncoder
負責視訊編碼;HwAvcEncoder
則會把編碼後的資料傳送給多個MediaCodecCallback
Streamer
進行網路傳輸實現直播功能,Mp4Recorder
負責本地錄製;
同一路視訊資料可以被多路消費,例如預覽、低位元速率編碼、高位元速率編碼,而同一路編碼資料,也可以被多路消費,例如推流、存檔案。
VideoCRE 使用
demo 工程裡實現了高低位元速率兩路本地 MP4 錄製功能,下面我們看看如何一步步實現這個功能。
首先是配置引數,標清和高清:
VideoConfig config = VideoConfig.builder()
.previewWidth(1280)
.previewHeight(720)
.outputWidth (448)
.outputHeight(800)
.fps(30)
.outputBitrate(800)
.build();
VideoConfig hdConfig = VideoConfig.builder()
.previewWidth(1280)
.previewHeight(720)
.outputWidth(720)
.outputHeight(1280)
.fps(30)
.outputBitrate(2000)
.build();
接下來是建立 VideoCapturer
:
VideoCapturer capturer = createVideoCapturer();
private VideoCapturer createVideoCapturer() {
switch (MainActivity.sVideoSource) {
case VideoSource.SOURCE_CAMERA1:
return VideoCapturers.createCamera1Capturer(true);
case VideoSource.SOURCE_CAMERA2:
return VideoCapturers.createCamera2Capturer(this);
default:
return null;
}
}
準備 Renderer 和 Encoder:
mVideoView = (SurfaceViewRenderer) findViewById(R.id.mVideoView1);
try {
String filename = "video_source_record_" + System.currentTimeMillis();
mMp4Recorder = new Mp4Recorder(
new File(Environment.getExternalStorageDirectory(), filename + ".mp4"));
mHdMp4Recorder = new Mp4Recorder(
new File(Environment.getExternalStorageDirectory(), filename + "-hd.mp4"));
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "start Mp4Recorder fail!", Toast.LENGTH_SHORT).show();
finish();
return;
}
mHwAvcEncoder = new HwAvcEncoder(config, mMp4Recorder);
mHdHwAvcEncoder = new HwAvcEncoder(hdConfig, mHdMp4Recorder);
建立 VideoSink
和 VideoSource
,VideoSource
也需要視訊配置,但只需要使用預覽尺寸、幀率,所以用 config
或者 hdConfig
都可以:
mVideoSink = new VideoSink(mVideoView, mHwAvcEncoder, mHdHwAvcEncoder);
mVideoSource = new VideoSource(getApplicationContext(), config, capturer, mVideoSink);
初始化:
mVideoView.init(mVideoSource.getRootEglBase().getEglBaseContext(), null);
mHwAvcEncoder.start(mVideoSource.getRootEglBase());
mHdHwAvcEncoder.start(mVideoSource.getRootEglBase());
開始採集、錄製:
@Override
protected void onStart() {
super.onStart();
mVideoSource.start();
}
記憶體抖動優化
完成了 VideoCRE 的剝離後,我發現記憶體抖動非常嚴重,CPU 佔用也很高:
排查記憶體抖動,當然首選 Allocation Tracker 了,結果如下:
這裡我們可以看到,60% 的記憶體分配都發生在 BufferInfo 物件上,但這個物件非常小,只有幾個 primitive 資料成員,怎麼會出現這麼多分配呢?我們看次數,15s 內發生了 2.6 萬次,每毫秒分配了 1.7 次。看程式碼發現,是我在單獨的執行緒呼叫 dequeueOutputBuffer
時傳入的 timeout 為 0,所以在瘋狂的建立 BufferInfo 物件。
單獨的執行緒設定 timeout 為 0 顯然不合理,除了這裡的記憶體分配,CPU 佔用也會更高,所以我們可以設定一個合適的值,這裡我換成 3000,也就是 3ms,結果如下:
我們可以看到,記憶體抖動減緩了很多,但仍比較明顯。CPU 佔用率倒是下降很多了。
這時 Allocation Tracker 的結果如下:
優化效能切忌盲目,要找準瓶頸,並且測量對比成效。
我們發現最大的分配竟是由一條日誌程式碼引起的!所以不要以為在日誌工具函式內通過變數控制是否打日誌就夠了,即便日誌最終沒有打印出來,但拼接日誌字串就可能已經成為瓶頸。
除了日誌,還存在兩處很高的分配:allocateDirect 和 BufferInfo。
其實 BufferInfo 沒必要每次建立,我們消費 MediaCodec 輸出是單執行緒行為,只需要分配一次即可。同理,容納輸出資料的 buffer 也沒必要每次分配,只有需要擴容時建立即可。
經過上述優化,記憶體抖動再次減弱:
分析 Allocation Tracker,較高的記憶體分配分別為:
Display#getRotation
:19.58%;- 取到 MediaCodec 輸出後,構造 ByteBuffer 物件的拷貝:17.31%;
- 幀資料傳遞過程中 matrix 陣列分配:16.32%;
上面這幾點都改了之後,再剩下的就是 I420Frame 的建立、日誌字串拼接、Runnable 物件建立了。此外還發現了一個注意點:使用 ExecutorService 時,每次 submit 任務,還會建立一個連結串列節點物件,而 Handler 會複用 Message 物件,所以我把 ExecutorService 換成了 HandlerThread + Handler 的組合。當然,for each 遍歷每次都會建立一個 Iterator 物件,雖然沒有成為瓶頸,但也確實可觀,何況可以一行程式碼進行優化,順手就給做了。
I420Frame 也許可以用物件池來優化,Runnable 則可以把區域性變數成員化,但現在其實已經優化了很多(對比測試一分鐘的記憶體分配從 2613976 優化到 240352,優化為了 9.2%),而這些做法需要比較複雜的處理才能確保不會發生消費者-生產者的競爭問題,所以就先告一段落啦!另外,這裡並沒有貼出具體優化程式碼,想看程式碼的朋友,可以檢視 GitHub 倉庫的這個 commit。
最後在 Nexus 5X 安卓 7.1.2 上測試發現,Camera2 採集時,存在大量的 Binder 通訊,記憶體抖動嚴重得多,而其中 48.86% 都是由 Binder 通訊導致的。
- Camera1 採集:一分鐘記憶體增長 0.32MB;
- Camera2 採集:一分鐘記憶體增長 3.13MB;
對此我就真是黔驢技窮了,只能寄希望於大谷歌了 :(
總結
至此,WebRTC(安卓流媒體)視訊的前段就已經差不多了,我們瞭解了採集、預覽、編碼的大體實現思路,也詳細分析了這些步驟裡面可能遇到的坑,最後將這三塊相關的程式碼剝離成為了一個可以單獨使用的模組:VideoCRE,並對其執行過程中的記憶體抖動進行了極大的優化,一分鐘記憶體分配優化為了 9.2%。
當然,流媒體前段還有不少內容沒有涵蓋:美顏、特效(結合人臉識別、場景分割)、更復雜的渲染……這些內容我還需要更深入的學習和理解,才敢分享,而流媒體的中段(傳輸)、後段(解碼播放)則還有更多的內容等著我們,好戲才剛剛開始 :)