1. 程式人生 > >WebRTC-Android 原始碼導讀(四):VideoCRE 與記憶體抖動優化

WebRTC-Android 原始碼導讀(四):VideoCRE 與記憶體抖動優化

前面三篇中,我們依次分析了 WebRTC Android 的視訊採集視訊渲染視訊硬編碼,Live Streaming 視訊的前段就已經全了。WebRTC 是個寶,初窺這部分程式碼時就被它的 Capturer 類的設計驚豔到了,仔細品鑑後越發佩服起來,裡面簡直填了太多坑了,如此寶貝,如不能為我所用,豈非一大憾事!而前三篇的解讀,正是為了今天能將其剝離出來所做的鋪墊,現在就有請我們今天的主角——VideoCRE, Video Capture, Render and Encode——閃亮登場。

VideoCRE 結構

我們當然可以直接使用 Capturer/Renderer/Encoder,但如果能將它們進行一定的封裝,讓基本的需求實現起來更加簡單,豈不妙哉。

下面介紹一下 VideoCRE 的結構:

  • 視訊資料由 VideoCapturer 採集,例如 Camera1Capturer
  • VideoCapturerSurfaceTextureHelper 等由 VideoSource 類管理;
  • VideoCapturer 採集到的資料會回撥給 VideoCapturer.CapturerObserverVideoSink 實現了該介面;
  • 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 和 VideoSourceVideoSource 也需要視訊配置,但只需要使用預覽尺寸、幀率,所以用 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%。

當然,流媒體前段還有不少內容沒有涵蓋:美顏、特效(結合人臉識別、場景分割)、更復雜的渲染……這些內容我還需要更深入的學習和理解,才敢分享,而流媒體的中段(傳輸)、後段(解碼播放)則還有更多的內容等著我們,好戲才剛剛開始 :)