编写简单RTSP协议-处理客户端请求
在RTSP(实时流协议)中,处理客户端请求步骤如下:
-
发送请求:客户端发送RTSP请求命令,常见的请求类型包括:
- OPTIONS:请求服务器支持的方法,了解服务器可用的方法。
- DESCRIBE:请求服务器发送媒体描述信息(通常是SDP格式),以了解可用的媒体流及其属性。
- SETUP:请求建立会话连接,准备接收音视频数据。此请求会指定传输参数,如RTP/RTCP端口、传输协议(TCP或UDP)、单播或组播等。
- PLAY:请求开始播放媒体流。
- PAUSE:请求暂停播放。
- TEARDOWN:请求终止会话并释放资源。
-
响应请求:服务器处理客户端的请求并返回响应。例如,对于SETUP请求,服务器会返回确认的传输参数和会话ID。
-
传输数据:一旦会话建立,媒体数据通过RTP(实时传输协议)进行传输,RTCP(实时传输控制协议)则用于传输控制信息和质量反馈。
1.接受客户端数据
使用 recv
函数从套接字 clientSockfd
接收数据,并将其存储在缓冲区 rBuf
中
recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0);
if(recvLen <= 0)
goto out;
从客户端接收请求消息。
2.解析请求命令
从缓冲区获取数据,并从数据中提前方法
、url
、版本
等信息。
bufPtr = getLineFromBuf(rBuf, line);
if(sscanf(line, "%s %s %s\r\n", method, url, version) != 3)
{
printf("parse err\n");
goto out;
}
getLineFromBuf
函数的作用从缓冲区 buf
中读取一行数据,并将其存储到 line
中。
static char* getLineFromBuf(char* buf, char* line)
{
while(*buf != '\n')//循环读取字符
{
*line = *buf; //复制字符
line++;
buf++;
}
*line = '\n';//添加换行符
++line;
*line = '\0';//添加终止符
++buf; //更新缓冲区指针
return buf;
}
3.解析序列号
解析RTSP请求中的序列号(CSeq)。在RTSP协议中,CSeq用于标识请求的顺序,确保请求和响应能够正确匹配。
/* 解析序列号 */
bufPtr = getLineFromBuf(bufPtr, line);
if(sscanf(line, "CSeq: %d\r\n", &cseq) != 1)
{
printf("parse err\n");
goto out;
}
4.解析客户端端口
解析 Transport
头部信息,以获取客户端的RTP和RTCP端口
if(!strcmp(method, "SETUP"))//检查请求方法
{
while(1)//循环读取行
{
bufPtr = getLineFromBuf(bufPtr, line);//逐行读取数据
if(!strncmp(line, "Transport:", strlen("Transport:"))) //检查当前行是否以 Transport: 开头。
{
sscanf(line, "Transport: RTP/AVP;unicast;client_port=%d-%d\r\n",
&clientRtpPort, &clientRtcpPort);//提取客户端的RTP和RTCP端口
break;
}
}
}
Transport
头部包含传输参数,如传输协议、传输模式和端口信息。
5.根据不同方法响应客户端
5.1 OPTIONS方法响应
处理 OPTIONS
请求,OPTIONS
请求用于查询服务器支持的功能和方法
if(!strcmp(method, "OPTIONS")) //检查RTSP请求的方法
{
if(handleCmd_OPTIONS(sBuf, cseq))//处理请求
{
printf("failed to handle options\n");
goto out;
}
}
调用 handleCmd_OPTIONS
函数来处理 OPTIONS
请求。
static int handleCmd_OPTIONS(char* result, int cseq)/* result:存储生成的响应消息;cseq: 请求的序列号*/
{
/*使用 sprintf 函数生成响应消息,并将其存储在 result 中*/
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;
}
服务器响应 OPTIONS
请求,并响应客户端服务器支持的RTSP方法。
5.2 DESCRIBE方法响应
处理 DESCRIBE
请求,DESCRIBE
请求用于获取媒体资源的描述信息,通常是SDP(会话描述协议)格式。
else if(!strcmp(method, "DESCRIBE"))
{
if(handleCmd_DESCRIBE(sBuf, cseq, url))
{
printf("failed to handle describe\n");
goto out;
}
}
调用handleCmd_DESCRIBE
函数处理 DESCRIBE
请求。
static int handleCmd_DESCRIBE(char* result, int cseq, char* url)/* result:存储生成的响应消息;cseq: 请求的序列号;url:请求的媒体资源URL*/
{
char sdp[500];//存储生成的SDP描述信息
char localIp[100];//IP地址
sscanf(url, "rtsp://%[^:]:", localIp);//从URL中提取本地IP地址
/*生成SDP描述信息*/
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: %d\r\n\r\n"
"%s",
cseq,
url,
strlen(sdp),
sdp);
return 0;
}
SDP参数如下:
v=0
:版本号:SDP协议版本,目前固定为0。o=- 9%ld 1 IN IP4 %s
:会话所有者/创建者和会话标识符-
:用户名,表示不使用用户名。9%ld
:会话ID,使用当前时间戳生成。1
:会话版本号,通常为1。IN
:网络类型,表示Internet。IP4
:地址类型,表示IPv4地址。%s
:本地IP地址。
t=0 0
:时间描述:会话的起始和结束时间,0 0
表示会话永不过期。a=control:\*
:属性:会话级别的控制属性,*
表示控制整个会话。m=video 0 RTP/AVP 96
:媒体描述video
:媒体类型,表示视频。0
:端口号,0
表示端口未指定。RTP/AVP
:传输协议,表示RTP协议下的音视频配置。96
:媒体格式,动态负载类型96,通常用于H.264视频编码。
a=rtpmap:96 H264/90000
:属性:RTP映射,指定负载类型96对应H.264编码,时钟频率为90000Hz。a=control:track0
:属性:媒体级别的控制属性,track0
表示第一个媒体流。
响应消息的格式如下:
RTSP/1.0 200 OK\r\n
:表示请求成功,返回状态码200。CSeq: %d\r\n
:包含请求的序列号(CSeq),用于匹配请求和响应。Content-Base: %s\r\n
:包含请求的URL。Content-type: application/sdp\r\n
:指定内容类型为SDP。Content-length: %d\r\n\r\n
:指定内容长度。%s
:包含生成的SDP描述信息。
5.3 SETUP方法响应
处理 SETUP
请求,SETUP
请求用于初始化一个媒体流会话,设置传输参数。
else if(!strcmp(method, "SETUP"))//检查RTSP请求的方法
{
if(handleCmd_SETUP(sBuf, cseq, clientRtpPort))
{
printf("failed to handle setup\n");
goto out;
}
}
调用 handleCmd_SETUP
函数来处理 SETUP
请求。
static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort)
{
/生成一个RTSP响应消息
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;
}
-
状态行:
RTSP/1.0 200 OK
,表示请求成功。 -
CSeq:序列号,用于匹配请求和响应。
-
Transport:传输参数,指定了RTP(实时传输协议)和RTCP(实时传输控制协议)的传输方式、客户端和服务器的端口号。
-
Session:会话ID,用于标识RTSP会话。
-
设置传输端口:函数从参数中获取客户端的RTP端口(
clientRtpPort
),并设置服务器的RTP和RTCP端口(SERVER_RTP_PORT
和SERVER_RTCP_PORT
)。
5.4 PLAY方法响应
处理 PLAY
请求,PLAY
命令用于开始或恢复媒体流的播放。
else if(!strcmp(method, "PLAY"))//检查命令类型
{
if(handleCmd_PLAY(sBuf, cseq))//处理PLAY请求
{
printf("failed to handle play\n");
goto out;
}
}
调用handleCmd_PLAY
函数处理PLAY
请求。
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=60\r\n\r\n",
cseq);
return 0;
}
- 状态行:
RTSP/1.0 200 OK
,表示请求成功。 - CSeq:序列号,用于匹配请求和响应。
- Range:指定播放范围,这里表示从头开始播放。
- Session:会话ID和超时时间,用于标识RTSP会话并设置会话的超时时间。
6.发送RTP数据包
6.1 初始化RTP数据包头信息
RTP协议与RTSP(实时流协议)一起使用,用于传输视频数据。
rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0,
0, 0, 0x88923423);
调用rtpHeaderInit
函数初始化一个RtpPacket
结构体的RTP头部字段。函数参数包括CSRC长度、扩展标志、填充标志、版本号、负载类型、标记位、序列号、时间戳和同步源标识符(SSRC)。
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)
{
rtpPacket->rtpHeader.csrcLen = csrcLen;//CSRC(贡献源)标识符的个数
rtpPacket->rtpHeader.extension = extension;//扩展标志,指示是否有扩展头部。
rtpPacket->rtpHeader.padding = padding;//充标志,指示是否有填充字节。
rtpPacket->rtpHeader.version = version;//RTP协议版本号,通常为2。
rtpPacket->rtpHeader.payloadType = payloadType;//负载类型,指示传输的数据类型(例如音频或视频)。
rtpPacket->rtpHeader.marker = marker;//标记位,用于标记重要事件(例如帧的结束)。
rtpPacket->rtpHeader.seq = seq;//用于标识RTP数据包的顺序。
rtpPacket->rtpHeader.timestamp = timestamp;//时间戳,用于同步音视频数据。
rtpPacket->rtpHeader.ssrc = ssrc;//同步源标识符,用于标识数据流的来源。
}
- SRC长度、扩展标志和填充标志均为0。
- 版本号为2。
- 负载类型为96(通常表示动态负载类型,没有预先定义的固定编码格式)。
- 标记位为0。
- 序列号和时间戳均为0。
- SSRC为
0x88923423
,唯一标识发送端的媒体流。
6.2 从文件中读取一帧图像
读取fd文件句柄,读取的大小为500000字节,到缓冲区frame中。
frameSize = getFrameFromH264File(fd, frame, 500000);
if(frameSize < 0)
{
break;
}
调用getFrameFromH264File
函数从一个H.264文件中读取一帧数据
static int getFrameFromH264File(int fd, char* frame, int size)//从一个 H.264 文件中读取一帧数据
{
int rSize, frameSize;
char* nextStartCode;
if(fd < 0)
return fd;
rSize = read(fd, frame, size);
if(!startCode3(frame) && !startCode4(frame))//读取的数据必须以H.264的起始码(start code)开头。
return -1;
nextStartCode = findNextStartCode(frame+3, rSize-3);//查找下一帧的起始码位置
if(!nextStartCode)
{
//lseek(fd, 0, SEEK_SET);
//frameSize = rSize;
return -1;
}
else
{
frameSize = (nextStartCode-frame);//计算当前帧的大小
lseek(fd, frameSize-rSize, SEEK_CUR);//调整文件指针以准备读取下一帧
}
return frameSize;//返回当前帧的大小
}
6.3 检查起始码的长度
检查frame
缓冲区中的数据是否以3字节/4字节的起始码。
if(startCode3(frame))
startCode = 3;
else
startCode = 4;
检查给定缓冲区中的数据是否以3字节的H.264起始码(0x000001
)开头。
static inline int startCode3(char* buf)
{
if(buf[0] == 0 && buf[1] == 0 && buf[2] == 1)//检查缓冲区buf的前三个字节是否为0x00 0x00 0x01
return 1;
else
return 0;
}
6.4 调整帧的大小
减去起始码的长度(3或4字节),调整帧的实际大小。
frameSize -= startCode;
6.5 发送视频帧
将H.264视频帧通过RTP(实时传输协议)发送到客户端。
rtpSendH264Frame(serverRtpSockfd, clientIP, clientRtpPort,
rtpPacket, frame+startCode, frameSize);
serverRtpSockfd
:服务器的RTP套接字文件描述符。clientIP
:客户端的IP地址。clientRtpPort
:客户端的RTP端口。rtpPacket
:包含RTP头部信息的数据包。frame+startCode
:指向视频帧数据的指针,跳过起始码。frameSize
:调整后的帧大小。
6.5.1 单一NAL单元模式
RTP(实时传输协议)数据包的封装和发送。
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);//将视频帧数据复制到RTP数据包的负载部分
ret = rtpSendPacket(socket, ip, port, rtpPacket, frameSize);//通过RTP套接字发送数据包
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;//增加RTP头部的序列号
sendBytes += ret;//累加发送的字节数
if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳
goto out;
}
H.264视频帧封装到RTP数据包中,并通过网络发送到客户端。在单一NAL单元的情况,发送SPS和PPS帧时不增加时间戳。
6.5.2 分片模式
处理和发送大于RTP(实时传输协议)数据包最大负载大小的H.264视频帧。
int pktNum = frameSize / RTP_MAX_PKT_SIZE; // 有几个完整的包
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
int i, pos = 1;
/* 发送完整的包 */
for (i = 0; i < pktNum; i++)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28; //设置FU Indicator字节,表示这是分片单元类型
rtpPacket->payload[1] = naluType & 0x1F; // 设置FU Header字节,表示NALU类型
if (i == 0) //第一包数据
rtpPacket->payload[1] |= 0x80; // start 第8位设置1,表示分片单元的开始
else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据,并且没有剩余数据
rtpPacket->payload[1] |= 0x40; // end 第7位设置为1,表示这是分片单元的结束。
//跳过FU Indicator 和 FU Header的前两个字节,复制视频帧数据到RTP负载。
memcpy(rtpPacket->payload+2, frame+pos, RTP_MAX_PKT_SIZE);
ret = rtpSendPacket(socket, ip, port, rtpPacket, RTP_MAX_PKT_SIZE+2);//发送RTP数据包
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;//更新序列号
sendBytes += ret; //统计数据量
pos += RTP_MAX_PKT_SIZE; //计算下一个位置
}
/* 发送剩余的数据 */
if (remainPktSize > 0)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28; //设置 FU Indicator 字节
rtpPacket->payload[1] = naluType & 0x1F; //设置 FU Header 字节
rtpPacket->payload[1] |= 0x40; //设置结束位
memcpy(rtpPacket->payload+2, frame+pos, remainPktSize);//复制剩余数据到RTP负载
ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize);//发送RTP数据包
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
}
将大于RTP最大负载大小的H.264视频帧分片,并通过RTP协议发送到客户端。
6.5.3 通过UDP套接字发送RTP包
通过UDP套接字发送RTP数据包,并在发送前后正确处理字节序转换,以便在网络传输中保持数据的一致性。
int rtpSendPacket(int socket, const char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize)
{
struct sockaddr_in addr; //存储目标地址信息
int ret;
addr.sin_family = AF_INET; //地址族,设置为 AF_INET 表示 IPv4
addr.sin_port = htons(port); //目标端口
addr.sin_addr.s_addr = inet_addr(ip);//目标 IP 地址
//将 16 位和 32 位的主机字节序转换为网络字节序
rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);//将RTP头部的序列号从主机字节序转换为网络字节序。
rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);//将RTP头部的时间戳从主机字节序转换为网络字节序。
rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);// 将RTP头部的同步源标识符(SSRC)从主机字节序转换为网络字节序。
ret = sendto(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE, 0,
(struct sockaddr*)&addr, sizeof(addr));//使用sendto函数通过UDP套接字发送数据包
//将 16 位和 32 位的网络字节序转换为主机字节序
rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);//将RTP头部的序列号从网络字节序转换回主机字节序。
rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);//将RTP头部的时间戳从网络字节序转换回主机字节序。
rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);//将RTP头部的同步源标识符(SSRC)从网络字节序转换回主机字节序。
return ret;
}
6.6 更新时间戳
更新RTP头部中的时间戳,以反映视频帧的播放时间。
rtpPacket->rtpHeader.timestamp += 90000/25;
假设视频帧率为25帧每秒,因此每帧的时间戳增量为90000/25
(RTP时间戳单位为90kHz)。