FFmpeg5入门教程11.02:H264+RTP+RTSP数据包解析

系列教程索引:系列教程索引地址

上一篇:FFmpeg5入门教程11.01:番外篇

本文介绍RTSP流数据解析,RTSP数据包为RTSP包头+RTP数据包,RTP数据包为RTP数据包头+H264数据包。

简单来说:

structure

RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学、网景和RealNetworks公司提交的IETF RFC标准。该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。RTSP在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。HTTP与RTSP相比,HTTP请求由客户机发出,服务器作出响应;使用RTSP时,客户机和服务器都可以发出请求,即RTSP可以是双向的。RTSP是用来控制声音或影像的多媒体串流协议,并允许同时多个串流需求控制,传输时所用的网络通讯协定并不在其定义的范围内,服务器端可以自行选择使用TCP或UDP来传送串流内容,它的语法和运作跟HTTP 1.1类似,但并不特别强调时间同步,所以比较能容忍网络延迟。而前面提到的允许同时多个串流需求控制(Multicast),除了可以降低服务器端的网络用量,更进而支持多方视讯会议(Video Conference)。因为与HTTP1.1的运作方式相似,所以代理服务器〈Proxy〉的快取功能〈Cache〉也同样适用于RTSP,并因RTSP具有重新导向功能,可视实际负载情况来转换提供服务的服务器,以避免过大的负载集中于同一服务器而造成延迟。

准备

FFmpeg+wireshark+easydarwin

先按照FFmpeg5入门教程10.24:搭建UDP/TCP/HTTP(S)/RTP/RTMP/RTSP推流服务器搭建一个推流服务器,本文是解析RTSP流数据,自然是搭建RTSP推流服务器。

然后按照wireshark,我使用的版本为3.4.8

然后推流

1
ffmpeg -re -i /home/jackey/Videos/test.mkv -rtsp_transport tcp -vcodec h264 -f rtsp rtsp://localhost/test

然后收流看看有没有推流成功

1
ffplay rtsp://localhost/test

解析

使用管理员/root打开wireshart。

wireshark

不同的版本界面不一样,操作也可能不同。

双击any那一行

any

在过滤规则栏填入rtsp用于过滤rtsp相关的数据

然后在终端新开一个收流客户端,就可以看到

filter

新收流客户端的前10个是类似TCP/IP三次握手的过程。

握手

我们来看一下握手的过程

OPTIONS

首先向推流服务器发送OPTIONS请求。

options

我们可以得到

  • 请求类型为:OPTIONS
  • 请求的地址为:rtsp://localhost:554/test
  • 自动在地址后面加上了RTSP的端口
  • 请求的版本为:RTSP/1.0
  • CSeq:1
  • User-Agent: Lavf58.7.100
  • 所有的行结尾为:\r\n

我们用代码实现此流程时会用到。

同时,推流服务器会返回给我们请求结果

replay

我们可以得到:

  • 请求结果:RTSP/1.0 200 OK
  • CSeq:1
  • Session:8h********
  • Public:DESCRIBE, …
  • 所有行尾为:\r\n
  • CSeq和请求的值一样
  • Public应该是表示服务器支持的参数

DESCRIBE

describe

第二条信息,CSeq加1,Session是OPTIONS回应的值。

回应为:

reply

CSeq、Session是我们发送的。

SETUP

describe

CSeq加1,发送的参数变了,视频有音视频两条流,所以SETUP有两个。

回应为:

reply

PLAY

describe

CSeq在SETUP的基础上加一(SETUP有两个)

回应为:

reply

代码实现

如果我们是在纯代码中开发这个流程,就需要按照这个流程进行发送接收。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
int RTSPData::options(int to)
{
/*
* OPTIONS url RTSP/1.0\r\n
* CSeq: n\r\n
* User-Agent: Darkise rtsp player\r\n
*/
#define OPTIONS_CMD "OPTIONS %s " RTSP_VERSIION "\r\nCSeq: %d\r\n" RTSP_USERAGENT "\r\n"
char cmd[1024];
int size = snprintf(cmd, sizeof(cmd), OPTIONS_CMD, _url, CSeq);
_send_request(cmd, size);
// Waiting for response
char resp[2048];
if (_wait_response(to, resp, sizeof(resp))) {
return -1;
}
// Parse response

CSeq++;
return 0;
}

