QRTC 介绍

首先,我们对 QRTC 做一个简单的介绍。从这张图可以看到,QRTC 是由数据发送的传输端、媒体转发服务以及接收端所组成的。

那具体来讲,比如说我们现在有一个客户端,可能是电脑、手机,或者小程序、网页,它们进行音视频数据收集后,通过我们媒体的链路,把音视频数据传输到七牛云的一个媒体节点上去;然后七牛云的媒体节点进行业务功能处理以后,会经过加速节点,转发到需要的接收方去。业务接收方就可以接收我们的音视频数据,来实现音视频的互动,并只有 100 多毫秒的延时。可以实现语音房聊天、视频直播场景上的应用。

我们再从 SDK 的视角来看,RTC 模块从上到下是如何划分的。

首先从 API 这个层次来说,是作为一个 PaaS 平台对外提供的接口,保证接口的易用和稳定;接下来就是我们 SDK 的实现层,包括了 API 的实现、异常情况的处理、SDK 和服务交互的实现。

再往下去看的话,是设备管理层,我们作为一个音视频的产品,要去管理对应硬件设备上的音视频设备,包括开启、关闭、采集等等一些操作;再接下来可以去把它抽象理解为,是对音视频数据进行处理的一个模块,比如说在娱乐类场景中,要对视频做美颜,或者根据业务需要,做旋转、裁剪等操作。同样音频方面也会根据业务需要做二次处理,比如说做一些效果类的处理,然后做混音或声音大小的处理等;然后再往下就是音视频编码压缩模块,比如说音频使用 OPUS 的编码,视频方面 H.264\H.265 的编码。FEC也可以作为一个 codec 去理解,因为它也是去生成新的数据。

接下来就得到压缩后的数据,对我们的数据进行拆分处理,根据 RTC 应用层的网络传输协议,把它拆分成一个一个的数据包,再去根据视频、音频的不同来进行标识;然后再往下看,可以理解为传输控制的一个层次,它实际上要感知当前网络上的一些情况,对将要进行传输的数据进行速度和大小方面的控制,去适应当下网络的情况。再接下来就是我们的网络传输层,把数据通过网络传输出去。

以上是从发送端的视角,从上至下地来看一个模块划分的情况。如果我们在接收端,右边的视角来看的话,我们可以把它理解为有一个网络传输层去接受音视频的数据,并根据网络传输协议进行解析得到音频、视频,进而可以通过对应的视频解码还原成音视频。这个过程中可能会有对抗网络抖动的模块。比如音频在网络传输过程中,有出现丢失或者乱序的情况。那么我们会对音频数据进行还原处理,然后通过快速播放或者慢速播放,来实现平滑处理。同时视频方面也会有一个渲染速度的控制。

以上就是从 SDK 视角来看一个 RTC 模块的划分。

接下来我们从 RTC 传输的角度,来进一步地了解。在 RTC 音视频交互过程中,可能会出现音频声音不连续、卡顿,视频也可能会出现抖动、卡顿的情况,甚至极端情况下,可能会出现断线、黑屏这些问题。

那么这些问题我们把它抽象把它归纳一下,可以理解为数据源的发送端不能适应当前所在的网络情况。那么再进一步去描述这个问题:发送端应该有什么样的能力呢?

我们的发送端应该能够去感知它所在的网络链路,根据带宽探索获取当前网络的质量情况,做自适应的处理。

然后将得到的可信的带宽,把所需要传输的音频数据、视频数据、冗余数据,按照优先级和业务的需要去完成分配的处理;也就是说当前所在网络的能力是什么样的,就要在传输过程中进行相应的分配。同时,根据当前网络的具体情况,去选取策略做发送端冗余的发送,或者是采取大小流,从传输的策略去适应当下网络的情况。然后在接收端,也可以根据当前接收到的数据,去做恢复、抖动、播放的控制等。来实现我们从发送到接收端,全链路的控制策略,来尽量去平衡和适应当前链路的变化。

媒体质量优化路径

然后刚刚介绍了一些我们 QRTC 的基本情况,我们再来进一步看一下,我们媒体传输质量优化的常用方式有哪些。

那我们可以从发送和接收端的视角去描述常用的一些技术。

如果我们在一段时间内,发现有数据包丢弃的情况,可以根据接收端反馈的重传请求,发送端可以进行丢包重传,这是我们最常见的一个路径策略。

另外就是我们可以使用前向纠错的技术,提前在发送端去发送冗余包的数据,然后去对抗网络传输过程中可能会发生的丢失情况。也就是我们的前向纠错,接收端去进行恢复的情况。

另外一类就是在网络拥塞情况下,可以控制发送端的数据量。比如说视频中调整帧率、分辨率、码率,在音频中做码率处理。

