導讀:WebRTC 中的Android VDM(Video Device Manager)技術模組,是指 WebRTC 基於 Android 系統,對視訊資料採集、編碼、 解碼和渲染的管理。當你拿到一部Android 手機,通過網易雲信 SDK 進行 RTC 通訊時,你是否好奇, Android 系統的 VDM 是如何實現的?WebRTC 又是如何使用 Android VDM 的?本文對 WebRTC 中 Android VDM 的實現進行了分解和梳理。
文|Iven
網易雲信資深音視訊客戶端開發工程師
01 Android 圖形系統介紹
視訊是由一幅幅影像組成的序列, 在 Android 系統中, 影像的載體是 Surface。Surface 可以理解為 Android 系統記憶體中的一段繪圖緩衝區。無論開發者使用什麼渲染 API,一切內容都會渲染到 Surface 上。Android 的採集、編解碼、渲染,都是基於對 Surface 的處理。Surface 表示緩衝區佇列中的生產方,而緩衝區佇列通常會被 SurfaceFlinger 或顯示 OpenGL ES 消耗。在 Android 平臺上建立的每個視窗都由 Surface 提供支援。所有被渲染的可見 Surface 都被 SurfaceFlinger 合成到螢幕,最後顯示到手機螢幕上。
Android 圖形系統中的生產者和消費者模型
前面提到,Surface 表示緩衝區佇列中的生產方,對應下圖的 Producer。BufferQueues 是 Android 圖形元件之間的粘合劑,它是一個佇列,將可生成圖形資料緩衝區的元件(生產方)連線到接收資料以便進行顯示或進一步處理的元件(消費方)。一旦生產方移交其緩衝區,消費方便會負責將生產出來的內容進行處理。
影像流生產方可以是生成圖形緩衝區以供消耗的任何內容。例如 Canvas 2D 和 mediaserver 視訊解碼器。影像流的最常見消耗方是 SurfaceFlinger,該系統服務會消耗當前可見的 Surface,並使用視窗管理器中提供的資訊將它們合成到螢幕。
整個 Android 影像的資料流管線很長,所以生產者和消費者的角色其實是相對的,同一個模組對應的可能既是生產者,也是消費者。OpenGL ES 既可以作為生產方提供相機採集影像流,也可以作為消費方消耗視訊解碼器解碼出來的影像流。
Android 圖形顯示 pipeline
SurfaceFlinger 是整個 Android 螢幕顯示的核心。SurfaceFlinger 程式由 init 程式建立,把系統中所有應用程式的最終繪圖結果合併成一張,然後統一顯示到物理螢幕上。
一個應用程式在送給 SurfaceFlinger 之前,是多個生產者在同時工作的,也就是有多個 Surface 通過 BufferQueue 向 SurfaceFlinger 輸送資料。如下圖的 Status Bar、System Bar、Icons/Widgets 等;但無論有多少個生產者,最終都在 SurfaceFlinger 這個消費者這裡,合併成一個 Surface。
值得一提的是 SurfaceFlinger 的硬體加速功能,如果所有的合成都交給 SurfaceFlinger 進行處理, 對 GPU 的負擔會加重,所以現在大部分手機都支援硬體加速進行合成,也就是 HWComposer。HWComposer 通過專門的硬體加速減輕 GPU 負擔,幫助 Surfaceflinger 高效快速地進行 Surface 的合成。
如下圖, 以開啟系統 Camera 為例, 可以看到下圖螢幕截圖的顯示其實對應了6個 Surface, 圖中標註了4個 Surface, 另外的2個在圖中不可見,所以沒有標註出來。Camera 資料的顯示作為其中一個 Surface(SurfaceView Layer), 佔用大部分的 SurfaceFlinger 合成計算, 由於 Camera 資料內容會不斷的變化,所以 GPU 需要重新繪製。
那麼另外兩個不可見的 Surface 是哪兩個呢?其實就是下圖對應的 NavigationBar,因為隱藏了,所以圖中看不到。
另外一個是 Dim Layer, 因為“USB 用於”這個視窗置頂了,使他後面的視窗產生了一個變暗的透明效果,這也是一個單獨的 Surface。
02 WebRTC 中 Android 端 VDM
講完 Android 系統的 VDM 模組, 那麼 WebRTC 是如何管理使用Capturer、Encoder、Decoder、Render 這四個模組的呢?還是按照生產者消費者模型,我們分為:
- 生產者(綠色):Capturer、Decoder;
Capturer 採集到資料、Decoder 解碼到資料後,不斷往 SurfaceTexture 的 Surface 中送資料。SurfaceTexture 通過 OnFrameAvailableListener 通知消費者進行處理。WebRTC 中 Capturer 使用 Camera1/Camera2 實現,Decoder 使用 MediaCodec 實現。 - 消費者(藍色):Render、Encoder;
Render 和 Encoder 各自的 Surface 通過 eglCreateWindowSurface() 跟 EGLSurface 進行關聯,而 EGLSurface 跟 SurfaceTexture 又是共用同一個 EGLContext。這樣 EGLSurface 就打通了 SurfaceTexture 跟 render/Encoder 的資料通道。EGLSurface 通過讀取 SurfaceTexture 的 Surface 資料,進行 shader 語言的圖形繪製。最終通過 eglSwapBuffers() 來提交當前幀,資料最終繪製到 Render 和 Encoder 的 Surface 上。
WebRTC 中 Render 使用 SurfaceView,不過 TextureView 在開源的 WebRTC 程式碼中並沒有實現,感興趣的小夥伴可以自行實現。Encoder 使用 MediaCodec 實現。
採集
Android 系統的採集在 WebRTC 中主要使用的是 Camera 採集和螢幕採集。WebRTC 中還有外部採集,比如從外部輸入紋理資料和 buffer 資料,但是外部採集不依賴 Android 原生系統功能, 所以不在本文討論範圍內。
- Camera 採集
經歷了多個系統相機架構迭代。目前提供 Camera1/Camera2/CameraX 三種使用方式。
Camera1 在5.0以前系統上使用,使用方法比較簡單。開發者可以設定的引數有限。可以通過 SurfaceTexure 獲取紋理資料。如果視訊前處理或者軟體編碼需要獲取 buffer 資料,可通過設定攝像頭採集視訊流格式和 Nv21 資料。
Camera2 是5.0時,谷歌針對攝像頭新推出的一套 API。開發使用上比 Camera1 複雜,有更多的 Camera 控制引數,可以通過 SurfaceTexure 獲取紋理資料。如果視訊前處理或者軟體編碼需要獲取 buffer 資料,可通過 ImageReader 設定監聽,拿到 i420/rgba 等資料。
CameraX 是 jetpack 的一個支援庫提供的方法。使用方法比 Camera2 更簡單,從原始碼看主要是封裝了 Camera1/Camera2 的實現,讓使用者不必去考慮什麼時候使用 Camera1,什麼時候使用 Camera2。CameraX 讓使用者更關注採集資料本身,而不是繁雜的呼叫方式和頭疼的相容性/穩定性問題。CameraX 在 WebRTC 原始碼中沒有實現,感興趣同學可以自行研究。
- 螢幕採集
5.0 以後,Google 開放了螢幕共享 API:MediaProjection,但是會彈出錄屏許可權申請框,使用者同意後才能開始錄屏。在 targetSdkVersion 大於等於29時,系統加強了對螢幕採集的限制,必須先啟動相應的前臺 Service,才能正常呼叫 getMediaProjection 方法。對於資料的採集,跟 Camera2 的資料採集方式類似,也是通過 SurfaceTexure 獲取紋理資料,或者通過 ImageReader 獲取 i420/rgba 資料。筆者嘗試在螢幕共享時獲取 i420,沒有成功,看起來大部分手機是不支援在螢幕共享時輸出 i420 資料的。螢幕共享的採集幀率沒法控制,主要規律是在螢幕靜止時,採集幀率降低。如果運動畫面,採集幀率可以達到最高的 60fps。螢幕共享 Surface 的長寬設定如果跟螢幕比例不一致,在部分手機上可能存在黑邊問題。
編解碼
說起 Android MediaCodec, 這張圖一定會被反覆提及。MediaCodec 的作用是處理輸入的資料生成輸出資料。首先生成一個輸入資料緩衝區,將資料填入緩衝區提供給 Codec,Codec 會採用非同步的方式處理這些輸入的資料,然後將填滿輸出緩衝區提供給消費者,消費者消費完後將緩衝區返還給 Codec。
在編碼的時候,如果輸入的資料是 texture,需要從 MediaCodec 獲取一個 Surface,通過 EGLSurface 將 Texture 資料繪製到這個 Surface 上。這種方式全程基於 Android 系統 Surface 繪製管線,認為是最高效的。
在解碼的時候,如果想要輸出到 texture,需要將 SurfaceTexture 的 Surface 設定給 MediaCodec,MediaCodec 作為生產者源源不斷地將解碼後的資料傳遞給 SurfaceTexture。這種方式全程基於 Android 系統 Surface 繪製管線,認為是最高效的。
除了高效的基於 texture 的操作,MediaCodec 可以對壓縮編碼後的視訊資料進行解碼得到 NV12 資料,也支援對 i420/NV12 資料進行編碼。
WebRTC 原始碼除了基於 MediaCodec 的硬體編解碼,還實現了軟體編解碼。通過軟硬體的切換策略,很好的考慮了效能和穩定性的平衡。
MediaCodec 其實也有軟體編解碼的實現。MediaCodec 的底層實現是基於開源 OpenMax 框架,整合了多個軟硬體編解碼器。不過一般在實際使用過程中,並沒有使用 Android 系統自帶的軟體編解碼,我們更多的是使用硬體編解碼。
在使用 MediaCodec 硬體編解碼時,可以獲取 Codec 相關資訊。如下以“OMX.MTK.VIDEO.ENCODER.AVC”編碼器為例,可通過 MediaCodecInfo 提供編碼器名字、支援的顏色格式、編碼 profile/level、可建立的最大例項個數等。
渲染
SurfaceView:從 Android 1.0(API level 1) 時就有。與普通 View 不同,SurfaceView 有自己的 Surface,通過 SurfaceHolder 進行管理。視訊內容可以單獨在這個 Surface上進行單獨執行緒渲染,不會影響主執行緒對事件的響應,但是不能進行移動、旋轉、縮放、動畫等變化。
TextureView:從 Android 4.0 中引入,可以跟普通 View 一樣進行移動、旋轉、縮放、動畫等變化。TextureView 必須在硬體加速的視窗中,當有其他 View 在 TextureView 頂部時,更新 TextureView 內容時,會觸發頂部 View 進行重繪,這無疑會增加效能方面消耗。在 RTC 場景中, 大部分時候都會在視訊播放視窗上面增加一些控制按鈕,這時候使用 SurfaceView 無疑效能上更有優勢。
03 VDM 的跨平臺工程實現
說到 WebRTC,不得不說它的跨平臺特點。那麼 Android VDM 是如何通過跨平臺這個框架進行工作的呢?
根據筆者的理解,將 Android VDM 在 WebRTC 中的實現分為4層。從上到下分為:Android Java Application、Java API、C++ Wrapper、All In One API。
- All In One API :
瞭解 WebRTC 的同學都知道,跨平臺的程式碼都是 C/C++ 實現的,因為 C/C++ 語言在各平臺具有良好的通用性。WebRTC 通過對各平臺,包括 Android/IOS/Windows/MAC、Encoder/Decoder/Capturer/Render 模組的抽象,形成了 All In One API。各平臺基於這些 API,各自基於不同作業系統去實現對應功能。這裡不得不讚嘆 C++ 的多型性的厲害之處。通過 All In One API,WebRTC 在 PeerConnection 建立後的媒體資料傳輸、編解碼器的策略控制、大小流、主輔流的切換等功能,才能順利搭建, All In One API 是整個音視訊通訊建立的基礎。
- C++ Wrapper:
這一層,是 Android 對應 Java 模組在 native 層的封裝,並且繼承自 All In One API 層的對應模組,由 C++ 實現。通過 Wrapper 使C++層可以無感知的訪問 Android 的 Java 物件。技術上,通過 Android 的 JNI 來實現。以 VideoEncoderWrapper 為例,VideoEncoderWrapper 封裝了 Java 的 VideoEncoder 物件,VideoEncoderWrapper 又繼承自 All In One API 的 VideoEncoder。這樣通過呼叫 All In One API 的VideoEncoder,實際上也就是執行到了 Android Java 的具體實現。除了 Android 平臺,其他平臺也可以通過同樣的方法在這一層進行封裝,這一層可以說是一個大熔爐,Android/IOS/Windows/MAC 的平臺屬性都可以得到封裝。C++ Wrapper 這一層,真是 WebRTC 跨平臺層和各平臺具體實現的完美橋樑。
- Java API
這一層提供了 WebRTC 在 Java 實現的 API 介面, 通過繼承這些 API,使得 Android SDK Application 的實現具有更好的擴充套件性。比如 CameraVideoCapturer 和 ScreenCapturerAndroid 通過繼承 VideoCapturer, 實現了 Camera 和 Screen 的採集。當後續開發維護者想要新增其他視訊採集方式時, 通過繼承 VideoCapturer,可以實現良好擴充套件性。再比如圖中的 SurfaceViewRender 繼承自 VideoSink,如果開發者想要實現基於 TextureView 的 Render,同樣的通過繼承 VideoSink, 即可快速實現。
- Android SDK Application
這一層是真正的 Android VDM 實現的地方,是基於 Android SDK API 對 Encode/Decode/Capture/Render功能的具體實現。這是離 Android 系統最近的一層。在這一層的實現中值得注意的是:Capturer/Encode/Decode 是由跨平臺層觸發物件的建立和銷燬,而 Render 是從 Java 建立物件,然後主動傳遞到跨平臺層的。所以對於 Render 的建立/銷燬,需要格外注意,防止野指標的出現。
04 RTC 場景中的 VDM 引數適配優化
上一章提到了 Android Java Application 層是具體功能實現的地方,而對於 All In One API 這一層,是對所有平臺的抽象。所以在呼叫的時候,並不關心平臺相關的一些相容性問題。而對於 Android 系統,繞不開的也是相容性的問題。所以如果想要 Android VDM 功能在基於 All In One API 層的複雜呼叫下,保持穩定執行,相容性適配問題的解決是不可忽視的,需要有個比較完善的相容適配框架,通過線上下發、本地配置讀取、程式碼層面的邏輯處理等手段,對不同的裝置機型、不同 CPU 型號、不同 Android 系統版本、不同業務場景等進行全方位的是適配優化。下圖是對相容性問題的下發配置方式框架圖。通過維護一份相容配置引數,通過 Compat 設定到 VDM 各模組,以解決相容性問題。
05 總結
本文通過對 Android 顯示系統的介紹,進而引出 WebRTC 在 Android 平臺上的 VDM 實現,並且深入 WebRTC 原始碼,將 Android VDM 在 WebRTC 中的實現剖解為4層,從上到下分為:Android SDK Application、Java API、C++ Wrapper、All In One API。
同時對於 Android 不能忽視的相容性問題的工程實現,做了簡單介紹。通過分析 WebRTC 在 Android VDM 上的實現,我們可以更加深入瞭解 WebRTC 的視訊系統的實現架構,以及跨平臺實現的架構思維。
作者介紹
Iven ,網易雲信資深音視訊客戶端開發工程師,主要負責視訊工程,Android VDM 相關工作。