int RTSPData::describe(int to)
{
/*
* DESCRIBE url RTSP/1.0\r\n
* CSeq: n\r\n
* User-Agent: Darkise rtsp player\r\n
* Accept: application/sdp\r\n
*/
#define DESCRIBE_CMD "DESCRIBE %s " RTSP_VERSIION "\r\n""CSeq: %d\r\n" RTSP_USERAGENT "Accept: application/sdp\r\n\r\n"
char cmd[1024];
int size = snprintf(cmd, sizeof(cmd), DESCRIBE_CMD, _url, CSeq);
// Send command the RTSP server
_send_request(cmd, size);
// Waiting for response
char resp[2048];
if (_wait_response(to, resp, sizeof(resp))) {
return -1;
}
// Parse response
_parse_sdp(resp);

CSeq++;

return 0;
}

int RTSPData::_setup_interleaved(int to)
{
/*
* SETUP (attribute control) RTSP/1.0\r\n
* CSeq: n\r\n
* User-Agent: Darkise rtsp player\r\n
* Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n
*/
#define SETUPI_CMD_I "SETUP %s " RTSP_VERSIION "\r\nCSeq: %d\r\n" RTSP_USERAGENT "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"
char cmd[1024];
int size = snprintf(cmd, sizeof(cmd), SETUPI_CMD_I, control, CSeq);
_send_request(cmd, size);

// Waiting for response
char resp[2048];
if (_wait_response(to, resp, sizeof(resp))) {
return -1;
}
// Parse response
_parse_session(resp);

CSeq++;

return 0;
}

int RTSPData::setup(int to)
{
return _setup_interleaved(to);
}

int RTSPData::play(int to)
{
/*
* PLAY url RTSP/1.0\r\n
* CSeq: 1\r\n
* User-Agent: Darkise rtsp player\r\n
* Session: \r\n
* Range: npt=0-\r\n
*/
#define PLAY_CMD "PLAY %s " RTSP_VERSIION "\r\nCSeq: %d\r\n" RTSP_USERAGENT "Session: %s\r\nRange: npt=0-\r\n\r\n"
char cmd[1024];
int size = snprintf(cmd, sizeof(cmd), PLAY_CMD, _url, CSeq, sessionId);
_send_request(cmd, size);

// Waiting for response
char resp[2048];
if (_wait_response(to, resp, sizeof(resp))) {
return -1;
}
// Parse response

CSeq++;
return 0;
}

int RTSPData::rtsp_init()
{
CSeq = 1;
struct hostent *hp;
struct sockaddr_in server;
// Rtp content buffer
rtp_content = (uint8_t*)malloc(rtp_size);

// Get server IP
hp = gethostbyname(host);
if (NULL == hp) {
printf("gethostbyname(%s) error.\n", host);
return -1;
}
// Connect to Server
rtspSocket = socket(AF_INET, SOCK_STREAM, 0);
if (rtspSocket < 0) {
printf("Create socket failed.\n");
return -1;
}
memset(&server, 0, sizeof(struct sockaddr_in));
memcpy(&(server.sin_addr), hp->h_addr, hp->h_length);
server.sin_family = AF_INET;
server.sin_port = htons((uint16_t)(port));
if (::connect(rtspSocket, (struct sockaddr*)&server, sizeof(struct sockaddr_in)) != 0) {
printf("Connect to server [%x:%d] error.\n", server.sin_addr.s_addr, server.sin_port);
close(rtspSocket);
rtspSocket = -1;
return -1;
}

/// Set socket to non-blocking
printf("Set non-blocking socket.\n");
int on = 1;
int rc = ioctl(rtspSocket, FIONBIO, (char *)&on);
if (rc < 0) {
close(rtspSocket);
rtspSocket = -1;
return -1;
}

/** RTSP控制协议初始化 */
// OPTIONS
if (options(rtspTimeout)) {
close(rtspSocket);
rtspSocket = -1;
return -1;
}
// DESCRIBE
if (describe(rtspTimeout)) {
close(rtspSocket);
rtspSocket = -1;
return -1;
}
// SETUP
if (setup(rtspTimeout)) {
close(rtspSocket);
rtspSocket = -1;
return -1;
}
// PLAY
if (play(rtspTimeout)) {
close(rtspSocket);
rtspSocket = -1;
return -1;
}

// Success
return 0;
}

