1. 程式人生 > >WebRTC-Android 原始碼導讀(二):預覽實現分析

WebRTC-Android 原始碼導讀(二):預覽實現分析

本系列第一篇中,我們分析了 WebRTC-Android 相機採集的實現,本文中我們將分析預覽的實現。

有過一定相機開發經驗的朋友可能會疑惑,預覽還有什麼好分析的,不是直接 camera.setPreviewDisplay 或者 camera.setPreviewTexture 就能在 SurfaceView/TextureView上預覽了嗎?實際上預覽還有更高階的玩法,尤其是需要加上影象處理功能(美顏、特效)時。WebRTC 使用了 OpenGL 進行渲染(預覽),涉及下面三個問題:

  • 資料怎麼來?
  • 渲染到哪兒?
  • 怎麼渲染?

接下來我們就逐步尋找這三個問題的答案。

資料怎麼來?

在第一篇中我們已經知道,WebRTC 的資料採集由 VideoCapturer

 完成,VideoCapturer 定義了一個 CapturerObserver 來接收採集到的資料。而相機資料的輸出,無外乎兩個途徑:Camera.PreviewCallback(Camera1) 和 SurfaceTexture(Camera1 和 Camera2)。當然,Camera2 也可以獲取 YUV 記憶體資料,但這裡就不展開了。

camera.setPreviewCallbackWithBuffer 的呼叫在 Camera1Session 中,取得記憶體資料後將一路回撥通知到 VideoCapturer#onByteBufferFrameCaptured

為 SurfaceTexture

 設定資料回撥 surfaceTexture.setOnFrameAvailableListener 的呼叫則在 SurfaceTextureHelper 中,視訊記憶體資料更新後將一路回撥通知到VideoCapturer#onTextureFrameCaptured

資料的來源已經一清二楚了,那我們看看 CapturerObserver 有哪些實現。

這裡我就忍不住想要吐個槽,Java 的 IDE IntelliJ IDEA,以及基於它開發的 Android Studio,閱讀程式碼那是槓槓的,但要看 WebRTC 的 native 程式碼時,Mac 下無論是 Xcode 還是 Visual Studio,都渣得不行,看看一個類有哪些子類都做不到。

言歸正傳,CapturerObserver 只有一個實現類,那就是 AndroidVideoTrackSourceObserver,而它在收到資料之後負責把資料拋到 native 層,WebRTC 在 native 層做了很多事情,包括影象處理、編碼(軟)、傳輸等,AndroidVideoTrackSourceObserver 是幀資料從 Java 層到 native 層的起點。

幀資料在 native 層的旅途我們暫且跳過,下面我們從渲染的終點繼續探尋資料流動的線索。

渲染到哪兒?

我們要實現預覽肯定得有一個 View 來顯示,WebRTC 裡用的是 SurfaceView,雖然 WebRTC 使用了 OpenGL,但它並沒有使用 GLSurfaceView。其實 GLSurfaceView 是 SurfaceView 的子類,它實現了 OpenGL 環境的管理,如果不用它,我們就得自己管理 OpenGL 環境。

那為什麼好好的程式碼放著不用呢?因為使用框架/已有程式碼雖然能省卻一番工夫,但它肯定也會帶來一些限制,例如使用 GLSurfaceView 我們的渲染模式就只有 continously 和 when dirty 了,而如果我們自己管理 OpenGL 環境,那我們的渲染將是完全自定義的。這和捨棄 TCP 保證的可靠傳輸,自己基於 UDP 實現可靠傳輸,是一個道理,圖的就是靈活性。

實際上 WebRTC 的渲染不需要侷限在 SurfaceView 及其子類上,OpenGL 只是利用了 SurfaceView 提供的 Surface,除了 Surface,OpenGL 也可以用 SurfaceTexture,而 TextureView 就能提供 SurfaceTexture,所以我們也可以渲染在 TextureView 上。

WebRTC 的渲染介面定義為 VideoRenderer,它用於預覽的實現就是 SurfaceViewRenderer,接下來就讓我們看看它究竟是如何渲染的。

怎麼渲染?

既然渲染是用 OpenGL 實現的,那我們就需要了解一下 OpenGL 的一些基礎知識。

OpenGL 和 EGL

OpenGL(Open Graphics Library)是一套跨平臺的渲染 2D、3D 計算機圖形的庫,通常用於視訊、遊戲,利用 GPU 進行硬體加速處理。OpenGL ES(Open Graphics Library for Embedded Systems,也叫 GLES)是 OpenGL 的一個子集,用於嵌入式系統,在安卓平臺上,我們使用的實際上是 GLES API。GLES 也是跨平臺的,既然跨平臺,那就一定有連線跨平臺 API 和具體平臺實現的東西,這就是 EGL。EGL 是連線 OpenGL/GLES API 和底層系統 window system(或者叫做“作業系統的視窗系統”)的橋樑(抽象層),它負責上下文管理、視窗/緩衝區繫結、渲染同步(上層繪製 API 和下層渲染 API),讓我們可以利用 OpenGL/GLES 實現高效能、硬體加速的 2D/3D 圖形開發。

