LV04-视频传输和网络对抗-01-RTP与RTCP

本文主要是攻克视频技术课程视频传输和网络对抗——RTP & RTCP:如何正确地将视频装进RTP中?的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

在前面的课程中,我们详细地讲述了视频编码的原理以及预测编码和变换编码的知识。通过这些我们了解了视频编码的基本原理和步骤。同时,我们还用了一节课的时间深入探讨了 H264 的码流结构,相信你已经清楚了 H264 码流是什么样的,以及如何从码流中分离出一帧帧图像数据,并学会了如何判断这些帧的类型。

那么从这节课开始呢,我们就要进入视频传输和网络对抗部分了。我们会在视频编码码流的基础上,讲讲如何将码流打包成一个个数据包发送到网络上,并进一步讨论如何避免在发送的过程中引起网络拥塞,从而保证视频的流畅性。同时,我们会进一步在后面的课程中讲解如何在网络不断变化的时候做好视频码控算法,如何防止视频出现花屏,以及如何尽量减少视频卡顿等非常有难度的实际工程问题。

这些问题是视频开发过程中经常会遇到且迫切需要解决的重要问题。而解决这些问题的基础就是需要熟悉 RTP 和 RTCP 协议,也就是我们这节课的重点。

接下来我们会分别从 RTP 协议、RTCP 协议和 H264 的 RTP 打包方法这三个方面来展开这节课。首先让我们一起来认识一下 RTP 协议。

一、RTP 协议

RTP(Real-time Transport Protocol)协议,全称是实时传输协议。它主要用于音视频数据的传输。那它的作用是什么呢?

一般我们在实时通信的时候,需要传输音频和视频数据。我们通常是这样做的,先将原始数据经过编码压缩之后,再将编码码流传输到接收端。在传输的时候我们通常不会直接将编码码流进行传输,而是先将码流打包成一个个 RTP 包再进行发送

那为什么需要打包成 RTP 包呢?这是因为我们的接收端要能够正确地使用这些音视频编码数据,不仅仅需要原始的编码码流,还需要一些额外的信息。比如说:

  • 当前视频码流是哪种视频编码标准,是 H264、H265、VP8、VP9 还是 AV1 呢?我们知道每种不同的编码标准,其码流解析的方式肯定也不一样。这个就需要通过 RTP 协议告知接收端。
  • 当我们知道编码标准了,我们就可以正确地解析码流,并解码出图像了。但是我们又会遇到一个新的问题,那就是按照什么速度播放视频呢?这个也需要 RTP 协议告知接收端。

这就是 RTP 协议的一个重要的作用,即告知接收端一些必要的信息。当然 RTP 协议的作用不止这些,它其实在网络带宽预测和拥塞控制的时候也发挥出了至关重要的作用。我们在之后的课程中会继续讨论,这里就先不讲了,你大体有个印象就可以。

我们知道 RTP 包需要附带很多额外的信息,那这些信息在 RTP 包中是怎么存在的呢?其实 RTP 包包括两个部分:第一个部分是 RTP 头;另外一个部分是 RTP 有效载荷。其中 RTP 头主要是用来携带前面说的那些额外信息的,等会儿我会详细介绍一下 RTP 头部每个字段的意义。

这里我先稍微跟你解释一下另外一个部分,也就是 RTP 有效载荷。RTP 有效载荷,其实就是 RTP 包里面的实际数据。如果是 H264 编码打包成 RTP 包,那有效载荷就是经过 H264 编码的码流;如果是 VP8 编码呢,那就是 VP8 码流。

1. RTP 包的头部

接下来,我们重点来看看 RTP 包的头部。具体如下图所示:

img

是不是有点懵,别急,下面我给了一张表格,你可以对照着表格看看 RTP 包头的每一个字段占用的位数和具体的含义。其中绿色部分是很重要的知识点,需要你重点掌握。

img

上面讲的就是 RTP 头部的主要组成部分。在这里需要单独提一下 RTP 头部的另外一个比较重要的部分,就是 RTP 扩展头。从上表我们可以看到,RTP 包头有一个扩展头标志位 X,当扩展头标志位 X 为 1 的时候,说明有 RTP 扩展头。RTP 扩展头由于平时大家很少用看似不怎么重要,但是在 RTC 场景中,尤其是 WebRTC 中经常会用到。另外,RTP 扩展头我们在带宽预测的时候也会用到。所以建议你也了解一下。