以上代码为标准初始化流程,在各个初始化函数中还有一些函数调用

通过socket发送指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int RTSPData::_send_request(const char *req, int size)
{
qDebug("send request. %.*s\n", size, req);
ssize_t snd = 0;
if (rtspSocket < 0) {
qDebug("Connection to server has not been set up.\n");
return -1;
}
ssize_t s;
do {
s = send(rtspSocket, req + snd, size - snd, 0);
if (s <= 0) {
qDebug("Send request error. %s.\n", strerror(errno));
return -1;
}
snd += s;
} while (snd < size);

return 0;
}

等待接收服务器返回数据并解析

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
45
46
47
48
49
50
51
52
53
54
55
56
int RTSPData::_wait_response(int to, char resp[], size_t size)
{
int rc = 0;
struct pollfd fds;
//int poll(struct pollfd *fds, nfds_t nfds, int timeout);
if (rtspSocket < 0) {
qDebug("Connection to server has not been set up.\n");
return -1;
}
memset(&fds, 0, sizeof(fds));
fds.fd = rtspSocket;
fds.events = POLLIN;

rc = poll(&fds, 1, to);
if (rc < 0) {
qDebug("poll call error. %s.\n", strerror(errno));
return -1;
}
else if (0 == rc) {
qDebug("Time out.\n");
}

// Receiving
int rcvs = recv(rtspSocket, resp, size, 0);
qDebug("Response[%.*s].\n", rcvs, resp);
// Is response ok?
/// checking "RTSP/1.0 200 OK \r\n"
#define RTSP_SUCESS RTSP_VERSIION" 200 OK\r\n"
if (strncmp(resp, RTSP_SUCESS, sizeof(RTSP_SUCESS)-1) != 0) {
qDebug("Response error.\n");
return -1;
}
return 0;
}

int RTSPData::_parse_session(const char *resp)
{
// Get session ID
/// Session: 1416676415;timeout=60
char const* pr = strstr(resp, RTSP_SESSION_NAME);
if (NULL == pr) {
qDebug("Not " RTSP_SESSION_NAME " entry.\n");
return -1;
}
pr += (sizeof(RTSP_SESSION_NAME) - 1);
// Skip blank or '\t'
while (' ' == *pr || '\t' == *pr) pr++;
// Copy the number character
int w = 0;
while (*pr >= '0' && *pr <= '9') {
sessionId[w++] = *pr++;
}
sessionId[w] = '\0';

return 0;
}

session字段就是使用的第一次从服务器接收的数据。有的命令只是将发送的指令部分数据返回表示运行正常,不解析也可以,但是第一次的OPTIONS必须要解析。

此流程是RTSP 的CS握手流程,但是我们不能每次读取数据就进行握手。

我们可以看到,CSeq这个变量会随着通信进行值变化。以此我们作为是否初始化完成、是否需要再次初始化的标志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int RTSPData::isStart()
{
// 检查环境是否准备就绪
if (rtspSocket >= 0 && CSeq > 4) {
return 1;
}
if (rtspSocket >= 0) {
close(rtspSocket);
rtspSocket = -1;
}
rtsp_init();
///socket RTSP play已完成
if (rtspSocket < 0 || CSeq <= 4)
return 0;
// Normal
return 1;
}

先判断基本流程是否走完,如果走完了就直接返回。如果没有走完就再做一遍。

数据获取流程为:

1
2
3
4
5
6
7
8
9
10
11
while(1){
if(!isStart()){
//延时等待下一次连接
}
read_data();//从socket获取数据
if(rtsp_packet()){//获取RTSP中的H264数据
//解码函数
}else{
//数据包读取错误,或者没读完,延时
}
}

RTSP数据包

握手完毕后就是视频数据了

data

Payload是H264,H264在RTP后面,RTP在RTSP后面,RTSP在TCP(推流时使用的是TCP协议)后面。

TCP数据

先把数据从socket中读出来,以便后续处理

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
int RTSPData::rtsp_read()
{
static uint8_t buff[2048];
// socket 有数据吗?
int rcvs = recv(rtspSocket, buff, sizeof(buff), 0);
if (rcvs < 0) {
if (errno != EAGAIN) {
// ERROR
close(rtspSocket);
rtspSocket = -1;
}
}
else if (rcvs > 0) {
//rtsp_dump(buff, rcvs);
// 移动缓冲区剩余内容到页首
if (rtp_read != rtp_write) {
if (rtp_read != 0) {
memmove(rtp_content, rtp_content+rtp_read, rtp_write - rtp_read);
rtp_write -= rtp_read;
rtp_read = 0;
}
}
else {
rtp_read = rtp_write = 0;
}
// 复制到缓冲区
memcpy(rtp_content+rtp_write, buff, rcvs);
rtp_write += rcvs;
}
//else /* if (0 == rcvs) */ { }

return rcvs;
}

RTSP数据

一个包结构为:

1
2
3
+-------+------+-------+
|RTSP包头|RTP包头|RTP负载|
+-------+------+-------+

RTSP包结构为:

1
2
3
+-----+-------+------+
|Magic|Channel|Length|
+-----+-------+------+
  • Magic魔数,一个字节,固定为0x24,如果不是就把此包丢掉
  • Channel,一个字节取值由RTSP协议中Setup阶段设置的interleaved来决定,默认0-1,0代表后面的是RTP包,1代表RTCP包,本文中所有手动的值都为0x02,所以不看这个值,具体请查看标准文档
  • Length,两个字节,表示RTP/RTCP数据包的长度

根据此结构,我们构造一个结构体

1
2
3
4
5
6
typedef struct RtspCntHeader {
uint8_t magic; // 0x24
uint8_t channel;
uint16_t length;
uint8_t payload[0]; // >> RtpHeader
} RtspCntHeader_st;

如果获取的数据小于这个结构体大小,那就表示这个包连包头都不完整,直接丢掉

1
2
if (_rtsp_remaining <= sizeof(RtspCntHeader_st))
return 0;

将读取的数据指针强转为RTSP数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
RtspCntHeader_st* rtspH = (RtspCntHeader_st*)(rtp_content+rtp_read);
if (0x24 != rtspH->magic) {
qDebug("Magic number error. %02x\n", rtspH->magic);
// Magic number ERROR, discarding all the data
rtp_read = rtp_write = 0;
break;
}
size_t rtsplen = ntohs(rtspH->length);
if (rtsplen > _rtsp_remaining - sizeof(RtspCntHeader_st)) {
qDebug("No enough data. %lu|%lu\n", rtsplen, _rtsp_remaining);
// No enough data, try next loop
break;
}

然后验证魔数,忽略channel,获取RTP包长度。

RTP数据

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 |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • V:2bit,版本号
  • P:1bit,Padding标记,取值0-1,0表示Payload后面没有填充,1代表Payload 后跟有1个或最多8个字节的填充,如果有填充,RTP包最后一个字节是填充计数器,表示包含自身在内的填充的字节数
  • X:1bit,扩展标记
  • CSRC:4bit,Contributing Source identifiers Count,CSRC计数器,特约信源计数器
  • M:标记,取值0-1,0代表不是一帧的结束,1代表一帧数据的结束,该值是由h264定义的NAL单元传输三个结构中的FU(Fragmentation unit 分片单元) 结构的Header中E结束位决定的
  • PT:7bit,有效载荷类型,Payload Type
  • Seq:16bit,sequence number序列号,在前包的seq上自增1。如果没有扩展,RTP包除去前面12个字节的报头后,就是Payload,如果有Padding,还要减去后面的填充RTP Payload
  • RTP Payload的结构,是由h264协议定义的,可能会有三种情况:
    1.当NAL单元小于MTU时,可以传输一个完整的NAL单元,即Payload就是原始的h264 NAL单元
    2.当NAL单元特别小时,可以同时传送两个或多个NAL单元,即组合封包模式
    3.当NAL单元大于MTU是,就分两个或多个RTP包发送,即分片封包(Fragmentation Units)传输H.264

以此创建RTP数据包结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct RtpCntHeader {
#if 0
uint8_t version:2;
uint8_t padding:1;
uint8_t externsion:1;
uint8_t CSrcId:4;
uint8_t marker:1;
uint8_t pt:7; // payload type
#else
uint8_t exts;
uint8_t type;
#endif
uint16_t seqNo; // Sequence number
uint32_t ts; // Timestamp
uint32_t SyncSrcId; // Synchronization source (SSRC) identifier
// Contributing source (SSRC_n) identifier
uint8_t payload[0]; // Frame data
} RtpCntHeader_st;

将上面RtspCntHeader_st的payload部分强转为RtpCntHeader_st。

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
RtpCntHeader_st* rtpH = (RtpCntHeader_st*)rtspH->payload;
if (0x60 != (rtpH->type & 0x7f)) {
qDebug("No video stream %02x.\n", rtpH->type);
// 不是RTP视频数据,不处理
rtp_read += (sizeof(RtspCntHeader_st) + rtsplen);
continue;
}
// 将数据复制到packet buffer中,数据足够时使用mpp解码
uint8_t h1 = rtpH->payload[0];
uint8_t h2 = rtpH->payload[1];
uint8_t nal = h1 & 0x1f;
uint8_t flag = h2 & 0xe0;
int paylen = rtsplen - sizeof(RtpCntHeader_st);
if (0x1c == nal) {
if (0x80 == flag) {
packet_buffer[packet_wpos++] = 0;
packet_buffer[packet_wpos++] = 0;
packet_buffer[packet_wpos++] = 0;
packet_buffer[packet_wpos++] = 1;
packet_buffer[packet_wpos++] = ((h1 & 0xe0) | (h2 & 0x1f));
}
memcpy(packet_buffer + packet_wpos, &(rtpH->payload[2]), paylen - 2);
packet_wpos += (paylen - 2);
}else {
packet_buffer[packet_wpos++] = 0;
packet_buffer[packet_wpos++] = 0;
packet_buffer[packet_wpos++] = 0;
packet_buffer[packet_wpos++] = 1;
memcpy(packet_buffer + packet_wpos, rtpH->payload, paylen);
packet_wpos += paylen;
}
// Move read pointer
rtp_read += (paylen + sizeof(struct RtpCntHeader) + sizeof(struct RtspCntHeader));

至此为止,从RTSP流中解析出了H264数据了。本代码的原来目的是在国产瑞芯微rockchip平台上开发视频硬件解码并显示的程序。代码第一版开发时是由公司提供的硬件平台(现在以已作他用),只需要把H264数据安装rockchip硬件解码的流程填充就可以了,代码没有什么问题,但是Qt/QML在显示多路硬件解码的视频时会出现问题。

完整代码是属于公司客户的项目,只提供RTSP解析部分的内容,硬解部分忽略。

可公开代码在ffmpeg_beginner中的RTSParser中。

本文到此结束。

下一篇:FFmpeg5入门教程11.03:jetson nano搭建EasyDarwin+FFmpeg+x264+RTSP推流服务器


FFmpeg5入门教程11.02:H264+RTP+RTSP数据包解析
https://feater.top/ffmpeg/rtsp-rtp-h264-data-packet-parser/
作者
JackeyLea
发布于
2021年10月7日
许可协议