您的位置:首页 > 编程语言 > PHP开发

流媒体传输协议之 RTP (上篇)

2021-02-09 16:08 686 查看

本系列文章将整理各个流媒体传输协议,包括 RTP/RTCP,RTMP,希望通过深入梳理协议的设计细节,能够给流媒体领域的开发者带来一定的启发。

作者:逸殊
审核:泰一

介绍

RTP,即 real-time transport protocol(实时传输协议),为实时传输交互的音频和视频提供了端到端传输服务。其中包括载荷的类型确认,序列编码,时间戳和传输监控功能。一般应用都是基于 UDP 协议,来使用 RTP 的多路技术以及验和服务。然而,RTP 还可以与其它适合的协议并用,如果底层网络支持多路分发,RTP 还可以将数据传输给多个目标。

需要注意的是 RTP 不提供任何机制以保证数据的实时性和 QOS (quality-of-service),而是依赖底层的服务来提供这些功能,RTP 既不保证传输的可靠性也不保证无序传输,同时也不假定底层网络是可信任的和有序的。接收端可以利用 RTP 中的序列号排序收到的报文。

RTP 与 RTCP

  • 实时传输协议 (RTP),传输具有实时特性的数据

  • RTP 控制协议 (RTCP),监控 QOS 和传递会话中参与者的信息。它没有明确的成员控制功能和 Session 建立过程,但这些对一个相对宽松的 Session 控制来说已经足够了,它没有必要包含一个应用的所有控制功能。

RTP 代表了一种新型协议,它遵循 Application level framing 和 Integrated layer processing。即 RTP 可以比较容易的拓展以传递某些特定需要的内容,而且可以比较容易地集成进某个应用,而不是作为一个独立的补充层。RTP 协议被故意地设计成不完整的协议框架。

RTP 的使用场景

下面的例子描述了 RTP 的部分特性,选择的例子是用来阐明基于 RTP 的应用的基本操作,而不是说 RTP 仅能用于此类应用。

简单的多播音频会议

一个小组要通过网络开一个音频会议,他们用了 IP 多播服务。基于某种分配机制,小组得到了一个多播组地址和一对端口,其中一个端口是用来传输音频数据的,另一个是用来传输 RTCP 报文的。这个组播地址和端口发给了所有与会者。如果想要引入一些安全策略,可以对数据报文和控制报文加密,然后把加密时用到的密钥分发给与会者。

这个音频会议软件,可能会一直发送时长为 20ms 的音频数据包。每个实际音频数据包,都以 RTP 头数据开始,然后再以 UDP 协议封装并发送。RTP 包的头部标识了该包的数据类型,以便消息发送器来改变数据的编码。例如,针对低带宽的与会者进行一些调节,或者对网络拥堵作出反应。

像 UDP 这类包类型的网络,偶尔会丢包,乱序,延迟不定长时间。为了解决这类意外情况,RTP 包中包含了时间信息和序列号,这样接收者就可以通过它们重排数据包的时序。在这个例子中,我们就可以按顺序地播放每个 20ms 的音频数据。在会议中对每个数据源的 RTP 报文时序重排都是独立进行的。接收者也可以通过序列号来确定丢失了多少报文。

因为这个小组开会期间,会有一些人加入或退出这个网络会议,所以我们需要知道具体是谁加入了会议,以及他们有没有正常地接收到音频数据。出于这个目的,每个网络会议的客户端都会周期性的通过 RTCP 端口报告使用者的名字以及自己接收数据的情况,如果有人接收数据不正常,可能就需要对应的改变编码。而且,除了用户的名字之外,还会有一些别的信息,用来控制带宽限制。当有人从视频会议中退出时,还需要发送一个 RTCP BYE 报文。

音频和视频会议

如果这个会议既要传输音频又要传输视频的话,它们会以独立的 RTP Session 传输。也就是说,负责音频传输的部分和负责视频传输的部分会通过不同的组播地址(和端口对)分别传输各自的 RTP 报文和 RTCP 报文。在 RTP 协议这一层,音频和视频 Session 并没有被组合到一起。我们期望与会者用同一个名字来建立音频和视频 Session,这样这两个 Session 就能联系起来了。

RTP 协议之所以这样设计,一个原因是某些与会者可以选择只接收某一种类型的数据(只接收 Audio)。即便 Audio 数据和 Video 数据是独立分发的,但是我们仍然可以通过参考 RTCP 协议中时间信息来同步播放它们。

Mixers & Translators

到目前为止,我们都是假设所有的与会者想要接收同一格式的媒体数据。但是这显然不太合适,考虑一下,可能某些与会者网速相对较慢,而其他人网速却比较快。对于这种情况,我们不应该强迫所有人都用低带宽并降低音频编码的质量,而是使用 RTP 级别的中继节点(Mixer)来给周围低带宽用户分发低带宽消耗的数据。

这个 Mixer 将接收到的不同与会者的音频数据同步,并将它们耦合到一个单一流中,然后将这个流用低带宽消耗的编码方案进行压缩,最后发送给那些低带宽的与会者。Mixer 可以在 RTP 头部写一些特殊内容,来表明该 Mixer 包具体耦合了哪些与会者,这样,接收到该 Mixer 包的人就能确定当前说话的人是谁了。

