LV010-RTP简介
RTP & RTCP:如何正确地将视频装进 RTP 中?
一、概述
在前面的课程中,我们详细地讲述了视频编码的原理以及预测编码和变换编码的知识。通过这些我们了解了视频编码的基本原理和步骤。同时,我们还用了一节课的时间深入探讨了 H264 的码流结构,相信你已经清楚了 H264 码流是什么样的,以及如何从码流中分离出一帧帧图像数据,并学会了如何判断这些帧的类型。
那么从这节课开始呢,我们就要进入视频传输和网络对抗部分了。我们会在视频编码码流的基础上,讲讲如何将码流打包成一个个数据包发送到网络上,并进一步讨论如何避免在发送的过程中引起网络拥塞,从而保证视频的流畅性。同时,我们会进一步在后面的课程中讲解如何在网络不断变化的时候做好视频码控算法,如何防止视频出现花屏,以及如何尽量减少视频卡顿等非常有难度的实际工程问题。
这些问题是视频开发过程中经常会遇到且迫切需要解决的重要问题。而解决这些问题的基础就是需要熟悉 RTP 和 RTCP 协议,也就是我们这节课的重点。
接下来我们会分别从 RTP 协议、RTCP 协议和 H264 的 RTP 打包方法这三个方面来展开这节课。首先让我们一起来认识一下 RTP 协议。
二、RTP 协议简介
1. 简介
RTP(Real-time Transport Protocol)协议,全称是实时传输协议。它主要是用于 Internet 上针对多媒体数据流的一种传输层协议(简单说就是 网络传输协议),主要用于实时的音视频数据的传输,RTP 协议和 RTP 控制协议 RTCP 一起使用。
RTP 被定义为在一对一或一对多的传输情况下工作,其目的是提供时间信息和实现流同步。RTP 的典型应用建立在 UDP 上,但也可以在 TCP 或 ATM 等其他协议之上工作。
RTP 本身只保证实时数据的传输,并不能为按顺序传送数据包提供可靠的传送机制,也不提供流量控制或拥塞控制,它依靠 RTCP 提供这些服务。
2. 有什么作用?
那它的作用是什么呢?
一般我们在实时通信的时候,需要传输音频和视频数据。我们通常是这样做的,先将原始数据经过编码压缩之后,再将编码码流传输到接收端。在传输的时候我们通常不会直接将编码码流进行传输,而是 先将码流打包成一个个 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 码流。
3. 工作机制
RTP 协议就是提供了时间标签,序列号以及其它的结构用于控制适时数据的流放。在流的概念中” 时间标签” 是最重要的信息。发送端依照即时的采样在数据包里隐蔽的设置了时间标签。在接受端收到数据包后,就依照时间标签按照正确的速率恢复成原始的适时的数据。不同的媒体格式调时属性是不一样的。
但是 RTP 本身并不负责同步,RTP 只是传输层协议,为了简化运输层处理,提高该层的效率。 将部分运输层协议功能(比如流量控制)上移到应用层完成。同步就是属于应用层协议完成的。它没有运输层协议的完整功能,不提供任何机制来保证实时地传输数据,不支持资源预留,也不保证服务质量。RTP 报文甚至不包括长度和报文边界的描述。同时 RTP 协议的数据报文和控制报文的使用相邻的不同端口,这样大大提高了协议的灵活性和处理的简单性。
RTP 协议和 UDP 二者共同完成运输层协议功能。UDP 协议只是传输数据包,不管数据包传输的时间顺序。RTP 的协议数据单元是用 UDP 分组来承载的。在承载 RTP 数据包的时候,有时候一帧数据被分割成几个包具有相同的时间标签,则可以知道时间标签并不是必须的。而 UDP 的多路复用让 RTP 协议利用支持显式的多点投递,可以满足多媒体会话的需求。
4. 端口
RTP 在端口号 1025 到 65535 之间选择一个未使用的偶数 UDP 端口号,而在同一次会话中的 RTCP 则使用下一个基数 UDP 端口号。RTP 默认端口号 5004,所以 RTCP 端口号默认为 5005。
5. RTP over UDP/TCP
RTSP 协商成功以后就是 RTP 收流,又分为两种模式: RTP over RTSP (TCP)和 RTP over UDP。
5.1 RTP over UDP
RTP over UDP - 标准且最常见的方式,RTP 数据包被直接封装在 UDP 数据包中发送。
- 形式一

