导读:WebRTC 中的Android VDM(Video Device Manager)技术模块,是指 WebRTC 基于 Android 系统,对视频数据采集、编码、 解码和渲染的管理。当你拿到一部Android 手机,通过网易云信 SDK 进行 RTC 通信时,你是否好奇, Android 系统的 VDM 是如何实现的?WebRTC 又是如何使用 Android VDM 的?本文对 WebRTC 中 Android VDM 的实现进行了分解和梳理。
文|Iven
网易云信资深音视频客户端开发工程师
01 Android 图形系统介绍
视频是由一幅幅图像组成的序列, 在 Android 系统中, 图像的载体是 Surface。Surface 可以理解为 Android 系统内存中的一段绘图缓冲区。无论开发者使用什么渲染 API,一切内容都会渲染到 Surface 上。Android 的采集、编解码、渲染,都是基于对 Surface 的处理。Surface 表示缓冲区队列中的生产方,而缓冲区队列通常会被 SurfaceFlinger 或显示 OpenGL ES 消耗。在 Android 平台上创建的每个窗口都由 Surface 提供支持。所有被渲染的可见 Surface 都被 SurfaceFlinger 合成到屏幕,最后显示到手机屏幕上。
Android 图形系统中的生产者和消费者模型
前面提到,Surface 表示缓冲区队列中的生产方,对应下图的 Producer。BufferQueues 是 Android 图形组件之间的粘合剂,它是一个队列,将可生成图形数据缓冲区的组件(生产方)连接到接收数据以便进行显示或进一步处理的组件(消费方)。一旦生产方移交其缓冲区,消费方便会负责将生产出来的内容进行处理。
图像流生产方可以是生成图形缓冲区以供消耗的任何内容。例如 Canvas 2D 和 mediaserver 视频解码器。图像流的最常见消耗方是 SurfaceFlinger,该系统服务会消耗当前可见的 Surface,并使用窗口管理器中提供的信息将它们合成到屏幕。
整个 Android 图像的数据流管线很长,所以生产者和消费者的角色其实是相对的,同一个模块对应的可能既是生产者,也是消费者。OpenGL ES 既可以作为生产方提供相机采集图像流,也可以作为消费方消耗视频解码器解码出来的图像流。
Android 图形显示 pipeline
SurfaceFlinger 是整个 Android 屏幕显示的核心。SurfaceFlinger 进程由 init 进程创建,把系统中所有应用程序的最终绘图结果合并成一张,然后统一显示到物理屏幕上。
一个应用程序在送给 SurfaceFlinger 之前,是多个生产者在同时工作的,也就是有多个 Surface 通过 BufferQueue 向 SurfaceFlinger 输送数据。如下图的 Status Bar、System Bar、Icons/Widgets 等;但无论有多少个生产者,最终都在 SurfaceFlinger 这个消费者这里,合并成一个 Surface。
值得一提的是 SurfaceFlinger 的硬件加速功能,如果所有的合成都交给 SurfaceFlinger 进行处理, 对 GPU 的负担会加重,所以现在大部分手机都支持硬件加速进行合成,也就是 HWComposer。HWComposer 通过专门的硬件加速减轻 GPU 负担,帮助 Surfaceflinger 高效快速地进行 Surface 的合成。
如下图, 以打开系统 Camera 为例, 可以看到下图屏幕截图的显示其实对应了6个 Surface, 图中标注了4个 Surface, 另外的2个在图中不可见,所以没有标注出来。Camera 数据的显示作为其中一个 Surface(SurfaceView Layer), 占用大部分的 SurfaceFlinger 合成计算, 由于 Camera 数据内容会不断的变化,所以 GPU 需要重新绘制。
那么另外两个不可见的 Surface 是哪两个呢?其实就是下图对应的 NavigationBar,因为隐藏了,所以图中看不到。
另外一个是 Dim Layer, 因为“USB 用于”这个窗口置顶了,使他后面的窗口产生了一个变暗的透明效果,这也是一个单独的 Surface。
02 WebRTC 中 Android 端 VDM
讲完 Android 系统的 VDM 模块, 那么 WebRTC 是如何管理使用Capturer、Encoder、Decoder、Render 这四个模块的呢?还是按照生产者消费者模型,我们分为:
- 生产者(绿色):Capturer、Decoder;
Capturer 采集到数据、Decoder 解码到数据后,不断往 SurfaceTexture 的 Surface 中送数据。SurfaceTexture 通过 OnFrameAvailableListener 通知消费者进行处理。WebRTC 中 Capturer 使用 Camera1/Camera2 实现,Decoder 使用 MediaCodec 实现。 - 消费者(蓝色):Render、Encoder;
Render 和 Encoder 各自的 Surface 通过 eglCreateWindowSurface() 跟 EGLSurface 进行关联,而 EGLSurface 跟 SurfaceTexture 又是共用同一个 EGLContext。这样 EGLSurface 就打通了 SurfaceTexture 跟 render/Encoder 的数据通道。EGLSurface 通过读取 SurfaceTexture 的 Surface 数据,进行 shader 语言的图形绘制。最终通过 eglSwapBuffers() 来提交当前帧,数据最终绘制到 Render 和 Encoder 的 Surface 上。
WebRTC 中 Render 使用 SurfaceView,不过 TextureView 在开源的 WebRTC 代码中并没有实现,感兴趣的小伙伴可以自行实现。Encoder 使用 MediaCodec 实现。
采集
Android 系统的采集在 WebRTC 中主要使用的是 Camera 采集和屏幕采集。WebRTC 中还有外部采集,比如从外部输入纹理数据和 buffer 数据,但是外部采集不依赖 Android 原生系统功能, 所以不在本文讨论范围内。
- Camera 采集
经历了多个系统相机架构迭代。目前提供 Camera1/Camera2/CameraX 三种使用方式。
Camera1 在5.0以前系统上使用,使用方法比较简单。开发者可以设置的参数有限。可以通过 SurfaceTexure 获取纹理数据。如果视频前处理或者软件编码需要获取 buffer 数据,可通过设置摄像头采集视频流格式和 Nv21 数据。
Camera2 是5.0时,谷歌针对摄像头新推出的一套 API。开发使用上比 Camera1 复杂,有更多的 Camera 控制参数,可以通过 SurfaceTexure 获取纹理数据。如果视频前处理或者软件编码需要获取 buffer 数据,可通过 ImageReader 设置监听,拿到 i420/rgba 等数据。
CameraX 是 jetpack 的一个支持库提供的方法。使用方法比 Camera2 更简单,从源码看主要是封装了 Camera1/Camera2 的实现,让用户不必去考虑什么时候使用 Camera1,什么时候使用 Camera2。CameraX 让使用者更关注采集数据本身,而不是繁杂的调用方式和头疼的兼容性/稳定性问题。CameraX 在 WebRTC 源码中没有实现,感兴趣同学可以自行研究。
- 屏幕采集
5.0 以后,Google 开放了屏幕共享 API:MediaProjection,但是会弹出录屏权限申请框,用户同意后才能开始录屏。在 targetSdkVersion 大于等于29时,系统加强了对屏幕采集的限制,必须先启动相应的前台 Service,才能正常调用 getMediaProjection 方法。对于数据的采集,跟 Camera2 的数据采集方式类似,也是通过 SurfaceTexure 获取纹理数据,或者通过 ImageReader 获取 i420/rgba 数据。笔者尝试在屏幕共享时获取 i420,没有成功,看起来大部分手机是不支持在屏幕共享时输出 i420 数据的。屏幕共享的采集帧率没法控制,主要规律是在屏幕静止时,采集帧率降低。如果运动画面,采集帧率可以达到最高的 60fps。屏幕共享 Surface 的长宽设置如果跟屏幕比例不一致,在部分手机上可能存在黑边问题。
编解码
说起 Android MediaCodec, 这张图一定会被反复提及。MediaCodec 的作用是处理输入的数据生成输出数据。首先生成一个输入数据缓冲区,将数据填入缓冲区提供给 Codec,Codec 会采用异步的方式处理这些输入的数据,然后将填满输出缓冲区提供给消费者,消费者消费完后将缓冲区返还给 Codec。
在编码的时候,如果输入的数据是 texture,需要从 MediaCodec 获取一个 Surface,通过 EGLSurface 将 Texture 数据绘制到这个 Surface 上。这种方式全程基于 Android 系统 Surface 绘制管线,认为是最高效的。
在解码的时候,如果想要输出到 texture,需要将 SurfaceTexture 的 Surface 设置给 MediaCodec,MediaCodec 作为生产者源源不断地将解码后的数据传递给 SurfaceTexture。这种方式全程基于 Android 系统 Surface 绘制管线,认为是最高效的。
除了高效的基于 texture 的操作,MediaCodec 可以对压缩编码后的视频数据进行解码得到 NV12 数据,也支持对 i420/NV12 数据进行编码。
WebRTC 源码除了基于 MediaCodec 的硬件编解码,还实现了软件编解码。通过软硬件的切换策略,很好的考虑了性能和稳定性的平衡。
MediaCodec 其实也有软件编解码的实现。MediaCodec 的底层实现是基于开源 OpenMax 框架,集成了多个软硬件编解码器。不过一般在实际使用过程中,并没有使用 Android 系统自带的软件编解码,我们更多的是使用硬件编解码。
在使用 MediaCodec 硬件编解码时,可以获取 Codec 相关信息。如下以“OMX.MTK.VIDEO.ENCODER.AVC”编码器为例,可通过 MediaCodecInfo 提供编码器名字、支持的颜色格式、编码 profile/level、可创建的最大实例个数等。
渲染
SurfaceView:从 Android 1.0(API level 1) 时就有。与普通 View 不同,SurfaceView 有自己的 Surface,通过 SurfaceHolder 进行管理。视频内容可以单独在这个 Surface上进行单独线程渲染,不会影响主线程对事件的响应,但是不能进行移动、旋转、缩放、动画等变化。
TextureView:从 Android 4.0 中引入,可以跟普通 View 一样进行移动、旋转、缩放、动画等变化。TextureView 必须在硬件加速的窗口中,当有其他 View 在 TextureView 顶部时,更新 TextureView 内容时,会触发顶部 View 进行重绘,这无疑会增加性能方面消耗。在 RTC 场景中, 大部分时候都会在视频播放窗口上面增加一些控制按钮,这时候使用 SurfaceView 无疑性能上更有优势。
03 VDM 的跨平台工程实现
说到 WebRTC,不得不说它的跨平台特点。那么 Android VDM 是如何通过跨平台这个框架进行工作的呢?
根据笔者的理解,将 Android VDM 在 WebRTC 中的实现分为4层。从上到下分为:Android Java Application、Java API、C++ Wrapper、All In One API。
- All In One API :
了解 WebRTC 的同学都知道,跨平台的代码都是 C/C++ 实现的,因为 C/C++ 语言在各平台具有良好的通用性。WebRTC 通过对各平台,包括 Android/IOS/Windows/MAC、Encoder/Decoder/Capturer/Render 模块的抽象,形成了 All In One API。各平台基于这些 API,各自基于不同操作系统去实现对应功能。这里不得不赞叹 C++ 的多态性的厉害之处。通过 All In One API,WebRTC 在 PeerConnection 建立后的媒体数据传输、编解码器的策略控制、大小流、主辅流的切换等功能,才能顺利搭建, All In One API 是整个音视频通讯建立的基础。
- C++ Wrapper:
这一层,是 Android 对应 Java 模块在 native 层的封装,并且继承自 All In One API 层的对应模块,由 C++ 实现。通过 Wrapper 使C++层可以无感知的访问 Android 的 Java 对象。技术上,通过 Android 的 JNI 来实现。以 VideoEncoderWrapper 为例,VideoEncoderWrapper 封装了 Java 的 VideoEncoder 对象,VideoEncoderWrapper 又继承自 All In One API 的 VideoEncoder。这样通过调用 All In One API 的VideoEncoder,实际上也就是执行到了 Android Java 的具体实现。除了 Android 平台,其他平台也可以通过同样的方法在这一层进行封装,这一层可以说是一个大熔炉,Android/IOS/Windows/MAC 的平台属性都可以得到封装。C++ Wrapper 这一层,真是 WebRTC 跨平台层和各平台具体实现的完美桥梁。
- Java API
这一层提供了 WebRTC 在 Java 实现的 API 接口, 通过继承这些 API,使得 Android SDK Application 的实现具有更好的扩展性。比如 CameraVideoCapturer 和 ScreenCapturerAndroid 通过继承 VideoCapturer, 实现了 Camera 和 Screen 的采集。当后续开发维护者想要添加其他视频采集方式时, 通过继承 VideoCapturer,可以实现良好扩展性。再比如图中的 SurfaceViewRender 继承自 VideoSink,如果开发者想要实现基于 TextureView 的 Render,同样的通过继承 VideoSink, 即可快速实现。
- Android SDK Application
这一层是真正的 Android VDM 实现的地方,是基于 Android SDK API 对 Encode/Decode/Capture/Render功能的具体实现。这是离 Android 系统最近的一层。在这一层的实现中值得注意的是:Capturer/Encode/Decode 是由跨平台层触发对象的创建和销毁,而 Render 是从 Java 创建对象,然后主动传递到跨平台层的。所以对于 Render 的创建/销毁,需要格外注意,防止野指针的出现。
04 RTC 场景中的 VDM 参数适配优化
上一章提到了 Android Java Application 层是具体功能实现的地方,而对于 All In One API 这一层,是对所有平台的抽象。所以在调用的时候,并不关心平台相关的一些兼容性问题。而对于 Android 系统,绕不开的也是兼容性的问题。所以如果想要 Android VDM 功能在基于 All In One API 层的复杂调用下,保持稳定运行,兼容性适配问题的解决是不可忽视的,需要有个比较完善的兼容适配框架,通过线上下发、本地配置读取、代码层面的逻辑处理等手段,对不同的设备机型、不同 CPU 型号、不同 Android 系统版本、不同业务场景等进行全方位的是适配优化。下图是对兼容性问题的下发配置方式框架图。通过维护一份兼容配置参数,通过 Compat 设置到 VDM 各模块,以解决兼容性问题。
05 总结
本文通过对 Android 显示系统的介绍,进而引出 WebRTC 在 Android 平台上的 VDM 实现,并且深入 WebRTC 源码,将 Android VDM 在 WebRTC 中的实现剖解为4层,从上到下分为:Android SDK Application、Java API、C++ Wrapper、All In One API。
同时对于 Android 不能忽视的兼容性问题的工程实现,做了简单介绍。通过分析 WebRTC 在 Android VDM 上的实现,我们可以更加深入了解 WebRTC 的视频系统的实现架构,以及跨平台实现的架构思维。
作者介绍
Iven ,网易云信资深音视频客户端开发工程师,主要负责视频工程,Android VDM 相关工作。