此外,有些与会者可能处于应用级防火墙的后面,无法仅通过 IP 组播访问。这种情况下 Mixer 就没有什么意义了,他们需要另一类 RTP 级别的中继(Translator)。我们需要两个 Translator,安装在防火墙的两面,外面的 Translator 将收到的所有组播报文,通过一个安全连接传输给防火墙里面的 Translator。然后,防火墙里的 Translator 再将这些报文分发给内网的与会者。

层编码

多媒体应用可以根据接收者的能力或者网络拥堵的情况调整传输速率。许多实现将码率控制的责任放在了发送端。这和组播模式不太兼容,因为各个不同的数据接收者会有不同的带宽情况,这就会产生木桶效应,即带宽最差的接收者会拖垮整个会议的通讯质量。

因此,带宽自适应的工作应该放到接收者这里,发送者需要拆分出面向不同带宽与会者的媒体流(500K,2M,5M),它们分别对应了不同的组播地址,数据的接收者根据自己的带宽情况,选择加入适合的组播。

定义

  • RTP payload:RTP 包中传输的数据,比如音频采样数据或者压缩过的视频数据。
  • RTP packet:由定长 RTP 头部,数据来源者的列表,RTP payload 组成的数据包。一些下层协议可能会自己定义 RTP 的封装格式。一般来说,一个下层协议包只包含一个 RTP 包,但是也有可能多个 RTP 包被合并到一起。
  • RTCP packet:RTP 控制报文,由定长的 RTC 头部开始,之后会跟着一些结构化的元素,它们在 RTCP 发挥不同功能时,会有不同的结构。通常多个 RTCP 包会被合在一起,通过一个下层协议包一起发送。
  • Port:传输层协议中用来区分某一主机下不同应用的抽象。RTP 协议依赖更底层网络提供端口机制,继而提供多播的 RTP 和 RTCP 报文。
  • Transport address:网络地址和端口的组合,用来定位传输层的节点。
  • RTC media type:一个 RTP Session 中所用到的所有 payload 类型的合集。
  • Multimedia Session:视频会议组中同时工作的一组 RTP Session。例如,视频会议中的 Audio Session 和 Video Session。
  • RTP Session:一组参与者利用 RTP 来通讯的组合。一个参与者可以同时加入到多个 RTP Session 中。在 Multimedia Session 中,除非特意将多媒体编码进同一数据流,否则,每个数据流会通过不同的 RTP Session 传输。与会者通过 Transport address 来区分不同的 RTP Session。同一 RTP Session 的不同与会者会共享同一个 Transport address,也可能每个与会者都有自己的 Transport address。在单播的情况时,一个与会者可能用同一对端口(RTP&RTCP)来接收所有其他与会者的数据,也可能对不同的与会者采用不同的端口对(RTP&RTCP)。
  • Synchronization source (***C):RTP 报文流的一个 Source,由 RTP 头中定义的 32-bit 的 ***C identifier 来标识,这样做是为了不依赖网络地址。同一个 ***C 中发送的所有包都具有同一时序和序列号间隔,因此接收者可以通过 ***C 将收到的数据包分组并排序。一个信号源(麦克风,摄像头,Mixer)的报文流会有由一个 ***C 的发送器发送。一个 ***C 可能会随着时间的变化,改变其数据格式,例如音频编码。***C 的身份识别码都是随机生成的,但是必须保证整个 RTP Session 中该身份识别码不会重复,这些工作是通过 RTCP 来完成的。如果一个与会者在一个 RTP Session 中发送不同的媒体数据流,那么每个流的 ***C 必须不同。
  • Contributing source (CSRC):RTP Mixer 所混合的所有数据对应的 ***C 的列表。Mixer 会将一个 ***C 列表写入 RTP 头中,该列表包含了这个混合报文中包含的所有来源 ***C。
  • End system:一个生成 RTP payload 和消费收到的 RTP payload 的应用。一个 End system 可以扮演一个或者多个 ***C 角色,但是通常是一个。
  • Mixer:一个中介系统,它接收一个或多个 Source 的数据,随后它可能会改变这些数据的格式,并将它们合并为一个新的 RTP packet。因为,多个输入源的时序通常来说都不一致,所以 Mixer 通常会同步不同源的时间,并生成一个自己的时序来处理合并数据流。所有从 Mixer 输出的数据包都会标记上该 Mixer 的 ***C。
  • Translator:一个中介系统,它会转发 RTP packet 但是不改变其原本的 ***C。
  • Monitor:一个在 RTP Session 中接收 RTCP 报文的应用,它会总结数据被接收的报告,并为当前分发系统评估 QOS,诊断错误,长期统计。Monitor 可以集成进会议应用中,也可以是独立的第三方应用,只接收 RTCP 报文,但是什么都不发送。
  • Non-RTP means:为了让 RTP 提供可用服务而加入的协议或者机制。特别是在多媒体会议中,需要一种控制协议来分发组播地址和加密密钥,协调加密算法,定义 RTP payload 格式和 RTP payload 类型的动态映射。