2. RTP通用头部扩展

扩展头主要是用来给用户自定义扩展使用的。因为协议是标准的,但是用户使用场景却是多种多样的,所以 RTP 需要考虑的比较全面,留了一个扩展头可以让用户根据使用场景和需求,自己定义扩展头,用来传输需要在 RTP 包中传输的信息。具体的可以参考这里:RFC 5285 - A General Mechanism for RTP Header Extensions (ietf.org)。在RFC3550中, 一个通用的RTP头部如下:

1
2
3
4
5
6
7
8
9
10
11
12
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

其中X位如果为1,就表示CSRC后面还有一些额外的RTP扩展头,其形式如下

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| defined by profile | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| header extension |
| .... |

但是这种形式只能够附加一个扩展头,为了支持多个扩展头,RFC5285以defined by profile进行了扩展

2.1 One-Byte Header

扩展头为one-byte的情况下,一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0xBE | 0xDE | length =3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ID | L=0 | data | ID | L=1 | data...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...data | 0 (pad) | 0 (pad) | ID | L=3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RTP头后的第一个16为固定为0XBEDE标志,意味着这是一个one-byte扩展,length = 3 说明后面有三个扩展头,每个扩展头首先以一个byte开始,前4位是这个扩展头的ID, 后四位是data的长度-1,譬如说L=0意味着后面有1个byte的data,同理第二个扩展头的L=1说明后面还有2个byte的data,但是注意,其后没有紧跟第三个扩展头,而是添加了2个byte大小的全0的data,这是为了作填充对齐,因为扩展头是以为32bit作填充对齐的。

2.2 Two-Byte Header

扩展头为Two-Byte的情况下, RTP头后的第一个16为如下所示, 一个0x100 + appbitsappbits可以用来填充应用层级别的数据:

1
2
3
4
5
 0                   1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x100 |appbits|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

一个例子如下, 可以看到开头为 0x100 + 0x0, 接下来的为length=3表示接下来有3个头,接下来的就是扩展头和数据,扩展头除了ID和L相对于one-byte header从4bits变成了8bits之后,其余都一样:

1
2
3
4
5
6
7
8
9
10
11
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x10 | 0x00 | length=3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ID | L=0 | ID | L=1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data | 0 (pad) | ID | L=4 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

3. 总结

好了,以上就是 RTP 协议的主要知识点。有了 RTP 协议,我们就能够将码流打包成 RTP 包发给接收端了。如果你只负责传输 RTP 包,而不需要管传输过程中有没有丢包,以及传输 RTP 包的时候有没有引起网络拥塞的话,那你只需要使用 RTP 协议就可以了。比如说,你选择使用 TCP 协议传输 RTP 包的话就可以不用管这些事情,因为 TCP 协议具有丢包重传、拥塞控制等功能。

但是通常情况下,我们在传输音视频数据的时候不会使用 TCP 协议作为传输层协议。这是因为 TCP 协议更适合传输文本和文件等数据,而不适合传输实时音频流和视频流数据,所以我们通常会使用 UDP 协议作为音视频数据的传输层协议。但 UDP 协议不具有丢包重传和拥塞控制的功能,需要我们自己实现。那怎么办呢?

其实,真要做好丢包重传和拥塞控制是非常难的,一节课也讲述不清楚,所以,我们会在接下来的好几节课里详细解释。接下来我们可以先关注下丢包重传和拥塞控制的基础之一,也就是 RTP 协议的“好兄弟”,RTCP 协议。

二、RTCP 协议

RTCP(Real-time Transport Control Protocol)协议,全称是实时传输控制协议。它是辅助 RTP 协议使用的。RTCP 报文有很多种,分别负责不同的功能。常用的报文有发送端报告(SR)、接收端报告(RR)、RTP 反馈报告(RTPFB)等。而每一种报告的有效载荷都是不同的。我们就是通过这些报告在接收端和发送端传递当前统计的 RTP 包的传输情况的。我们使用这些统计信息来做丢包重传,以及预测带宽。

不过,我需要再次强调一下,RTCP 协议只是用来传递 RTP 包的传输统计信息,本身不具有丢包重传和带宽预测的功能,而这些功能需要我们自己来实现。

