本文中我们将分析webrtc渲染的实现。
视频渲染
代码位置: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。
有过一定相机开发经验的朋友可能会疑惑,预览还有什么好分析的,不是直接 camera.setPreviewDisplay 或者 camera.setPreviewTexture 就能在 SurfaceView/TextureView 上预览了吗?实际上预览还有更高级的玩法,尤其是需要加上图像处理功能(美颜、特效)时。WebRTC 使用了 OpenGL 进行渲染(预览),涉及下面三个问题:
数据怎么来?
渲染到哪儿?
怎么渲染?
接下来我们就逐步寻找这三个问题的答案。
数据怎么来?
这在第一篇文章已经详细描述了,参考WebRTC Native 源码1:相机采集实现分析
渲染到哪儿?
WebRTC 里用的是 SurfaceView,虽然 WebRTC 使用了 OpenGL,但它并没有使用 GLSurfaceView。其实 GLSurfaceView 是 SurfaceView 的子类,它实现了 OpenGL 环境的管理,如果不用它,我们就得自己管理 OpenGL 环境。
那为什么好好的代码放着不用呢?因为使用框架/已有代码虽然能省却一番工夫,但它也会带来一些限制,例如使用 GLSurfaceView 我们的渲染模式就只有 continously 和 when dirty 了,而如果我们自己管理 OpenGL 环境,那我们的渲染将是完全自定义的。
实际上 WebRTC 的渲染不需要局限在 SurfaceView 及其子类上,OpenGL 只是利用了 SurfaceView 提供的 Surface,除了 Surface,OpenGL 也可以用 SurfaceTexture,而 TextureView 就能提供 SurfaceTexture,所以我们也可以渲染在 TextureView 上。
WebRTC 的渲染接口定义为 VideoRenderer,它用于预览的实现就是 SurfaceViewRenderer,接下来就让我们看看它究竟是如何渲染的。
怎么渲染?
先介绍一下OpenGL 的一些基础知识。
1、GLES 和 EGL
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 实现高性能、利用 GPU 进行硬件加速处理的 2D/3D 图形开发。
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 的原因了。
2、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
的局部变量中。当然这个接力的过程需要加锁,以保证多线程安全,一旦完成接力,双方的操作就无需加锁了,这样能有效减少加锁的范围,提升性能。
renderFrameOnRenderThread
中会调用 GlDrawer
的 drawOes
/drawYuv
来绘制 OES 纹理数据/YUV 内存数据。绘制完毕后,调用 eglBase.swapBuffers
交换 Surface 的前后 buffer,把绘制的内容显示到屏幕上。
3、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不一样的是这里并没有对 vertex 坐标进行变换,而是对 texture 坐标进行的变换,所以如果我们需要对图像进行旋转操作。以 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
的流程也基本相同。
4、TextureViewRenderer
WebRTC 中 实现了 Renderer 的 View 只有 SurfaceView 版本,如果我们有多个视频同时渲染叠加显示,我们会发现拖动小窗口时会留下黑色残影,这是因为 SurfaceView 的 Surface 和 View 树是独立的,两者位置的更新没有保持同步,TextureView 不存在拖动残影的问题,但 WebRTC 并没有实现 TextureViewRenderer。不过这点小问题肯定难不倒技术小能手们,对 SurfaceViewRenderer 稍作修改就可以得到 TextureViewRenderer 了。