字节序,数据对齐,时间格式

所有的整数字段都使用网络字节序(大端序),除了特别声明,数字常量由十进制表示。

所有头部数据都会根据其数据的原始长度进行对齐,比如,16-bit 的数据会对齐到偶数偏移,32-bit 的数据会对齐到可被 4 整除的偏移。此外,用 0 来作为填充字节。

Wallclock time(绝对日期和时间)是用网络时间协议(NTP)的时间格式来表示,即从 1900 年一月一日 0 点到现在的秒数。NTP 的时间戳使用了 64-bit 的无符号固定小数点的形式表示,其中头 32-bit 用来表示整数部分,后 32-bit 用来表示小数部分。RTP 的时间格式采用了 NTP 的简化版,他只用了 NTP 的 64-bit 数据的中间 32-bit,即前 16-bit 表示整数,后 16-bit 表示小数。

NTP 时间戳到 2036 年就会循环回 0,但是因为 RTP 只会使用不同 NTP 时间的差值,所以这不会有什么影响。只要一对时间戳都在同一个循环周期里,直接用模块化的架构相减或者比较就可以,NTP 的循环问题就不重要了。

RTP 数据传输协议

RTP 的定长头字段

RTP 头的格式如下:

上图中前 96-bit 的数据是每个 RTP 包都有的部分,CSRC 部分只有 Mixer 发送的报文才会有。这些字段的意义如下:

  • Version(V):2 bits,RTP 版本号,现在用的是 2。(第一个 RTP 草案用的 1)
  • Padding(P):1 bit,如果设置了该字段,报文的末尾会包含一个或多个填充字节,这些填充字节不是 payload 的内容。最后一个填充字节标识了总共需要忽略多少个填充字节(包括自己)。Padding 可能会被一些加密算法使用,因为有些加密算法需要定长的数据块。Padding 也可能被一些更下层的协议使用,用来一次发送多个 RTP 包。
  • Extension(X):1 bit,如果设置了该字段,那么头数据后跟着一个拓展数据。
  • CSRC count(CC):4 bits,CSRC 列表的长度。
  • Marker(M):1 bit,Marker 会在预设中进行定义(预设和 RTP 的关系可以参考 rfc3551,我的理解是预设是对 RTP 的补充,以达到某一类实际使用场景的需要),在报文流中用它来划分每一帧的边界。预设中可能会定义附加的 marker,或者移除 Marker 来拓展 payload type 字段的长度。
  • Payload type(PT): 7bits,该字段定义 RTP payload 的格式和他在预设中的意义。上层应用可能会定义一个(静态的类型码 <->payload 格式)映射关系。也可以用 RTP 协议外的方式来动态地定义 payload 类型。在一个 RTP Session 中 payload 类型可能会改变,但是不应该用 payload 类型来区分不同的媒体流,正如之前所说,不同的媒体流应该通过不同 Session 分别传输。
  • Sequence number:16 bits,每发送一个 RTP 包该序列号 + 1,RTP 包的接收者可以通过它来确定丢包情况并且利用它来重排包的顺序。这个字段的初始值应该是随机的,这会让 known-plaintext 更加困难。
  • Timestamp:32 bits,时间戳反映了 RTP 数据包生成第一块数据时的时刻。这个时间戳必须恒定地线性增长,因为它会被用来同步数据包和计算网络抖动,此外这个时钟解决方案必须有足够的精度,像是一个视频帧只有一个时钟嘀嗒这样是肯定不够的。如果 RTP 包是周期性的生成的话,通常会使用采样时钟而不是系统时钟,例如音频传输中每个 RTP 报文包含 20ms 的音频数据,那么相邻的下一个 RTP 报文的时间戳就是增加 20ms 而不是获取系统时间。和序列号一样时间戳的初始值也应该是随机的,而且如果多个 RTP 包是一次性生成的,那它们就会有相同的时间戳。不同媒体流的时间戳可能以不同的步幅增长,它们通常都是独立的,具有随机的偏移。这些时间戳虽然足以重建单一媒体流的时序,但是直接比较多个媒体流的时间戳是没办法进行同步的。每一时间戳都会和参考时钟(wallclock)组成时间对,而且需要同步的不同流会共用同一个参考时钟,通过对比不同流的时间对,就能计算出不同流的时间戳偏移量。这个时间对并不是和每个 RTP 包一同发送,而是通过 RTCP 协议,以一个相对较低的频率进行共享。
  • ***C:32 bits,该字段用来确定数据的发送源。这个身份标识应该随机生成,并且要保证同一个 RTP Session 中没有重复的 ***C。虽然 ***C 冲突的概率很小,但是每个 RTP 客户端都应该时刻警惕,如果发现冲突就要去解决。
  • CSRC list:0 ~ 15 items, 32 bits each,CSRC list 表示对该 payload 数据做出贡献的所有 ***C。这个字段包含的 ***C 数量由 CC 字段定义。如果有超过 15 个 ***C,只有 15 个可以被记录。

RTP Session 多路复用

