Skip to content

LV010-RTP简介

RTP & RTCP:如何正确地将视频装进 RTP 中?

一、概述

在前面的课程中,我们详细地讲述了视频编码的原理以及预测编码和变换编码的知识。通过这些我们了解了视频编码的基本原理和步骤。同时,我们还用了一节课的时间深入探讨了 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 码流。

三、RTP头部格式

1. RTP 包的头部

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

md
    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 (CSRCs) identifiers            |
   |                             ....                              |
   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
   |               Payload(audio, video ...)                       |
   |                                             +=+=+=+=+=+=+=+=+=+
   |                             ....            |  padding        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

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

字段 占用位数 含义
版本号(V) 2 比特 用来标志使用的 RTP 版本,当前应该是 “10”。
填充位(P) 1 比特 填充标志。当为 1 时,表示在 RTP 包的最后面有若干个填充字节。注意填充字节只是用于字节对齐填充,并不是视频数据。这个时候填充部分的最后一个字节表示该部分的长度(包括这个字节本身), 一般是 RTP 包长度不是 4Byte 的整数倍的时候建议使用该字段。
扩展位(X) 1 比特 扩展标志位。当为 1 时, 表示在 RTP 默认头部后面会有一个用于自定义的扩展头部。包含私有数据的时候,这个 X 位固定位 1,所有的私有数据均包含在头扩展字段内,一般来说这种私有数据所在的 RTP 包实际负载数据长度为 0。
CSRC 计数器(CC) 4 比特 CSRC 个数。表示 CSRC 标识符个数。
标记位(M) 1 比特 Marker 位。对于不同的有效载荷 Marker 位含义不同。传输单元是视频编码的一个图像帧或者是一个音频帧。对于视频,表示当前 RTP 包是一帧的最后一个包(对于标准 H264 编码,一帧包含多个 NALU 的时候,该帧的最后一个 RTP 包中 M = 1,其他的 M = 0)
载荷类型(PT) 7 比特 有效载荷类型。用于说明 RTP 报文中有效载荷的类型, 比如说可以是 H264、VP8 等, 用于接收端进行解析 RTP 的有效载荷。常见类型详见 RFC 3551 。表中有一些初始固定的类型,后续出现的比较晚的类型是使用 97-127 范围内的动态值,其中 H264 常用 96,但是其实也可以协商定义,对于一些私有包就可以自己来定义使用哪个值。
序列号(SN) 16 比特 序列号。用于表示当前 RTP 包的序号, 每发送一个 RTP 包, 序列号加 1。它是 RTP 包的标识,接收端可以使用它来告诉发送端 RTP 包丢失了, 要求发送端重传,也可以标识 RTP 包发送的顺序,也是接收端收到后送到解码器解码的顺序。序列号的初始值是随机的,另外包含私有数据的 RTP 包的 SN 应该与其起作用的那路元素流的 SN 一致(当你发送一个承载私有数据的 RTP 包时,这个包的序列号 SN 不应该从 1 开始独立计数,而应该与它关联的那个主媒体流如视频流的序列号保持同步递增)。
时间戳 32 比特 时间戳。记录了该包中数据的第一个字节的采样时刻。接收端使用时间戳来控制播放速度和音视频同步。使用 90000Hz 的时间基(就是说时间戳的单位是 1/90000 秒)。在一次会话开始时,时间戳初始化成一个初始值,时间戳的数值随时间而不断地增加。时间戳是去除抖动和实现同步不可缺少的。同一个帧的不同分片的时间戳是相同的。这样就省去了起始标志和结束标志。一定要记住,不同帧的时间戳肯定是不一样的
同步源标识符(SSRC) 32 比特 同步信源。比如说当前 RTP 用于传输 H264 视频数据和 OPUS 音频数据,H264 的 RTP 包使用同一个 SSRC, OPUS 的 RTP 包使用同一个 SSRC。在比如摄像头和拾音器应该对应不同的 SSRC 值。接收端根据 SSRC 标识符来区分当前 RTP 包是 H264 RTP 包还是 OPUS RTP 包。该标识符是随机选取的,RFC1889 推荐了 MD5 随机算法,是全局唯一的
特约信源标识符(CSRC List) 0~15 项,每项 32 比特 提供信源。可以有 0~15 个 CSRC。每个 CSRC 标识了包含在 RTP 包有效载荷中的所有提供信源。主要用在混合器中。比较少遇到。它主要是用来标志对一个 RTP 混合器产生的新包有贡献的所有 RTP 包的源。由混合器将这些有贡献的 SSRC 标识符插入表中。SSRC 标识符都被列出来,以便接收端能正确指出交谈双方的身份
上面讲的就是 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 头部如下:

txt
    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 扩展头,其形式如下

txt
    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 进行了扩展。对于一些私有数据来说,也是这样的格式:

txt
    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
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |      private data type        |           length              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                          private data                         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

前两个字节表示类型,后两个字节表示后续负载数据长度。

2.1 One-Byte Header

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

txt
       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 可以用来填充应用层级别的数据:

txt
       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 之后,其余都一样:

txt
       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 协议。

参考资料:

RFC 3550 - RTP: A Transport Protocol for Real-Time Applications

RFC 5285 - A General Mechanism for RTP Header Extensions

RFC 6184 - RTP Payload Format for H.264 Video

RFC 7798 - RTP Payload Format for High Efficiency Video Coding (HEVC)

RTP 有效负载(载荷)类型 (RTP Payload Type) - 苦涩的茶 - 博客园