WebRTC 源码分析(二):安卓预览
有过一定相机开发经验的朋友可能会疑惑,预览还有什么好分析的,不是直接 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/
WebRTC 源码分析(二):安卓预览的更多相关文章
- Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题
4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...
- 十、Spring之BeanFactory源码分析(二)
Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...
- 框架-springmvc源码分析(二)
框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...
- Tomcat源码分析二:先看看Tomcat的整体架构
Tomcat源码分析二:先看看Tomcat的整体架构 Tomcat架构图 我们先来看一张比较经典的Tomcat架构图: 从这张图中,我们可以看出Tomcat中含有Server.Service.Conn ...
- Vue源码分析(二) : Vue实例挂载
Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...
- 多线程之美8一 AbstractQueuedSynchronizer源码分析<二>
目录 AQS的源码分析 该篇主要分析AQS的ConditionObject,是AQS的内部类,实现等待通知机制. 1.条件队列 条件队列与AQS中的同步队列有所不同,结构图如下: 两者区别: 1.链表 ...
- WebRTC 源码分析(三):安卓视频硬编码
数据怎么送进编码器? 怎么从编码器取数据? 如何做流控? 在开始之前,我们先了解一下 MediaCodec 的基本知识. MediaCodec 基础 Developer 官网 上的描述已经很清楚了,下 ...
- ConcurrenHashMap源码分析(二)
本篇博客的目录: 一:put方法源码 二:get方法源码 三:rehash的过程 四:总结 一:put方法的源码 首先,我们来看一下segment内部类中put方法的源码,这个方法它是segment片 ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
- spring源码分析(二)Aop
创建日期:2016.08.19 修改日期:2016.08.20-2016.08.21 交流QQ:992591601 参考资料:<spring源码深度解析>.<spring技术内幕&g ...
随机推荐
- 更改 AWS RDS mysql时区 -摘自网络
AWS RDS AWS上搭建数据库的时候,不是DB on EC2就是RDS,但是选择RDS时,Timezone怎么处理? 「面向全球提供的AWS来讲理所当然的是UTC」,而RDS也不是例外.把服务器迁 ...
- UITableView/UIScrollView 不能响应TouchBegin 的处理 及窥见 hitTest:withEvent:
重写touchBegin 方法是不行的,在UITableView/UIScrollView 解决方案 重写hitTest:withEvent: 在他们的子类中 - (UIView *)hitTest ...
- xcode自动打ipa包脚本 资料
http://webfrogs.me/2012/09/19/buildipa/ http://blog.csdn.net/baxiaxx/article/details/8267295 http:// ...
- Tomcat7启动报Error listenerStart错误
问题 Tomcat7在启动时报错,详细信息如下: 十一月 23, 2013 7:21:58 下午 org.apache.catalina.core.StandardContext startInter ...
- jmeter经验---java 追加写入代码一例
最近最项目参数化的时候用到,场景是这样的,需要测试A和B两个接口,其中B接口传入的参数必须是传递给A接口过的,所以整理一个思路就是: 1. 正常调用A接口,但是将传递给A接口的参数保存到文本里,此处要 ...
- 揭秘QQ 安全password框的原理
这篇文章也算是朝花夕拾.事实上非常早曾经就知道的原理,如今拿出来和大家交流分享一下. 故事总要有缘由.那么这个故事的缘由就是,当我曾经写了一个获取其他进程password框password的时候(前几 ...
- 关于Suppressing notification from package com.xxx.xxx by user request.的异常
其实以下都是废话. 如果你的测试的真机或者是模拟器是android4.1以上, 就有可能遇到这个Toast或者通知不能弹出. 自己不懂为什么. 想想你自己的应用设置是否有勾上这个 没有的话.就活该显示 ...
- 趣味讲解:移动互联网 VS 传统互联网
趣味讲解:移动互联网 VS 传统互联网 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转 ...
- [Windows Azure] How to Manage Cloud Services
How to Manage Cloud Services To use this feature and other new Windows Azure capabilities, sign up f ...
- 每日英语:Nelson Mandela Dies at 95
Nelson Mandela, who rose from militant antiapartheid activist to become the unifying president of a ...