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