WebRTC 系列之 GPU 方案的探索与落地

MCtalk 技术文章/2021.12.20 文|陶金亮 网易云信资深音视频工程师
导读:
WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话或视频对话的 API。W3C 和 IETF 在2021年1月26日共同宣布 WebRTC 1.0 定稿,促使 WebRTC 从事实上的互联网通信标准成为了官方标准,其在不同场景的应用将得到更为广泛的普及。WebRTC 提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:Windows,Mac,iOS,Android。本文主要介绍基于 WebRTC 的基础搭建视频前后处理框架,探索 GPU 方案与 RTC 场景的结合落地。
一、背景
现阶段为了完成 G2 SDK(网易云信第二代实时音视频SDK)接入视频矫正、美颜、超分等 GPU 算法,鉴于 G2 目前尚未支持整个 GPU 方案,因此需要改造 G2 现有的视频数据链路走向,形成统一的 G2 前处理模块,包含视频矫正、美颜、视频降噪、视频增强等算法。考虑到整体性能的最佳状态,整体方案会涉及到 Camera 采集、基础前处理(裁剪、缩放、旋转、编码镜像、编码方向)、算法前处理(transform层、算法层)、渲染、编码等模块的整体调整。
二、关键点
- 新增 Process 的数据节点,现有 Capture 能力迁移到 Process 节点,由 Process 节点负责数据处理和中转分发。
-
Process 节点开辟视频前处理线程,将原先基础前处理(裁剪、缩放、旋转等位于 Camera 线程;编码镜像、编码方向等位于编码线程)的操作都移植到前处理线程,防止阻塞 Camera 线程和编码线程。
-
将原先 CPU 基础前处理,调整为 GPU 基础前处理,从而减少性能开销。
-
形成统一的视频前后处理管理模块,负责 PreProcess 前处理和 PostProcess 后处理的管理和调度。实现一个合理的视频框架来承载各类算法,解决代码臃肿,可拓展性差,维护成本高等问题。
-
视频前后处理模块与 Camera 模块、渲染模块、编解码模块的整体协调;Camera 模块输出的各类数据格式类型,视频前处理模块都要支持;算法输出的各类数据格式类型,渲染模块都要支持;不同编码器要求的数据格式类型(包括 fallback 切换编码器),能够反向调节视频前处理模块输出的数据格式类型;解码模块输出的各类数据格式类型,视频后处理模块都要支持。
-
需要实现统一的不同视频数据格式转换的 trasnfrom 层,包括 CPU 到 GPU、GPU 到 CPU、以及 GPU 上不同纹理格式之间的转换(使用 Shader 完成),同时具有高效的性能。
三、方案介绍
目前整体方案采取单路数据流,支持 CPU 方案和 GPU 方案并行。中高端机型实现 GPU 方案,低端机型实现 CPU 方案,CPU 方案可以作为 GPU 方案的灾备。
CPU 方案
Camera 输出 I420+CPU 基础前处理
GPU 方案
Camera 输出纹理+ GPU 基础前处理
方案选型
这个是之前讨论的双路数据的方案,那么我们为什么不考虑呢?
-
根据 Camera1/Camera2 单双流数据采集性能对比报告显示,光一个2路数据采集,在测试的大部分手机上,归一化的 CPU 百分比就多出2个点,8核的手机总的 CPU 消耗就是多出16%。
-
路数据对于人脸检测的数据,必须保证是同一张画面,且大小都一致,否则人脸检测的坐标点就无法统一起来。在 RTC 的框架里面,存在 Adapter 的操作,会有丢帧帧率,因此很难控制2路数据的一致性。其次纹理数据做 GPU 基础前处理,那么另外一路 I420 数据也需要做同样的 CPU 基础前处理,这部分的开销也是会增长的。
-
对于人脸检测的这一路 I420 数据肯定需要单独再开线程来同步人脸检测的结果坐标点到原始纹理数据上。如果都是在前处理线程上,必然会引起前处理线程的阻塞。这2路数据必须要做到互相等的情况,纹理处理快了,需要等人脸检测的数据,人脸检测快了,需要等纹理,因此多线程操作会相当复杂。
-
对于这种2路数据方案的提出,本来就是为了人脸检测这个模块考虑的。为了满足该需求,我们使用上面的 CPU 方案,只需要使 Camera 输出 I420 数据,同时基础前处理选CPU操作就可以满足需求;而且这种方案的性能消耗和复杂度都比双路有优势。
综上所述,我们将淘汰双路的方案,采取单路方案,支持 CPU 方案和 GPU 方案并行。中高端机型的 GPU 性能较好,对于 GPU 方案有较为明显的优势;低端机型的 GPU 性能较差,对于 CPU 方案有较为明显的优势;因此中高端机型实现 GPU 方案,低端机型实现 CPU 方案,CPU 方案可以作为 GPU 方案的灾备,整体使用下发控制来做到兼容性的适配。
框架设计
框架介绍
一个满足整个视频前后处理的框架,整体由 ProcessManager 管理 GPU 的上下文环境,同时负责前处理 PreProcess 和后处理 PostProcess 的创建和销毁。
第一层为 Process Control:内部包含具体的前处理 PreProcess 和后处理 PostProcess,它们各自管理一个线程,进行耗时操作。
第二层为 Process Unit:它们负责不同类型的操作管理,正如上面方案里面具体的处理单元,包括 CPU 算法处理单元、RGB 纹理处理单元、YUV 纹理处理单元、基础前处理的预览处理单元、基础前处理的编码处理单元、编码格式要求的处理单元等。
第三层为 Transform:主要负责 RTC 中各个平台的数据类型的转换,包括 CPU 数据和 GPU 数据的互转,以及 GPU 数据不同格式之间的转换;Apple 平台使用 Metal 实现、Android 平台使用 OpenGL ES 实现。
第四层为算法层:主要包含 AI 算法和传统算法,目前主要包含的算法有:超分、屏幕增强、视频降噪、人脸检测、美颜、人脸画质增强、视频矫正等。有的使用 CPU 实现,有的使用 GPU 实现。
第五层为平台层:主要负责各自平台算子的实现,会有 NPU、CPU、GPU 等方案,像 GPU 内部又会分为 Vulkan、Metal、OpenCL、Cuda 等。针对 AI 的 GPU 加速,我们有好几种 GPU API Backend,这样做的目的主要也是为了在不同平台不同系统上可以让 GPU 有更好的加速,例如:在 IOS 平台上当前比较优的 GPU 方案就是采用 Metal 去加速,而在 Android 平台上比较通用的方法是 OpenCL 去加速,所以我们的框架需要去对这不同的 GPU API 做兼容和适配。
模型设计
每个 Process Unit 处理单元内部包含一个 Transfrom 层负责数据格式转换和同类型的多个算法,它们犹如管线一样,依次串联,从而达到视频数据的流动。譬如 RGBTexProcessUnit 内部包含一个 rgb 纹理的 transfrom,多个需要 rgb 纹理输入的算法,包括视频矫正、人脸画质增强、美颜以及美颜内部串联美白、磨皮、贴纸、特效等。
Transfrom 层实现
**Transform 层主要分为三步:输入数据、格式转换、输出数据。**输入部分需要关注各种数据类型的输入,做到各种数据输入的兼容;格式转换部分需要满足各种数据类型格式的转换,并且是高效的;输出部分需要关注外部指定的格式要求。
1、颜色空间的转换
YUV 是一种颜色编码方法,主要用在视频、图形处理流水线中。相对于 RGB 颜色空间,设计 YUV 的目的就是为了编码、传输的方便,减少带宽占用和信息出错。因为人眼的视觉特点是对亮度更敏感,对位置、色彩相对来说不敏感。在视频编码系统中为了降低带宽,可以保存更多的亮度信息,保存较少的色差信息。
对于图像显示器来说,它是通过 RGB 模型来显示图像的,而在传输图像数据时又是使用 YUV 模型。因此就需要采集图像时将 RGB 模型转换到 YUV 模型,显示时再将 YUV 模型转换为 RGB 模型。
在介绍 YUV 和 RGB 的颜色空间转换前,请先阅读一文读懂 YUV 的采样与格式,能够帮助你更好的理解下面的过程。
对于标清电视应用 (SDTV),我们往往使用 ITU-R BT.601;对于高清电视 (HDTV),我们往往使用 ITU-R BT.709。
对于数字分量视频,使用颜色格式 YCbCr。对于标清电视应用 (SDTV),以下等式描述了从 RGB 到 YCbCr 的颜色转换(根据 ITU-R BT.601):
为了从 YCbCr 颜色恢复 RGB 颜色,使用以下逆矩阵:
亮度和色度的取值范围做了一些保留,亮度范围是 16~235,色度范围是 16~240。对于使用 RGB 和 YCbCr 颜色格式的计算机应用程序,在许多情况下,使用 8 位的完整范围,而不保留多余范围。通常这种全范围颜色格式用于 JPEG 图像。
RGB 颜色到全范围 YCbCr 颜色的转换由以下等式描述:
反过来,将全范围 YCbCr 颜色转换为 RGB 由以下等式描述:
对于高清电视 (HDTV),需要使用不同的系数。亮度和色度的取值范围与 SDTV 相同,以提供必要的动态余量。以下等式描述了 HDTV 从 RGB 到 YCbCr 的颜色转换(根据 ITU-R BT.709):
这是从 YCbCr 颜色中获取 RGB 颜色分量的相应逆矩阵:
VideoRange 与 FullRange
Apple 平台下,存在2种常用的 I420 格式:
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 【Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240])】
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange 【Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255])】
BT.601 与 BT.709
在 iOS 和 Mac 平台下获取的 CMSampleBuffer 基本都是 BT601
mediaType:'vide'
mediaSubType:'BGRA'
mediaSpecific: {
codecType: 'BGRA' dimensions: 1080 x 1920
}
extensions: {{
CVBytesPerRow = 4352;
CVImageBufferColorPrimaries = "ITU_R_709_2";
CVImageBufferTransferFunction = "ITU_R_709_2";
CVImageBufferYCbCrMatrix = "ITU_R_601_4";
Version = 2;
}
Metal Performance Shaders 与 MTLComputeCommandEncoder
Metal Performance Shaders 框架包含一系列高度优化的计算和图形着色器,旨在轻松高效地集成到您的 Metal 应用程序中。这些数据并行经过专门调整,利用每个 GPU 系列的独特硬件特性来确保最佳性能。同时它也是 Apple 上实现深度学习的工具,它主要封装了 MPSImage 来存储数据管理内存,相当于 Caffe 中的 Blob、MXNet 中的 NDArray,实现了Convolution、Pooling、Fullconnetcion、ReLU 等常用的卷积神经网络中的 Layer。我们的 GPU 传统超分就是通过 Metal Performance Shaders 实现的。我们的AI 超分中也用到了 MTLComputeCommandEncoder 并行计算任务。
在 Apple 平台下,不同纹理格式之间的转换都是通过 Metal Performance Shaders 和 MTLComputeCommandEncoder 并行数据计算任务编码来完成的。
丰富的格式转换
Android 平台使用 OpenGL ES 的 Shader 实现,目前主要实现了这些转换,后期看需求再扩充。
iOS 平台使用 Metal 的 Shader 实现,目前主要实现了这些转换,后期看需求再扩充。
2、CPU 与 GPU 的高效方式
在 RTC 场景中 CPU 数据主要指 I420 数据,GPU 数据主要指 RGB 纹理。
因为 CPU 和 GPU 是两个不同的处理器,它们可以并行地执行各自的指令。GPU 侧会存在一个命令队列用于暂存还未发送到 GPU 的命令,实际上我们调用的绝大多数 OpenGL ES 或者 Metal 方法,只是往命令队列时送入命令而已,并不会在 CPU 同步等命令执行完。因此绝大部分情况,从 CPU 往 GPU 异步提交任务是非常高效的,但部分接口涉及到同步等待,因此会比较耗时,例如显示器显示图像时,需要交换前后缓冲区。
CPU 数据转 GPU 数据
由于移动端设备算力的有限,越来越多的传统算法和 AI 算法计算量又比较高,如果需要把性能做到极致,总避不开使用 GPU 的算力。因此如果 Camera 输出的是 CPU 数据,那就无法避免将 CPU 数据上传到 GPU 数据。
GPU 数据转 CPU 数据
同时由于部分算法如人脸检测算法又必须使用 CPU,以及软件编码器需要的输入数据格式是 I420,因此无法避免从 GPU 往 CPU 的数据格式转换。而提供一种高效的方法能够帮助全链路实现性能最佳。
Android 平台的方法:输出的纹理不仅要包含所有的 YUV 信息,还要方便我们一次性读取 I420 格式数据(glReadPixels)。同时考虑到 YUV 数据的排列情况,因此提出了3种方案。
方案一:
U 和 V 存在虚线部分的内存重叠,因此在计算 I420 时,需要注意 U 和 V 步长 stride 为 w,建议转为标准的 I420 格式。同时按这个内存排列方式,内存占用为w*h*3/2
是一种比较节省内存的方式。
方案二:
标准的 I420 排列方式,但是由于需要作为绘制目标的纹理时,必然是一个连续的内存块,因此实际的内存大小为w*h*2
同时 U 和 V 步长 stride 为 w。
方案三:
该方案的内存排列方式,内存占用为:wh3/2。但是由于类似于 4:1:1 的模式,因此将该部分数据上传 GPU 时,而非标准的 I420 格式,存在 uv 分量显示异常的问题。
综上所述考虑,基于内存占用和 I420 排列的特性,因此我们选取方案一作为 RGB 纹理按 YUV 数据排列转 YUV 的方式。
下图主要表现的是 toI420 接口单帧耗时的测试数据:
3、输入输出类型
Transform 层的输入输出数据类型涉及多个模块的联动,包括 Camera、SDK 外部处理、算法模块、渲染模块、编码模块等,因此是一个复杂的模块。
Camera 的输出类型 Android 平台:Camera1 支持 OES 纹理和 NV21 数据;Camera2 支持 OES 纹理和 I420 数据
Apple 平台:支持 RGB 数据和 NV12 数据的 CVPixelBuffer,NV12 包括 420v 和 420f 两种
渲染的输入类型
Android 平台(OpenGL ES):主要支持 I420 数据、YUV 纹理、OES 纹理、RGB 纹理
Apple 平台(Metal+OpenGL ES):主要支持 I420 数据、NV12 数据、RGB 数据、YUV 纹理、YCbCr 纹理、RGB 纹理
编码的输入类型
Android 平台:硬件编码 MediaCodc(H264、H265)支持纹理和 I420 数据,RTC 场景中软件编码(NE264、NE265、NEVC)支持 I420 数据
Apple 平台:硬件编码 VideoToolBox(H264、H265)支持 NV12 数据的 CVPixelBuffer,RTC 场景中软件编码(NE264、NE265、NEVC)支持 I420 数据
不同平台的数据封装转换
C++ 的数据封装类和渲染各平台的数据封装类之间的关系和转换。渲染是各平台系统 API 强相关的。
Mac 平台和 iOS 平台类似,渲染部分实现是 Metal 和 OpenGL,OpenGL ES 是专门为移动平台设计的,Mac 平台的 Metal 内存存储管理方面和 iOS 不同,其他部分基本相同,因此 Metal 基本可以做到2个平台的共用。Windows 平台则相对简单,目前在 RTC 场景基本主要使用 I420 数据格式。
GPU 基础前处理
RTC 场景的基础前处理主要包括裁剪、缩放、旋转、编码镜像、编码方向等功能,而这些对于图像的操作都可以直接通过纹理坐标和绘制视图来实现。
1、OpenGL的纹理坐标和Metal的纹理坐标区别
DirectX、Metal、Vulkan 三者的纹理坐标相同
2、以 Metal 为例,顶点坐标和纹理坐标的理解
3、那怎么理解裁剪、缩放、旋转、镜像映射到纹理坐标和绘制视图的操作呢?
纹理坐标的排列:{(左下),(右下),(左上),(右上)}
裁剪:可以理解为由蓝色 ABCD 4个点组成的新画面,相对于由红色 ABCD 4个点组成的原始画面,在宽和高上都做了相应的裁剪。裁剪是从坐标 (x,y) 开始,裁剪为宽为 w,高为 h 的画面。体现到纹理坐标为 {(L,B),(R,B),(L,T),(R,T)}。
镜像:可以分为水平镜像和垂直镜像,通常画面镜像都是指水平镜像。那么镜像就是指将红色 A 点和红色 B 点位置互换,红色 D 点和红色 C 点位置互换。体现到纹理坐标为 {(1,1),(0,1),(1,0),(0,0)}。
旋转:图像的旋转操作就是红色 ABCD 4个点的旋转操作。顺时针旋转90度体现到纹理坐标为 {(1,1),(1,0),(0,1),(0,0)}。
缩放:主要是指图像的放大和缩小,如果借助于 FBO,那就是绘制到FBO绑定的纹理大小。OpenGL ES 通过 glViewport 体现,Metal 通过setViewport 体现。
4、题外话:那怎么做到把三维空间中所展示的内容显示到一个二维空间上呢?
那就需要使用到视图变换和投影变换,视频矫正算法就是基于此实现的,涉及多个坐标系的转换。
总结
随着 RTC 业务场景的不断丰富,对于不同场景的算法落地要求也越来越高,即需要实时性,又需要低功耗。与此同时随着硬件的不断发展,硬件的低功耗存在得天独厚的优势,但是由于硬件的兼容性问题,因此在全平台实现 CPU 和 GPU 并行的方案将是当下大趋势。而一个简单合理、高效稳定、可拓展的前后处理框架是算法快速落地的基础。因此我们也在探索一辆 CPU 和 GPU 并驾齐驱的马车,不管是架构设计上还是算法实现上。