在 RTP 中,多路复用由目标传输地址(address:port)提供,不同的 RTP Session 有不同的传输地址。

独立的音频和视频流不应该包含在同一个 RTP Session 中,也不应该通过 payload 类型和 ***C 来区分不同的流。如果用同一个 ***C 发送了不同的数据流,会引入如下问题:

  1. 假设 2 个音频流共享了一个 RTP Session,并且用了同一个 ***C,如果其中一个要改变编码,这就导致了 payload 类型的改变,但是协议中没有提供方法来让接收者知道具体是哪个音频流改变了编码。

  2. 一个 ***C 只有一个对应的时序和序列号,如果多个流有不同的时钟周期的话,就需要不同的时序。而且还不能用序列号来确认是哪个流丢包了。

  3. RTCP 发送者报告和接收者报告只描述了时序和序列号而不包含 payload 类型数据。

  4. Mixer 无法将不兼容的两个流合并。

  5. 如果一个 RTP Session 中包含了多个媒体流后就会失去如下优势:
      使用不同的网络路径或者分配网络资源
    • 只接收某一种媒体数据(网络较差时只接收 audio)
    • 接收方对不同的媒体类型做不同的处理

不同的流使用不同的 ***C 但是仍然用同一个 RTP Session 发送确实可以解决前三个问题,但是仍然无法解决后两个问题。

预设可能对 RTP 头的改动

现有的这些 RTP 报文头对一般应用来说已经足够了。如果有需要,头字段可以根据预设进行一些修改,但仍要保证检测和统计功能的正常使用。

RTP 头拓展

RTP 提供了一个拓展机制,让上层应用可以将自定义的信息存储在 RTP 报文头。如果上层应用收到了无法识别的头部拓展数据,它们会忽略它。

值得一提的是,这个头部拓展是有一些限制的。如果附加信息只对某些 payload 格式才有意义,那么最好还是别把这些信息放到头部拓展中,而是放到 payload 部分。


如果 RTP Header 中的 X 位设置为 1,那么 Header 后必须跟着一个不定长度的拓展块,紧跟着 CSRC list(如果有的话)。拓展部分的头部包含一个 16-bit 的数据来描述拓展块包含多少个 32-bit 字(不包括拓展部分的头部)。因为 RTP 头部后面只能连接一个拓展块,考虑到有些应用可能会有多种类型的拓展块,所以拓展块的头 16-bit 留给开发者去自定义一些参数。

RTP 控制协议

同一个 Session 所有参与者会周期性地发送控制报文,RTP 控制协议就是通过这种方式进行的,和 RTP 数据的传播一样采用了组播的机制。下层协议必须提供数据包和控制报文的多路复用功能,例如使用独立的 UDP 端口分别传输数据和控制报文。RTCP 协议具有如下四大功能:

  1. 最主要的功能是反馈数据分发的质量。这也是 RTP 作为一个传输协议来说最关键的功能,而且它和流量控制,拥塞控制息息相关。反馈信息可能会直接影响自适应编码的控制。发送反馈报告给所有的参与者可以让它们评估遇到的数据分发问题是个人问题还是全局问题。通过 IP 组播这样的分发机制,像网络提供商这样的机构即便不加入到这个 RTP Session 中也能收到反馈信息,它们会扮演一个第三方监测者的角色去确认数据分发问题。这个反馈的功能无论是 RTCP 的发送者还是接收者都会进行报告。

  2. RTCP 还会给每个 RTP source 带一个不变的传输层身份识别符(CNAME),因为 ***C 可能会中途改变(程序重启),所以接收者需要这个 CNAME 来持续追踪每个与会者。而且,接收者可以通过 CNAME 来将同一个与会者的所有数据流联系在一起,比如同步音频和视频。单个媒体内部的数据同步也需要 NTP 和 RTP 时间戳,这些数据都在数据发送者发送的 RTCP 报文中。

  3. 因为前两个功能需要所有的与会者都发送 RTCP 报文,所以需要适当的控制报文发送的频率以保证 RTP 协议可以在大量客户端一同加入时也能正常工作。通过每个参与者都广播控制报文的方式,每个人都能独立地计算出参与者的总数。

  4. 还有一个可有可无的功能,RTCP 可以用来共享小量的 Session 控制信息,例如辨认参与者的身份。通常来说,该功能会被那些管理比较松散的 Session 使用。RTCP 可以作为一个方便的与其他参与者沟通的通道,但是你也别期望 RTCP 可以满足一个应用的所有传输控制需求,这类需求往往是通过一个更高层的 Session 控制协议来满足。

这四个功能中,前三个应该会被所有应用场景使用(IP 组播机制下)。RTP 应用的设计者应该避免自己的应用只能工作在单播模式,RTP 应用应该设计成可拓展的,要考虑大量使用者并发时的情况。此外,RTCP 的传输应该根据发送者和接收者角色的不同而分别进行控制,例如一些单项连接,接收者的反馈信息就发不出来。

