音视频篇|RTSP协议定义和RTP报头与H.264的关系

最近打算开始学习音视频相关知识,本篇博客做一些相关知识的总结。

参考链接是B站UP北小菜,学习音视频讲解非常细致,对入门音视频和有编程基础的同学非常有用。

【音视频开发】《从零开始编写一个RTSP服务器》

《从零开始编写一个RTSP服务器》系列视频教程源码

RTSP协议定义

RTSP是实时传输协议,并且是一个在应用层的协议。通常在项目沟通交流的RTSP协议包括了:RTSP、RTP、RTCP。几个协议简要概括如下:

RTSP协议

负责服务端和客户端之间的请求和响应

RTP协议

负责服务端和客户端传输媒体数据

RTCP协议

负责提供有关RTP传输质量的反馈,确保传输的质量

三者的关系

RTSP协议不会发送媒体数据,只是完成服务器和客户端之间的信令交互;RTP协议负责媒体数据传输,RTCP负责RTP数据包的监视和反馈。

RTP和RTCP并没有规定传输层的类型,可以选择UDP和TCP。RTSP的传输层要求是基于TCP。

RTSP协议常用的方法

  • DESCRIBE:响应必须包含描述资源的所有媒体初始化信息。
  • OPTIONS:查询该流媒体服务器有哪些方法可以使用。
  • SETUP:请求与服务端建立RTSP连接。
  • PLAY:请求码流进行播放。码流将在多个报文中回复,收到之后进行解码播放
  • PAUSE:暂停码流传输,但RTSP连接没有断开。

SDP协议

SDP是一个会话描述协议。通常传输过程中包含一个会话级描述多个媒体级描述。

会话级描述

  • 会话的名称和目的
  • 会话存活时间
  • 会话中包括多个媒体信息

包括常用的IP、端口等建立连接的通用描述信息

媒体级描述

  • 媒体格式
  • 传输协议
  • 纯属IP和端口
  • 媒体负载类型

比如一段视频播放中,视频流算作一路,音频流算作一路,这样被称为多个媒体级描述。

RTP报头格式

一个常见的RTP报头格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 /*
* 0 1 2 3
* 7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |V=2|P|X| CC |M| PT | sequence number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | timestamp |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | synchronization source (SSRC) identifier |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | contributing source (CSRC) identifiers |
* : .... :
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
*/

struct RtpHeader //网络字节序大端,这样定义在后续定义、使用、计算都比较方便
{
/* byte 0 */
uint8_t csrcLen : 4;//CSRC计数器,占4位,指示CSRC 标识符的个数。
uint8_t extension : 1;//占1位,如果X=1,则在RTP报头后跟有一个扩展报头。
uint8_t padding : 1;//填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
uint8_t version : 2;//RTP协议的版本号,占2位,当前协议版本号为2。

/* byte 1 */
uint8_t payloadType : 7;//有效载荷类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等。
uint8_t marker : 1;//标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。

/* bytes 2,3 */
uint16_t seq;//占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。接收者通过序列号来检测报文丢失情况,重新排序报文,恢复数据。

/* bytes 4-7 */
uint32_t timestamp;//占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。

/* bytes 8-11 */
uint32_t ssrc;//占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。

/*标准的RTP Header 还可能存在 0-15个特约信源(CSRC)标识符

每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源

*/
};

标志位说明 解释说明
V(version) RTP协议的版本号,占2位。取值范围在0~3
P(padding) 填充标志,占1位。如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
X(extension) 扩展标志,占1位。如果X=1,则在RTP报头之后跟一个扩展报头。
CC(crcsLen) CSRC计数器,占4位。指示CSRC标识符的个数。
M(marker) 标记字段,占一位。根据载荷的类型会有不同的意义,对于视频,标记一帧的结束。对于音频,标记会话的结束。
PT(payloadType) 有效载荷类型,占7位。用于说明RTP报文中载荷类型。常见类型请继续查看下文。
sequence number(序列号) 用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增加1。接收者通过序列号检测报文丢失的情况,重新排序报文恢复数据。
timestamp(时间戳) 占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。接收者可以通过时间戳计算延迟和延迟抖动,并进行同步控制。
synchronization source (SSRC同步信源) 占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
contributing source (CSRC特约信源) 占32位,可以有0~15个。每个CSRC标识了包含在RTP报文有效荷载的所有特约信源。