EGL™ is an interface between Khronos rendering APIs such as OpenGL ES or OpenVG and the underlying native platform window system. It handles graphics context management, surface/buffer binding, and rendering synchronization and enables high-performance, accelerated, mixed-mode 2D and 3D rendering using other Khronos APIs.

所謂的 OpenGL 環境管理,其實就是 EGL 環境的管理:EGLContextEGLSurface 和 EGLDisplay

  • EGLContext 是一個容器,裡面儲存著各種內部的狀態(view port,texture 等)以及對這個 context 待執行的 GL 指令,可以說它儲存著渲染的輸入(配置和指令);
  • EGLSurface 則是一個 buffer,儲存著渲染的輸出(a color buffer, a depth buffer, and a stencil buffer),它有兩種型別,EGL_SINGLE_BUFFER 和 EGL_BACK_BUFFER,single 就是隻有一個 buffer,在裡面畫了就立即顯示到了 display 上,而 back 則有兩個 buffer,一個用於在前面顯示,一個用於在後面繪製,繪製完了就用 eglSwapBuffers 進行切換;
  • EGLDisplay 是和“作業系統的視窗系統”的一個連線,它代表了一個顯示視窗,我們最常用的是系統預設的顯示視窗(螢幕);

我們首先在渲染執行緒建立 EGLContext,它的各種狀態都是 ThreadLocal 的,所以 GLES API 的呼叫都需要在建立了 EGLContext 的執行緒呼叫。有了上下文還不夠,我們還需要建立 EGLDisplay,我們用 eglGetDisplay 獲取 display,引數通常用 EGL_DEFAULT_DISPLAY,表明我們要獲取的是系統預設的顯示視窗。最後就是利用 EGLDisplay 建立 EGLSurface 了:eglCreateWindowSurface,這個介面除了需要 EGLDisplay 引數,還需要一個 surface 引數,它的型別可以是 Surface 或者 SurfaceTexture,這就是前面說的 OpenGL 既能用 Surface 也能用 SurfaceTexture 的原因了。

SurfaceViewRenderer 和 EglRenderer

現在讓我們回到 WebRTC 的程式碼中。WebRTC 把 EGL 的操作封裝在了 EglBase 中,並針對 EGL10 和 EGL14 提供了不同的實現,而 OpenGL 的繪製操作則封裝在了 EglRenderer 中。視訊資料在 native 層處理完畢後會丟擲到 VideoRenderer.Callbacks#renderFrame 回撥中,在這裡也就是 SurfaceViewRenderer#renderFrame,而 SurfaceViewRenderer 又會把資料交給 EglRenderer 進行渲染。所以實際進行渲染工作的主角就是 EglRenderer 和 EglBase14(EGL14 實現)了。

EglRenderer 實際的渲染程式碼在 renderFrameOnRenderThread 中,前面已經提到,GLES API 的呼叫都需要在建立了 EGLContext 的執行緒呼叫,在 EglRenderer 中這個執行緒就是 RenderThread,也就是 renderThreadHandler 對應的執行緒。

由於這裡出現了非同步,而且提交的 Runnable 並不是每次建立一個匿名物件,所以我們就需要考慮如何傳遞幀資料,EglRenderer 的實現還是比較巧妙的:它先把需要渲染的幀儲存在 pendingFrame成員變數中,儲存好後非同步執行 renderFrameOnRenderThread,在其中首先把 pendingFrame 的值儲存在區域性變數中,然後將其置為 null,這樣就實現了一個“接力”的效果,利用一個成員變數,把幀資料從 renderFrame 的引數傳遞到了 renderFrameOnRenderThread 的區域性變數中。當然這個接力的過程需要加鎖,以保證多執行緒安全,一旦完成接力,雙方的操作就無需加鎖了,這樣能有效減少加鎖的範圍,提升效能。

在第一篇的結尾,我們提到了記憶體抖動的問題,記憶體抖動肯定是由不合理的記憶體分配導致的,如果我們分析定位渲染每幀資料時建立的 RunnableI420Frame 物件成為了瓶頸,那我們就可以按照這種技巧避免每次建立新的物件。具體怎麼實現這裡我們先按下不表,且聽下回分解

renderFrameOnRenderThread 中會呼叫 GlDrawer 的 drawOes/drawYuv 來繪製 OES 紋理資料/YUV 記憶體資料。繪製完畢後,呼叫 eglBase.swapBuffers 交換 Surface 的前後 buffer,把繪製的內容顯示到螢幕上。

GlRectDrawer

GlDrawer 的實現是 GlRectDrawer,在這裡我們終於見到了期待已久的 shader 程式碼、vertex 座標和 texture 座標。

private static final String VERTEX_SHADER_STRING =
      "varying vec2 interp_tc;\n"
    + "attribute vec4 in_pos;\n"
    + "attribute vec4 in_tc;\n"
    + "\n"
    + "uniform mat4 texMatrix;\n"
    + "\n"
    + "void main() {\n"
    + " gl_Position = in_pos;\n"
    + " interp_tc = (texMatrix * in_tc).xy;\n"
    + "}\n";