如果基于接收端,我们同样也可以做一些事情。比如说根据音频数据包相关性的特点,可以去做数据包的恢复,如 RTC netEQ 模块。在接收端,如果我们视频存在多路流的情况下,可以根据接收端的网络情况主动接收数据量小的一路流,就是发送端 SVC 或 simulcast 和接收端相互配合,去适应我们当前网络的情况。

音频冗余纠错

下面再来具体介绍音频冗余纠错的具体方案,以及它的实施细节和经验分享。

在介绍具体的方案之前,我们先来对音频编码器,也就是 OPUS 编码器内部的冗余技术进行介绍。

从这张图上可以看出,OPUS 是一个混合的编码器,它实际上是由两种编码器组成的。一个是主要是用于语音信号编码的 SILK,一个是用于音乐信号编码的 CELT。这样就可以让 OPUS 编码器的各个频带质量都比较均衡。

SILK 编码具有 LBRR 的技术,也就是编码器内部的一个冗余。它由于在丢包时触发,可能会有一些延时开始的特点。另外就是它可能考虑到带宽的因素,在冗余出去的时候,实际上会占用我们原始音频的数据量,降低原始音频的编码质量。这是它的一些局限性。

上图右侧是 OPUS 官方给出的数据,图中绿色的线在 20% 左右情况下,能够在一定程度上提高我们主观的感受。但是在更高的情况下,效果就没有保证

下面想和大家分享的问题是,为什么在音频传输过程中要使用冗余模式?

首先,在语音传输过程中,占用的数据量是比较少的,大概 16 到 60 多 kbps 的码率,是常见的音频配置。这个较小的传输量也就决定了它比较适合做冗余发送,来尽量对抗网络中出现的一些异常情况。

另外一点是,对于语音信号来说,在网络波动比较大的情况下,使用重传策略的效果提升可能是有问题的。因为我们的人耳对语音信号是非常敏感的。可以想象一下,假设有一个数据包从 A 端到 B 端,B 端隔一段时间后数据没有收到,就发送一个重传请求给到 A 端。A 端收到这个重传请求再来把这个数据去发送到 B 端。这么一来一回大概是需要 1.5 个 RTT,再加上去发送一个重传请求的等待时间。那么在网络变化比较大情况下,这个时间是比较长的,对语音质量的连续性会比较不好。

另外在实际方案中,我们使用单纯的冗余方式去对我们的语音信号进行处理,那么在我们转发系统对它进行处理的时候,它的复杂度和延时就是非常简单的,对于转发服务非常有利。

介绍完背后的原因,我们再来看一下具体方案的过程中,我们要做哪些事情。

首先我们应该要选取一个数据传输的规范,也就是冗余数据应该以怎样的方式,添加到网络数据当中。这边常见的一个模式就是 RED 规范,上图展示就是这个规范。它能够帮我们定义好原始的数据、冗余数据等不同数据的区域,在网络传输中按照这个规范,进行发送端的组包和接收端的解析工作。

在选取对应的传输协议之后,接下来要做的事情就是要把冗余数据的发送和解析做对应模块的修改。首先音频数据经过编码生成数据之后,插入冗余包的生成模块。生成模块要根据项目的需要选取冗余模式,比如说我们对前面的一个数据进行冗余,或者对两个数据进行冗余,或者是间隔一个数据包进行冗余。生出冗余数据包以后再根据图上的协议规范,把冗余数据和原始数据填充到我们对应的网络包当中,然后通过我们的音频模块发送出去。

发送出去以后,接收端如果接收到冗余数据包,要对它进行识别,然后去解析冗余数据里面的原始数据和冗余数据,完成恢复后再送到音频缓存里,去做对应音频的解码并播放。

冗余数据的解析模块里,实际上要考虑很多细节。比如我们的数据传输过程中,可能冗余数据被恢复出来后,和原始数据重合。那么我们就要根据时间戳来进行去重处理,这样就可以说完成了我们冗余的一个方案。

另外就是在 RTC 转发服务中需要考虑的一个实际情况,由于 RTC 是由转发服务和各个不同的参与端组成的。如果我们的参与端只有部分支持冗余数据的接收,也就是异构的场景下,需要额外处理。

那么简单来说,转发服务对于参与方,需要去提前识别它的能力。假设一个参与方支持冗余数据的发送和接收,当它把冗余数据发送出去以后,转发服务需要去识别其他参与方的能力。如果是不支持冗余数据的处理,SFU 要把冗余数据进行拆解,把原始数据发送给接收方。如果对方支持冗余数据处理,那么直接把冗余数据分发给对应的接收方。

在实际的项目过程中,考虑到冗余数据的复杂度比较低,只要去解析数据就可以。实际上 SFU 可以把原始数据和冗余数据拆分,做恢复以后再发送给接收方,来提高利用效率。

