本文中我們將分析webrtc渲染的實(shí)現(xiàn)。
視頻渲染
代碼位置:webrtc/src/sdk/objc/components/renderer
metal
RTCMTLVideoView.h RTCMTLVideoView.m
RTCMTLNSVideoView.h RTCMTLNSVideoView.m
RTCMTLRenderer.h RTCMTLRenderer.mm
RTCMTLRenderer+Private.h
RTCMTLRGBRenderer.h RTCMTLRGBRenderer.mm
RTCMTLNV12Renderer.h RTCMTLNV12Renderer.mm
RTCMTLI420Renderer.h RTCMTLI420Renderer.mm
opengl
RTCNSGLVideoView.h RTCNSGLVideoView.m
RTCEAGLVideoView.h RTCEAGLVideoView.m
RTCOpenGLDefines.h RTCVideoViewShading.h
RTCDefaultShader.h RTCDefaultShader.mm
RTCShader.h RTCShader.mm
RTCNV12TextureCache.h RTCNV12TextureCache.m
RTCI420TextureCache.h RTCI420TextureCache.mm
RTCDisplayLinkTimer.h RTCDisplayLinkTimer.m
視頻渲染方式有兩種,分別為 Metal,OpenGL。
有過一定相機(jī)開發(fā)經(jīng)驗(yàn)的朋友可能會(huì)疑惑,預(yù)覽還有什么好分析的,不是直接 camera.setPreviewDisplay 或者 camera.setPreviewTexture 就能在 SurfaceView/TextureView 上預(yù)覽了嗎?實(shí)際上預(yù)覽還有更高級(jí)的玩法,尤其是需要加上圖像處理功能(美顏、特效)時(shí)。WebRTC 使用了 OpenGL 進(jìn)行渲染(預(yù)覽),涉及下面三個(gè)問題:
數(shù)據(jù)怎么來?
渲染到哪兒?
怎么渲染?
接下來我們就逐步尋找這三個(gè)問題的答案。
數(shù)據(jù)怎么來?
這在第一篇文章已經(jīng)詳細(xì)描述了,參考WebRTC Native 源碼1:相機(jī)采集實(shí)現(xiàn)分析
渲染到哪兒?
WebRTC 里用的是 SurfaceView,雖然 WebRTC 使用了 OpenGL,但它并沒有使用 GLSurfaceView。其實(shí) GLSurfaceView 是 SurfaceView 的子類,它實(shí)現(xiàn)了 OpenGL 環(huán)境的管理,如果不用它,我們就得自己管理 OpenGL 環(huán)境。
那為什么好好的代碼放著不用呢?因?yàn)槭褂每蚣?已有代碼雖然能省卻一番工夫,但它也會(huì)帶來一些限制,例如使用 GLSurfaceView 我們的渲染模式就只有 continously 和 when dirty 了,而如果我們自己管理 OpenGL 環(huán)境,那我們的渲染將是完全自定義的。
實(shí)際上 WebRTC 的渲染不需要局限在 SurfaceView 及其子類上,OpenGL 只是利用了 SurfaceView 提供的 Surface,除了 Surface,OpenGL 也可以用 SurfaceTexture,而 TextureView 就能提供 SurfaceTexture,所以我們也可以渲染在 TextureView 上。
WebRTC 的渲染接口定義為 VideoRenderer,它用于預(yù)覽的實(shí)現(xiàn)就是 SurfaceViewRenderer,接下來就讓我們看看它究竟是如何渲染的。
怎么渲染?
先介紹一下OpenGL 的一些基礎(chǔ)知識(shí)。
1、GLES 和 EGL
OpenGL ES(Open Graphics Library for Embedded Systems,也叫 GLES)是 OpenGL 的一個(gè)子集,用于嵌入式系統(tǒng),在安卓平臺(tái)上,我們使用的實(shí)際上是 GLES API。GLES 也是跨平臺(tái)的,既然跨平臺(tái),那就一定有連接跨平臺(tái) API 和具體平臺(tái)實(shí)現(xiàn)的東西,這就是 EGL。EGL 是連接 OpenGL/GLES API 和底層系統(tǒng) window system(或者叫做“操作系統(tǒng)的窗口系統(tǒng)”)的橋梁(抽象層),它負(fù)責(zé)上下文管理、窗口/緩沖區(qū)綁定、渲染同步(上層繪制 API 和下層渲染 API),讓我們可以利用 OpenGL/GLES 實(shí)現(xiàn)高性能、利用 GPU 進(jìn)行硬件加速處理的 2D/3D 圖形開發(fā)。
OpenGL 環(huán)境管理,其實(shí)就是 EGL 環(huán)境的管理:EGLContext,EGLSurface 和 EGLDisplay。
- EGLContext 是一個(gè)容器,里面存儲(chǔ)著各種內(nèi)部的狀態(tài)(view port,texture 等)以及對(duì)這個(gè) context 待執(zhí)行的 GL 指令,可以說它存儲(chǔ)著渲染的輸入(配置和指令);
- EGLSurface 則是一個(gè) buffer,存儲(chǔ)著渲染的輸出(a color buffer, a depth buffer, and a stencil buffer),它有兩種類型,EGL_SINGLE_BUFFER 和 EGL_BACK_BUFFER,single 就是只有一個(gè) buffer,在里面畫了就立即顯示到了 display 上,而 back 則有兩個(gè) buffer,一個(gè)用于在前面顯示,一個(gè)用于在后面繪制,繪制完了就用 eglSwapBuffers 進(jìn)行切換;
- EGLDisplay 是和“操作系統(tǒng)的窗口系統(tǒng)”的一個(gè)連接,它代表了一個(gè)顯示窗口,我們最常用的是系統(tǒng)默認(rèn)的顯示窗口(屏幕);
我們首先在渲染線程創(chuàng)建 EGLContext,它的各種狀態(tài)都是 ThreadLocal 的,所以 GLES API 的調(diào)用都需要在創(chuàng)建了 EGLContext 的線程調(diào)用。有了上下文還不夠,我們還需要?jiǎng)?chuàng)建 EGLDisplay,我們用 eglGetDisplay 獲取 display,參數(shù)通常用 EGL_DEFAULT_DISPLAY,表明我們要獲取的是系統(tǒng)默認(rèn)的顯示窗口。最后就是利用 EGLDisplay 創(chuàng)建 EGLSurface 了:eglCreateWindowSurface,這個(gè)接口除了需要 EGLDisplay 參數(shù),還需要一個(gè) surface 參數(shù),它的類型可以是 Surface 或者 SurfaceTexture,這就是前面說的 OpenGL 既能用 Surface 也能用 SurfaceTexture 的原因了。
2、SurfaceViewRenderer 和 EglRenderer
WebRTC 把 EGL 的操作封裝在了 EglBase 中,并針對(duì) EGL10 和 EGL14 提供了不同的實(shí)現(xiàn),而 OpenGL 的繪制操作則封裝在了 EglRenderer 中。視頻數(shù)據(jù)在 native 層處理完畢后會(huì)拋出到 VideoRenderer.Callbacks#renderFrame 回調(diào)中,在這里也就是 SurfaceViewRenderer#renderFrame,而 SurfaceViewRenderer 又會(huì)把數(shù)據(jù)交給 EglRenderer 進(jìn)行渲染。所以實(shí)際進(jìn)行渲染工作的主角就是 EglRenderer 和 EglBase14(EGL14 實(shí)現(xiàn))了。
EglRenderer 實(shí)際的渲染代碼在 renderFrameOnRenderThread 中,前面已經(jīng)提到,GLES API 的調(diào)用都需要在創(chuàng)建了 EGLContext 的線程調(diào)用,在 EglRenderer 中這個(gè)線程就是 RenderThread,也就是 renderThreadHandler 對(duì)應(yīng)的線程。
由于這里出現(xiàn)了異步,而且提交的 Runnable 并不是每次創(chuàng)建一個(gè)匿名對(duì)象,所以我們就需要考慮如何傳遞幀數(shù)據(jù),EglRenderer 的實(shí)現(xiàn)還是比較巧妙的:它先把需要渲染的幀保存在 pendingFrame 成員變量中,保存好后異步執(zhí)行 renderFrameOnRenderThread,在其中首先把 pendingFrame 的值保存在局部變量中,然后將其置為 null,這樣就實(shí)現(xiàn)了一個(gè)“接力”的效果,利用一個(gè)成員變量,把幀數(shù)據(jù)從 renderFrame 的參數(shù)傳遞到了 renderFrameOnRenderThread 的局部變量中。當(dāng)然這個(gè)接力的過程需要加鎖,以保證多線程安全,一旦完成接力,雙方的操作就無需加鎖了,這樣能有效減少加鎖的范圍,提升性能。
renderFrameOnRenderThread 中會(huì)調(diào)用 GlDrawer 的 drawOes/drawYuv 來繪制 OES 紋理數(shù)據(jù)/YUV 內(nèi)存數(shù)據(jù)。繪制完畢后,調(diào)用 eglBase.swapBuffers 交換 Surface 的前后 buffer,把繪制的內(nèi)容顯示到屏幕上。
3、GlRectDrawer
GlDrawer 的實(shí)現(xiàn)是 GlRectDrawer,在這里我們終于見到了期待已久的 shader 代碼、vertex 坐標(biāo)和 texture 坐標(biāo)。
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 封裝了繪制矩形的操作,而我們的預(yù)覽/渲染也確實(shí)只需要繪制一個(gè)矩形。WebRTC 用到的 shader 代碼非常簡(jiǎn)單,與傳統(tǒng)OpenGL不一樣的是這里并沒有對(duì) vertex 坐標(biāo)進(jìn)行變換,而是對(duì) texture 坐標(biāo)進(jìn)行的變換,所以如果我們需要對(duì)圖像進(jìn)行旋轉(zhuǎn)操作。以 drawOes 為例,我們發(fā)現(xiàn)確實(shí)都是比較基礎(chǔ)的 OpenGL 調(diào)用了:
@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 變量賦值、為頂點(diǎn) attribute 賦值、綁定 texture、繪制矩形……當(dāng)然這里對(duì)代碼做了適當(dāng)?shù)姆庋b,增加了代碼的復(fù)用性,使得 drawYuv/drawRgb 的流程也基本相同。
4、TextureViewRenderer
WebRTC 中 實(shí)現(xiàn)了 Renderer 的 View 只有 SurfaceView 版本,如果我們有多個(gè)視頻同時(shí)渲染疊加顯示,我們會(huì)發(fā)現(xiàn)拖動(dòng)小窗口時(shí)會(huì)留下黑色殘影,這是因?yàn)?SurfaceView 的 Surface 和 View 樹是獨(dú)立的,兩者位置的更新沒有保持同步,TextureView 不存在拖動(dòng)殘影的問題,但 WebRTC 并沒有實(shí)現(xiàn) TextureViewRenderer。不過這點(diǎn)小問題肯定難不倒技術(shù)小能手們,對(duì) SurfaceViewRenderer 稍作修改就可以得到 TextureViewRenderer 了。