提醒:像是指定源组播路由(SSM),只有一个人可以发送数据,其他接收者不能用组播来和其他人直接通讯。对于这种情况,建议完全关闭接收者的原始 RTCP 功能,然后为这个 SSM 设定一个 RTCP 的适配器,来接收所有的反馈。

RTCP 包格式

RTCP 定义了许多包类型来传输不同的控制信息:

  • SR:发送者报告,发送者数据发送和接收的统计。
  • RR:接收者报告,只接收数据的节点的接收统计。
  • SDES:Source 描述,包括 CNAME。
  • BYE:表示退出。
  • APP:上层应用自定义。

每个 RTCP 包都有一个和 RTP 类似的固定格式的头,后面跟着长度不定的结构化数据,在不同 RTCP 类型时,这些结构化数据各不一样,但是它们必须都要 32-bit 对齐。RTCP 的头部是定长的,而且在头部有一个字段来描述这个 RTCP 数据的长度,因此 RTCP 可以被复合成一组一同发送,还不需要任何分隔符来分割出单个的 RTCP 包。下层协议可能会根据自己的情况决定将多少个 RTCP 报文复合在一起组成一个复合包。

复合包中的每个独立的 RTCP 报文都是无序的,而且可能会被随意复合。为了让协议的功能正常运作,会有如下限制:

  • 接收统计(SR|RR)的发送频率需要达到带宽的最大限制,因此每个周期发送的 RTCP 复合包都需要包含一个这类报文。
  • 一个新来的接收者需要尽可能快地得到数据源的 CNAME,因为它要用 CNAME 来确定每个数据源分别对应哪个人,并将数据源联系在一起进行同步,所以每个 RTCP 复合包必须包含 SDES CNAME(除非这个复合包被拆成两半一半加密,一般明文,这部分后面会介绍)。
  • 复合包中包类型的数量需要限制,这可以减少其他发错的包或者不相关的包被识别成 RTCP 包的可能性,还能增加第一个字中固定比特的数量。

因此,一个复合包中至少需要含有 2 种类型的 RTCP 报文,它的格式如下:

  • Encryption prefix:当且仅当这个复合包需要加密的时,那复合包在头部插入一个随机的 32-bit 数。如果加密算法需要填充数据的话,需要填充到复合包中的最后一个 RTCP 包后。
  • SS 或 RR:复合包中第一个 RTCP 包必须是一个报告报文,这可以加速报文头部数据的校验。即便没有 RTP 数据的发送和接收也要有一个报告报文,这种情况下必须发送一个空的 RR 报文,并且即便是这个复合包中的其他 RTCP 报文是 BYE 也要这么做。
  • Additional RRs:如果接收的 RTP 数据来自超过 31 个不同的源,前 31 个接收报告会写进 SR 或者 RR 报文中,多出来的接收报告应该紧跟着默认的报告报文(SR 或 RR)。
  • SDES:SDES 包必须包含 CNAME,每个复合包必须包含一个 SDES 包。如果上层应用有需要,也可以加入一些别的 SDES 报文,这视带宽限制而定。
  • BYE 或 APP:其他 RTCP 包类型(包括协议中还未定义的),可能以任意顺序跟在 SDES 后面,但是希望 BYE 包写在最后面(BYE 包需要和 ***C/CSRC 一同发送)。

一个单独的 RTP 参与者应该在一个报告周期中只发送一个复合 RTCP 包,该周期每个参与者应该视带宽情况来估算,除非一个复合包被拆分加密。如果数据发送者的数量太多,以至于除了增加 MTU 这个方法之外,没办法将所有 RR 报文塞进一个复合包时,那么一次只会将部分 RR 数据塞进这个复合包,其他的数据就不发送了。当然,为了让所有源的接收情况都得到报告,会在多个周期内以环的形式循环共享所有源的接收情况。

为了减少数据包的开销,一般建议 Translator 和 Mixer 无论何时都能将多个源的 RTCP 报文复合成一个复合包。下图展示的就是一个 Mixer 生成的复合包的例子:

如果一个复合包的长度超过了下层网络协议的 MTU 的话,这个复合包会被拆分成多个更小的复合包分别发送。这不会对 RTCP 的带宽估计产生任何影响,因为即便 Mixer 的复合包被拆分成了多个更小的复合包,但是这个些更小的复合包也要满足 "每个复合包都要包含 SS 或 RR" 这一条件,所以每个更小的复合包至少也对应了一个参与者,这样 Mixer 生成的复合包就和它收到的 RTCP 包数量基本匹配,甚至更少。

如果某一客户端收到了它无法解析的 RTCP 类型的包,那它应该忽略这个包。附加的 RTCP 包类型会通过 IANA 进行注册。

RTCP 传输周期

RTP 的设计理念是它要能根据 Session 参与者的人数增加而进行自适应处理。例如,音频会议中同一时刻说话的一般也就那么一两个人(这就从内部限制了音频数据的传输),那么可以认为组播数据分发所用到的带宽资源和与会人数无关。控制信息的发送和音频数据的传输不同,每个人都会不停的发送 RTCP 报文,如果每个参与者的接收报告以同一个周期发送的话,RTCP 报文传输所消耗的资源会随着与会人数的增加而线性增加。因此,当与会人数增加时,RTCP 报文的发送间隔应该相应的动态地增大。