那从我们评价体系来讲,可以结合下图来看。假设我们使用音频的冗余数据到 RTC 产品当中去,可以看到大概在 50% 左右的网络丢包的情况下,都能够有比较好的音频连续性和质量保证。对于用户在提升各种不同网络情况下的质量,是非常有帮助的。它的实现难度也是相对比较简单。

以上就是我们音频冗余方案的实施的过程和它的评价结果。

视频冗余纠错

接下来为大家介绍视频冗余方案。首先介绍一下纠错码相关的情况。刚才我们讲过音频,由于它本身的特点可以实现简单重复传输这种冗余的模式,但由于视频的数据量比较大,我们一般会对视频数据进行算法级别处理,去生成对应的冗余数据,再根据算法恢复出来,而不是做简单的重复。

上图可以看到我们纠错码技术的简单介绍。在我们的通信系统中,可以把数据的流向做这样的划分。我们的数据源经过我们的编码,传输到解码模块,再到解码模块恢复后的数据消费。那在编码这个层面,实际上可以划分为两个部分,一个是数据源的编码,它的目的是把数据冗余除掉。另外一个部分是对信道的编码,目的是为了去增加冗余,来对抗信道传输过程中可能存在的噪声、丢失等情况。

那为什么纠错码发展有它的适用场景?比如说在卫星通信这样的领域,如果要去实现重复传输的话,显然链路特点造成重复传输的延时非常大,如果要做重复传输,就表示传输系统中需要做反馈的,它是一个双向通信的模型,从成本角度而言也非常不好。所以这是纠错码发展的背景。

通常来说,纠错码实际上可以分为分组码和卷积码两大类。卷积码会把历史数据参与到生成当前冗余包的过程中来。分组码就是常见的一个 Hamming 码,Hamming 码实际上以比特为单位来生成纠错数据,去进行一部分的恢复,这是在打孔机年代发明出来做数据恢复的。另外就是 Reed-Solomon 纠错码,它的变体是 BCH 码。

介绍了纠错码相关的背景知识,我们再来看一下用异或来生成前向冗余纠错的情况。因为 RTC 去实现视频的前向纠错,运用的就是 XOR 方案,所以我们要了解它的特点和限制情况。

上图左侧描述的是最基本的 XOR 方式。有一个数据包是 001,有一个数据包是 101,它通过 XOR 的运算,可以生成一个数据包是 100。如果这三个数据包当中有一个被丢弃,那么就可以通过 XOR 的运算去恢复。假设 101 这个数据包丢弃,显然我可以通过 001 和 100 进行 XOR 运算将其恢复。

那在实际过程中,我们可以使用多次冗余的方式来提高一定的恢复效果。比如说这张图上所描述的,我们可以用 A 和 B 生成一个冗余数据包 E,然后用 B 和 C 生成一个冗余包的 F。如果 A 和 C 同时丢失的情况下,我可以用 B 和 E 恢复 A,由 B 和 F 恢复出 C。当然这里面也有一个隐含的条件,就是如果我用 AB生成了 E,那 A 和 B 同时丢失的情况下,显然这个数据就无法恢复了,因为使用 XOR 方式的特点,就是参与 XOR 的数据只能恢复出其中一个数据包。

下面我们再来介绍一下,基于 XOR 处理模式的典型方案。

如上图所示的两种方式,一种是 ULP 的 XOR 生成方式,即 Uneven Level Protection ,它的核心就是对数据重要性做差异化保护。比如说我可以用 P1 和 P2 得前半部分生成一个冗余数据 F1,同时用 P1、P2、P3、P4 的前半部分生成一个冗余数据,放在 F2 里面的前面部分。然后 P1、P2、P3、P4 里面后面部分生成冗余数据,放置在 F2 里面 level1 的这个部分。假设 P1、P2 前半部分是比较重要的数据部分,它可以在 F1 里面做一个 XOR 的保护。同时在 F2 里面的 level0 这一部分数据,做了二次的重要性保护。

实际上它假设大的前提,就是数据包是根据重要性来划分去做拆解的。实际上对于我们编解码器后续的拆分与恢复,在落地方面会有比较大的问题。也就是说,RTC 里面是没有实现标准 ULP 的一个 XOR 冗余的。

另外一种方式就是 flexible 的 XOR 冗余生成方式。它想做的是把发送端的数据包,按照行列的方式先组织起来,然后对行和列去形成冗余数据。从上图右侧可以看到,是以四行四列分别生成了冗余数据。那么在 P1、P2、P3、P4 全部丢弃的情况下,我们可以通过列的方式,把 P1、P2、P3、P4 都给恢复出来。如果 P1 丢失的情况下,我可以用这一组恢复出 P1 数据,同理恢复出 P2、P3、P4 这样的情况。

