有過一定相機開發經驗的朋友可能會疑惑,預覽還有什么好分析的,不是直接 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 有哪些實現。
CapturerObserver 只有一個實現類,那就是 AndroidVideoTrackSourceObserver,而它在收到數據之后負責把數據拋到 native 層,WebRTC 在 native 層做了很多事情,包括圖像處理、編碼(軟)、傳輸等,AndroidVideoTrackSourceObserver 是幀數據從 Java 層到 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 環境的管理:EGLContext,EGLSurface 和 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 把 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 的局部變量中。當然這個接力的過程需要加鎖,以保證多線程安全,一旦完成接力,雙方的操作就無需加鎖了,這樣能有效減少加鎖的范圍,提升性能。
在第一篇的結尾,我們提到了內存抖動的問題,內存抖動肯定是由不合理的內存分配導致的,如果我們分析定位渲染每幀數據時創建的 Runnable、I420Frame 對象成為了瓶頸,那我們就可以按照這種技巧避免每次創建新的對象。
renderFrameOnRenderThread 中會調用 GlDrawer 的 drawOes/drawYuv 來繪制 OES 紋理數據/YUV 內存數據。繪制完畢后,調用 eglBase.swapBuffers 交換 Surface 的前后 buffer,把繪制的內容顯示到屏幕上。
GlRectDrawer
GlDrawer 的實現是 GlRectDrawer,在這里我們終於見到了期待已久的 shader 代碼、vertex 坐標和 texture 坐標。
privatestaticfinalStringVERTEX_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";privatestaticfinalStringOES_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";privatestaticfinalFloatBufferFULL_RECTANGLE_BUF=GlUtil.createFloatBuffer(newfloat[]{-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 才能正常。例如下圖:
說到這里我就不得不提另一個開源項目 Grafika 了,那里面預覽繪制的 shader 代碼和 WebRTC 如出一轍,也對 texture 坐標做了變換,之前我嘗試旋轉圖像時就遇到了上圖的窘境,最后在一位商湯“老大哥”的幫助下才解決了問題,當然,他也是從 StackOverflow 上找到的答案。如果大家打開了這個 StackOverflow 的鏈接,而且知道 fadden 這個 id,一定會感嘆,原來大神也會瞎扯淡。fadden 在媒體開發領域的地位,應該不遜於 JakeWharton 在應用開發領域的地位,bigflake、Grafika、Graphics architecture 都是 fadden 的大作,但 fadden 大神對這個問題的回答確實有失水准 :)
好了讓我們繼續看 GlRectDrawer 的代碼。以 drawOes 為例,我們發現確實都是比較基礎的 OpenGL 調用了:
@OverridepublicvoiddrawOes(intoesTextureId,float[]texMatrix,intframeWidth,intframeHeight,intviewportX,intviewportY,intviewportWidth,intviewportHeight){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);}privatevoidprepareShader(StringfragmentShader,float[]texMatrix){finalShadershader;if(shaders.containsKey(fragmentShader)){shader=shaders.get(fragmentShader);shader.glShader.useProgram();}else{// Lazy allocation.shader=newShader(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 視頻硬編碼的實現,敬請期待 :)
https://blog.piasy.com/2017/07/26/WebRTC-Android-Render-Video/