1. RTSP协议简介

配置属性 -> 调试 -> 环境 :
PATH=D:\ffmpeg\ffmpeg-master-latest-win64-gpl-shared\bin;D:\ffmpeg\SDL2-2.28.5\lib\x64
用分号来写多个路径,可以指定dll文件,不必再必须配置环境变量
.
配置属性 -> VC++目录 -> 包含目录 :
D:\ffmpeg\SDL2-2.28.5\include
D:\ffmpeg\ffmpeg-master-latest-win64-gpl-shared\include
D:\ffmpeg\vcpkg\include
.
配置属性 -> VC++目录 -> 库目录 :
D:\ffmpeg\ffmpeg-master-latest-win64-gpl-shared\lib
D:\ffmpeg\SDL2-2.28.5\lib\x64
D:\ffmpeg\vcpkg\lib
.
链接器 -> 输入 -> 附加依赖项 :
avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib
SDL2.lib
SDL2main.lib
SDL2test.lib
fdk-aac.lib
zlib.lib
event.lib
event_core.lib
event_extra.lib

1.1 RTSP服务器基础框架

以下代码并没有使用功能,只是通过wireshark进行抓包分析来了解RTSP协议的大致流程

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include <string>
#pragma comment(lib, "ws2_32.lib") //链接阶段将指定的库文件 ws2_32.lib 加入到当前项目中。(套接字编程的文件)
#include <stdint.h>
#include <iostream>
#pragma warning( disable : 4996 )
#define SERVER_PORT 8554
#define SERVER_RTP_PORT 55532
#define SERVER_RTCP_PORT 55533

int CreateTcpSocket()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
int on = 1; //在setsockopt中如果是1就会避免地址被占用的问题,帮助快速启动服务器

//SOL_SOCKET 是一个常量,表示套接字级别的选项,适用于所有套接字类型,不限于特定的传输协议
//如果是其他值,可能会代表,用于IPV6,4,TCP,UDP协议相关的选项
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
return fd;
}

static int BindSocketAddr(int fd) //加上static封装函数,仅当前文件能够使用
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(SERVER_PORT);
addr.sin_addr.s_addr = INADDR_ANY; //0 == 0.0.0.0(本机任意IP地址)
if (bind(fd, (sockaddr*)&addr, sizeof(sockaddr)) < 0)
return -1;
return 0;
}

static int AcceptClient(int fd, char* ip, int* port)
{
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t len = sizeof(addr);
int clientfd = accept(fd, (sockaddr*)&addr, &len);
if (clientfd < 0)
return -1;
//32位地址转换为点分十进制字符串
strcpy(ip, inet_ntoa(addr.sin_addr)); //inet_ntop更安全(包括线程安全),但是参数更多
*port = ntohs(addr.sin_port); //转换为小端后存储
return clientfd;
}


static void Handle_Option(char* buf, int cseq)
{
sprintf(buf, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Public: OPTIONS, DESCRIBE, SETUP, PlAY\r\n"
"\r\n",
cseq);
}

static void Handle_Describe(char* buf, int cseq, char* url)
{
char sdp[500];
char local_ip[100];
sscanf(url, "rtsp://%[^:]:", local_ip); //%[^:] 匹配除了:以外的任意字符
sprintf(sdp, "v=0\r\n"
"o=- 9%ld 1 IN IP4 %s\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=control:track0\r\n",
time(NULL), local_ip);

sprintf(buf, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
"Content-Base: %s\r\n"
"Content-type: application/sdp\r\n"
"Content-length: %zu\r\n\r\n"
"%s",
cseq,url,strlen(sdp),sdp);
}

static void Handle_Setup(char* buf, int cseq, int rtp_port)
{
sprintf(buf, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
"Session: 66334873\r\n"
"\r\n",
cseq, rtp_port, rtp_port + 1, SERVER_RTP_PORT, SERVER_RTCP_PORT);
}

static void Handle_Play(char* buf, int cseq)
{
sprintf(buf, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Range: npt=0.000-\r\n"
"Session: 66334873; timeout=10\r\n\r\n",
cseq);
}

static void DoClient(int fd, const char* ip, int port)
{
//客户端可能发送来的信息(请求) DESCRIBE rtsp ://127.0.0.1:8554/stream1 RTSP/1.0
char method[40]; //DESCRIBE
char url[100]; //rtsp://127.0.0.1:8554/stream1
char version[40]; //RTSP/1.0

int CSeq; //请求中的序列号
int client_Rtp_port;
int client_Rtcp_port;

char* r_buf = (char*)malloc(10000); //读缓冲区,读取客户端数据
char* w_buf = (char*)malloc(10000); //写缓冲区,向客户端发送数据

while (1) //more reuqest
{
int recv_len = recv(fd, r_buf, 2000, 0); //从客户端(fd)中读取最多2000个数据到r_buf;
if (recv_len <= 0) //return readed len
break;
r_buf[recv_len] = '\0';
std::cout << std::endl << __FUNCTION__ << " r_buf = " << r_buf << std::endl;
const char* sepe = "\n";
char* line = strtok(r_buf, sepe);
while (line) //only one request do
{
if (strstr(line, "OPTIONS") ||
strstr(line, "DESCRIBE") ||
strstr(line, "SETUP") ||
strstr(line, "PLAY"))
{
if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3) //return success args num
{
std::cout << "sscanf method url version fail" << std::endl;
exit(-1);
}
}
else if (strstr(line, "CSeq"))
{
if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1)
{
std::cout << "sscanf CSeq" << std::endl;
exit(-1);
}
}
else if (!strncmp(line, "Transport:", strlen("Transport:"))) //suces
{
if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n", &client_Rtp_port, &client_Rtcp_port) != 2)
{
std::cout << "sscanf 2 port fail" << std::endl;
exit(-1);
}
}
line = strtok(NULL, sepe);
//nullptr
}

if (!strcmp(method, "OPTIONS"))
Handle_Option(w_buf, CSeq); //no check
else if (!strcmp(method, "DESCRIBE"))
Handle_Describe(w_buf, CSeq, url);
else if (!strcmp(method, "SETUP"))
Handle_Setup(w_buf, CSeq, client_Rtp_port);
else if (!strcmp(method, "PLAY"))
Handle_Play(w_buf, CSeq);
else
std::cout << "nb method : " << method << std::endl;

std::cout << std::endl << __FUNCTION__ << "w_buf = " << w_buf << std::endl;
send(fd, w_buf, strlen(w_buf), 0);

if (!strcmp(method, "PLAY"))
{
std::cout << "start play\n" << "client ip:" << ip << std::endl << "client port:" << client_Rtp_port << std::endl;
while (1)
{
//...
Sleep(40);
}
break;
}

memset(method, 0, sizeof(method));
memset(url, 0, sizeof(url));
CSeq = 0;
} //next while

closesocket(fd);
free(r_buf);
free(w_buf);
}

int main()
{
WSADATA winsock;
if (WSAStartup(MAKEWORD(2, 2), &winsock) != 0)
{
std::cout << "winsock start error" << std::endl;
return -1;
}

int server_fd = CreateTcpSocket();
if (server_fd < 0)
{
WSACleanup();
std::cout << "tcpsocket create fail" << std::endl;
return -1;
}

if (BindSocketAddr(server_fd) < 0)
{
std::cout << "bind fail" << std::endl;
return -1;
}

if (listen(server_fd, 128) < 0)
{
std::cout << "listen fail" << std::endl;
return -1;
}

std::cout << __FILE__ << " rtsp://127.0.0.1:" << SERVER_PORT << std::endl;

while (1)
{
int client_fd;
char client_ip[40];
int client_port;
client_fd = AcceptClient(server_fd, client_ip, &client_port);
if (client_fd < 0)
{
std::cout << "accept fail" << std::endl;
return -1;
}
std::cout << "accept client ip:" << client_ip << " port:" << client_port;
DoClient(client_fd, client_ip, client_port);
}
closesocket(server_fd);
return 0;
}

1.2 分析抓包数据

以下服务器的回复抓包数据都是根据以上代码的接受发送数据决定的

客户端第一次发出请求(OPTIONS)
向服务器询问有那种请求方式

服务器回应OPTIONS
服务器给出发出请求的方式

客户端第二次发出请求(DESCRIBE)
请求指定流媒体的描述信息(SDP,Session Description Protocol)

服务器回应DESCRIBE
回应会话描述(SDP) 返回流媒体的描述信息,包括媒体类型、编解码器、流地址等。

客户端第三次发出请求(SETUP)
请求建立媒体流传输的通道
两个(RTCP和RTP通道)端口号

服务器回应SETUP
返回确认或者错误消息。
在这个步骤中,可以指定传输协议(UDP、TCP、RTP 等)和端口号等参数。

客户端第四次发出请求(PLAY)
请求开始播放流媒体

服务器回应PLAY
接受请求,并开始向客户端发送媒体数据。

2. UDP的RTP传输h264的RTSP服务器,能拉流播放

上一部分已经实现了一个RTSP协议交互的案例,客户端播放器能够向我们的RTSP服务端发起连接建立的请求,并且客户端在发起RTSP的Play请求以后,RTSP服务端也已经回复了Play请求的确认。

这部分需要实现,客户端建立与RTSP服务端的连接后,并且在RTSP服务端回复了客户端的Play请求以后.
服务端需要源源不断的读取一个本地h264视频文件,并将读取到的h264视频流封装到RTP数据包中,再推送至客户端。
这样我们就实现了一个简单的支持RTSP协议流媒体分发服务。

问题的关键就是 如何将H264封装到RTP数据包

2.1 RTP理解

2.1.1 RTP简述

RTP:实时传输协议(Real-time Transport Protocol或简写RTP)是一个网络传输协议.
RTP定义了两种报文:RTP报文RTCP报文

  • RTP报文用于传送媒体数据(如音频和视频,包括自定义类型,如字幕等)
    它由 RTP报头和数据两部分组成,RTP数据部分称为有效载荷(payload);
  • RTCP报文用于传送控制信息,以实现协议控制功能。
  • RTP报文和RTCP报文将作为下层协议的数据单元进行传输。
    如果使用UDP,则RTP报文和RTCP报文分别使用两个相邻的UDP端口,RTP报文使用低端口,RTCP报文使用高端口。
    如果使用其它的下层协议,RTP报文和RTCP报文可以合并,放在一个数据单元中一起传送,控制信息在前,媒体数据在后。通常,RTP是由应用程序实现的。
    eg: TCP作为协议时,RTP和RTCP甚至RTSP都是一个通道进行传输的

2.1.2 RTP报文格式

  • V:RTP协议的版本号,占2位
    当前协议版本号为2。
  • P:填充标志,占1位
    如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
  • X:扩展标志,占1位
    如果X=1,则在RTP报头后跟有一个扩展报头。
  • CC:CSRC计数器,占4位
    指示CSRC 标识符的个数。
  • M: 标记,占1位, 不同的有效载荷有不同的含义
    对于视频,标记一帧的结束;
    对于音频,标记会话的开始。
  • PT: 有效载荷类型,占7位
    用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等。
  • 序列号:占16位
    用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。
    接收者通过序列号来检测报文丢失情况,重新排序报文,恢复数据。
  • 时戳(Timestamp):占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。
    接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。
  • 同步信源(SSRC)标识符:占32位,用于标识同步信源。
    该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
  • 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。
    每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。

每一行占4字节,32位

视频会议,直播连麦等类似情况时(多路流汇成一路流),CSRC才会存在(多个SSRC汇集为CSRC)

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
#pragma once
#pragma comment(lib, "ws2_32.lib")
#include <stdint.h>

#define RTP_VESION 2

#define RTP_PAYLOAD_TYPE_H264 96
#define RTP_PAYLOAD_TYPE_AAC 97

#define RTP_HEADER_SIZE 12
#define RTP_MAX_PKT_SIZE 1400

/*
* 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
{
//位段中,定义的占位数是倒着来的,即在第0个字节中,最后四位是csrcLen
/* byte 0 */
uint8_t csrcLen : 4;//CSRC计数器,占4位,指示 CSRC 标识符的个数。
uint8_t extension : 1;//占1位,如果X=1,则在RTP报头后跟有一个扩展报头。
//一般只有传输类型是非音频,非视频的自定义类型或许需要这个X=1
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报文的序列号,每发送一个报文(eg:每发送一帧),序列号增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报文有效载荷中的所有特约信源

*/
};

//RTP包的结构体
struct RtpPacket
{
struct RtpHeader rtpHeader;
uint8_t payload[0];
};
//包含一个RTP头部和RTP载荷

2.1.3 RTP包发送前缀

RTP,RTCP 数据和 RTSP 数据共享 TCP 数据通道,所以必须有一个标识来区别三种数据。
RTP 和 RTCP 数据会以 $符号 + 1 个字节的通道编号 + 2 个字节的数据长度 共 4 个字节的前缀开始,RTSP 数据没有前缀数据

RTP 数据和 RTCP 数据的区别在于第二个字节的通道编号, RTP 通道编号是偶数,RTCP通道编号是奇数。

2.2 H264理解

2.2.1 压缩方式

H264压缩技术主要采用了以下几种方法对视频数据进行压缩:

  • 帧内预测压缩,解决的是空域数据冗余问题。(eg:jpg) 不借助于其他数据
  • 帧间预测压缩(运动估计与补偿),解决的是时域数据冗徐问题。
  • 整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化。
  • CABAC压缩。

经过压缩后的帧分为:I帧,P帧和B帧:

  • I帧:关键帧,采用帧内压缩技术。
  • P帧:向前参考帧,在压缩时,只参考前面已经处理的帧。
    采用帧间压缩技术。占I帧的一半大小
  • B帧:双向参考帧,在压缩时,它即参考前而的帧,又参考它后面的帧。采用帧间压缩技术。占I帧1/4大小
  • 图像序列GOP(GOP组),将一个视频分为若干部分,防止因为部分帧损坏而导致视频崩盘.GOP至少有一个I帧

虽然B帧的压缩率是最高,但是他占用的CPU以及耗时非常多,既然耗时,在实时直播视频情况就会导致一个延迟的情况。
如果只是想减少空间大小,B帧将是一个很好的选择。
所以一般在直播中是没有B帧,只有I帧和P帧。

IDR帧和I帧的关系。

IDR(Instantannous Decoder Refresh) 解码器立即刷新

作用:在解码的过程,一旦有一帧数据出现错误,将是无法恢复的过程,后面数据帧不能使用。
当有了IDR帧,解码器收到IDR帧时,就会将缓冲区的数据清空,找到第一个IDR帧,重新解码。
I和IDR帧都使用帧内预测,在编码解码中为了方便,首个I帧要和其他I帧区别开,把第一个I帧叫IDR,这样方便控制编码和解码流程。
IDR帧必须是一个I帧,但是I帧不一定是IDR帧,这个帧出现的时候,是告诉解码器,可以清除掉所有的参考帧,这是一个全新的序列,新的GOP已经开始。I帧有被跨帧参考的可能,IDR不会。
每个GOP中的第一帧就是IDR帧。
IDR帧是一种特殊的I帧。

帧与分组的关系:

2.2.2 h264组成

  1. 网络提取层 (Network Abstraction Layer,NAL), 编码(压缩)后的流
  2. 视讯编码层 (Video Coding Layer,VCL), 产生NAL的过程

2.2.3 码流结构

H.264的功能分为两层,视频编码层(VCL)和网络提取层(NAL)。
1.VCL数据即被压缩编码后的视频数据序列。
2.在VCL数据要封装到NAL单元中之后,才可以用来传输或存储。

  • SPS:序列参数集,作用于一系列连续的编码图像;
    包含视频序列的全局参数信息,比如图像的宽度和高度、帧率、色度格式等。这些信息对于整个视频流是固定的,或者在一段时间内是固定的。
  • PPS:图像参数集,作用于编码视频序列中一个或多个独立的图像;
    包含具体图像的参数,比如解码参数、量化参数等。PPS 也是在一段时间内固定的,不会频繁变化。

NALU根据nal_unit_type的类型,可以分为:
VCL的NAL单元 非VCL的NAL单元

2.3 解封装mp4生成h264文件

1
ffmpeg -i test.mp4 -codec copy -bsf: h264_mp4toannexb -f h264 test.h264

2.4 将H264封装到RTP数据包

H.264由一个一个的NALU组成,每个NALU之间使用00 00 00 0100 00 01分隔开,每个NALU的第一次字节都有特殊的含义,

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

常用Nalu_type:

  • 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
    0x61 (0 11 00001) I帧 type = 1
    0x41 (0 10 00001) P帧 type = 1
    0x01 (0 00 00001) B帧 type = 1
    0x09 (0 00 01001) AUD type = 9

访问单元分隔符(Access Unit Delimiter, AUD)
AU Delimiter 是用于标记接入单元的起始位置
接入单元是一个视频帧或一组视频帧的集合,这些帧可以独立解码, AU Delimiter 帮助解码器识别接入单元的边界
AUD 是一个可选的 NALU(网络抽象层单元),可以用于在基本流中分隔帧
它不是必需的(除非容器/协议另有规定,例如 TS),并且通常不包括在内以节省空间,但它在不需要完全解析每个 NALU 的情况下找到帧的起始位置时非常有用
在处理 H.264 视频数据时,Access Unit Delimiter (AUD) 类型的 NALU 通常并不包含视频数据本身,而是用于标记接入单元(即帧)的起始位置。
为了节省空间,AUD 通常是可选的,因此解析器在遇到 AUD 类型的 NALU 时通常会跳过它并继续处理下一个 NALU

对于H.264格式了解这些就够了,我们的目的是想从一个H.264的文件中将一个一个的NALU提取出来,然后封装成RTP包,下面介绍如何将NALU封装成RTP包。

H.264可以由三种RTP打包方式

  • 单NALU打包: 一个RTP包包含一个完整的NALU

  • 聚合打包:对于较小的NALU,一个RTP包可包含多个完整的NALU

  • 分片打包:对于较大的NALU,一个NALU可以分为多个RTP包发送

注意:这里要区分好概念,每一个RTP包都包含一个RTP头部和RTP荷载,这是固定的。而H.264发送数据可支持三种RTP打包方式

比较常用的是单NALU打包和分片打包,这里只介绍两种

单NALU打包
所谓单NALU打包就是将一整个NALU的数据放入RTP包的载荷中,这是最简单的一种方式。

分片打包
每个RTP包都有大小限制的,因为RTP一般都是使用UDP发送,UDP没有流量控制,所以要限制每一次发送的大小,所以如果一个NALU的太大,就需要分成多个RTP包发送,至于如何分成多个RTP包,如下:

首先要明确,RTP包的格式是绝不会变的,永远多是RTP头+RTP载荷

RTP头部是固定的,那么只能在RTP载荷中去添加额外信息来说明这个RTP包是表示同一个NALU
如果是分片打包的话,那么在RTP载荷开始有两个字节的信息,然后再是NALU的内容

第一个字节位FU Indicator,其格式如下

高三位:与NALU第一个字节的高三位相同
Type:28,表示该RTP包一个分片,为什么是28?
因为H.264的规范中定义的,此外还有许多其他Type

第二个字节位FU Header,其格式如下

S:标记该分片打包的第一个RTP包
E:比较该分片打包的最后一个RTP包
Type:NALU的Type

2.5 代码实现

rtp.h

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
//#pragma once
#pragma comment(lib, "ws2_32.lib")
#include <stdint.h>

#define RTP_VESION 2

#define RTP_PAYLOAD_TYPE_H264 96
#define RTP_PAYLOAD_TYPE_AAC 97

#define RTP_HEADER_SIZE 12
#define RTP_MAX_PKT_SIZE 1400

/*
* 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报文有效载荷中的所有特约信源

*/
};

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

void RtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
uint16_t seq, uint32_t timestamp, uint32_t ssrc);

int RtpSendPacketOverTcp(int clientSockfd, struct RtpPacket* rtpPacket, uint32_t dataSize);
int RtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize);

rtp.cpp

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
#include <sys/types.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include "rtp.h"

void RtpHeaderInit(RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
uint16_t seq, uint32_t timestamp, uint32_t ssrc)
{
rtpPacket->rtpHeader.csrcLen = csrcLen;
rtpPacket->rtpHeader.extension = extension;
rtpPacket->rtpHeader.padding = padding;
rtpPacket->rtpHeader.version = version;

rtpPacket->rtpHeader.payloadType = payloadType;
rtpPacket->rtpHeader.marker = marker;

rtpPacket->rtpHeader.seq = seq;
rtpPacket->rtpHeader.timestamp = timestamp;
rtpPacket->rtpHeader.ssrc = ssrc;
}
int RtpSendPacketOverTcp(int clientSockfd, struct RtpPacket* rtpPacket, uint32_t dataSize)
{

rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

uint32_t rtpSize = RTP_HEADER_SIZE + dataSize;
char* tempBuf = (char*)malloc(4 + rtpSize);
tempBuf[0] = 0x24;//$
tempBuf[1] = 0x00;
tempBuf[2] = (uint8_t)(((rtpSize) & 0xFF00) >> 8);
tempBuf[3] = (uint8_t)((rtpSize) & 0xFF);
memcpy(tempBuf + 4, (char*)rtpPacket, rtpSize);

int ret = send(clientSockfd, tempBuf, 4 + rtpSize, 0);

rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);

free(tempBuf);
tempBuf = NULL;

return ret;
}
int RtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, RtpPacket* rtpPacket, uint32_t dataSize)
{
sockaddr_in addr;

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);

rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);//从主机字节顺序转变成网络字节顺序
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

//从rtpPacket指针缓冲区读取数据,通过fd发送至sockaddr结构体中的位置
int ret = sendto(serverRtpSockfd, (char*)rtpPacket, dataSize + RTP_HEADER_SIZE, 0, (sockaddr*)&addr, sizeof(addr));

rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);

return ret;

}

main.cpp

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//关于程序ffplay后速度和帧率都比较small的解释
//因为这个案例中读取h264码流的时候,每次读取一个nalu就会封装一次rtp包并发送,
//但事实上读取到一个nalu并不是对应一个视频帧,可能是sps,也可能是pps,也可能是其他类型的nalu,
//事实上这些nalu都不允许参与到休眠间隔时间的。所以sleep = 40,肯定会慢的,sleep = 20,肯定会快的。
//只有准确判断nalu类型,再进行封装rtp。 这个例子只是演示这样服务的原理,并没有处理这些细节。
#pragma warning( disable : 4996 )
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include <iostream>
#include "rtp.h"
//#pragma warning( disable : 4996 )
#define H264_FILE_NAME "D:\\ffmpeg\\learn\\test.h264"
#define SERVER_PORT 8554
#define SERVER_RTP_PORT 55532
#define SERVER_RTCP_PORT 55533
#define BUF_MAX_SIZE (1024*1024)

static int CreateTcpSocket()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
return fd;
}

static int CreateUdpSocket()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return -1;
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
return fd;
}

static int BindSocketAddr(int fd, int port) //加上static封装函数,仅当前文件能够使用
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY; //0 == 0.0.0.0(本机任意IP地址)
if (bind(fd, (sockaddr*)&addr, sizeof(sockaddr)) < 0)
return -1;
return 0;
}

static int AcceptClient(int fd, char* ip, int* port)
{
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t len = sizeof(addr);
int clientfd = accept(fd, (sockaddr*)&addr, &len);
if (clientfd < 0)
return -1;
//32位地址转换为点分十进制字符串
strcpy(ip, inet_ntoa(addr.sin_addr)); //inet_ntop更安全(包括线程安全),但是参数更多
*port = ntohs(addr.sin_port); //转换为小端后存储
return clientfd;
}


static void Handle_Option(char* buf, int cseq)
{
sprintf(buf, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Public: OPTIONS, DESCRIBE, SETUP, PlAY\r\n"
"\r\n",
cseq);
}

static void Handle_Describe(char* buf, int cseq, char* url)
{
char sdp[500];
char local_ip[100];
sscanf(url, "rtsp://%[^:]:", local_ip); //%[^:] 匹配除了:以外的任意字符
sprintf(sdp, "v=0\r\n"
"o=- 9%ld 1 IN IP4 %s\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=control:track0\r\n",
time(NULL), local_ip);

sprintf(buf, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
"Content-Base: %s\r\n"
"Content-type: application/sdp\r\n"
"Content-length: %zu\r\n\r\n"
"%s",
cseq, url, strlen(sdp), sdp);
}

static void Handle_Setup(char* buf, int cseq, int rtp_port)
{
sprintf(buf, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
"Session: 66334873\r\n"
"\r\n",
cseq, rtp_port, rtp_port + 1, SERVER_RTP_PORT, SERVER_RTCP_PORT);
}

static void Handle_Play(char* buf, int cseq)
{
sprintf(buf, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Range: npt=0.000-\r\n"
"Session: 66334873; timeout=10\r\n\r\n",
cseq);
}
static inline int StartCode3(char* buf)
{
//每个nalu单元都以这个开头
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 1) //00 00 01
return 1;
else
return 0;
}

static inline int StartCode4(char* buf)
{
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 1) // 00 00 00 01
return 1;
else
return 0;
}

static char* FindNextStartCode(char* buf, int len)
{
if (len < 3) return nullptr;
for (int i = 0; i < len - 3; ++i)
{
if (StartCode3(buf) || StartCode4(buf))
return buf;
++buf;
}
//if (StartCode3(buf))
//return buf; 可能冗余了,运行test
return nullptr;
}

static int GetFrameFromH264File(FILE* fp, char* frame, int size)
{
int read_size, framesize;
char* next_startcode;

read_size = fread(frame, 1, size, fp); //从fp读取size个1大小的数据到frame,fp指针此时会移动
if (!StartCode3(frame) && !StartCode4(frame)) //none startcode
return -1;
next_startcode = FindNextStartCode(frame + 3, read_size - 3); //返回起始码处的指针
if (!next_startcode)
return -1;
else
{
framesize = (next_startcode - frame); //返回起始的是3还是4
//由于前面的fread所以要将fp重新偏移回下一个frame的起始位置
fseek(fp, framesize - read_size, SEEK_CUR); //fp指针相对于当前位置,偏移framesize - r_size
}
return framesize;
}

static int RtpSendH264Frame(int serverRtpSockfd, const char* ip, int16_t port,
RtpPacket* rtpPacket, char* frame, uint32_t frameSize)
{
int sendBytes = 0;
int ret;
//H.264由一个一个的NALU组成,每个NALU之间使用00 00 00 01或00 00 01分隔开,每个NALU的第一次字节都有特殊的含义
uint8_t naluType = frame[0];
std::cout << "frame size = " << frameSize << std::endl;

if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包长:单一NALU单元模式
{
//* 0 1 2 3 4 5 6 7 8 9
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//* |F|NRI| Type | a single NAL unit ... |
//* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

memcpy(rtpPacket->payload, frame, frameSize); //将有效数据传输给有效载荷
ret = RtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, frameSize);
if (ret < 0)
return -1;

rtpPacket->rtpHeader.seq++; //序号加一
sendBytes += ret;
//0 00 11111 & 0 11 00111 0 00 00111
if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳
goto out;
}
else // nalu长度大于最大包场:分片模式
{

//* 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
//* 0 1 2 3 4 5 6 7
//* +-+-+-+-+-+-+-+-+
//* |F|NRI| Type |
//* +---------------+

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


int pktNum = frameSize / RTP_MAX_PKT_SIZE; // 有几个完整的包
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
int i, pos = 1;

// 发送完整的包
for (i = 0; i < pktNum; i++)
{
// 0110 0000 | 0001 1100
//分片打包载荷的第一个字节中,与NALU第一个字节的高三位相同,处理后, 0 ii 11100
rtpPacket->payload[0] = (naluType & 0x60) | 28; //2,3位 第一位保持一直为0
//0001 1111
rtpPacket->payload[1] = naluType & 0x1F; //后5位,处理后 0 0 0 iiiii 代表既不是第一个也不是最后一个包

if (i == 0) //第一包数据中的
rtpPacket->payload[1] |= 0x80; // start 0 1 0 00000 : S标为1代表是第一个包
else if (remainPktSize == 0 && i == pktNum - 1) //最后一个整包数据
rtpPacket->payload[1] |= 0x40; // end 0 0 1 00000 : E标为1代表是最后一个包

//frame就是NALU类型,第一个字节是header,给予了RTP格式的前两位信息后,RTP之后就是数据,这个数据就从NALU+1,跳过header得到
memcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE); //由于前两个字节是载荷的信息不是有效数据
ret = RtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, RTP_MAX_PKT_SIZE + 2);
if (ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendBytes += ret;
pos += RTP_MAX_PKT_SIZE;
}

// 发送剩余的数据
if (remainPktSize > 0)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;
rtpPacket->payload[1] |= 0x40; //end

memcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2);
ret = RtpSendPacketOverUdp(serverRtpSockfd, ip, port, rtpPacket, remainPktSize + 2);
if (ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendBytes += ret;
}
}


rtpPacket->rtpHeader.timestamp += 90000 / 25; // 一秒定义为90000的单位 / 帧数 加时间戳
out:

return sendBytes;

}

static void DoClient(int fd, const char* ip, int port)
{
char method[40],url[100],version[40];
int CSeq, client_Rtp_port, client_Rtcp_port;

int server_Rtp_fd = -1;
int server_Rtcp_fd = -1;

char* r_buf = (char*)malloc(BUF_MAX_SIZE); //读缓冲区,读取客户端数据
char* w_buf = (char*)malloc(BUF_MAX_SIZE); //写缓冲区,向客户端发送数据

while (1)
{
int recv_len = recv(fd, r_buf, BUF_MAX_SIZE, 0);
if (recv_len <= 0) break;
r_buf[recv_len] = '\0';
std::cout << std::endl << __FUNCTION__ << " r_buf = " << r_buf << std::endl;
const char* sepe = "\n";
char* line = strtok(r_buf, sepe);
while (line)
{
if (strstr(line, "OPTIONS") ||
strstr(line, "DESCRIBE") ||
strstr(line, "SETUP") ||
strstr(line, "PLAY"))
{
if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3) //return success args num
{
std::cout << "sscanf method url version fail" << std::endl;
exit(-1);
}
}
else if (strstr(line, "CSeq"))
{
if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1)
{
std::cout << "sscanf CSeq" << std::endl;
exit(-1);
}
}
else if (!strncmp(line, "Transport:", strlen("Transport:"))) //suces
{
if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n", &client_Rtp_port, &client_Rtcp_port) != 2)
{
std::cout << "sscanf 2 port fail" << std::endl;
exit(-1);
}
}
line = strtok(NULL, sepe);
//nullptr
}

if (!strcmp(method, "OPTIONS"))
Handle_Option(w_buf, CSeq); //no check
else if (!strcmp(method, "DESCRIBE"))
Handle_Describe(w_buf, CSeq, url);
else if (!strcmp(method, "SETUP"))
{
Handle_Setup(w_buf, CSeq, client_Rtp_port);
server_Rtp_fd = CreateUdpSocket();
server_Rtcp_fd = CreateUdpSocket();
if (server_Rtp_fd < 0 || server_Rtcp_fd < 0)
{
std::cout << "create server udp fail" << std::endl;
exit(-1);
}
if(BindSocketAddr(server_Rtp_fd, SERVER_RTP_PORT) < 0||
BindSocketAddr(server_Rtcp_fd, SERVER_RTCP_PORT) < 0)
{
std::cout << "bind server_fd fail" << std::endl;
exit(-1);
}
}
else if (!strcmp(method, "PLAY"))
Handle_Play(w_buf, CSeq);
else
std::cout << "nb method : " << method << std::endl;

std::cout << std::endl << __FUNCTION__ << "w_buf = " << w_buf << std::endl;
send(fd, w_buf, strlen(w_buf), 0);

if (!strcmp(method, "PLAY"))
{
int framesize, startcode;
char* frame = (char*)malloc(500000);
RtpPacket* rtp_packet = (RtpPacket*)malloc(500000);
FILE* fp = fopen(H264_FILE_NAME, "rb"); //只读二进制
if (!fp)
{
std::cout << "file fp fail" << std::endl;
exit(-1);
}
RtpHeaderInit(rtp_packet, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0, 0, 0, 0X88923423);
std::cout << "start play\n" << "client ip:" << ip << std::endl << "client port:" << client_Rtp_port << std::endl;
while (1)
{
framesize = GetFrameFromH264File(fp, frame, 500000); //获取的是当前 00至00的大小
if (framesize < 0)
{
std::cout << "read " << H264_FILE_NAME << "end, framesize = " << framesize << std::endl;
break;
}
if (StartCode3(frame))
startcode = 3;
else
startcode = 4;

framesize -= startcode; //减去开头的起始码,真正的数据大小
RtpSendH264Frame(server_Rtp_fd, ip, client_Rtp_port, rtp_packet, frame + startcode, framesize);
Sleep(30);
}
free(frame);
free(rtp_packet);
break;
}

memset(method, 0, sizeof(method));
memset(url, 0, sizeof(url));
CSeq = 0;
} //next while

closesocket(fd);
if (server_Rtp_fd) {
closesocket(server_Rtp_fd);
}
if (server_Rtcp_fd > 0) {
closesocket(server_Rtcp_fd);
}
free(r_buf);
free(w_buf);
}

int main()
{
WSADATA winsock;
if (WSAStartup(MAKEWORD(2, 2), &winsock) != 0)
{
std::cout << "winsock start error" << std::endl;
return -1;
}

int server_fd = CreateTcpSocket();
if (server_fd < 0)
{
WSACleanup();
std::cout << "tcpsocket create fail" << std::endl;
return -1;
}

if (BindSocketAddr(server_fd,SERVER_PORT) < 0)
{
std::cout << "bind fail" << std::endl;
return -1;
}

if (listen(server_fd, 128) < 0)
{
std::cout << "listen fail" << std::endl;
return -1;
}

std::cout << __FILE__ << " rtsp://127.0.0.1:" << SERVER_PORT << std::endl;

while (1)
{
int client_fd;
char client_ip[40];
int client_port;
client_fd = AcceptClient(server_fd, client_ip, &client_port);
if (client_fd < 0)
{
std::cout << "accept fail" << std::endl;
return -1;
}
std::cout << "accept client ip:" << client_ip << " port:" << client_port;
DoClient(client_fd, client_ip, client_port);
}
closesocket(server_fd);
return 0;
}

3. 音频原理

3.1 PCM数据格式

PCM(Pulse Code Modulation)也被称为 脉码编码调制。
PCM中的声音数据没有被压缩,如果是单声道的文件,采样数据按时间的先后顺序依次存入。(它的基本组织单位是BYTE(8bit)或WORD(16bit))

一般情况下,一帧PCM是由2048次采样组成的

如果是双声道的文件,采样数据按时间先后顺序交叉地存入。如图所示:

对于解析PCM数据有所帮助

麦克风在录音采样后,直接获得的就是音频原始数据pcm。
但由于pcm数据较大,所以通常需要压缩之后才能传输或保存,常见的音频压缩技术有aac,g711,opus,mp3等,最常用的就是aac

1秒钟的pcm的声音大小 = 44100 * 2(声道) * 2(字节) / 1024 * 60(秒) /1024
一分钟约等于5MB

3.2 几个可能会用到的命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//指定时间段录制
ffmpeg -i input.mp4 -c:v copy -c:a copy -ss 00:10:20 -to 00:30:20 out.mp4

//指定录制时长
ffmpeg -i input.mp4 -c:v copy -c:a copy -t 30 out.mp4
备注: -t 30 表示指定30秒的录制时长

//ffmpeg命令行 从mp4视频文件提取aac 音频文件
ffmpeg -i test.mp4 -vn -acodec aac test.aac
备注:-i 表示输入文件
-vm disable video / 丢掉视频
-acodec 设置音频编码格式


//ffmpeg 从aac音频文件解码为pcm音频文件
ffmpeg -i test.aac -f s16le test.pcm
备注:-i 表示输入文件
-f 表示输出格式

//ffplay 播放.pcm音频文件
备注:-i 表示指定的输入文件
-f 表示强制使用的格式
-ar 表示播放的音频数据的采样率
-ac 表示播放的音频数据的通道数

3.3 代码

Adts.h

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
#pragma once
#include <cstdint>
#include <iostream>
struct AdtsHeader
{
unsigned int syncword; //12 bit 同步字 '1111 1111 1111',说明一个ADTS帧的开始
unsigned int id; //1 bit MPEG 标示符, 0 for MPEG-4,1 for MPEG-2
unsigned int layer; //2 bit 总是'00'
unsigned int protectionAbsent; //1 bit 1表示没有crc,0表示有crc
unsigned int profile; //1 bit 表示使用哪个级别的AAC //2bit
unsigned int samplingFreqIndex; //4 bit 表示使用的采样频率
unsigned int privateBit; //1 bit
unsigned int channelCfg; //3 bit 表示声道数
unsigned int originalCopy; //1 bit
unsigned int home; //1 bit

/*下面的为改变的参数即每一帧都不同*/
unsigned int copyrightIdentificationBit; //1 bit
unsigned int copyrightIdentificationStart; //1 bit
unsigned int aacFrameLength; //13 bit 一个ADTS帧的长度包括ADTS头和AAC原始流
unsigned int adtsBufferFullness; //11 bit 0x7FF 说明是码率可变的码流

/* number_of_raw_data_blocks_in_frame
* 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧
* 所以说number_of_raw_data_blocks_in_frame == 0
* 表示说ADTS帧中有一个AAC数据块并不是说没有。(一个AAC原始帧包含一段时间内1024个采样及相关数据)
*/
unsigned int numberOfRawDataBlockInFrame; //2 bit
};
int Parse_AdtsHeader(uint8_t* headerBuf, struct AdtsHeader* adtsHeader);
int Convert_AdtsHeader_ToBuf(struct AdtsHeader* adtsHeader, uint8_t* adtsHeaderBuf);

Adts.cpp

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
#include "Adts.h"
#include <cstring>
#include <stdio.h>

int Parse_AdtsHeader(uint8_t* headerBuf, AdtsHeader* adtsHeader)
{
memset(adtsHeader, 0, sizeof(*adtsHeader)); //初始化
if ((headerBuf[0] == 0xFF) && ((headerBuf[1] & 0xF0) == 0xF0))//三个 '1111'代表 1个ADTS帧的开始
{
adtsHeader->id = ((unsigned int)headerBuf[1] & 0x08) >> 3; //如果id==1, >>3后变为1
adtsHeader->layer = ((unsigned int)headerBuf[1] & 0x06) >> 1;// 0000 0110, layer一般为0 &后为0
adtsHeader->protectionAbsent = (unsigned int)headerBuf[1] & 0x01;
adtsHeader->profile = ((unsigned int)headerBuf[2] & 0xc0) >> 6; //1100 0000
adtsHeader->samplingFreqIndex = ((unsigned int)headerBuf[2] & 0x3c) >> 2; // 0011 1100
adtsHeader->privateBit = ((unsigned int)headerBuf[2] & 0x02) >> 1; // 0000 0010
// 0000 0001 -> 0000 0100 | 1100 0000 -> 0000 0011
adtsHeader->channelCfg = ((((unsigned int)headerBuf[2] & 0x01) << 2) |
(((unsigned int)headerBuf[3] & 0xc0) >> 6));
adtsHeader->originalCopy = ((unsigned int)headerBuf[3] & 0x20) >> 5; // 0010 0000
adtsHeader->home = ((unsigned int)headerBuf[3] & 0x10) >> 4;
adtsHeader->copyrightIdentificationBit = ((unsigned int)headerBuf[3] & 0x08) >> 3;
adtsHeader->copyrightIdentificationStart = ((unsigned int)headerBuf[3] & 0x04) >> 2;

//0000 0000 0000 0011 -> 0001 1111 1111 1111
adtsHeader->aacFrameLength = (((((unsigned int)headerBuf[3]) & 0x03) << 11) |
(((unsigned int)headerBuf[4] & 0xFF) << 3) |
((unsigned int)headerBuf[5] & 0xE0) >> 5);
adtsHeader->adtsBufferFullness = (((unsigned int)headerBuf[5] & 0x1f) << 6 |
((unsigned int)headerBuf[6] & 0xfc) >> 2);
adtsHeader->numberOfRawDataBlockInFrame = ((unsigned int)headerBuf[6] & 0x03);
return 0;
}
else
{
std::cout << "fail to parse" << std::endl;
return -1;
}
}
int Convert_AdtsHeader_ToBuf(struct AdtsHeader* adtsHeader, uint8_t* adtsHeaderBuf)
{
adtsHeaderBuf[0] = 0xFF;
adtsHeaderBuf[1] = 0xF1;
adtsHeaderBuf[2] = ((adtsHeader->profile) << 6) + (adtsHeader->samplingFreqIndex << 2) + (adtsHeader->channelCfg >> 2);
adtsHeaderBuf[3] = (((adtsHeader->channelCfg & 3) << 6) + (adtsHeader->aacFrameLength >> 11));
adtsHeaderBuf[4] = ((adtsHeader->aacFrameLength & 0x7FF) >> 3);
adtsHeaderBuf[5] = (((adtsHeader->aacFrameLength & 7) << 5) + 0x1F);
adtsHeaderBuf[6] = 0xFC;
return 0;
}

decode_aac, aac格式转pcm格式

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
#define AAC_FILE_NAME "D:/ffmpeg/learn/test.aac"
#pragma warning(disable:4996)
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "fdk-aac/aacdecoder_lib.h"

//data载入buf中的有效信息(包括帧头), data_size 赋值为buf中framesize;
int Get_ADTSframe(unsigned char* buf, int buf_size, unsigned char* data, unsigned int* data_size)
{
if (!buf || !data || !data_size)
return -1;

int framesize = 0;
while (1)
{
if (buf_size < 7) //头信息
return -1;
if (buf[0] == 0xff && ((buf[1] & 0xf0) == 0xf0)) //比较运算符优先级高于位运算符
{
//赋值运算符优先级最低
//aac_frame_length (<<>> 优先级 高于 &)
framesize |= ((buf[3] & 0x03) << 11);
framesize |= buf[4] << 3;
framesize |= ((buf[5] & 0xe0) >> 5);
break;
}
--buf_size;
++buf;
}
if (buf_size < framesize)
return 1;
memcpy(data, buf, framesize);
*data_size = framesize;
return 0;
}

int main()
{
FILE* aac_file = fopen(AAC_FILE_NAME, "rb");
if (!aac_file)
{
std::cout << "open aac fail\n";
return -1;
}
HANDLE_AACDECODER aac_coder = aacDecoder_Open(TT_MP4_ADTS, 1);
AAC_DECODER_ERROR aac_error;
unsigned char* aac_frame = (unsigned char*)malloc(1024 * 5);
unsigned char* aac_buf = (unsigned char*)malloc(1024 * 1024);
unsigned int pcm_size = 8 * 1024 * sizeof(INT_PCM);
unsigned char* pcm_data = (unsigned char*)malloc(pcm_size);

int count = 0;
int offset = 0;
int aac_datasize = 0;
unsigned int aac_framesize = 0;
unsigned int valid = 0;

while (!feof(aac_file)) //函数返回0 代表文件尚未读取结束
{
aac_datasize = fread(aac_buf + offset, 1, 1024 * 1024 - offset, aac_file);
unsigned char* aac_data = aac_buf; //因为要得到framesize就要移动指针来找头
while (1)
{
//其实buf和frame都是一样的
int ret = Get_ADTSframe(aac_data, aac_datasize, aac_frame, &aac_framesize);
if (ret == -1)
{
std::cout << "adts frame end\n";
break;
}
else if (ret == 1) //ADTS中信息不完整
{
memcpy(aac_buf, aac_data, aac_datasize);
offset = aac_datasize;
std::cout << "adts frame broken\n";
}
valid = aac_framesize;
aac_error = aacDecoder_Fill(aac_coder, &aac_frame, &aac_framesize, &valid);
if (aac_error > 0)
std::cout << "fill decoder\n";

aac_error = aacDecoder_DecodeFrame(aac_coder, (INT_PCM*)pcm_data,
pcm_size / sizeof(INT_PCM), 0);
if (aac_error > 0)
std::cout << "decode error\n";

/*sampleRate: 解码后的PCM音频信号的采样率(单位:Hz)(经过SBR处理后的采样率)。
frameSize : 解码后的PCM音频信号的帧大小。
对于AAC - LC:1024或960。
对于HE - AAC(v2):2048或1920。
对于AAC - LD和AAC - ELD:512或480。
numChannels : 解码后的PCM音频信号中输出音频通道的数量。
pChannelType : 每个输出音频通道的音频通道类型。
pChannelIndices : 每个输出音频通道的音频通道索引。
aacSampleRate : 不带SBR的采样率(来自配置信息)。
profile : MPEG - 2配置文件(来自文件头)( - 1:不适用(例如,MPEG - 4))。
aot : 音频对象类型(来自ASC):针对MPEG - 2比特流设置为适当的值(例如,AAC - LC为2)。
channelConfig : 通道配置(0:PCE定义,1:单声道,2:立体声,...)。
bitRate : 瞬时比特率。
aacSamplesPerFrame : AAC核心的每帧样本数(来自ASC)。
对于AAC - LC:1024或960。
对于AAC - LD和AAC - ELD:512或480。
aacNumChannels : 经过AAC核心处理后(在PS或MPS处理之前)的音频通道数量。注意:这不是最终的输出通道数量!
extAot : 扩展音频对象类型(来自ASC)。
extSamplingRate : 扩展采样率(来自ASC)。
outputDelay : 解码器额外延迟的样本数。
flags : 内部标志的副本。仅由解码器写入,外部只能读取。
epConfig : epConfig级别(来自ASC):只支持级别0, - 1表示没有ER(例如,AOT = 2,MPEG - 2 AAC等)。
numLostAccessUnits : 如果 aacDecoder_DecodeFrame() 返回 AAC_DEC_TRANSPORT_SYNC_ERROR,则此整数将反映估计的丢失访问单元的数量。如果估算失败,则会小于0。
numTotalBytes : 通过解码器传递的总字节数。
numBadBytes : 从 numTotalBytes 中考虑为带有错误的总字节数。
numTotalAccessUnits : 通过解码器传递的总访问单元数。
numBadAccessUnits : 从 numTotalAccessUnits 中考虑为带有错误的总访问单元数。
drcProgRefLev : DRC(动态范围控制)程序参考电平。定义低于满量程的参考级别。
drcPresMode : DRC(动态范围控制)呈现模式。*/
CStreamInfo* pcm_frame = aacDecoder_GetStreamInfo(aac_coder); //获取解码后的音频流的相关信息
printf("pcmFrame: channels=%d,simmpleRate=%d,frameSize=%d\n",
pcm_frame->numChannels, pcm_frame->sampleRate, pcm_frame->frameSize);

//码流格式
char profile_str[10] = { 0 };
unsigned char profile = (aac_frame[2] & 0xC0) >> 6; //aac级别,profile
switch (profile)
{
case 0: sprintf(profile_str, "Main"); break;
case 1: sprintf(profile_str, "LC"); break;
case 2: sprintf(profile_str, "SSR"); break;
default: sprintf(profile_str, "unknown"); break;
}

//采样率
char frequence_str[10] = { 0 };
unsigned char sampling_frequency_index = (aac_frame[2] & 0x3C) >> 2;
switch (sampling_frequency_index)
{
case 0: sprintf(frequence_str, "96000Hz"); break;
case 1: sprintf(frequence_str, "88200Hz"); break;
case 2: sprintf(frequence_str, "64000Hz"); break;
case 3: sprintf(frequence_str, "48000Hz"); break;
case 4: sprintf(frequence_str, "44100Hz"); break;
case 5: sprintf(frequence_str, "32000Hz"); break;
case 6: sprintf(frequence_str, "24000Hz"); break;
case 7: sprintf(frequence_str, "22050Hz"); break;
case 8: sprintf(frequence_str, "16000Hz"); break;
case 9: sprintf(frequence_str, "12000Hz"); break;
case 10: sprintf(frequence_str, "11025Hz"); break;
case 11: sprintf(frequence_str, "8000Hz"); break;
default:sprintf(frequence_str, "unknown"); break;
}

printf("%5d| %8s| %8s| %5d| %5d |\n",
count, profile_str, frequence_str, aac_framesize, pcm_size);
aac_datasize -= aac_framesize;
aac_data += aac_framesize;
++count;
}
std::cout << "---------------------------------------------\n";
}
free(pcm_data);
free(aac_buf);
free(aac_frame);
fclose(aac_file);
return 0;
}

sdl2_play_aac.cpp播放aac音频

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
//播放aac格式的音频
#define SDL_MAIN_HANDLED //SDL 不要提供默认的 main 函数入口
#pragma warning( disable : 4996 )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <SDL.h>
#include "Adts.h"
#include "fdk-aac/aacdecoder_lib.h"
#define AAC_FILE_NAME "D:/ffmpeg/learn/test.aac"

static Uint8* audio_chunk;
static Uint32 audio_len;
static Uint8* audio_pos;

//data : SDL_AudioSpec.userdata
//stream 这个指针是 SDL 内部音频数据内存的指针,只要把数据拷贝到这个指针的地址,就能播放声音了。
//len 表示音频缓存区的大小
//SDL 打开音频硬件设备的时候,SDL 库就会创建一个线程,来及时执行回调函数 sdl_audio_callback()
//至于 SDL 线程多久回调一次函数,不需要太关心
void Fill_Audio(void* data, Uint8* stream, int len)
{
SDL_memset(stream, 0, len);
if (audio_len == 0) return;
len = len > audio_len ? audio_len : len; //取min, audio_len是pcm_framesize
//音量范围为0~128,设置为SDL_MIX_MAXVOLUME表示完整的音频音量
//将audio_pos音频 混进 stream, len以字节计数大小
SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
audio_pos += len; //跳至下一个播放帧的起点
audio_len -= len; //使主函数中的while等待终止,同时也给予自己足够时间执行改函数
}

int main()
{
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER)) //初始化音频子系统 | 初始化定时器子系统
{
std::cout << "init SDL fail - " << SDL_GetError() << std::endl;
return -1;
}
//typedef struct SDL_AudioSpec
//{
// int freq; // 采样率 (samples per second)
// SDL_AudioFormat format; // 音频格式
// Uint8 channels; // 声道数 (1 for mono, 2 for stereo)
// Uint8 silence; // 静音值 (通常为 0)
// Uint16 samples; // 音频缓冲区大小 (单位是采样数)
// Uint16 padding; // 对齐用的填充 (通常为 0)
// Uint32 size; // 音频缓冲区大小 (单位是字节)
// SDL_AudioCallback callback; // 回调函数指针,用于处理音频数据
// void* userdata; // 用户自定义数据
//};
SDL_AudioSpec SDL_spec;
SDL_spec.freq = 44100;
SDL_spec.format = AUDIO_S16SYS; //16-bit signed integer 的音频数据格式,使用主机系统的字节顺序
SDL_spec.channels = 2;
SDL_spec.silence = 0;
SDL_spec.samples = 1024; //音频缓冲区大小(单位是采样数)
SDL_spec.callback = Fill_Audio;

if (SDL_OpenAudio(&SDL_spec, nullptr) < 0) //打开音频设备并根据 SDL_spec 的要求进行初始化。
{
std::cout << "open audio fail\n";
return -1;
}

SDL_PauseAudio(0);//控制SDL音频设备的暂停状态。参数为0,音频设备将被恢复到正常状态并开始播放音频数据;
//而当参数为 1 时,音频设备将被暂停,停止播放音频数据。

FILE* aac_file = fopen(AAC_FILE_NAME, "rb");
if (!aac_file)
{
std::cout << "open file fail\n";
return -1;
}

HANDLE_AACDECODER aac_coder = aacDecoder_Open(TT_MP4_ADTS, 1); //aac解码器实例化
uint8_t* frame = (uint8_t*)malloc(2000); //存放aac数据
unsigned int pcm_framesize = 2 * 1024 * sizeof(INT_PCM); //signed short
unsigned char* pcm_frame = (unsigned char*)malloc(pcm_framesize); //存放pcm格式的数据

AdtsHeader adts_header;
int adts_headerlen; //ADTS头长
int adts_contentlen;//ADTS负载长
unsigned int adts_len = 0; //ADTS当前帧总长(头+负载)

int count = 0; //count循环次数, 计帧
AAC_DECODER_ERROR aac_error;

while (1)
{
adts_headerlen = fread(frame, 1, 7, aac_file); //头长是目前自定的7字节, 读头
if (adts_headerlen <= 0) //没有数据或出错
{
std::cout << "read header error\n";
break;
}
if (Parse_AdtsHeader(frame, &adts_header) < 0) //处理头数据放置于包内
{
std::cout << "parse header error\n";
break;
}

//读取aacfile中头后面(一段帧)的数据
adts_contentlen = fread(frame + 7, 1, adts_header.aacFrameLength - 7, aac_file);
if (adts_contentlen < 0)
{
std::cout << "read content error\n";
break;
}

adts_len = adts_header.aacFrameLength;

//AAC解码器是解码用的,解什么,就通过这个函数传进去
//从缓冲区中(frame)
//外部输入缓冲区的大小(length)
//返回信息(len) 以便后续调用aacDecoder Fill时,可以确定pBuffer中的正确位置以获取下一个数据。
aac_error = aacDecoder_Fill(aac_coder, &frame, &adts_header.aacFrameLength, &adts_len);
if (aac_error > 0)
{
std::cout << "decoder fill error\n";
break;
}

//开始编码数据, 将解析为 pcm 格式的数据存储至pcm_frame中
//最后一位是标志位 :
//0 无附加信息
//AACDEC_CONCEAL(值为 1):表示需要进行数据的掩盖(concealment)处理。
//AACDEC_FLUSH(值为 2):表示需要丢弃输入数据,并清空滤波器组(filter banks),输出延迟的音频。
//AACDEC_INTR(值为 4):表示输入数据是不连续的(discontinuous),需要重新同步解码器内部状态。
aac_error = aacDecoder_DecodeFrame(aac_coder, (INT_PCM*)pcm_frame,
pcm_framesize / sizeof(INT_PCM), 0);
if (aac_error > 0)
{
std::cout << "decode frame error\n";
break;
}

audio_chunk = (Uint8*)pcm_frame;
audio_len = pcm_framesize;
audio_pos = audio_chunk;
while (audio_len > 0) //等待时间让SDL线程完成FillAudio函数的任务
SDL_Delay(1);

printf("count=%d, aac_FrameLength=%d, pcm_FrameSize=%d \n", count, adts_header.aacFrameLength, pcm_framesize);
count++;
}
free(frame);
free(pcm_frame);
fclose(aac_file);
SDL_Quit();
return 0;
}

sdl2_play_pcm.cpp, 播放pcm音频

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
#define SDL_MAIN_HANDLED
#define PCM_FILE_NAME "D:/ffmpeg/learn/test.pcm"
#pragma warning( disable : 4996 )
#include <iostream>
#include <stdio.h>
#include <SDL.h>

static Uint8* audio_chunk;
static Uint32 audio_len;
static Uint8* audio_pos;

void Fill_Audio(void* data, Uint8* stream, int len)
{
SDL_memset(stream, 0, len);
if (audio_len == 0) return;
len = len > audio_len ? audio_len : len;
SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
audio_pos += len;
audio_len -= len;
}

int main()
{
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER))
{
std::cout << "init SDL fail - " << SDL_GetError << std::endl;
return -1;
}

SDL_AudioSpec SDL_spec;
SDL_spec.freq = 44100;
SDL_spec.format = AUDIO_S16SYS;
SDL_spec.channels = 2;
SDL_spec.silence = 0;
SDL_spec.samples = 1024;
SDL_spec.callback = Fill_Audio;

if (SDL_OpenAudio(&SDL_spec, nullptr) < 0)
{
std::cout << "SDL open fail\n";
return -1;
}
SDL_PauseAudio(0);

FILE* pcm_file = fopen(PCM_FILE_NAME, "rb+"); //允许以二进制格式读取和写入文件
if (!pcm_file)
{
std::cout << "open file fail\n";
return -1;
}

int pcm_buf_size = 409600; //一次性从pcm文件中读取的数据大小
char* pcm_buf = (char*)malloc(pcm_buf_size);
int data_sum = 0;
size_t count;

while (1)
{
count = fread(pcm_buf, 1, pcm_buf_size, pcm_file);
if (count != pcm_buf_size) //播放完后重新循环播放
{
std::cout << "read pcm end, count = " << count << std::endl;
fseek(pcm_file, 0, SEEK_SET); //0 代表将文件指针重新放置开头位置
fread(pcm_buf, 1, pcm_buf_size, pcm_file);
data_sum = 0;
//break;
}
std::cout << "now playing " << data_sum << "bytes data\n";

data_sum += pcm_buf_size;
audio_chunk = (Uint8*)pcm_buf;
audio_len = pcm_buf_size;
audio_pos = audio_chunk;
/*假设音频的采样率位44100,即每秒钟采样44100次。
AAC一般将1024次采样编码成一帧,所以一秒就有44100 / 1024 = 43帧。
RTP包发送的每一帧数据的时间增量为44100 / 43 = 1025。
每一帧数据的时间间隔为1000 / 43 = 23ms。*/
while (audio_len > 0)
SDL_Delay(23);
}
free(pcm_buf);
SDL_Quit();
return 0;
}

4. UDP的RTP传输音频aac的RTSP服务器,能拉流播放

4.1 aac码流格式

  • ADIF(Audio Data Interchage Format),音频数据交换格式:
    只有一个统一的头,必须得到所有数据后解码,适用于本地文件。
  • ADTS(Audio Data Transport Stream),音视数据传输流:
    每一帧都有头信息,任意帧解码,适用于传输流。

AAC共有9种规格,以适应不同的场合的需要:

  • MPEG-2 AAC LC 低复杂度规格(Low Complexity) 注:比较简单,没有增益控制,但提高了编码效率,在中等码率的编码效率以及音质方面,都能找到平衡点

  • MPEG-2 AAC Main 主规格

  • MPEG-2 AAC SSR 可变采样率规格(Scaleable Sample Rate)

  • MPEG-4 AAC LC 低复杂度规格(Low Complexity)—现在的手机比较常见的MP4文件中的音频部份就包括了该规格音频文件

  • MPEG-4 AAC Main 主规格 注:包含了除增益控制之外的全部功能,其音质最好

  • MPEG-4 AAC SSR 可变采样率规格(Scaleable Sample Rate)

  • MPEG-4 AAC LTP 长时期预测规格(Long Term Predicition)

  • MPEG-4 AAC LD 低延迟规格(Low Delay)

  • MPEG-4 AAC HE 高效率规格(High Efficiency)—这种规格适合用于低码率编码,有Nero ACC 编码器支持

序号 字段 长度 说明
1 synword 12bit 同步头,总是0xFFF,代表着1个ADTS帧的开始。
2 id 1bit 设置MPEG标识符,1bit,0标识MPEG-4,1标识MPEG-2。
3 layer 2bit 总是00。
4 protection_absent 1bit 误码校验,标识是否进行误码校验。0表示有CRC校验,1表示没有CRC校验。为0时头部7bytes后面+2bytesCRC检验位。
5 profile 2bit AAC级别,0表示AAC Main, 1表示AAC LC, 2表示AAC SSR。
profile的值等于Audio Object Type的值减1。
6 sampling_frequency_index 4bit 采样率下标,下标对应的采样率如下:
0: 96000 Hz
1: 88200 Hz
2 : 64000 Hz
3 : 48000 Hz
4 : 44100 Hz
5 : 32000 Hz
6 : 24000 Hz
7 : 22050 Hz
8 : 16000 Hz
9 : 12000 Hz
10 : 11025 Hz
11 : 8000 Hz
12 : 7350 Hz
13 : Reserved
14 : Reserved
15 : frequency is written explictly
7 private_bit 1bit 私有位,编码时设置为0,解码时忽略。
8 channel_configuration 3bit 声道数。
0: Defined in AOT Specifc Config
1: 1 channel : front - center
2 : 2 channels : front - left, front - right
3 : 3 channels : front - center, front - left, front - right
4 : 4 channels : front - center, front - left, front - right, back - center
5 : 5 channels : front - center, front - left, front - right, back - left, back - right
6 : 6 channels : front - center, front - left, front - right, back - left, back - right, LFE - channel
7 : 8 channels : front - center, front - left, front - right, side - left, side - right, back - left, back - right, LFE - channel
8 - 15 : Reserved
9 orininal_copy 1bit 编码时设置为0,解码时忽略。
10 home 1bit 编码时设置为0,解码时忽略。
11 copyrigth_identification_bit 1bit 编码时设置为0,解码时忽略。
12 copyrigth_identification_stat 1bit 编码时设置为0,解码时忽略。
13 aac_frame_length 13bit 一个ADTS帧的长度,包括ADTS头和AAC原始流。
14 adts_bufferfullness 11bit 缓冲区充满度,0x7FF说明是码率可变的码流,不需要此字段。CBR可能需要此字段,不同编码器使用情况不同。具体查看附录。
15 number_of_raw_data_blocks_in_frame 2bit 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧,为0表示说ADTS帧中只有一个AAC数据.
16 CRC 16bit CRC校验码
序号 字段 长度
1 synword 12bit 111111111111
2 id 1bit 0
3 layer 2bit 00
4 protection_absent 1bit 1
5 profile 2bit 01
6 sampling_frequency_index 4bit 0100(十进制4)
7 private_bit 1bit 0
8 channel_configuration 3bit 010 (十进制2)
9 orininal_copy 1bit 0
10 home 1bit 0
11 copyrigth_identification_bit 1bit 0
12 copyrigth_identification_stat 1bit 0
13 aac_frame_length 13bit 0000101111100(十进制380)
14 adts_bufferfullness 11bit 00010010000
15 number_of_raw_data_blocks_in_frame 2bit 00

aac的RTP打包方式

AAC的RTP打包方式并没有向H.264那样丰富,一种方式,主要AAC一帧数据大小都是几百个字节,不会向H.264那么少则几个字节,多则几千。
AAC的RTP打包方式就是将ADTS帧取出ADTS头部,取出AAC数据,每帧数据封装成一个RTP包
需要注意的是,并不是将AAC数据直接拷贝到RTP的载荷中
。AAC封装成RTP包,在RTP载荷中的前四个字节是有特殊含义的,然后再是AAC数据,如下图所示。

其中RTP载荷的一个字节为0x00,第二个字节为0x10
第三个字节和第四个字节保存AAC Data的大小,最多只能保存13bit
第三个字节保存数据大小的高八位
第四个字节的高5位保存数据大小的低5位.

1
2
3
4
rtpPacket->payload[0] = 0x00;
rtpPacket->payload[1] = 0x10;
rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5; //高8位
rtpPacket->payload[3] = (frameSize & 0x1F) << 3; //低5位

RTP包时间戳的计算

假设音频的采样率位44100,即每秒钟采样44100次。
AAC一般将1024次采样编码成一帧,所以一秒就有44100/1024=43帧。
RTP包发送的每一帧数据的时间增量为44100/43=1025。
每一帧数据的时间间隔为1000/43=23ms。

可能会用到的ffmpeg命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//ffmpeg命令行 从mp4视频文件提取aac 音频文件
ffmpeg -i test.mp4 -vn -acodec aac test.aac
备注:-i 表示输入文件
-vm disable video / 丢掉视频
-acodec 设置音频编码格式

//ffmpeg 从aac音频文件解码为pcm音频文件
ffmpeg -i test.aac -f s16le test.pcm
备注:-i 表示输入文件
-f 表示输出格式

//ffplay 播放.pcm音频文件
ffplay -ar 44100 -ac 2 -f s16le -i test.pcm
备注:-i 表示指定的输入文件
-f 表示强制使用的格式
-ar 表示播放的音频数据的采样率
-ac 表示播放的音频数据的通道数

4.2 代码实现

rtp.h

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
#pragma once
#pragma comment(lib, "ws2_32.lib")
#include <stdint.h>
#define RTP_VESION 2
#define RTP_PAYLOAD_TYPE_H264 96
#define RTP_PAYLOAD_TYPE_AAC 97
#define RTP_HEADER_SIZE 12
#define RTP_MAX_PKT_SIZE 1400
struct RtpHeader
{
uint8_t csrcLen : 4;
uint8_t extension : 1;
uint8_t padding : 1;
uint8_t version : 2;
uint8_t payloadType : 7;
uint8_t marker : 1; //音频中标记会话的开始
uint16_t seq;
uint32_t timestamp;
uint32_t ssrc;
};

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

void rtpHeaderInit(RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
uint16_t seq, uint32_t timestamp, uint32_t ssrc);

int rtpSendPacketOverTcp(int clientSockfd, RtpPacket* rtpPacket, uint32_t dataSize);
int rtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, RtpPacket* rtpPacket, uint32_t dataSize);

rtp.cpp

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
#include <sys/types.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include "rtp.h"

void rtpHeaderInit(RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
uint16_t seq, uint32_t timestamp, uint32_t ssrc)
{
rtpPacket->rtpHeader.csrcLen = csrcLen;
rtpPacket->rtpHeader.extension = extension;
rtpPacket->rtpHeader.padding = padding;
rtpPacket->rtpHeader.version = version;
rtpPacket->rtpHeader.payloadType = payloadType;
rtpPacket->rtpHeader.marker = marker;
rtpPacket->rtpHeader.seq = seq;
rtpPacket->rtpHeader.timestamp = timestamp;
rtpPacket->rtpHeader.ssrc = ssrc;
}
int rtpSendPacketOverTcp(int clientSockfd, RtpPacket* rtpPacket, uint32_t dataSize)
{

rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

uint32_t rtpSize = RTP_HEADER_SIZE + dataSize;
char* tempBuf = (char*)malloc(4 + rtpSize);
tempBuf[0] = 0x24;//$
tempBuf[1] = 0x00;
tempBuf[2] = (uint8_t)(((rtpSize) & 0xFF00) >> 8);
tempBuf[3] = (uint8_t)((rtpSize) & 0xFF);
memcpy(tempBuf + 4, (char*)rtpPacket, rtpSize);

int ret = send(clientSockfd, tempBuf, 4 + rtpSize, 0);

rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);

free(tempBuf);
tempBuf = NULL;

return ret;
}
int rtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, RtpPacket* rtpPacket, uint32_t dataSize)
{

sockaddr_in addr;

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);

rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

int ret = sendto(serverRtpSockfd, (char*)rtpPacket, dataSize + RTP_HEADER_SIZE, 0,
(sockaddr*)&addr, sizeof(addr));
rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);
return ret;
}

main.cpp

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//关闭SDL检查
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include "rtp.h"
#pragma warning(disable:4996)
#define SERVER_PORT 8554
#define SERVER_RTP_PORT 55532
#define SERVER_RTCP_PORT 55533
#define BUF_MAX_SIZE (1024*1024)
#define AAC_FILE_NAME "D:/ffmpeg/learn/test.aac"

struct AdtsHeader
{
unsigned int syncword;
uint8_t id;
uint8_t layer;
uint8_t protectionAbsent;
uint8_t profile;
uint8_t samplingFreqIndex;
uint8_t privateBit;
uint8_t channelCfg;
uint8_t originalCopy;
uint8_t home;
uint8_t copyrightIdentificationBit;
uint8_t copyrightIdentificationStart;
unsigned int aacFrameLength;
unsigned int adtsBufferFullness;
uint8_t numberOfRawDataBlockInFrame;
};

static int Parse_AdtsHeader(uint8_t* in, AdtsHeader* res)
{
static int frame_number = 0;
memset(res, 0, sizeof(*res));

if ((in[0] == 0xFF) && ((in[1] & 0xF0) == 0xF0))
{
res->id = ((uint8_t)in[1] & 0x08) >> 3;
res->layer = ((uint8_t)in[1] & 0x06) >> 1;
res->protectionAbsent = (uint8_t)in[1] & 0x01;
res->profile = ((uint8_t)in[2] & 0xc0) >> 6;
res->samplingFreqIndex = ((uint8_t)in[2] & 0x3c) >> 2;
res->privateBit = ((uint8_t)in[2] & 0x02) >> 1;
res->channelCfg = ((((uint8_t)in[2] & 0x01) << 2) | (((unsigned int)in[3] & 0xc0) >> 6));
res->originalCopy = ((uint8_t)in[3] & 0x20) >> 5;
res->home = ((uint8_t)in[3] & 0x10) >> 4;
res->copyrightIdentificationBit = ((uint8_t)in[3] & 0x08) >> 3;
res->copyrightIdentificationStart = ((uint8_t)in[3] & 0x04) >> 2;
res->aacFrameLength = (((((unsigned int)in[3]) & 0x03) << 11) |
(((unsigned int)in[4] & 0xFF) << 3) |
((unsigned int)in[5] & 0xE0) >> 5);

res->adtsBufferFullness = (((unsigned int)in[5] & 0x1f) << 6 |
((unsigned int)in[6] & 0xfc) >> 2);
res->numberOfRawDataBlockInFrame = ((uint8_t)in[6] & 0x03);
return 0;
}
else
{
std::cout << "parse adts header fail\n";
return -1;
}
}

static int createTcpSocket()
{
int on = 1;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
return -1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
return sockfd;
}

static int createUdpSocket()
{
int on = 1;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
return -1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
return sockfd;
}

static int bindSocketAddr(int sockfd, const char* ip, int port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if (bind(sockfd, (sockaddr*)&addr, sizeof(sockaddr)) < 0)
return -1;
return 0;
}

static int acceptClient(int sockfd, char* ip, int* port)
{
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t len = sizeof(addr);
int clientfd = accept(sockfd, (sockaddr*)&addr, &len);
if (clientfd < 0)
return -1;

strcpy(ip, inet_ntoa(addr.sin_addr));
*port = ntohs(addr.sin_port);
return clientfd;
}



static int handleCmd_OPTIONS(char* result, int cseq)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"
"\r\n"
,cseq);
return 0;
}

static int handleCmd_DESCRIBE(char* result, int cseq, char* url)
{
char sdp[500], localIp[100];
sscanf(url, "rtsp://%[^:]:", localIp);
sprintf(sdp, "v=0\r\n"
"o=- 9%ld 1 IN IP4 %s\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=audio 0 RTP/AVP 97\r\n"
"a=rtpmap:97 mpeg4-generic/44100/2\r\n"
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210;\r\n"
"a=control:track0\r\n",
time(NULL), localIp);

sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
"Content-Base: %s\r\n"
"Content-type: application/sdp\r\n"
"Content-length: %d\r\n\r\n"
"%s",
cseq, url, strlen(sdp), sdp);

return 0;
}

static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
"Session: 66334873\r\n"
"\r\n",
cseq, clientRtpPort, clientRtpPort + 1, SERVER_RTP_PORT, SERVER_RTCP_PORT);
return 0;
}

static int handleCmd_PLAY(char* result, int cseq)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Range: npt=0.000-\r\n"
"Session: 66334873; timeout=10\r\n\r\n",
cseq);
return 0;
}

static int Rtp_SendAACFrame(int socket, const char* ip, int16_t port,
RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize)
{
rtpPacket->payload[0] = 0x00;
rtpPacket->payload[1] = 0x10;
rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5;
rtpPacket->payload[3] = (frameSize & 0x1F) << 3;

memcpy(rtpPacket->payload + 4, frame, frameSize);
int ret = rtpSendPacketOverUdp(socket, ip, port, rtpPacket, frameSize + 4);
if (ret < 0)
{
printf("failed to send rtp packet\n");
return -1;
}
rtpPacket->rtpHeader.seq++;
/*
* 如果采样频率是44100
* 一般AAC每个1024个采样为一帧
* 所以一秒就有 44100 / 1024 = 43帧
* 时间增量就是 44100 / 43 = 1025
* 一帧的时间为 1 / 43 = 23ms
*/
rtpPacket->rtpHeader.timestamp += 1025;
return 0;
}

//static char* getLineFromBuf(char* buf, char* line) {
// while (*buf != '\n')
// {
// *line = *buf;
// line++;
// buf++;
// }
// *line = '\n';
// ++line;
// *line = '\0';
// ++buf;
// return buf;
//}

static void doClient(int clientSockfd, const char* clientIP, int clientPort)
{
char method[40], url[100], version[40];
int serverRtpSockfd = -1, serverRtcpSockfd = -1;
int clientRtpPort, clientRtcpPort, CSeq;
char* rBuf = (char*)malloc(BUF_MAX_SIZE);
char* sBuf = (char*)malloc(BUF_MAX_SIZE);

while (true)
{
int recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0);
if (recvLen <= 0)
break;

rBuf[recvLen] = '\0';
printf("%s rBuf = %s \n", __FUNCTION__, rBuf);
const char* sep = "\n";
char* line = strtok(rBuf, sep);
while (line)
{
if (strstr(line, "OPTIONS") || strstr(line, "DESCRIBE") ||
strstr(line, "SETUP") || strstr(line, "PLAY"))
{
if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3)
{} // error
}
else if (strstr(line, "CSeq"))
{
if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1)
{} // error
}
else if (!strncmp(line, "Transport:", strlen("Transport:")))
{
if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
&clientRtpPort, &clientRtcpPort) != 2)
{
printf("parse Transport error \n");
}
}
line = strtok(NULL, sep);
}

if (!strcmp(method, "OPTIONS"))
{
if (handleCmd_OPTIONS(sBuf, CSeq))
{
printf("failed to handle options\n");
break;
}
}
else if (!strcmp(method, "DESCRIBE"))
{
if (handleCmd_DESCRIBE(sBuf, CSeq, url))
{
printf("failed to handle describe\n");
break;
}
}
else if (!strcmp(method, "SETUP"))
{
if (handleCmd_SETUP(sBuf, CSeq, clientRtpPort))
{
printf("failed to handle setup\n");
break;
}

serverRtpSockfd = createUdpSocket();
serverRtcpSockfd = createUdpSocket();
if (serverRtpSockfd < 0 || serverRtcpSockfd < 0)
{
printf("failed to create udp socket\n");
break;
}

if (bindSocketAddr(serverRtpSockfd, "0.0.0.0", SERVER_RTP_PORT) < 0 ||
bindSocketAddr(serverRtcpSockfd, "0.0.0.0", SERVER_RTCP_PORT) < 0)
{
printf("failed to bind addr\n");
break;
}

}
else if (!strcmp(method, "PLAY"))
{
if (handleCmd_PLAY(sBuf, CSeq))
{
printf("failed to handle play\n");
break;
}
}
else
{
printf("未定义的method = %s \n", method);
break;
}

printf("%s sBuf = %s \n", __FUNCTION__, sBuf);
send(clientSockfd, sBuf, strlen(sBuf), 0);

//开始播放,发送RTP包
if (!strcmp(method, "PLAY"))
{
AdtsHeader adtsHeader;
RtpPacket* rtpPacket;
uint8_t* frame;
int ret;

FILE* fp = fopen(AAC_FILE_NAME, "rb");
if (!fp)
{
printf("读取 %s 失败\n", AAC_FILE_NAME);
break;
}

frame = (uint8_t*)malloc(5000);
rtpPacket = (RtpPacket*)malloc(5000);
rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_AAC, 1, 0, 0, 0x32411);

while (true)
{
ret = fread(frame, 1, 7, fp);
if (ret <= 0)
{
printf("fread err\n");
break;
}
printf("fread ret=%d \n", ret);

if (Parse_AdtsHeader(frame, &adtsHeader) < 0)
{
printf("parseAdtsHeader err\n");
break;
}

ret = fread(frame, 1, adtsHeader.aacFrameLength - 7, fp);
if (ret <= 0)
{
printf("fread err\n");
break;
}

Rtp_SendAACFrame(serverRtpSockfd, clientIP, clientRtpPort,
rtpPacket, frame, adtsHeader.aacFrameLength - 7);

Sleep(1);
//usleep(23223);//1000/43.06 * 1000
}
free(frame);
free(rtpPacket);
break;
}
memset(method, 0, sizeof(method));
memset(url, 0, sizeof(url));
CSeq = 0;
}

closesocket(clientSockfd);
if (serverRtpSockfd)
closesocket(serverRtpSockfd);
if (serverRtcpSockfd > 0)
closesocket(serverRtcpSockfd);
free(rBuf);
free(sBuf);
}

int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}
int rtspServerSockfd;
int ret;

rtspServerSockfd = createTcpSocket();
if (rtspServerSockfd < 0)
{
printf("failed to create tcp socket\n");
return -1;
}

ret = bindSocketAddr(rtspServerSockfd, "0.0.0.0", SERVER_PORT);
if (ret < 0)
{
printf("failed to bind addr\n");
return -1;
}

ret = listen(rtspServerSockfd, 10);
if (ret < 0)
{
printf("failed to listen\n");
return -1;
}

printf("%s rtsp://127.0.0.1:%d\n", __FILE__, SERVER_PORT);

while (1)
{
int clientSockfd, clientPort;
char clientIp[40];
clientSockfd = acceptClient(rtspServerSockfd, clientIp, &clientPort);
if (clientSockfd < 0)
{
printf("failed to accept client\n");
return -1;
}
printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);
doClient(clientSockfd, clientIp, clientPort);
}
closesocket(rtspServerSockfd);
return 0;
}


5. TCP的RTP同时传输h264和aac的RTSP服务器,能拉流播放

相对于之前实现的功能,变化如下
1,客户端请求RTSP的Describe请求时,RTSP服务器返回的SDP协议,需要同时包含音频流和视频流的信息。
2,客户端请求RTSP的Setup请求时,RTSP服务器不需要再对应创建RTP和RTCP的UDP连接通道,因为TCP版的RTP传输,客户端与服务器交互时,无论是RTSP信令还是RTP数据包或者是RTCP数据包,都是使用同一个tcp连接通道。只不过这个tcp连接通道在发送rtp数据包或者rtcp数据包时,需要加一些分隔字节。
3,客户端请求RTSP的Play请求时,RTSP服务器在对Play请求回复以后,还需要源源不断的同时向客户端发送音频流和视频流的RTP数据包。
4,有几点注意,在这个案例项目中,使用的h264视频文件,对应的fps需要是25。另外由于Nalu的数量并不等于视频帧数量的原因,该案例的音视频并不能同步。

1
ffplay -i -rtsp_transport tcp rtsp://127.0.0.1:8554

5.1 ffmpeg命令行

1
2
3
4
5
6
7
8
9
10
//ffmpeg命令行 从mp4视频文件提取h264 码流文件
ffmpeg -i test.mp4 -an -vcodec copy -f h264 test.h264


//ffmpeg命令行 从mp4视频文件提取aac 音频文件
ffmpeg -i test.mp4 -vn -acodec aac test.aac

备注:-i 表示输入文件
-vm disable video / 丢掉视频

5.2 代码实现

rtp.h

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
#pragma once
#pragma comment(lib, "ws2_32.lib")
#include <stdint.h>
#define RTP_VESION 2
#define RTP_PAYLOAD_TYPE_H264 96
#define RTP_PAYLOAD_TYPE_AAC 97
#define RTP_HEADER_SIZE 12
#define RTP_MAX_PKT_SIZE 1400
struct RtpHeader
{
uint8_t csrcLen : 4;
uint8_t extension : 1;
uint8_t padding : 1;
uint8_t version : 2;
uint8_t payloadType : 7;
uint8_t marker : 1; //音频中标记会话的开始
uint16_t seq;
uint32_t timestamp;
uint32_t ssrc;
};

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

void rtpHeaderInit(RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
uint16_t seq, uint32_t timestamp, uint32_t ssrc);

int rtpSendPacketOverTcp(int clientSockfd, RtpPacket* rtpPacket, uint32_t dataSize, char channel);
int rtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, RtpPacket* rtpPacket, uint32_t dataSize);

rtp.cpp

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
#include <sys/types.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include "rtp.h"

void rtpHeaderInit(RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
uint16_t seq, uint32_t timestamp, uint32_t ssrc)
{
rtpPacket->rtpHeader.csrcLen = csrcLen;
rtpPacket->rtpHeader.extension = extension;
rtpPacket->rtpHeader.padding = padding;
rtpPacket->rtpHeader.version = version;
rtpPacket->rtpHeader.payloadType = payloadType;
rtpPacket->rtpHeader.marker = marker;
rtpPacket->rtpHeader.seq = seq;
rtpPacket->rtpHeader.timestamp = timestamp;
rtpPacket->rtpHeader.ssrc = ssrc;
}
int rtpSendPacketOverTcp(int clientSockfd, RtpPacket* rtpPacket, uint32_t dataSize, char channel)
{

rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

uint32_t rtpSize = RTP_HEADER_SIZE + dataSize;
char* tempBuf = (char*)malloc(4 + rtpSize);
tempBuf[0] = 0x24;//$
tempBuf[1] = channel; //0x00
tempBuf[2] = (uint8_t)(((rtpSize) & 0xFF00) >> 8);
tempBuf[3] = (uint8_t)((rtpSize) & 0xFF);
memcpy(tempBuf + 4, (char*)rtpPacket, rtpSize);

int ret = send(clientSockfd, tempBuf, 4 + rtpSize, 0);

rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);

free(tempBuf);
tempBuf = NULL;
return ret;
}
int rtpSendPacketOverUdp(int serverRtpSockfd, const char* ip, int16_t port, RtpPacket* rtpPacket, uint32_t dataSize)
{

sockaddr_in addr;

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);

rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

int ret = sendto(serverRtpSockfd, (char*)rtpPacket, dataSize + RTP_HEADER_SIZE, 0,
(sockaddr*)&addr, sizeof(addr));
rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);
return ret;
}

main.cpp

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include "rtp.h"
#include <thread>
#pragma warning(disable:4996)
#define AAC_FILE_NAME "D:/ffmpeg/learn/test.aac"
#define H264_FILE_NAME "D:/ffmpeg/learn/test.h264"
#define SERVER_PORT 8554
#define BUF_MAX_SIZE (1024*1024)

static int createTcpSocket()
{
int on = 1;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
return -1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
return sockfd;
}

static int bindSocketAddr(int sockfd, const char* ip, int port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(sockaddr)) < 0)
return -1;
return 0;
}

static int acceptClient(int sockfd, char* ip, int* port)
{
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t len = sizeof(addr);
int clientfd = accept(sockfd, (struct sockaddr*)&addr, &len);
if (clientfd < 0)
return -1;
strcpy(ip, inet_ntoa(addr.sin_addr));
*port = ntohs(addr.sin_port);
return clientfd;
}

static inline int startCode3(char* buf)
{
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 1)
return 1;
else
return 0;
}

static inline int startCode4(char* buf)
{
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 1)
return 1;
else
return 0;
}

static char* findNextStartCode(char* buf, int len)
{
if (len < 3)
return NULL;
for (int i = 0; i < len - 3; ++i)
{
if (startCode3(buf) || startCode4(buf))
return buf;
++buf;
}
/*if (startCode3(buf))
return buf;*/
return NULL;
}

static int getFrameFromH264File(FILE* fp, char* frame, int size)
{
int frameSize = -1;
int rSize = fread(frame, 1, size, fp);

if (!startCode3(frame) && !startCode4(frame))
return -1;

char* nextStartCode = findNextStartCode(frame + 3, rSize - 3);
if (!nextStartCode)
return -1;
else
{
frameSize = (nextStartCode - frame);
fseek(fp, frameSize - rSize, SEEK_CUR);
}
return frameSize;
}

struct AdtsHeader
{
unsigned int syncword;
uint8_t id;
uint8_t layer;
uint8_t protectionAbsent;
uint8_t profile;
uint8_t samplingFreqIndex;
uint8_t privateBit;
uint8_t channelCfg;
uint8_t originalCopy;
uint8_t home;
uint8_t copyrightIdentificationBit;
uint8_t copyrightIdentificationStart;
unsigned int aacFrameLength;
unsigned int adtsBufferFullness;
uint8_t numberOfRawDataBlockInFrame;
};

static int Parse_AdtsHeader(uint8_t* in, AdtsHeader* res)
{
static int frame_number = 0;
memset(res, 0, sizeof(*res));

if ((in[0] == 0xFF) && ((in[1] & 0xF0) == 0xF0))
{
res->id = ((uint8_t)in[1] & 0x08) >> 3;
res->layer = ((uint8_t)in[1] & 0x06) >> 1;
res->protectionAbsent = (uint8_t)in[1] & 0x01;
res->profile = ((uint8_t)in[2] & 0xc0) >> 6;
res->samplingFreqIndex = ((uint8_t)in[2] & 0x3c) >> 2;
res->privateBit = ((uint8_t)in[2] & 0x02) >> 1;
res->channelCfg = ((((uint8_t)in[2] & 0x01) << 2) | (((unsigned int)in[3] & 0xc0) >> 6));
res->originalCopy = ((uint8_t)in[3] & 0x20) >> 5;
res->home = ((uint8_t)in[3] & 0x10) >> 4;
res->copyrightIdentificationBit = ((uint8_t)in[3] & 0x08) >> 3;
res->copyrightIdentificationStart = ((uint8_t)in[3] & 0x04) >> 2;
res->aacFrameLength = (((((unsigned int)in[3]) & 0x03) << 11) |
(((unsigned int)in[4] & 0xFF) << 3) |
((unsigned int)in[5] & 0xE0) >> 5);

res->adtsBufferFullness = (((unsigned int)in[5] & 0x1f) << 6 |
((unsigned int)in[6] & 0xfc) >> 2);
res->numberOfRawDataBlockInFrame = ((uint8_t)in[6] & 0x03);
return 0;
}
else
{
std::cout << "parse adts header fail\n";
return -1;
}
}

static int rtpSendAACFrame(int clientSockfd, RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize)
{
rtpPacket->payload[0] = 0x00;
rtpPacket->payload[1] = 0x10;
rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5;
rtpPacket->payload[3] = (frameSize & 0x1F) << 3;
memcpy(rtpPacket->payload + 4, frame, frameSize);
int ret = rtpSendPacketOverTcp(clientSockfd, rtpPacket, frameSize + 4, 0x02);
if (ret < 0)
{
printf("failed to send rtp packet\n");
return -1;
}
rtpPacket->rtpHeader.seq++;
rtpPacket->rtpHeader.timestamp += 1025;
return 0;
}


static int rtpSendH264Frame(int clientSockfd, RtpPacket* rtpPacket, char* frame, uint32_t frameSize)
{
int sendByte = 0;
int ret;
uint8_t naluType = frame[0];
printf("%s frameSize=%d \n", __FUNCTION__, frameSize);
if (frameSize <= RTP_MAX_PKT_SIZE)
{
memcpy(rtpPacket->payload, frame, frameSize);
ret = rtpSendPacketOverTcp(clientSockfd, rtpPacket, frameSize, 0x00);
if (ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendByte += ret;
if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8)
return sendByte;
}
else
{
int pktNum = frameSize / RTP_MAX_PKT_SIZE;
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE;
int pos = 1;
for (int i = 0; i < pktNum; i++)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;

if (i == 0)
rtpPacket->payload[1] |= 0x80;
else if (remainPktSize == 0 && i == pktNum - 1)
rtpPacket->payload[1] |= 0x40;

memcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE);
ret = rtpSendPacketOverTcp(clientSockfd, rtpPacket, RTP_MAX_PKT_SIZE + 2, 0x00);
if (ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendByte += ret;
pos += RTP_MAX_PKT_SIZE;
}

if (remainPktSize > 0)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;
rtpPacket->payload[1] |= 0x40; //end

memcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2);
ret = rtpSendPacketOverTcp(clientSockfd, rtpPacket, remainPktSize + 2, 0x00);
if (ret < 0)
return -1;

rtpPacket->rtpHeader.seq++;
sendByte += ret;
}
}
return sendByte;
}

static int handleCmd_OPTIONS(char* result, int cseq)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"
"\r\n",
cseq);
return 0;
}

static int handleCmd_DESCRIBE(char* result, int cseq, char* url)
{
char sdp[500];
char localIp[100];

sscanf(url, "rtsp://%[^:]:", localIp);

sprintf(sdp, "v=0\r\n"
"o=- 9%ld 1 IN IP4 %s\r\n"
"t=0 0\r\n"
"a=control:*\r\n"
"m=video 0 RTP/AVP/TCP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=control:track0\r\n"
"m=audio 1 RTP/AVP/TCP 97\r\n"
"a=rtpmap:97 mpeg4-generic/44100/2\r\n"
"a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210;\r\n"
"a=control:track1\r\n",
time(NULL), localIp);

sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
"Content-Base: %s\r\n"
"Content-type: application/sdp\r\n"
"Content-length: %zu\r\n\r\n"
"%s",
cseq, url, strlen(sdp), sdp);
return 0;
}

static int handleCmd_SETUP(char* result, int cseq)
{
if (cseq == 3)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n"
"Session: 66334873\r\n"
"\r\n",
cseq);
}
else if (cseq == 4)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP/TCP;unicast;interleaved=2-3\r\n"
"Session: 66334873\r\n"
"\r\n",
cseq);
}
return 0;
}

static int handleCmd_PLAY(char* result, int cseq)
{
sprintf(result, "RTSP/1.0 200 OK\r\n"
"CSeq: %d\r\n"
"Range: npt=0.000-\r\n"
"Session: 66334873; timeout=10\r\n\r\n",
cseq);
return 0;
}

static void doClient(int clientSockfd, const char* clientIP, int clientPort)
{
char method[40], url[100], version[40];
int CSeq;
char* rBuf = (char*)malloc(BUF_MAX_SIZE);
char* sBuf = (char*)malloc(BUF_MAX_SIZE);

while (true)
{
int recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0);
if (recvLen <= 0)
break;

rBuf[recvLen] = '\0';
printf("接收请求 rBuf = %s \n", rBuf);

const char* sep = "\n";
char* line = strtok(rBuf, sep);
while (line)
{
if (strstr(line, "OPTIONS") ||
strstr(line, "DESCRIBE") ||
strstr(line, "SETUP") ||
strstr(line, "PLAY"))
{
if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3)
{} // error
}
else if (strstr(line, "CSeq"))
{
if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1)
{} // error
}
else if (!strncmp(line, "Transport:", strlen("Transport:")))
{
if (sscanf(line, "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n") != 0)
{
printf("parse Transport error \n");
}
}
line = strtok(NULL, sep);
}

if (!strcmp(method, "OPTIONS"))
{
if (handleCmd_OPTIONS(sBuf, CSeq))
{
printf("failed to handle options\n");
break;
}
}
else if (!strcmp(method, "DESCRIBE"))
{
if (handleCmd_DESCRIBE(sBuf, CSeq, url))
{
printf("failed to handle describe\n");
break;
}
}
else if (!strcmp(method, "SETUP"))
{
if (handleCmd_SETUP(sBuf, CSeq))
{
printf("failed to handle setup\n");
break;
}
}
else if (!strcmp(method, "PLAY"))
{
if (handleCmd_PLAY(sBuf, CSeq))
{
printf("failed to handle play\n");
break;
}
}
else
{
printf("未定义的method = %s \n", method);
break;
}
printf("响应 sBuf = %s \n", sBuf);

send(clientSockfd, sBuf, strlen(sBuf), 0);
if (!strcmp(method, "PLAY"))
{

std::thread t1([&]() {
int frameSize, startCode;
char* frame = (char*)malloc(500000);
RtpPacket* rtpPacket = (RtpPacket*)malloc(500000);
FILE* fp = fopen(H264_FILE_NAME, "rb");
if (!fp)
{
printf("读取 %s 失败\n", H264_FILE_NAME);
return;
}
rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264,
0,0, 0, 0x88923423);

printf("start play\n");

while (true)
{
frameSize = getFrameFromH264File(fp, frame, 500000);
if (frameSize < 0)
{
printf("读取%s结束,frameSize=%d \n", H264_FILE_NAME, frameSize);
break;
}
if (startCode3(frame))
startCode = 3;
else
startCode = 4;
frameSize -= startCode;
rtpSendH264Frame(clientSockfd, rtpPacket, frame + startCode, frameSize);
rtpPacket->rtpHeader.timestamp += 90000 / 25;
//Sleep(40);//->30,20,
Sleep(20);
//usleep(40000);//1000/25 * 1000
}
free(frame);
free(rtpPacket);
});

std::thread t2([&]() {
AdtsHeader adtsHeader;
FILE* fp = fopen(AAC_FILE_NAME, "rb");
if (!fp)
{
printf("读取 %s 失败\n", AAC_FILE_NAME);
return;
}
uint8_t* frame = (uint8_t*)malloc(5000);
RtpPacket* rtpPacket = (RtpPacket*)malloc(5000);
rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_AAC, 1, 0, 0, 0x32411);
int ret;
while (true)
{
ret = fread(frame, 1, 7, fp);
if (ret <= 0)
{
printf("fread err\n");
break;
}
printf("fread ret=%d \n", ret);

if (Parse_AdtsHeader(frame, &adtsHeader) < 0)
{
printf("parseAdtsHeader err\n");
break;
}
ret = fread(frame, 1, adtsHeader.aacFrameLength - 7, fp);
if (ret <= 0)
{
printf("fread err\n");
break;
}

rtpSendAACFrame(clientSockfd,
rtpPacket, frame, adtsHeader.aacFrameLength - 7);

Sleep(23);
//usleep(23223);//1000/43.06 * 1000
}
free(frame);
free(rtpPacket);
});

t1.join();
t2.join();
break;
}
memset(method, 0, sizeof(method));
memset(url, 0, sizeof(url));
CSeq = 0;
}
closesocket(clientSockfd);
free(rBuf);
free(sBuf);
}

int main(int argc, char* argv[])
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}

int serverSockfd;
serverSockfd = createTcpSocket();
if (serverSockfd < 0)
{
WSACleanup();
printf("failed to create tcp socket\n");
return -1;
}

if (bindSocketAddr(serverSockfd, "0.0.0.0", SERVER_PORT) < 0)
{
printf("failed to bind addr\n");
return -1;
}

if (listen(serverSockfd, 10) < 0)
{
printf("failed to listen\n");
return -1;
}

printf("%s rtsp://127.0.0.1:%d\n", __FILE__, SERVER_PORT);

while (true)
{
int clientSockfd;
char clientIp[40];
int clientPort;
clientSockfd = acceptClient(serverSockfd, clientIp, &clientPort);
if (clientSockfd < 0)
{
printf("failed to accept client\n");
return -1;
}
printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);
doClient(clientSockfd, clientIp, clientPort);
}
closesocket(serverSockfd);
WSACleanup();
return 0;
}

5.3 分析抓包理解源码

两个m

第一路流的rtp和rtcp的通道序号

返回的同理,第一路流的0通道和1通道在这里插入图片描述

同理第二路流用2,3通道,replay同理

97代表的是aac, 96代表的是h264

$符号(间隔符),通道00,第一路流的第一个通道,长度

$符号,02,第二路流的第一个通道,长度


6. RTSPServer

GitHub源码

整体逻辑如下图

Linux配置环境

  1. 配置SDL环境

一键下寨!

1
2
3
4
5
6
7
8
9
tar -zxvf SDL-1.2.15.tar.gz

cd SDL-1.2.15/

./configure --prefix=/usr/local

make

make install
  1. 安装ffmpeg库
1
sudo apt install ffmpeg
  1. 整理文件

先切换到RTSP_Server目录中

1
2
3
4
5
6
7
mkdir src
mv *.cpp src

mkdir include
mv *.h include

mkdir build

准备好 .aac 和 .h264 文件放置RTSP_Server目录中

  1. 运行
1
2
3
4
5
cd build
cmake ..
make
cd ..
./app

另起一个终端

1
2
3
4
5
#over tcp
ffplay -i -rtsp_transport tcp rtsp://127.0.0.1:8554/test

#over udp
ffplay -i rtsp://127.0.0.1:8554/test

7. RTCP

实时传输控制协议(Real-time ControlProtocol,RTCP)是和 RTP一起工作的控制协议。
在RTP会话期间,Udp传输时通过使用不同的端口号可把RTP数据包和RTCP信息包区分开来
Tcp使用相同的端口号, 每个会话参与者周期性地向所有其他参与者发送RTCP控制信息包。

功能:
在RTP会话期间,每个会话参与者周期性地向所有其他参与者发送RTCP控制信息包。
每个RTCP信息包不是封装声音或者视频数据,而是封装发送端或接收端的统计报表。
这些信息包括发送的信息包数目、丢失的信息包数目和信息包的抖动等情况
信息包具体的有: 假设是直播推流的场景,
截至目前已经发送了多少的RTP数据包, RTP对应的字节流的长度的数据, RTP最新的时间戳, NTP(精准时间戳,ms级)等

1.1 RTCP包类型

根据所携带的控制信息不同,RTCP信息包可分为
RR(接收者报告包)、SR(源报告包)、SEDS(源描述包)、BYE(离开申明)和APP(特殊应用包)

类型 缩写 用途
200 SR 发送端报告
201 RR 接收端报告
202 SDES 源点描述
203 BYE 结束传输
204 APP 特定应用

实际RTCP真正拥有的类型有20多种

SR:

  • 版本(V):2比特,RTCP版本。
  • 填充(P):1比特,如果该位置为1,则该RTCP包的尾部就包含附加的填充字节。
  • 接收报告计数器(RC):5比特,该SR包中的接收报告块的数目,可以为零。
  • 包类型(PT):8比特,SR包是200。
  • 长度域(Length):16比特,RTCP包的长度,包括填充的内容。
  • 同步源(SSRC of sender):32比特,SR包发送者的同步源标识符。与对应RTP包中的SSRC一样。
  • NTP timestamp(MSW+LWS):64比特, 表示发送此报告时以挂钟时间测量的时间点。 结合来自各个接收器的接收报告中返回的时间戳,它可用于估计往返于接收器的往返传播时间。
  • RTP timestamp:32比特,与NTP时间戳对应,与RTP数据包中的RTP时间戳具有相同的单位和随机初始值。
  • Sender’s packet count:32比特,从开始发送包到产生这个SR包这段时间里,发送者发送的RTP数据包的总数. SSRC改变时,这个域清零。
  • Senders octet count:32比特,从开始发送包到产生这个SR包这段时间里,发送者发送的净荷数据的总字节数(不包括头部和填充)。发送者改变其SSRC时,这个域要清零。
  • SSRC_n :32比特,在此块中报告其接收的发送者的 SSRC 标识符
  • 丢失率(Fraction Lost) :8比特,表明从上一个SR或RR包发出以来从同步源n(SSRC_n)来的RTP数据包的丢失率
  • 累计的包丢失数目(cumulative number of packets lost ):24比特,从开始接收到SSRC_n的包到发送SR,从SSRC_n传过来的RTP数据包的丢失总数。
  • 收到的扩展最大序列号(extended highest sequence number received EHSN )
    从SSRC_n收到的RTP数据包中最大的序列号
    uint16_t seq_cycles; //Sequence number cycles count (序列号循环计数
    uint16_t seq_max; //highest sequence number received (序列最大值
  • 接收抖动(Interarrival jitter):32比特,RTP数据包接受时间的统计方差估计
  • 上次SR时间戳(Last SR,LSR):32比特,取最近从SSRC_n收到的SR包中的NTP时间戳的中间32比特。如果目前还没收到SR包,则该域清零
  • 上次SR以来的延时(Delay since last SR,DLSR):32比特,上次从SSRC_n收到SR包到发送本报告的延时
  • 扩展字段 profile-specific extensions

1.2 RTCP抓包

服务段先发送RR, 推流端收到RR后发送SR

推流端第一次发送SR

  • Sender's packet count : 截止到当前发送RTP包的数量
  • Sender's octet count : 发送RTP包对应字节流的长度
  • RTP timestamp : 当前发送RTCP时RTP最新的RTPHeader时间戳
  • Timestamp, MSWTimestamp, LSW: NTP用两个字段用来表示和传输

第二次发送SR:

demo(未完成): 6,7 RTCP
client实际发送的数据目前未知,client端表示发送成功, server端表示接受的ssrc,packetcount等超过类型最大值,cout失败
推测端口被占用


8. RTSP客户端

ffmpeg向RTSPServer推流, RTSPClient再向RTSPServer拉流 生成可播放的h264文件

1. SDP(Session Description Protocol) 简介

SDP 完全是一种会话描述格式, 它不属于传输协议
它只使用于不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP)。
SDP协议是也是基于文本的协议,这样就能保证协议的可扩展性比较强,这样就使其具有广泛的应用范围。SDP 不支持会话内容或媒体编码的协商,所以在流媒体中只用来描述媒体信息。媒体协商这一块要用RTSP来实现.

SDP 信息是文本信息,UTF-8 编码采用 ISO 10646 字符设置。
SDP 会话描述如下(标注*符号的表示可选字段):

v= (协议版本)
o= (所有者/创建者和会话标识符)
s= (会话名称)
i=* (会话信息)
u=* (URI 描述)
e=* (Email 地址)
p=* (电话号码)
c=* (连接信息 ― 如果包含在所有媒体中,则不需要该字段)
b=* (带宽信息)

一个或更多时间描述(如下所示):
z=* (时间区域调整)
k=* (加密密钥)
a=* (0个或多个会话属性线路)

0个或多个媒体描述(如下所示):
时间描述
t= (会话活动时间)
r=* (0或多次重复次数)

媒体描述:
m= (媒体名称和传输地址)
i=* (媒体标题)
c=* (连接信息 — 如果包含在会话层则该字段可选)
b=* (带宽信息)
k=* (加密密钥)
a=* (0个或多个会话属性线路)

SDP示例

1
2
3
4
5
6
7
8
9
10
11
12
13
v=0
o=mhandley 2890844526 2890842807 IN IP4 126.16.64.4
s=SDP Seminar
i=A Seminar on the session description protocol
u=http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps
e=mjh@isi.edu (Mark Handley)
c=IN IP4 224.2.17.12/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 31
m=application 32416 udp wb
a=orient:portrait

SDP解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//字段解释

V=0 ;Version 给定了SDP协议的版本
o=<username><session id> <version> <network type> <address type>
<address>; Origin ,给定了会话的发起者信息
s=<sessionname> ;给定了Session Name
i=<sessiondescription> ; Information 关于Session的一些信息
u=<URI> ; URI
e=<emailaddress> ;Email
c=<networktype> <address type> <connection address> ;Connect Data包含连接数据
t=<start time><stop time> ;Time
a=<attribute> ; Attribute
a=<attribute>:<value>
m=<media><port> <transport> <fmt list> ; MediaAnnouncements

2. 向 RTSP服务端 推流的ffmpeg命令

1
2
3
ffmpeg -re -stream_loop -1 -i xxx.mp4  \
-rtsp_transport tcp -c copy -f rtsp rtsp://127.0.0.1:554/live/test
#stream_loop 为了方便调试,使流循环推流

3. WireShark 常用过滤 及 抓包分析

1
2
3
4
5
tcp.srcport != 63639 and rtsp and !rtcp
ip.src_host == 127.0.0.1
ip.dst_host == 127.0.0.1
tcp.srcport == xx
tcp.dstport == xx

抓包分析:

Option

Reply1 : public 是允许的字段

Describe

Reply 2: 包含SDP(回应describe后, 由于SDP, 以后不管推拉流都要包含session中独特的序列

Setup 1 : 视频流

Reply 3:

Setup 2 : 音频流

Reply 4:

Play:

Reply 5:

**SetUp中若有,字幕流,弹幕流等 Setup就会++**

RTSPClient 源码

4. windows下查看端口命令

1
2
3
4
5
6
7
8
9
10
11
12
#windows查看端口占用进程
netstat -aon|findstr "10000"

#windows查看命令占用进程
tasklist | findstr "python"

#windows根据进程ID查看应用
tasklist|findstr "11748"

#windows根据进程ID杀死应用
taskkill /pid 11748 /F
taskkill -PID 进程号 -F