private static final String OES_FRAGMENT_SHADER_STRING =
      "#extension GL_OES_EGL_image_external : require\n"
    + "precision mediump float;\n"
    + "varying vec2 interp_tc;\n"
    + "\n"
    + "uniform samplerExternalOES oes_tex;\n"
    + "\n"
    + "void main() {\n"
    + " gl_FragColor = texture2D(oes_tex, interp_tc);\n"
    + "}\n";

private static final FloatBuffer FULL_RECTANGLE_BUF = GlUtil.createFloatBuffer(new float[] {
    -1.0f, -1.0f, // Bottom left.
    1.0f, -1.0f, // Bottom right.
    -1.0f, 1.0f, // Top left.
    1.0f, 1.0f, // Top right.
});

正如其名,GlRectDrawer 封裝了繪製矩形的操作,而我們的預覽/渲染也確實只需要繪製一個矩形。WebRTC 用到的 shader 程式碼非常簡單,幾乎和我在安卓 OpenGL ES 2.0 完全入門(二):矩形、圖片、讀取視訊記憶體等中編寫的程式碼一樣簡單。不過有一點不同尋常的是,這裡並沒有對 vertex 座標進行變換,而是對 texture 座標進行的變換,所以如果我們需要對影象進行旋轉操作,直接使用 Matrix.rotateM 會導致十分詭異的效果,必須搭配 Matrix.translateM 才能正常。例如下圖:

mk82T.png

說到這裡我就不得不提另一個開源專案 Grafika 了,那裡面預覽繪製的 shader 程式碼和 WebRTC 如出一轍,也對 texture 座標做了變換,之前我嘗試旋轉影象時就遇到了上圖的窘境,最後在一位商湯“老大哥”的幫助下才解決了問題,當然,他也是從 StackOverflow 上找到的答案。如果大家打開了這個 StackOverflow 的連結,而且知道 fadden 這個 id,一定會感嘆,原來大神也會瞎扯淡。fadden 在媒體開發領域的地位,應該不遜於 JakeWharton 在應用開發領域的地位,bigflakeGrafikaGraphics architecture 都是 fadden 的大作,但 fadden 大神對這個問題的回答確實有失水準 :)

好了讓我們繼續看 GlRectDrawer 的程式碼。以 drawOes 為例,我們發現確實都是比較基礎的 OpenGL 呼叫了:

@Override
public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight,
    int viewportX, int viewportY, int viewportWidth, int viewportHeight) {
  prepareShader(OES_FRAGMENT_SHADER_STRING, texMatrix);
  GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
  // updateTexImage() may be called from another thread in another EGL context, so we need to
  // bind/unbind the texture in each draw call so that GLES understads it's a new texture.
  GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId);
  drawRectangle(viewportX, viewportY, viewportWidth, viewportHeight);
  GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}

private void prepareShader(String fragmentShader, float[] texMatrix) {
  final Shader shader;
  if (shaders.containsKey(fragmentShader)) {
    shader = shaders.get(fragmentShader);
    shader.glShader.useProgram();
  } else {
    // Lazy allocation.
    shader = new Shader(fragmentShader);
    shaders.put(fragmentShader, shader);
    shader.glShader.useProgram();
    // ...
    GlUtil.checkNoGLES2Error("Initialize fragment shader uniform values.");
    // Initialize vertex shader attributes.
    shader.glShader.setVertexAttribArray("in_pos", 2, FULL_RECTANGLE_BUF);
    shader.glShader.setVertexAttribArray("in_tc", 2, FULL_RECTANGLE_TEX_BUF);
  }
  // Copy the texture transformation matrix over.
  GLES20.glUniformMatrix4fv(shader.texMatrixLocation, 1, false, texMatrix, 0);
}

為 uniform 變數賦值、為頂點 attribute 賦值、繫結 texture、繪製矩形……當然這裡對程式碼做了適當的封裝,增加了程式碼的複用性,使得 drawYuv/drawRgb 的流程也基本相同。

TextureViewRenderer

WebRTC 中 實現了 Renderer 的 View 只有 SurfaceView 版本,如果我們有多個視訊同時渲染疊加顯示,我們會發現拖動小視窗時會留下黑色殘影,我推測這是因為 SurfaceView 的 Surface 和 View 樹是獨立的,兩者位置的更新沒有保持同步,所以出現了殘影。不過 Nexus 5X 7.1.1 不存在此問題,應該是 7.1.1 解決了這個問題。

好訊息是 TextureView 不存在拖動殘影的問題,壞訊息是 WebRTC 並沒有 TextureViewRenderer。不過這點小問題肯定難不倒技術小能手們,對 SurfaceViewRenderer 稍作修改就可以得到 TextureViewRenderer 了。具體程式碼我將在後續的文章中釋出。

總結

在本文中,我們接著第一篇採集的內容,理清楚了幀資料在預覽過程中的流動,以及預覽實現過程的細節,OpenGL 相關的內容佔了較大的篇幅。接下來第三篇我將分析 WebRTC 視訊硬編碼的實現,敬請期待 :)