RTP报文格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 /*
* 0 1 2 3
* 7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |V=2|P|X| CC |M| PT | sequence number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | timestamp |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | synchronization source (SSRC) identifier |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | contributing source (CSRC) identifiers |
* : .... :
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | payload(audio, video...) |
* | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ...| padding | count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/

struct RtpPacket
{
struct RtpHeader rtpHeader;
uint8_t payload[0];
};

紧接着之前的报头,在报头之后连续一段RTP载荷(payload)。载荷内部保存的就是音频流和视频流。

H.264编码

H.264在视频采集到输出中属于编解码层次的数据,是在采集数据后做编码压缩时,通过编码标准编码后所呈现的数据。

H.264相关概念

帧类型

H.264定义了三种帧,分别是I帧、B帧、P帧。

  • I帧:关键帧,采用帧内压缩技术。
  • P帧:向前参考帧,在压缩时,只参考前面已经处理的帧。采用帧音压缩技术。
  • B帧:双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧。采用帧间压缩技术。

GOP(画面组)

就是一段时间内变化不大的图像集。GOP结构一般有两个数字,如M=3,N=12。M指定I帧和P帧之间的距离,N指定两个I帧之间的距离。上面的M=3,N=12,GOP结构为:IBBPBBPBBPBBI。在一个GOP内I frame解码不依赖任何的其它帧,P frame解码则依赖前面的I frameP frameB frame解码依赖前最近的一个I frameP frame 及其后最近的一个P frame

I帧是一个完整图像帧;而B\P帧是不编码全部图像的帧;这如何理解呢?

假若我们此时观看一个电影画面,是一个非常美丽的风景片段。在运镜的过程中,几秒内的画面中有一个鸟慢慢飞过,那么此时用H.264编码来描述:IPBPBPBPBPBI,你可以理解为这个I帧就是一个完整的风景图片,而在P/B帧之间只是针对飞过的鸟做一个参考处理。这样一个视频片段需要处理的帧画面计算量就没有那么大。

IDR帧(关键帧-引用链接)

在编码解码中为了方便,将GOP中首个I帧要和其他I帧区别开,把第一个I帧叫IDR,这样方便控制编码和解码流程,所以IDR帧一定是I帧,但I帧不一定是IDR帧;IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始算新的序列开始编码。I帧有被跨帧参考的可能,IDR不会。

1
2
3
4
5
I帧不用参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样,例如:
IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15 这里的B8可以跨过I10去参考P7
=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12 这里的B9就只能参照IDR8和P11,不可以参考IDR8前面的帧

作用:
H.264引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。

码流结构VCL和NALU

H.264的主要目标是为了有高的视频压缩比和良好的网络亲和性。H.264的功能分为两层,视频编码层VCL和网络提取层NALU。

VCL和NALU结构图

视频编码层(Video Coding Layer)

VCL数据是被压缩编码后的视频数据序列。负责有效表示视频数据的内容,最终输出编码完的数据SODB(数据比特串,是编码后的原始数据)

宏块

宏块是视频信息的主要承载者,因为它包含每一个像素的亮度和色度信息。视频解码最主要的工作是提供高效的的方式从码流中获取宏块中的像素阵列。

一个宏块由一个16×16亮度像素和附加的一个8×8 Cb和一个 8×8 Cr 彩色像素块组成。

每个图象中,若干宏块被排列成片的形式。在一帧图像中,宏块的展示如下图。每一个方块可看作为一个宏块。

宏块的展示

Slice和分区

一帧图片经过H.264编码器之后,就被编码成一个或者多个片,这些片的载体就是NALU。再由上图举例:

Slice片的展示

在这里也能看出:宏块是属于NALU片数据的一部分,而NALU是属于一帧图片的一片

网络抽象层(Network Abstraction Layer Unit)-NALU

从存储结构上来看,H.264由一个个NALU组成。NALU负责以网络所要求的恰当方式去格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。

1
2
3
4
5
6
7
8
 /*
* |NAL|NAL|NAL| NAL | NAL | NAL | NAL |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |SPS|PPS|IDR|SLICE|SLICE|SLICE|SLICE| ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/

其中SPS、PPS、IDR和SLICE是NAL单元某一类型的数据。

这里先对SPS和PPS两种NALU类型做一个标记说明,目前只了解了相关概念,具体的使用后续用到时在进行详细学习。

  • SPS(序列参数集):SPS对如标识符、帧数以及参考帧数目、解码图像尺寸和帧场模式等 解码参数进行标识记录。作用于一系列连续的编码图像。
  • PPS(图像参数集):PPS对如熵编码类型、有效参考图像的数目和初始化等解码参数进行标志记录。作用于编码视频序列中一个或多个独立的图像。

H.264的NAL结构

1
2
3
4
5
6
7
RBSP: 原始字节序列载荷,是在原始编码数据后面添加了结尾比特,一个bit“1”和若干个比特“0”,用于字节对齐。 
/*
* |NALU序列|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |NAL头|RBSP|NAL头|RBSP|NAL头|RBSP| ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/

在实际的网络数据传输过程中H264的数据结构是以NALU(NAL单元)进行传输的,传输数据结构组成为[NALU Header]+[RBSP]。这里引用一下参考链接中对NAL结构的分析:

H264的NAL结构

NAL的头部结构
1
2
3
4
/*   0 1 2 3 4 5 6 7 8 9
/* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/* |F|NRI| Type | a single NAL unit ... |
/* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

NAL单元的头部是由forbidden_bit(1bit)nal_reference_bit(2bits)(优先级),nal_unit_type(5bits)(类型)三个部分组成的

标志位 标志位解释
F 禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误
NRI 参考级别,占用NAL头的第二到第三个位;值越大,该NAL越重要
TYPE Nal单元数据类型,也就是标识该NAL单元的数据类型是哪种,占用NAL头的第四到第8个位

常用Nalu_type:

1
2
3
4
5
6
7
8
9
10
11
12
0x06 (0 00 00110) SEI   type = 6
0x67 (0 11 00111) SPS type = 7
0x68 (0 11 01000) PPS type = 8

0x65 (0 11 00101) IDR type = 5
0x65 (0 10 00101) IDR type = 5
0x65 (0 01 00101) IDR type = 5
0x65 (0 00 00101) IDR type = 5

0x61 (0 11 00001) I帧 type = 1
0x41 (0 10 00001) P帧 type = 1
0x01 (0 00 00001) B帧 type = 1
NAL数据类型

NAL分为VCL和非VCL的NAL单元。其中SPS、SEI、PPS等非VCL的NAL参数对解码和显示视频都是很有用的。

另外一个需要了解的概念就是 参数集(Parameter sets),参数集是携带解码参数的NAL单元,参数集对于正确解码是非常重要的,在一个有损耗的传输场景中,传输过程中比特列或包可能丢失或损坏,在这种网络环境下,参数集可以通过高质量的服务来发送,比如向前纠错机制或优先级机制。

NAL数据类型有以下三种数据块:

  1. 头信息块,包括宏块类型,量化参数,运动矢量。这些信息是最重要的,因为离开他们,编码数据块中的码元都无法使用。该数据分块称为A类数据分块。

  2. 帧内编码信息数据块,称为B类数据分块。它包含帧内编码宏块类型,帧内编码系数。对应的Slice片来说,B类数据分块的可用性依赖于A类数据分块。和帧间编码信息数据块不通的是,帧内编码信息能防止进一步的偏差,因此比帧间编码信息更重要。

  3. 帧间编码信息数据块,称为C类数据分块。它包含帧间编码宏块类型,帧间编码系数。它通常是Slice片中最大的一部分。帧间编码信息数据块是不重要的一部分。它所包含的信息并不提供编解码器之间的同步。C类数据分块的可用性也依赖于A类数据分块,但与B类数据分块无关。

以上三种数据块每种分割被单独的存放在一个NAL单元中,因此可以被单独传输。

H.264的三种RTP打包方式

  • 单NALU打包方式

    一个RTP包包含一个完整的NALU。这是最简单的打包方式,即将一整个NALU的数据放入RTP包的载荷中。

  • 聚合打包

    对于较小的NALU,一个RTP包可包含多个完整的NALU。

  • 分片打包

    对于较大的NALU,一个NALU可以分为多个RTP包发送。

每一个RTP包都包含一个RTP头部和一个RTP载荷,这是RTP包的固定格式。H.264在发送数据时支持三种RTP打包方式。

但是RTP包是有大小限制的,因为RTP一般使用UDP发送,UDP并没有流量控制,所以要限制每次发送的大小,所以如果一个NALU太大,那么将会分为多个RTP包发送。那么此时就有一个关键的问题,NALU如何确定是同一个呢?

所以在分片打包的情况下,需要在载荷部分用前两个字节标记NALU的类型。

1
2
3
4
5
//*  0                   1                   2
//* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//* | FU indicator | FU header | FU payload ... |
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第一个字节FU indicator

1
2
3
4
5
//*    FU Indicator
//* 0 1 2 3 4 5 6 7
//* +-+-+-+-+-+-+-+-+
//* |F|NRI| Type |
//* +---------------+

前三个字节与NALU的第一个字节的高三位相同。

所以载荷第一个字节赋值为rtpPacket->payload[0] = (naluType & 0x60) | 28;

其中0x60 = 01100000,对高三位赋值。而十进制的28表示分片扩展。当一个 NALU 被分割成多个分片时,H.264 规范中定义了分片类型(slice type)用于标识分片的作用。而类型为十进制 28 的分片类型是分片扩展(Slice Extension)。

第二个字节FU Header

1
2
3
4
5
//*    FU Header
//* 0 1 2 3 4 5 6 7
//* +-+-+-+-+-+-+-+-+
//* |S|E|R| Type |
//* +---------------+

S:标记该分片打包的第一个RTP包

E:比较该分片打包的最后一个RTP包

Type:NALU的Type

所以第二个字节赋值为rtpPacket->payload[1] = naluType & 0x1F;

先赋值NALU的Type类型,0x1F = 00011111。接着判断是第一个包还是最后一个包:如果为第一个包,rtpPacket->payload[1] |= 0x80设置S标记位。如果为最后一个包,rtpPacket->payload[1] |= 0x40设置E标记位。

音视频入门相关知识先总结第一波~