我们上面讲到了 RTCP 协议有很多种报告,而每种报告其实定义的具体内容都是不一样的。我们这里以 RTPFB 报告中的 NACK 报告(丢包提示报告)作为一个例子来看看 RTCP 协议大概是什么样子的。(RTPFB 报告包含了多种子报告,NACK 报告只是其中的一种,因为我们后面还会用到这个报告,所以这里我们就先以这个报告为例子。)

下图就是 NACK 报告的协议格式。

img

其中,每一个字段在下表中都有详细的解释。

img

从上面的 NACK 报告我们可以看到,RTCP 协议跟 RTP 不同,它们传递的东西是不一样的。

我们知道 RTP 是用来传输实际的视频数据的。它就像一个快递盒,先装好视频,然后填好运送的视频基本信息和收件人信息,最后将视频运送到收件人手上。

而 RTCP 协议则像是一个用来统计快递运送情况的记录表。其中的 NACK 报告就是快递丢件情况的记录表。它记录着哪些快递丢了。发件人收到了 NACK 之后,可以重新寄一个同样的快递给收件人,防止收件人没有收到快递。在这里也就是将丢失的视频 RTP 包重传一遍。

虽然我们只讲了一种 RTCP 报告,但是其它的报告也是类似的。大多数报告都是用来记录传输信息的。因为数量很多,我们这里就不一一展开了。如果你有兴趣的话,可以查看这个文档:RFC 3550 - RTP: A Transport Protocol for Real-Time Applications (ietf.org)

三、H264 RTP打包

好了,通过学习 RTP 和 RTCP 的基础知识,我们了解了 RTP 包的协议格式和主要负责的功能,也知道了 RTCP 的协议格式和其主要承担的责任。接下来我们就进入实际工程部分的知识了。

我相信通过前面课程的学习,你对 H264 的码流结构已经较为熟悉了,H264 是在工程中用得比较多的编码标准,所以这里我们以 H264 为例来讲讲实际工程开发中,我们怎么将 H264 码流打包成 RTP 包。

我们前面说了,H264 码流是放在 RTP 的有效载荷部分的。因此有效载荷前面的 RTP 头部跟码流本身是没有关系的,所以我们可以直接先将头部的字段填好就可以。接下来我们需要将 H264 码流填充到 RTP 有效载荷中去。

RTP H264 码流打包分为三种方式:分别是单 NALU 封包方式、组合封包方式、分片封包方式。顾名思义,单 NALU 封包方式是一个 NALU 打一个 RTP 包;而组合封包方式就是多个 NALU 打一个 RTP 包;分片封包方式则是一个 NALU 分开放在连续的多个 RTP 包中。下面我们来分别看一下各种打包方式是怎么样的。

1. 单 NALU 封包方式

单 NALU 封包方式非常简单。我们在 RTP 头部的后面,直接放置 NALU 数据即可。注意,根据 RTP 的规定,这里需要将 NALU 数据前面的起始码去除,不要将起始码也带入 RTP 包中。其格式如下:

img

为了让你更直观地理解这种打包方式,我给出了打包的示意图。具体如下所示:

img

这种打包方式适合于单个 RTP 包小于 1500 字节(MTU 大小)的时候。一般来说,一些 P 帧和 B 帧编码之后比较小,就可以使用这种打包方式。

2. 组合封包方式

组合封包方式稍微复杂一些。它是将多个 NALU 放置在一个 RTP 包中。在 RTP 头部之后,且放置 NALU 数据之前,我们需要放置一个 1 字节的 STAP-A 的头部。其中,STAP-A Header 跟 NALU Header 的格式是一样的,只是 Type 字段的值不一样。因此,你可以参考 H264 码流结构课程中 NALU 小节来理解 STAP-A 的头部的格式。具体如下图所示:

img

其中,Type 的取值如下表所示。这里我需要提醒你一下,表中的 24 和 25 类型就是 STAP 组合封包方式。注意,我们这里只讲 STAP-A,这是因为 STAP-B 很少用到。

img

放置完 STAP-A Header 之后,在每一个 NALU 的前面我们需要放置一个 2 字节的 size 字段,用于表示后面的 NALU 的大小。之后才是 NALU 的数据。记住同样需要去掉起始码。其格式如下:

img

同样地,为了让你更直观地理解这种打包方式,我也给出了打包的示意图。具体如下所示:

img

这种打包方式适合于单个 NALU 很小的时候。因此,我们将多个 NALU 打包到一起也小于 1500 字节的时候就可以使用。但是由于一般多个视频帧加到一起还小于 1500 的情况比较少,所以视频数据的 RTP 打包一般来说用组合封包方式的情况也很少。