对每个 Session 来说,会有一个总的带宽限制(Session bandwidth),它会被分配给每个独立的与会者。整个网络的带宽可能会有所保留,并从网络层面强制限制 Session 的带宽。如果网络的带宽没有保留的话,也可能会有一些别的限制,不过这些都跟网络环境有关,总之最后会得出一个靠谱的 Session 最大带宽。Session 带宽可能会通过实际会消耗的网络资源进行评估,或者中途根据 Session 的剩余可用带宽来变化。

这些都和媒体数据的编码无关,但是会根据带宽的限制来选择具体使用哪种编码。通常来说,会预估 Session 中有多少参与者会同时发送数据,然后根据同时发送这类数据大概需要多少带宽这种方式来评估 Session 的带宽。在音频会议中,通常来说就是一个音频发送者所需要的带宽(一般同一时间只会有一个人说话)。对于分层编码这种情况,每一层都在一个独立的 RTP Session 中,这些 Session 都有自己独立的带宽限制。

在 RTP Session 中应该有一个管理应用来调整 Session 带宽,但是那些音频会议应用可能会基于 Session 中选用的编码格式,假设只有一个发送者发送数据,给自己设定一个默认的带宽限制。这个音频会议应用可能也会受到组播网络(或其他因素)的带宽限制。同一个 Session 的所有参与者必须使用统一的 Session 带宽限制,因为只有这样大家才是以一个相同的频率发送 RTCP 包。

Session 带宽评估过程需要考虑到下层的传输层和网络层是否有一些资源保留机制。而且上层应用也需要知道 RTP 下层使用了什么协议,但是不需要知道数据链路层及以下的协议,因为从数据链路层开始数据包的头就各不相同了。

控制报文的传输应该只使用 Session 带宽中很小的一部分,这样媒体数据的传输才会不受影响。建议 RTCP 传输使用 Session 带宽的 5%,媒体数据发送者至少要占用 1/4 的 RTCP 带宽,因为这样做的话,新加进来的人可以更快的收到媒体数据发送者的 CNAME。在某些预设中,如果发送者的数量超过 1/4 可能会完全关闭接收报告,虽然 RTP 协议标准并不推荐这样做,但是那些只有单向链路的系统或者不需要接收者反馈的系统一般是这么做的。

RTCP 报文的传输间隔一般都会稍微长一点,这样,当参与者的数量陡增时,报文的数量就不会超过带宽限制太多。当一个应用启动时,它应该等一段时间(一般是最小 RTCP 报文间隔的一半)再发送第一个 RTCP 报文,这样这可让发送间隔的计算更快的收敛。推荐 RTCP 报文发送的最小间隔是 5 秒。

RTP 的上层应用可能会使用更短的 RTCP 发送间隔,但是也会遵循如下原则:

  • 对于组播形式的 Session,只有数据发送者会使用更短的 RTCP 发送间隔。
  • 对于单播形式的 Session,无论是发送者还是接收者都有可能使用更短的 RTCP 间隔,并且它们发送初始 RTCP 前可能不会等待一段时间。
  • 所有的 Session 都应该根据最小 RTCP 发送间隔来确定参与者的超时时间。
  • 推荐的最小 RTCP 发送间隔时间使用 "360 kb/Session 带宽(kb/s)" 这种方式计算。这样当 Session 带宽大于 72kb/s 时,RTCP 发送间隔会小于 5 秒。

此外,为了让 RTCP 能在大型 Session 中正常运行,现有的算法还具有如下特点:

  • RTCP 报文发送间隔随着 Session 参与者的人数增加而线性地降低。
  • RTCP 发包间隔通常会随机缩放 0.5~1.5 倍,这样做是为了避免大量的参与者同时发送 RTCP 报文。
  • RTCP 复合包中包含的控制报文数据会根据收发包情况动态变化。
  • 因为 RTCP 报文间隔是根据已知的 Session 参与者情况计算的,所以当有新的人要加入到 Session 时,可能会错估整个 Session 的规模,而是用了较短的 RTCP 间隔,尤其是当大批量的人一齐加入 Session 时这种现象更加明显。所以,可能会有一个 "发送时机重整" 算法,它实现了一个简单的撤回机制,可以在 Session 规模持续增长时,适当的撤回一些 RTCP 报文。
  • 当有人通过发送 BYE 报文或者因为超时退出 Session 时,RTCP 的发送间隔应该缩短。
  • BYE 报文和其他 RTCP 报文相比,有一些特殊的地方。当有人想要退出,并发送 BYE 报文时,它可以在下一个发送周期到来之前就发送。当然,如果一大批人同时退出时,也会受到前面提到的 RTCP 报文撤回机制的影响。

维护 Session 成员的数量