形式一画的这个 RTSP 只传输一个流,比如纯视频流或者纯音频流。如果有 2 个流,比如一个音频一个视频,那么再复制上面 2 个 UDP 的 socket 下,绘制上就行了。一个流对应 2 个 UDP socket——一个传输 RTP 包,一个传输 RTCP 控制协议。
RTSP 协议是走的 TCP 连接。RTP 包走的是 UDP 连接,服务端一个 UDP 的 socket 作为 RTP 包的发送客户端模式,客户端的 UDP 的 socket 有 1 个——是服务端模式,接受 RTSP 服务端发送的 RTP 数据包。
RTSP 服务端的一个 UDP socket 作为 RTCP 的连接,模式也是客户端模式,RTSP 客户端的 RTCP 链接是 1 个 socket,既可以接受 RTCP 报文,又可以回复响应 RTCP 报文。
- 形式二

上图画的这个 RTSP 只传输一个流,比如纯视频流或者纯音频流。如果有 2 个流,比如一个音频一个视频,那么再复制上面 2 个 UDP 的 socket 下,绘制上就行了。一个流对应 2 个 UDP socket——一个传输 RTP 包,一个传输 RTCP 控制协议。
这里 RTSP 协议是走的 TCP 连接。RTP 包走的是 UDP 连接,服务端一个 UDP 的 socket 作为 RTP 包的发送客户端模式,客户端的 UDP 的 socket 有 2 个——一般一个是服务端模式,接受 RTP 数据包,另一个一般是不会创建的除非我们想用 RTSP 客户端发送数据到服务端——但是 RTSP 客户端发送私有数据到 RTSP 服务端也可以通过上面的 TCP 连接,需要定制私有协议了。
RTSP 服务端的一个 UDP socket 作为 RTCP 的连接,模式也是客户端模式,rtsp 客户端的 RTCP 连接是 2 个 socket,一个是接受 RTCP 报文,另一个是响应 RTCP 报文。
5.2 RTP over TCP
网上搜的资料中,说是之前叫 RTP over RTSP,现在一般被称为 RTP over TCP,其实后者会更加贴切。它的特点是 RTSP 服务端和客户端是共用一个 TCP 连接,也就是说 RTSP 协议报文、RTP 包、RTCP 数据都是通过这一个连接来交互的。
这种情况下,RTP/RTCP Socket 和 RTSP Socket 共享 TCP Socket,所以必须要有一个标识来区别三个数据。RTP 和 RTCP 数据会以 "$" 符号 + 一个字节的通道编号 + 2 个字节的数据长度,共四个字节的前缀开始,RTSP 数据没有四个字节的前缀;RTP 和 RTCP 数据的区别在于第二个字节的通道编号。
所以第一个字节’$'用于与 RTSP 区分,第二个字节用于区分 RTP 和 RTCP(RTP 和 RTCP 的 channel 是在 RTSP 的 SETUP 过程中,客户端发送给服务端的——一般情况下 RTP 通道编号是偶数,RTCP 通道编号是奇数)。RTP/RTCP 的前缀四个字节如下所示:(在 rtp over tcp 发送协议下)
| 字节 | 描述 |
|---|---|
| 第一个字节 | ‘$’,标识符,用于与 RTSP 区分 |
| 第二个字节 | channel,用于区分 RTP 和 RTCP |
| 第三和第四个字节 | RTP 包的大小 |
三、RTP 包
一般 RTP 包结构如下:
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+一般,单个 NALU 独立封包完整数据结构为:
[4Byte私有头][12ByteRTP标准头][NALU头+NALU负载+RTP补齐数据](1)4Byte 私有头主要用于标识 rtp 包的长度。
(2)RTP 包中 包含 NALU 头和 NALU 负载, 但 不含起始码
(3)RTP 补齐数据是为了使整个 RTP 包 4Byte 对齐而补齐的数据, 通常以 0x00 补齐并且最后补齐数据为补齐数据总长度, 如需要补齐 3Byte, 则 RTP 扩展数据为 0x00 0x00 0x03。
1. RTP Header
1.1 header 结构
接下来,我们重点来看看 RTP 包的头部。每一个 RTP 数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前 12 个字节的含义是固定的,而负载则可以是音频或者视频数据。具体如下图所示(RFC 3550):
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 常用 0x60(96),传输私有数据时,一般为 0x70(112),但是其实也可以协商定义,对于一些私有包就可以自己来定义使用哪个值。 |
| 序列号(SN) | 16 比特 | 序列号。用于表示当前 RTP 包的序号, 每发送一个 RTP 包, 序列号加 1。它是 RTP 包的标识,接收端可以使用它来告诉发送端 RTP 包丢失了, 要求发送端重传,也可以标识 RTP 包发送的顺序,也是接收端收到后送到解码器解码的顺序。序列号的初始值是随机的,另外包含私有数据的 RTP 包的 SN 应该与其起作用的那路元素流的 SN 一致(当你发送一个承载私有数据的 RTP 包时,这个包的序列号 SN 不应该从 1 开始独立计数,而应该与它关联的那个主媒体流如视频流的序列号保持同步递增)。 |
| 时间戳(timestamp) | 32 比特 | 时间戳。记录了该包中数据的第一个字节的采样时刻。接收端使用时间戳来控制播放速度和音视频同步。对于 MPEG2、MPEG4、JPEG 和 H.264,采用 1/90000 秒为单位;对于 G.7xx 音频,采用 1/8000 秒为单位。在一次会话开始时,时间戳初始化成一个初始值,时间戳的数值随时间而不断地增加。时间戳是去除抖动和实现同步不可缺少的。同一个帧的不同分片的时间戳是相同的。这样就省去了起始标志和结束标志。一定要记住,不同帧的时间戳肯定是不一样的。 当一帧被分割成多个 RTP 包传输时,这些包的 timestamp 应保持一致,都是该帧的采样时刻。H.264 编码后数据的两个 AUD NAL unit 之间的单元所形成的 RTP 包 timestamp 应与前一个 AUD NAL unit 的 timestamp 一致。包含私有数据字段的 rtp 包 timestamp 需要根据私有字段的作用时刻准确填写。当该私有数据 rtp 包用于描述的编码码流包含视频流或与视频流相关的信息(如视频智能信息)时,其 timestamp 以该视频流为基准,单位亦相同(即以 1/90000 秒为单位);否则根据实际应用情况确定。同时,包含本文第 3 部分定义的 Basic Stream Info 和 Codec Info 等信息的 RTP 包,应在这些信息生效的视频 I 帧前传输,并且该 RTP 包所用的 timestamp 与该 I 帧的 timestamp 相等,以便于全局时间、相对时间和帧号的匹配计算。 |
| 同步源标识符(SSRC) | 32 比特 | 同步信源, 主要以区分不同的 RTP 发送端。比如说当前 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 扩展头我们在带宽预测的时候也会用到。所以建议你也了解一下。
1.2 RTP 通用头部扩展
扩展头主要是用来给用户自定义扩展使用的。因为协议是标准的,但是用户使用场景却是多种多样的,所以 RTP 需要考虑的比较全面,留了一个扩展头可以让用户根据使用场景和需求,自己定义扩展头,用来传输需要在 RTP 包中传输的信息。具体的可以参考这里:RFC 5285 - A General Mechanism for RTP Header Extensions (ietf.org)。在 RFC3550 中, 一个通用的 RTP 头部如下:
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 扩展头,其形式如下
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 进行了扩展。对于一些私有数据来说,也是这样的格式:
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+前两个字节表示类型,后两个字节表示后续负载数据长度。
1.2.1 One-Byte Header
扩展头为 one-byte 的情况下,一个例子如下:
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 作填充对齐的。
1.2.2 Two-Byte Header
扩展头为 Two-Byte 的情况下, RTP 头后的第一个 16 位如下所示, 一个 0x100 + appbits, appbits 可以用来填充应用层级别的数据:
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 之后,其余都一样:
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+2. Payload 负载
负载部分就是存仓 H264、H265 或者音频数据的地方:
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| Payload(audio, video ...) |
| +=+=+=+=+=+=+=+=+=+
| .... | padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+这个下一节会介绍封包方式。
3. 私有包
有的时候我们希望在码流中添加一些额外的信息,例如像人脸识别,识别出来后有人脸的区域坐标,我们想要拿到这个区域坐标,可以有哪些方案?
- (1)视频编码层注入:SEI(Supplemental Enhancement Information)
SEI(Supplemental Enhancement Information)帧是H.264和H.265视频编码标准中的一种NALU类型,用于传输元数据信息,例如时间码、版权信息或用户自定义数据。SEI帧的解析需要识别其类型、长度以及具体的负载内容。
在视频编码时,将人脸坐标等数据按照一定格式(如JSON字符串或自定义的二进制结构)写入SEI NAL Unit中,并与当前帧的图像NAL Unit组合在一起发送。由于元数据和图像数据在同一个码流中,保证了帧级别的严格同步。
能写多少数据?SEI中能写入的私有信息字节数,严格来说没有统一的硬性上限,但始终受到来自视频编码标准和具体平台实现两个层面的共同约束。从根本上看,它作为视频帧的一部分,总大小会受到最大传输单元、编码器设置及网络带宽的制约。在实际操作中,为了避免单个视频帧过大导致网络分片丢包,SEI的数据长度通常会被限制在几十KB以内。也可以约定,比如有些厂商就可能规定为4KB。
优点:绝对同步,网络抖动也不会导致画面和框错位。标准协议,普通的RTSP服务器(如Live555、MediaMTX)可以直接透传,无需修改服务器代码。兼容性好,普通播放器会自动丢弃不认识的SEI,不影响视频播放。
缺点:需要在编码端(或推流端)介入修改,具备编码器的控制权。接收端需要具备解析底层码流(NAL Unit)的能力,不能用普通的播放器SDK直接拿数据,需要用FFmpeg等库提取SEI。
- (2) 画面层叠加:直接画在视频帧上,芯片性能够强或者支持OSD的话也是可以的
优点:接收端零开发成本,任何播放器都能直接看到结果。
缺点:破坏了原始视频:画框后视频无法还原,坐标数据也丢失了,无法进行二次开发或分析。增加编码码率消耗(因为画面高频细节变多)。rtp流解封装后无法拿到人脸相关信息。
- (3)RTP Header Extension(RTP头部扩展)
RTP协议本身设计了扩展头部机制(RFC 5285),允许在RTP包头部添加自定义数据。与RTP包绑定,通过RTP的时间戳实现同步。
优点:在传输层解决,不需要修改视频编码器。同样具备严格的帧级同步。
缺点:长度受限:RTP头部扩展通常不适合放太长的数据(一般几十字节以内),如果一帧画面中人脸非常多,坐标数据可能放不下。服务器兼容性:部分RTSP服务器或中间件可能会丢弃不认识的RTP扩展头。
- (4)rtp私有包:也可以直接定义一个PT类型,负载就写自己要添加的那些私有数据。
优点:数据完全解耦:不影响视频编码,彻底规避了各平台对SEI大小、频率的限制。 容量与灵活性极高:可随意定义复杂格式(如JSON/Protobuf),想传多少就传多少,适合传输超大量元数据。独立控制:可单独配置这条流的QoS,比如做前向纠错(FEC)或重传。
缺点:帧级同步很难,这是最大的痛点。它不是视频流的一部分,必须自己在私有包里写入对应视频帧的RTP时间戳或序列号,接收端再手动建立映射。稍有偏差,人脸框就会对不上。兼容性差:必须修改SDP信令来声明这个私有载荷类型(PT)。而且,许多标准的流媒体服务器、CDN或录像系统,可能会直接丢弃这种无法识别的PT包,要想实现解析的话,需要定制播放库。
4. 识别 rtp 包
了解了协议构成, 如何识别一个 rtp 包呢?须知 rtp 不是一个独立协议, 经常搭配 rtsp 协议使用。当然如果没有上下文信息, 就只能靠解析 rtp header 来识别了, 两个 rtp header 中间的就是负载报文了。由于缺乏校验机制 (毕竟没有类似 h264 中 0001 那种防竞争机制), 无法保证识别到的 header 一定是 header, 所以独立的 rtp 包无法被确定解析成功。
在 rtsp 协议规范里, 使用 tcp 输出 rtp 包时, 由于 tcp 传输的是字节流, 并不标识数据长度, 会出现 粘包 现象, 为了识别 rtp 包、(rtcp 包)与 rtsp 报文, rtsp 协议会在发送 rtp 包时标识该包长度:
Stream data such as RTP packets is encapsulated by an ASCII dollar
sign (24 hexadecimal), followed by a one-byte channel identifier,
followed by the length of the encapsulated binary data as a binary,
two-byte integer in network byte order. The stream data follows
immediately afterwards, without a CRLF, but including the upper-layer
protocol headers. Each $ block contains exactly one upper-layer
protocol data unit, e.g., one RTP packet.即, 会在 rtp 报文前加入交错帧, 其构成为:
0x24 '$'+信道标识符(0x00 标识 rtp,0x01 标识 rtcp)+length(2 bytes)rtsp 服务器与客户端交互时, rtp/rtcp 数据包传输为:
S->C: $\000{2 byte length}{"length" bytes data, w/RTP header}
S->C: $\000{2 byte length}{"length" bytes data, w/RTP header}
S->C: $\001{2 byte length}{"length" bytes RTCP packet}这个好像也可以自己定义,比如添加一个字节,前两个字节固定位 0x30、0x00,后两个字节标识这个 rtp 包的长度这种。
四、传输模式
rtp 包的负载部分, 在一帧无法使用单 rtp 包传输时, 根据 pts 和 dts 是否一致分为两种模式:
- 非交错模式: 单个 NALU 单元模式。在非交错模式下, NAL 单元按 NAL 单元解码顺序传输。
- 交错模式: 多个 NALU 单元模式。交错模式允许在 NAL 单元解码顺序之外传输的 NAL 单元。
其实,后来搜了一下,这两个概念主要在 RTSP 流媒体传输 中出现,描述 RTP / RTCP 数据与 RTSP 控制信令之间的复用关系。在 SDP(会话描述协议)中有一个媒体格式参数。协议中有一个 packetization-mode 参数,表示支持的封包模式.
(1)packetization-mode 值为 0 时或不存在时, 必须使用单一 NALU 单元模式.
(2)packetization-mode 值为 1 时使用非交错(non-interleaved)封包模式.
(3)packetization-mode 值为 2 时使用交错(interleaved)封包模式.
1. 非交错模式(Non-interleaved)
一般是 UDP 传输(也可以 TCP,但少用)。数据包直接是原始的 RTP / RTCP 报文,没有额外封装。这种模式下,NAL 单元的发送顺序必须遵循 NAL 单元的解码顺序。
2. 交错帧模式(Interleaved Mode)
在 RTSP/RTP 传输中,当使用 TCP 作为传输协议时,RTP 数据会以交错帧(Interleaved Frame)的方式传输。这种模式允许乱序发送,它的延迟稍高,需要更复杂缓冲重排。交错帧格式:
[$][Channel][Length][RTP Data]
1B 1B 2B N bytes$(1 字节):交错帧标识符(0x24)- Channel(1 字节):通道号(指示这是 RTP 还是 RTCP 数据)
- Length(2 字节):后续 RTP 数据的长度
- RTP Data:实际的 RTP 包数据
所以 交错帧头占据 4 个字节(1+1+2 = 4)。
3. 占用 MSS 空间
最大报文段长度(Maximum Segment Size,MSS)是传输控制协议(TCP)的选项之一,用于在建立连接时协商双方可接收的 TCP 报文段数据部分最大值(不含协议头)。典型以太网场景中,基于 1500 字节 MTU 计算的 MSS 为 1460 字节(1500-40)。其数值受底层网络最大传输单元(MTU)限制,MTU 作用于 数据链路层 决定网络层数据包上限,MSS 则聚焦传输层协议数据分段控制。以 TCP 为例:
- 不使用交错帧:MSS = 1460 字节(全部用于 RTP 数据)
- 使用交错帧:MSS - 4 = 1456 字节(4 字节被交错帧头占用,剩余用于 RTP 数据)
Tips:为什么是 1500 减去 40 字节?这 40 字节是什么?
【TCP】
- IP 头部(IPv4 典型值 20 字节):包含版本、首部长度、服务类型、总长度、标识、标志、片偏移、TTL、协议、首部校验和、源地址、目的地址等。无选项固定部分正好 20 字节。
- TCP 头部(典型值 20 字节):包含源端口、目的端口、序号、确认号、数据偏移、标志位、窗口大小、校验和、紧急指针等等。无选项固定部分也是 20 字节。
所以总和
40 字节就是传输层以下完整的协议开销,剩下的 1460 字节才是可给上层应用(如 RTP、HTTP)使用的数据量。【UDP】
若用 UDP 传输,MSS 就会变成:
1500 - IP头(20) - UDP头(8) = 1472 字节(少 12 字节,因为 UDP 头部仅有 8 字节,且无连接、更精简)。
五、字节序问题
网络协议传输数据都是大端序, 即高位字节存放在低地址处。设备通常采用小端序,因此需要注意对数据进行转换。
六. 小结
好了,以上就是 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) - 苦涩的茶 - 博客园
【rtp over tcp/rtsp 解析】&&【H264 格式与 RTP 包解析】&&【以 H264 的打包方式-分片打包(type = 28 FU-A 方式为例)】
RTSP over UDP 与 RTSP over TCP 取流对比_rtp over udp-CSDN 博客
rtp 三种模式传输打包规则:single nalu、non-interleaved、non-interleaved_nalu single-CSDN 博客