在实际应用中,用四行四列的数据来做处理,所占用的冗余是比较大的。以图上的情况为例,大概有 50% 的冗余。另外它的特点也是之前所描述的一个既定原理,假设特定的数据包有两个不能恢复,那这个恢复就不能进行。所以说它的特点就是冗余量比较大,恢复情况受一些限制。

然后接下来我们介绍了 RTC 自带的 XOR 的冗余之后呢,我们再来介绍一下,我们自定义的,就是刚刚介绍到的 RS 的纠错码一个相关的情况。

RS 纠错码是把数据按照一个一个的 symbol 来进行划分,把多个比特作为一个块去处理,把每个比特做一个多项式的运算,生成冗余数据。那按照图式标准定义,可以看到它有 K 个原始数据包,原始的数据包和冗余数据包有 N 个的话,就可以去做任何恢复出 t 个 symbol 的情况。基于这样的特点,它可以比较适合在网络传输过程中去应对任何可能发生的情况。

由于 m 个 bits 组成的 symbol,它需要对于多项式进行矩阵运算,它的复杂度是存在的。实际过程中会有一个优化的版本来去降低复杂度,就是基于柯西矩阵来改造的一个版本。有兴趣的同学可以去看一下后面的具体算法细节。

那么在具体项目实施过程中,我们需要做哪些动作呢?首先如果使用一个自定义的冗余,我们对哪些数据包参与了冗余的生成,生成了对应的新的冗余包信息,我们要在网络传输中进行标识,包括它的大小等等情况。如下图所示,就是FEC headr 要带上的相关信息,然后再来放我们冗余数据的负载。

同时我们在实现过程中,可能要注意一个情况,就是假设有 A 和 B 两个数据,经过我们的纠错码生成 C 这个冗余数据。通常来讲我们的转发服务会对数据包中扩展字段和一些头信息做一定的修改。如果我们不知道这个情况的前提下,假设 A 数据和 C 数据变成 A' 和 C',显然我们对它进行逆恢复的情况下,是不能恢复出 B 这个数据的。因此要协商处理,如果不知道这个情况,进行处理的过程中如果出问题的话,定位是非常困难的。

下面我再来介绍一下,在发送端和接受端要实现自定义冗余所涉及的模块,以及修改大概有哪些部分。

首先就是我们的视频数据,编码以后再经过打包模块,生成了一个一个数据包。那这些数据包一方面是用来在视频发送模块进行发送,另一方面需要去送到冗余数据的生成模块。需要我们把一组数据包,根据冗余的算法去生成新的冗余包,也同样通过视频发送模块送到网络当中去。

这个模块要考虑细节有哪些呢?首先是生成的视频冗余量要多少,可以去设定一个固定值,或者是根据当前网络情况做一个自适应。还有冗余数据,是对视频一帧一帧进行数据冗余的生成,还是说多帧生出数据?都是我们可以调试的一些具体的情况。

然后视频原始数据和冗余数据发送出去后,经过接收端我们需要处理的模块是什么?先就是网络数据的解析模块,区分处理冗余数据包和媒体数据包。如果是正常的媒体数据包就送到包缓存模块做缓存和解码。如果是冗余数据包,则需要把数据包送到 FEC 的解析模块里面去。这个解析模块要完成的事情,就是要把网络包当中所标识的有效数据包情况收集起来,当它达到我们恢复的条件以后,对数据包进行恢复。进而将恢复的媒体数据包送到我们包缓存里面。同样,如果当数据包有重复的情况,我们要对重复的数据进行额外处理,避免后面的解码模块产生问题。

最后我们使用自定义的纠错方案,在分发系统中也要考虑异构能力的问题。如果参与方对冗余数据支持能力不同,分发服务就需要识别对应的参与方能力,做额外分发处理。

那么假设在用了冗余方案的情况下,它的衡量指标和结果如何呢?上图描述了视频使用冗余方案,可以看到在丢包比较高的情况下,对比红色线不使用冗余的时候,它的主观评分显然是要更高的。

从对重传数据优化的视角来看,如果使用冗余数据的情况下,因为接收端实际对网络传输过程中丢失的数据已经进行了一些恢复,所以说它可以减少接收端重传请求以及到重传数据的发送。那么从这张图表示的情况来看,蓝色这条线展示的重传比例,比不使用冗余数据情况下是明显比较低的。

另外我们也可以从视频卡顿率这个偏主观的视角来评判使用冗余方案的情况。在丢包比较高的情况下,蓝色这条线所展示出的卡顿,要比优化前卡顿率明显更低,这也是可以提升产品体验的一个实际的情况。