我们已经知道了,计算 RTCP 发送间隔是需要清楚整个 Session 中成员数量的,当一个新的节点被监听到时,它就会被加入到 Session 总数中,并且大家要把它加入到一个 ***C(CSRC)身份识别表中然后持续追踪。大家只有收到这个新节点的多个数据包,或者收到他的 SDES 包(CNAME)时才觉得这个新节点是靠谱的。当某个节点发了一个 BYE 之后,它的信息可能就会被大家删了,但是考虑到可能有丢包或者网络拥堵的情况,所以大家会先把它标记为 "收到 BYE",然后等一段时间,如果还没收到它的别的报文,这时候才会把它删了。

如果一个节点超过一个 RTCP 周期都没收到另一个节点的报文,它可能就会将其标记为不活跃,或者删了它,这就需要丢包的情况尽可能别发生。但是不丢包是不可能的,所以大家一般会将 RTCP 传输间隔乘以一个系数(大于 1 的数)作为超时时间。

对于那些参与者很超级多的 Session,可能没法去维护一个 ***C 表来存储所有参与者的信息。通常大家都会简化这个 ***C 表,但是需要注意的是无论怎么简化这个表都不能低估了参与者的总数,可以允许高估参与者总数。

RTCP 报文的收发规则

首先,无论是组播还是多个节点的单播都必须遵循前面提到的 RTCP 间隔。为了正常完成 RTCP 报文的收发操作,Session 中的每个参与者都会维护如下信息:

  • TP:最后 RTCP 报文的发送时间;
  • TC:当前时间;
  • TN:下一个要发送报文的时间点;
  • P-Members:计算上一个 TN 时参考的 Session 成员总数;
  • Members:当前的 Session 成员总是;
  • Senders:数据发送者总数;
  • RTCP_BW:RTCP 的目标带宽;
  • WE_Sent:从倒数第二个 RTCP 报文发送后,到现在为止,是否发送过数据;
  • AVG_RTCP_Size:平均 RTCP 复合包大小,包括传输层和网络层的头;
  • initial:是否一个 RTCP 报文都没发过。

计算 RTCP 发送间隔

为了让 RTP 协议具有可伸缩性,RTCP 的发送间隔需要随着 Session 总人数的变化而适当的缩放。结合上述的部分状态,我们按如下方式计算 RTCP 报文间隔:

  1. 如果媒体流发送者的数量小于总人数的 25% 时,这个间隔和当前节点是否是媒体流发送者有关(通过 WE_Sent 判断)。如果是媒体流发送者,计算公式为 Senders AVG_RTCP_Size / (25% RTCP_BW),如果是媒体流的接收者,计算公式为:(Members - Senders) AVG_RTCP_Size / (75% RTCP_BW)。当媒体流发送者的数量超过 25% 时,发送者和接收者会被同等对待,即它们的 RTCP 周期公式为:Members * AVG_RTCP_Size / RTCP_BW。

  2. 如果某个参与者一个 RTCP 包都还没发送,最小发送间隔间隔(Tmin)为 2.5 秒,否则为 5 秒。

  3. 决定的发送间隔(Td)会是第一步计算的值和 Tmin 中较大的那个。

  4. 发包时会在 Td 的基础上随机缩放 0.5~1.5 倍。

  5. 最终这个间隔还要除以 e-3/2=1.21828,这是为了弥补因为 "发送时机重整" 算法带来的影响(因为这个算法会导致最终 RTCP 使用的实际带宽比预计使用的带宽低)。

初始化

当一个人刚加入到 Session 中时,tp=0,tc=0,senders=0,p-members=0,members=1,we_sent=false,rtcp_bw = 5% * Session 带宽,initial=true,avg_rtcp_size 被设置为之后会发送的首个 RTCP 包的大小,然后计算发送间隔 T 时,会根据上述初始状态进行计算,并以此作为参考发送第一个包,最后将自己的 ***C 加入到成员列表中。

接收 RTP 和 Non-BYE RTCP 包

当 RTP 或者 RTCP 包被另一个人(A)接收到了,如果对 A 来说这个包的 ***C 他没见过,那么他就会将其加入到 ***C 表中,并更新 Session 总人数(Members)。对每个 CSRC 也会做同样的操作。

如果收到了一个 RTP 报文,并且其对应的 ***C 没在发送者 ***C 表中,那他就会把它加进发送者 ***C 表中,并更新发送者的总数(Senders)。

当每个复合 RTCP 报文被接收到时,平均 RTCP 报文大小(AVG_RTCP_Size)的状态就会更新,更新公式为:AVG_RTCP_Size = (1 / 16) last_rtcp_package_size + (15 / 16) previous_avg_rtcp_size。

接收 RTCP BYE 报文

如果接收到了 RTCP BYE 报文,会在成员列表中确认一下,如果有对应的 ***C 项,就会把它移除并更新成员总数(Members)。同时也会在发送者 ***C 表中做类似的操作,如果找到了就删除它并更新发送者总数(Senders)。

此外,为了让 RTCP 的传输率跟随 Session 中人数的变化而动态变化,如下算法会在收到 BYE 报文时执行:

  1. TN 按照如下公式更新:TN = TC + (Members / P-Members) * (TN - TC) 。

  2. TP 按照如下公式更新:TP = TC - (Members / P-Members) * (TC - TP) 。

  3. 下一个 RTCP 报文按照新的 TN 指示发送(比原来发的更早了)。

  4. 将 P-Members 设置成 Members 的值。