3. 分片封包方式

分片封包就更复杂一些了,但却是我们经常用到的打包方式。

它是将一个 NALU 分开打包在连续的多个 RTP 包中。因此,我们首先需要一个 1 字节的 FU indicator 来表示当前 RTP 包是不是分片封包方式,再用一个 1 字节的 FU Header 来表示当前这个 RTP 包是不是 NALU 的第一个包,是不是 NALU 的最后一个包,以及 NALU 的类型。

为什么需要表示是不是第一个包以及是不是最后一个包呢?这是因为一个 NALU 被分开放在多个 RTP 包中,我们需要知道哪个是第一个 NALU 分片,哪个是最后一个 NALU 分片,以及哪些是中间分片。这样我们才能组成一个完整的 NALU。

那你可能会问,NALU 不是已经在 NALU Header 中有了 NALU Type 字段吗?为什么 FU Header 中还要有 NALU Type 呢?这是因为分片封包时需要去掉 NALU Header。因此,我们需要通过 FU Header 中的 NALU Type 得到 NALU 的类型。

其中,分片封装中的 FU indicator 跟 NALU Header 的格式也是一样的,也只是 Type 字段的值不同,所以我们可以参考组合封包小节中的表格。因为我们一般只使用 FU-A,所以接下来讲述的将是 FU-A 的分片封包方式。另外,FU Header 格式如下所示:

img

这里我简单解释一下各字段的含义:

  • S:起始位,占 1bit,为 1 则表示是 NALU 的第一个 RTP 包。
  • E:结束位,占 1bit,为 1 则表示是 NALU 的最后一个 RTP 包。
  • R:预留位,占 1bit。
  • Type:占 5bits,表示 NALU 类型。

分片打包的格式如下:

img

分片打包的示意图如下:

img

这种打包方式主要用于将 NALU 数据打包成一个 RTP 包时大小大于 1500 字节的时候,这是经常使用的视频 RTP 打包方法。

好了,以上就是三种打包方式。我们怎么选择使用哪种方式打包呢?一般来说,我们在一个 H264 码流中会混合使用多种 RTP 打包方式。一般来说,对于小的 P 帧、B 帧还有 SPS、PPS 我们可以使用单个 NALU 封包方式。而对于大的 I 帧、P 帧或 B 帧,我们使用分片封包方式。当然,你可以根据实际情况进行选择。

四、小结

好了,以上就是这节课的主要内容。接下来我们来总结一下。

首先,我们一起讨论了 RTP 协议和 RTCP 协议的主要作用。RTP 协议用来封装音视频数据,并且将音视频数据和一些基本信息打包到 RTP 包中传输到接收端。而 RTCP 协议则辅助 RTP 协议使用,其中一个主要的功能就是用来统计 RTP 包的发送情况,比如说丢包率和具体哪些 RTP 包在网络发送的过程中丢失了。RTCP 包将这些信息收集起来发送给 RTP 包的发送端。

然后,我们说明了 RTP 和 RTCP 协议是带宽预测和拥塞控制的基础,并且重点强调了 RTCP 协议本身只统计信息,而带宽预测和拥塞控制算法是需要我们自己实现的,RTCP 协议本身并没有这个功能。

最后,我们介绍了 H264 的 RTP 打包方式,总共有三种,分别是单 NALU 封包方式、组合封包方式和分片封包方式。

  • 单 NALU 封包方式,一般适合 NALU 大小比较小,且打包出来的 RTP 大小小于 1500 字节的时候使用。
  • 组合封包方式,适合多个 NALU 都很小,且合并在一起打包的 RTP 包小于 1500 字节的时候使用。
  • 分片打包,则适合 NALU 比较大的情况,且打包成一个 RTP 包其大小会大于 1500 字节的时候使用。

这几种打包方式不是说只能选择一种,在一个 RTP 流中是可以存在多种打包方式的,即可以混合使用。

最后再一次强调,这节课和 H264 码流结构那节课都是非常重要的。它们在实际视频开发的过程中会经常用到,希望你可以熟练掌握。

思考题:为什么我们在选择 RTP 打包方式的时候,需要根据 NALU 大小是不是大于 1500 字节(MTU)来选择?

评论区答案:不超过1500主要是因为Udp协议的MTU为1500,超过了会导致Udp分片传输,而分片的缺点是丢了一个片,整包数据就废弃了