RTSP 协议交互流程(基础示例)
RTSP(Real Time Streaming Protocol,实时流传输协议)是一个应用层控制协议,主要用于实时音视频流的控制(如播放、暂停、快进等),常用于视频监控、IP摄像头、流媒体点播/直播场景。
- RTSP 本身不传输媒体数据 ,只负责信令交互(请求与响应)。
- 实际的音视频数据由 RTP 承载传输。
- RTCP 负责对 RTP 传输质量进行监控和反馈。

RTSP 客户端与服务器之间的标准交互过程如下:
1. OPTIONS - 查询服务器能力
html
OPTIONS rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 1
服务器响应:
html
RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE
2. DESCRIBE - 获取媒体描述(SDP)
html
DESCRIBE rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 2
Accept: application/sdp
服务器响应(SDP 信息):
html
RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: ...
v=0
o=- 123456 123456 IN IP4 192.168.1.1
s=Example Stream
t=0 0
a=control:*
m=video 49170 RTP/AVP 96
c=IN IP4 239.255.255.255
a=rtpmap:96 H264/90000
3. SETUP - 建立传输通道
html
SETUP rtsp://example.com/media.mp4/trackID=0 RTSP/1.0
CSeq: 3
Transport: RTP/AVP;unicast;client_port=49170-49171
服务器响应:
html
RTSP/1.0 200 OK
CSeq: 3
Session: 12345678
Transport: RTP/AVP;unicast;server_port=49172-49173
4. PLAY - 开始播放
html
PLAY rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 4
Session: 12345678
Range: npt=0.000-
服务器响应:
html
RTSP/1.0 200 OK
CSeq: 4
Session: 12345678
RTP-Info: url=rtsp://example.com/media.mp4/trackID=0;seq=1;rtptime=0
5. PAUSE - 暂停(可选)
html
PAUSE rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 5
Session: 12345678
6. TEARDOWN - 结束会话
html
TEARDOWN rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 6
Session: 12345678
基本的RTSP服务端
cpp
//
// Created by bxc on 2022/11/30.
//
#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")
#include <stdint.h>
#pragma warning( disable : 4996 )
#define SERVER_PORT 8554
#define SERVER_RTP_PORT 55532
#define SERVER_RTCP_PORT 55533
static int createTcpSocket()
{
int sockfd;
int on = 1;
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)
{
struct 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(struct sockaddr)) < 0)
return -1;
return 0;
}
static int acceptClient(int sockfd, char* ip, int* port)
{
int clientfd;
socklen_t len = 0;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
len = sizeof(addr);
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 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 96\r\n"
"a=rtpmap:96 H264/90000\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: %zu\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 void doClient(int clientSockfd, const char* clientIP, int clientPort) {
char method[40];
char url[100];
char version[40];
int CSeq;
int clientRtpPort, clientRtcpPort;
char* rBuf = (char*)malloc(10000);
char* sBuf = (char*)malloc(10000);
while (true) {
int recvLen;
recvLen = recv(clientSockfd, rBuf, 2000, 0);//从客户端socket读取数据(最多2000字节)
if (recvLen <= 0) {
break;
}
rBuf[recvLen] = '\0';
std::string recvStr = rBuf;
printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
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:"))) {
// Transport: RTP/AVP/UDP;unicast;client_port=13358-13359
// Transport: RTP/AVP;unicast;client_port=13358-13359
if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
&clientRtpPort, &clientRtcpPort) != 2) {
// error
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;
}
}
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("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
printf("%s sBuf = %s \n", __FUNCTION__, sBuf);
send(clientSockfd, sBuf, strlen(sBuf), 0);
//开始播放,发送RTP包
if (!strcmp(method, "PLAY")) {
printf("start play\n");
printf("client ip:%s\n", clientIP);
printf("client port:%d\n", clientRtpPort);
while (true) {
Sleep(40);
//usleep(40000);//1000/25 * 1000
}
break;
}
memset(method,0,sizeof(method)/sizeof(char));
memset(url,0,sizeof(url)/sizeof(char));
CSeq = 0;
}
closesocket(clientSockfd);
free(rBuf);
free(sBuf);
}
int main(int argc, char* argv[])
{
// 启动windows socket start
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}
// 启动windows socket end
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);
return 0;
}
关于 RTSP/RTP中 大端小端问题的总结:
为了适配小端序机器(Little-Endian,如 Windows/x86)的内存填充规则,我们在定义 RTP 结构体时必须**"反向"书写**。
这是因为网络标准(大端序)要求关键字段(如 Version)位于字节的高位 (Bit 7-6),而小端序编译器 是从低位 (Bit 0)开始填充位域的。因此,我们需要在代码中先定义低位字段(如 CSRC Count),让它们先占领低位,最后定义 Version,将其"挤"到剩下的高位去,从而在物理内存中拼凑出符合网络标准的数据格式。

ZLMediaKit
如果你把摄像头产生的视频流比作"原油",那么 ZLMediaKit 就是一个功能极其强大的**"炼油厂"兼"物流中心"**。它能接收各种协议的视频流,并实时把它们转化成几乎所有主流终端(网页、手机、监控软件)都能播放的格式。
1. 它能"听懂"并"翻译"几乎所有流媒体协议
ZLMediaKit 最强大的地方在于它的多协议转换能力(转封装)。你只要给它输入一个流,它就能自动生成多种格式的输出:
-
输入(推流/拉流): RTSP、RTMP、HLS、GB28181、HTTP-FLV、WebRTC、SRT。
-
输出(播放): 同上。
举个例子: 你把摄像头的 RTSP 流接入 ZLMediaKit,它会立刻为你生成:
-
一个 RTMP 地址(发给直播平台)。
-
一个 HTTP-FLV 或 WebSocket-FLV 地址(网页低延迟播放)。
-
一个 HLS 地址(手机浏览器播放)。
-
一个 WebRTC 地址(实现无插件、极低延迟的网页通话级体验)。
2. 为什么它在安防监控(GB28181)中是标配?
在 GB28181 视频网关 架构中,ZLMediaKit 通常充当 "流媒体服务器 (Media Server)" 的角色:
-
收流: 当 WVP(信令服务器)下发指令给摄像头后,摄像头会通过 国标 GB28181 (PS流) 把视频数据推送到 ZLMediaKit。
-
解析: ZLMediaKit 负责把复杂的 PS 流拆解,提取出 H.264/H.265 视频和音频。
-
对外分发: 你的前端网页只需要请求 ZLMediaKit 的 HTTP-FLV 接口,就能看到画面了。
3. ZLMediaKit 的核心优势
-
高性能: 采用 C++ 开发,并发能力极强,单机可以支撑数千路视频流。
-
功能全面: 自带录制(MP4/HLS)、截图、鉴权(防止别人偷看流)、多机级联(分布式部署)。
- 轻量级: 代码结构清晰,不依赖复杂的第三方库,非常适合部署在 Linux 服务器甚至一些高性能的嵌入式设备(如瑞芯微 RK3588)上。
RTSP拉流传输H264服务器
第一阶段:RTSP 握手(建立控制连接)
客户端(如 VLC)先和服务器通过 TCP 端口(常见 554 或你代码里的 8554)完成 RTSP 会话:
- OPTIONS:客户端问"支持哪些方法",服务器返回支持 OPTIONS、DESCRIBE、SETUP、PLAY。
- DESCRIBE:客户端请求流描述,服务器返回 SDP。
关键点:SDP 会声明视频编码是 H264,通常会有 payload type(如 96),以及 sprop-parameter-sets(SPS/PPS,供解码器初始化)。 - SETUP:客户端告诉服务器"我用 UDP 收流,RTP/RTCP 端口是多少"。服务器返回会话 ID(Session)和传输参数。
- PLAY:客户端下发"开始发送",服务器返回 200 OK,然后开始发 RTP 视频包。
第二阶段:H264 帧读取与拆解(生产者)
服务器进入循环,从本地 .h264 文件中按 NALU 边界取数据:
- 识别起始码:00 00 01 或 00 00 00 01。
- 截取一个完整 NALU(从当前起始码到下一个起始码前)。
- 解析 NALU 类型:
- 1 到 23:普通单 NALU(如 P/B/I 片)
- 5:IDR 关键帧
- 7/8:SPS/PPS(解码参数,通常在播放初期或关键帧前发送)
第三阶段:RTP 封装(包装快递)
服务器把 NALU 封装为 RTP 后再发 UDP,核心有两种情况:
RTP Header(固定 12 字节)
- Version = 2
- Payload Type = SDP 协商值(常见 96)
- Sequence Number:每发一个 RTP 包递增
- Timestamp:按视频时钟递增(H264 常用 90000 时钟)
- 例如 25fps 时,每帧增量约 90000/25 = 3600
- SSRC:流标识,固定一个随机值即可
1.单 NALU 模式(Small NALU),当一个 NALU 小于 MTU 可承载大小时:
- RTP 负载直接放完整 NALU(不含起始码)
- 通常一个 NALU 对应一个 RTP 包FU-A 分片模式(Large NALU)
2.当一个 NALU 太大时拆包发送:
- 第一个分片:S=1,E=0
- 中间分片:S=0,E=0
- 最后分片:S=0,E=1
- 接收端按序重组后恢复原始 NALU
这就是你客户端里看到的 FU-A 重组逻辑来源。
第四阶段:发送与定时(运输)
- UDP 发送:服务器用 sendto 发到客户端在 SETUP 指定的 RTP 端口。
- 节奏控制(Pacing):按帧率或时间戳节奏发送,不能一口气打满。
- 不控速会导致接收端抖动、缓存异常、卡顿
- 常见做法:每帧 Sleep 对应帧间隔(如 40ms 对应 25fps),或按时间戳调度更精细发送
H264在RTP中的包装
每个分片 RTP payload 开头 2 字节:
- 第 1 字节 FU indicator = (原 NALU 的 F 和 NRI) + Type=28
- 代码:
- naluType = frame[0];//frame是该片段H264开头指针
- payload[0] = (naluType & 0x60) | 28
- 第 2 字节 FU header = S/E/R + 原始 NALU Type
代码先写 payload[1] = naluType & 0x1F- 第一片置 S=1:rtpPacket->payload[1] |= 0x80
- 最后一片置 E=1:rtpPacket->payload[1] |= 0x40
RTSP拉流传输AAC服务器
第一阶段:RTSP 握手(建立连接)
客户端(如 VLC)与服务器通过 TCP 端口(默认 554)进行交互:
- OPTIONS: 客户端问:"你能做什么?" 服务器回:"我支持 OPTIONS, DESCRIBE, SETUP, PLAY"。
- DESCRIBE : 客户端问:"流的信息是什么?" 服务器发送 SDP (Session Description Protocol) 。
- 关键点 :SDP 里会告诉客户端这是 AAC 音频,采样率是多少(如 44100),以及
config字符串(用于解码初始化)。
- 关键点 :SDP 里会告诉客户端这是 AAC 音频,采样率是多少(如 44100),以及
- SETUP: 客户端说:"我想通过 UDP 接收音频,我的 RTP 端口是 1234,RTCP 端口是 1235"。服务器说:"收到,我的 RTP 端口是 6789,会话 ID 是 XXX"。
- PLAY: 客户端说:"开始发货吧!" 服务器回:"OK,马上发送"。
第二阶段:AAC 数据读取与拆解(生产者)
服务器进入一个 while(true) 循环,开始处理本地 .aac 文件:
- 读取 ADTS 头 :从文件取 7 字节。
- 通过
parseAdtsHeader解析出aacFrameLength(本帧总长度)。
- 通过
- 提取载荷 :根据
aacFrameLength - 7计算出纯音频数据的长度。 - 读取数据体 :调用
fread把这部分"纯肉"读入内存缓冲区frame。
第三阶段:RTP 包封装(包装快递)
这是最核心的一步,服务器构造一个 RTP 报文,由三部分拼接而成:
1. 构造 RTP Header (12 字节)
- Version (V): 固定为 2。
- Payload Type (PT): 设为 97(刚才 DESCRIBE 里商量好的)。
- Sequence Number : 每发一包
+1(接收端靠它检测是否丢包)。 - Timestamp : 时间戳。由于一帧 AAC 对应 1024 个采样点,所以每发一帧,时间戳增加 1024。
- SSRC: 一个随机的唯一标识符。
2. 构造 AAC 封装层 (4 字节 - AU Header Section)
为了让接收端知道 AAC 帧的边界,额外包了一层:
- AU-headers-length (2 字节) : 固定填
0x0010(即 16 bits)。 - AU-Header (2 字节) :
- 高 13 bits:填入刚才得到的音频数据长度。
- 低 3 bits:填入 0 (Index 字段)。
3. 填充数据体 (Payload)
- 将读取的纯 AAC 数据紧跟在上面这 4 字节后面。
第四阶段:发送与定时(运输)
- UDP 发送 :调用
sendto函数,目的地是客户端在 SETUP 阶段提供的 IP 和 RTP 端口。 - 控制节奏 (Pacing) :
- 发送完一帧后,服务器必须
Sleep一段时间(比如 23ms)。 - 原因:如果不休眠,服务器会在几毫秒内把整首歌发完,导致接收端缓冲区溢出,播放器直接崩溃或卡死。
- 发送完一帧后,服务器必须
完整代码
cpp
//
// Created by sun on 2022/12/9.
//
#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"
#define SERVER_PORT 8554
#define SERVER_RTP_PORT 55532
#define SERVER_RTCP_PORT 55533
#define BUF_MAX_SIZE (1024*1024)
#define AAC_FILE_NAME "../data/test-long.aac"
static int createTcpSocket() {
int sockfd;
int on = 1;
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 sockfd;
int on = 1;
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) {
struct 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(struct sockaddr)) < 0)
return -1;
return 0;
}
struct AdtsHeader {
unsigned int syncword; // 12 bit 同步字,固定为 '1111 1111 1111',表示 ADTS 帧开始
uint8_t id; // 1 bit,0 表示 MPEG-4,1 表示 MPEG-2
uint8_t layer; // 2 bit,固定为 0
uint8_t protectionAbsent; // 1 bit,1 表示无 CRC,0 表示有 CRC
uint8_t profile; // AAC 配置(MPEG-2 与 MPEG-4 下定义略有差异)
uint8_t samplingFreqIndex; // 4 bit,采样率索引
uint8_t privateBit; // 1 bit,通常置 0
uint8_t channelCfg; // 3 bit,声道配置
uint8_t originalCopy; // 1 bit,通常置 0
uint8_t home; // 1 bit,通常置 0
uint8_t copyrightIdentificationBit; // 1 bit,通常置 0
uint8_t copyrightIdentificationStart; // 1 bit,通常置 0
unsigned int aacFrameLength; // 13 bit,ADTS 帧总长度(头 + AAC 原始数据)
unsigned int adtsBufferFullness; // 11 bit,缓冲区满度,0x7FF 常用于 VBR
/* number_of_raw_data_blocks_in_frame
* 表示一个 ADTS 帧中包含的 AAC 原始帧数 = number_of_raw_data_blocks_in_frame + 1
* 通常 number_of_raw_data_blocks_in_frame == 0
* 即一个 ADTS 帧仅包含一个 AAC 原始帧(一般对应 1024 个采样点)
*/
uint8_t numberOfRawDataBlockInFrame; //2 bit
};
static int parseAdtsHeader(uint8_t* in, struct 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; // 取第 2 字节中的 id 位
res->layer = ((uint8_t)in[1] & 0x06) >> 1; // 取第 2 字节中的 layer 位
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
{
printf("failed to parse adts header\n");
return -1;
}
}
static int rtpSendAACFrame(int socket, const char* ip, int16_t port,
struct RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize) {
// 参考:AAC over RTP 打包格式说明
int ret;
rtpPacket->payload[0] = 0x00;
rtpPacket->payload[1] = 0x10;
rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5; // frameSize 的高 8 位
rtpPacket->payload[3] = (frameSize & 0x1F) << 3; // frameSize 的低 5 位
memcpy(rtpPacket->payload + 4, frame, frameSize);
ret = rtpSendPacketOverUdp(socket, ip, port, rtpPacket, frameSize + 4);
if (ret < 0)
{
printf("failed to send rtp packet\n");
return -1;
}
rtpPacket->rtpHeader.seq++;
/*
* 采样率为 44100Hz
* AAC-LC 通常每帧 1024 个采样点
* 每秒约 44100 / 1024 ≈ 43 帧
* RTP 时间戳每帧增量约为 1025
* 每帧时长约 23ms
*/
rtpPacket->rtpHeader.timestamp += 1025;
return 0;
}
static int acceptClient(int sockfd, char* ip, int* port) {
int clientfd;
socklen_t len = 0;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
len = sizeof(addr);
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 char* getLineFromBuf(char* buf, char* line) {
while (*buf != '\n')
{
*line = *buf;
line++;
buf++;
}
*line = '\n';
++line;
*line = '\0';
++buf;
return buf;
}
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=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=fmtp:97 SizeLength=13;\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 void doClient(int clientSockfd, const char* clientIP, int clientPort) {
int serverRtpSockfd = -1, serverRtcpSockfd = -1;
char method[40];
char url[100];
char version[40];
int CSeq;
int clientRtpPort, clientRtcpPort;
char* rBuf = (char*)malloc(BUF_MAX_SIZE);
char* sBuf = (char*)malloc(BUF_MAX_SIZE);
while (true) {
int recvLen;
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:"))) {
// Transport: RTP/AVP/UDP;unicast;client_port=13358-13359
// Transport: RTP/AVP;unicast;client_port=13358-13359
if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
&clientRtpPort, &clientRtcpPort) != 2) {
// error
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")) {
struct AdtsHeader adtsHeader;
struct 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 = (struct 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 (parseAdtsHeader(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 包,再发送
rtpSendAACFrame(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) / sizeof(char));
memset(url, 0, sizeof(url) / sizeof(char));
CSeq = 0;
}
closesocket(clientSockfd);
if (serverRtpSockfd) {
closesocket(serverRtpSockfd);
}
if (serverRtcpSockfd > 0) {
closesocket(serverRtcpSockfd);
}
free(rBuf);
free(sBuf);
}
int main() {
// 初始化 Windows Socket
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}
// Windows Socket 初始化结束
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;
char clientIp[40];
int clientPort;
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;
}
AAC在RTP中的包装(当前代码)
cpp
rtpPacket->payload[0] = 0x00;
rtpPacket->payload[1] = 0x10;
rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5; // 帧大小高8位
rtpPacket->payload[3] = (frameSize & 0x1F) << 3; // 帧大小低5位
memcpy(rtpPacket->payload + 4, frame, frameSize); // 实际AAC数据
格式(遵循RFC 3640):
- Payload前4字节是AAC特定的头
- 后面跟实际的AAC编码数据
MP3在RTP中的包装(RFC 3119)
cpp
// MP3 RTP Payload 结构
rtpPacket->payload[0] = 0x00; // MBZ (Must Be Zero)
rtpPacket->payload[1] = 0x00; // MBZ
rtpPacket->payload[2] = (offset >> 5); // 偏移量高位
rtpPacket->payload[3] = (offset << 3) | mbd; // 偏移量低位 + 边界标志
memcpy(rtpPacket->payload + 4, frame, frameSize); // 实际MP3数据
关键差异:
| 特性 | AAC | MP3 |
|---|---|---|
| 标准 | RFC 3640 | RFC 3119 |
| Payload类型 | 97 | 14 |
| 头部结构 | 4字节特定头 | 4字节头部 |
| 采样率处理 | 固定44100Hz | 支持多种采样率 |
| 时间戳增量 | ~1025 | 根据样本数计算 |
| 帧长度 | 1024采样点 | 1152采样点 |
心得:
很多涉及字符搬运的操作,基础变量要设置为uint8/char*
cpp
memcpy(rtpPacket->payload+2, frame+pos, RTP_MAX_PKT_SIZE);
比如这里payload是设置为uint8[0]的,frame是char*