这个算法没有考虑到一个意外情况,那就是当一大波人(并不是所有人)同时退出 Session 时,会导致 RTCP 的周期降到一个非常小的值,这样可能出现错误的 Timeout 判断,最终它会导致整个 Session 的总人数降到 0。但是,这种情况一般来说很少发生,所以大家都觉得问题不是很大。

***C 的超时

我们需要偶尔确认一下是不是太久没收到某个与会者的报文了,一般来说每个 RTCP 周期内都必须确认。如果发现了超时,就需要将这个 ***C 从成员列表(Members & Senders)中移除,并更新当前人数。

Member 表:一般超过 5 个发送周期(不考虑随机缩放因素)未收到某人的消息,会被确定为超时。

Sender 表:一般是 2 个发送周期。

如果某个成员被确定为超时,上一步介绍的算法就操作起来了。

发送倒计时

我们已经知道,每个 RTCP 都是周期性的发送的,当发送完一个 RTCP 报文时,就会根据 TN 新建一个倒计时,每次当倒计时归零时就会重复如下操作:

  1. 计算传输周期 T,引入随机缩放因素。

  2. 如果 TP + T <= TC,立即发送一个 RTCP 报文,并将 TP 设定为 TC,TN 设定为 TC + T,下一个倒计时会在 TN 时刻归零。如果 TP + T> TC,就不发送了 RTCP 报文,计算 TN = TC + T 后,然后重设一个定时器在 TN 归零。

  3. P-Members 设定为 Members。

如果发送了 RTCP 报文,initial 会被设定为 FALSE,AVG_RTCP_Size 会按如下方式更新:
AVG_RTCP_Size = (1 / 16) last_rtcp_package_size + (15 / 16) previous_avg_rtcp_size。

发送 BYE 报文

当某个人想要退出 Session 时,他就会发一个 BYE 报文给其他人。为了防止一大帮人同时退出 Session 时出现 BYE 报文井喷的情况,所以当 Session 人数超过 50 时,会按下述方式操作:

  1. 当一个参与者想要离开时,TP 会设置成 TC,Members 和 P-Members 会设置成 1,initial 设置成 1,we_send 设置成 false,senders 设置成 0,avg_rtcp_size 设置成复合 BYE 报文的大小。然后计算 RTCP 发送间隔 T,下个 BYE 报文会在 TN = TC + T 后发送。

  2. 每当这个要离开的人收到了别人的 BYE 报文时,Members 就会增加 1,无论这个人是否在成员列表中。Members 的数量只有收到 BYE 报文时才增加,其他报文都不管。同样,avg_rtcp_size 也只管收到的 BYE 报文的大小。Senders 数量也不变。

  3. 对了 BYE 报文来说,除了状态值的维护套路变了,发送逻辑和前面提到的都一样。通过上述方案,即可以让 BYE 报文正确地发送,还能控制整体带宽。最差的情况下,也只会导致 RTCP 报文传输占用 10% 的 Session 总带宽。

有些参与者可能不想按照上述的方式发送 BYE 报文,他们可能什么也不发就离开了。这类情况会被 Timeout 机制 hold 住。

如果一个参与者要离开时,Session 的总人数小于 50,他可能会直接发送一个 BYE 报文,也可能按照上述方案来进行。

此外还有一个无论如何都要遵循的规则,如果一个参与者一个 RTP 报文或者 RTCP 报文都没发送过的话,那他离开 Session 时绝对不能发送 BYE 报文。

更新 WE_Sent

当某个参与者最近发送过一个 RTP 后,他就会将 WE_Sent 置为 true 并将自己加入到 Senders 表中,否则如果超过两个 RTCP 发送周期的时间内都没发送过 RTP 报文,那他就会将自己从 Sender 表中移除,并将 WE_Sent 置为 false。

SDES 类报文的带宽分配

SDES 报文中除了必须要有的 CNAME 之外,还有一些别的信息,比如 NAME(个人名称),EMAIL(email 地址)等。上层应用也可以自定义的一些报文类型,但是要小心别付加了太多的自定义信息以至于拖慢了整个 RTCP 协议的运转。建议这些附加内容的带宽占用不要超过整个 RTCP 协议带宽的 20%。此外,也不要觉得每个上层应用都会包含所有的 SDES 内容。上层应用要根据实际使用的情况给这些内容分配一定的带宽,一般来说他们会通过控制发送间隔来控制这部分的带宽。

比如,一个应用的 SDES 可能只包含 CNAME,NAME,和 EMAIL,其中 NAME 可能就会比 EMAIL 分配更多的带宽。因为 NAME 会一直显示出来,而 EMAIL 可能只在点击查看的时候才显示。在每个 RTCP 发送周期里,SDES 中都会包含 CNAME。如果假设 RTCP 周期是 5 秒的话,可能每 15 秒 SDES 才会附带一个除 CNAME 以外的信息,以 2 分钟为例,其中 7 次附带的是 NAME 信息,1 次附带的是 EMAIL 信息。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: