C++实现RTSP/RTP服务器
前面介绍了rtsp,rtp,h264相关的知识,记不清的可以回顾一下。这篇我们来讲解如何用c++自己写一个简单的最基本的rtsp服务器。
一、RTSP代码实现
先来讲解RTSP代码实现,这个比较简单,直接参考前面讲的wireshark抓包内容,都是文本内容,就是一问一答的方式。
客户端发来的:
服务器端回复:
有些字段不是必须的。看看代码实现:
int Rtsp::ProcessCmdOptions(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;
}
剩下的几个方法就不一一介绍了,直接看代码吧。
二、RTP打包H264码流
看看前面讲解H264的一张图:
码流是一帧一帧的图片数据,所以传输的时候也是一帧帧来传输的,因此这里就会涉及到各种类型的帧处理了
I 帧: 关键帧,可以理解为是一帧完整画面,解码时只需要本帧数据就解码成一幅完整的图片,数据量比较大。
P帧: 差别帧,只有与前面的帧或I帧的差别数据,需要依赖前面已编码图象作为参考图象,才能解码成一幅完整的图片,数据量小。
B帧: 双向差别帧,也就是B帧记录的是本帧与前后帧的差别,需要依赖前后帧和I帧才能解码出一幅完整的图片,数据量小。
i帧是完整的数据比较大,已经超出mtu最大1500,所以需要拆包分片传输,这里说的拆包发送不是指发送超过1500的数据包时tcp的分段传输或者upd的ip分片传输,而是指rtp协议本身对264的拆包。 rtp打包后将数据交给tcp或者upd进行传输的时候就已经控制在1500以内,这样可以提高传输效率,避免一个I帧万一丢失就会造成花屏或者重传造成延时卡顿等等问题。(顺便提一句,rtmp打包就比较简单,由于是基于tcp的协议,大包直接交给tcp去做分段传输,rtmp通过设置合适的trunk size去发送一帧帧数据)既然要进行拆包发送与接收,就少不了需要相关的包结构以及打包组包了。
H264在网络传输的单元:NALU
NALU结构:NALU header(1byte) + NALU payload,header 部分可以判断类型等信息,从右往左5个bit位
上一篇H264我们有讲解NALU头TYPE字段各个值得意义,( 0x1f的二进制是:0 00 11111)
SPS: 0x67 NALU_header & 0x1F = 7
PPS: 0x68 NALU_header& 0x1F = 8
SEI: 0x66 NALU_header & 0x1F = 6
RTP格式定义三种不同的基本荷载结构,接收者可以通过RTP荷载的第一个字节后5位。
1、单个NAL单元包:荷载中只包含一个NAL单元。NAL头类型域等于原始 NAL单元类型,即在范围1到23之间。
P帧或者B帧比较小的包, 只需要在RTP包头后添加去掉startcode的NALU数据即可
格式:RTP header(12bytes) + NALU header (1byte) + NALU payload
2、聚合包:本类型用于聚合多个NAL单元到单个RTP荷载中。本包有四种版本,单时间聚合包类型A (STAP-A),单时间聚合包类型B (STAP-B),多时间聚合包类型(MTAP)16位位移(MTAP16), 多时间聚合包类型(MTAP)24位位移(MTAP24)。赋予STAP-A, STAP-B, MTAP16, MTAP24的NAL单元类型号分别是 24,25, 26, 27。这种用的比较少。
3、分片单元:用于分片单个NAL单元到多个RTP包。现存两个版本FU-A,FU-B,用NAL单元类型 28,29标识。
常用的打包时的分包规则是:如果小于MTU采用单个NAL单元包,如果大于MTU就采用FUs分片方式。因为常用的打包方式就是单个NAL包和FU-A方式,所以我们只实现这两种。
格式:RTP header (12bytes)+ FU Indicator (1byte) + FU header(1 byte) + NALU payload
1. 这里没有NALU头了,如何判断类型?实际上NALU头被分散填充到FU indicator和FU header里面了,bit位按照从左到右编号0-7来算,nalu头中0-2前三个bit放在FU indicator的0-2前三个bit中,后3-7五个bit放入FU header的后3-7五个中
//NALU header = (FU indicator & 0xe0) | (FU header & 0x1F) 取FU indicator前三和FU header后五
headerStart[1] = (headerStart[0]&0xE0)|(headerStart[1]&0x1F);
看图:
S: 1 bit 当设置成1,开始位指示分片NAL单元的开始,分片的第一包。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。
E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的 FU荷载不是分片NAL单元的最后分片,结束位设置为0。
R: 1 bit 保留位必须设置为0,接收者必须忽略该位。
打包时,原始的NAL头的前三位为FU indicator的前三位,原始的NAL头的后五位为FU header的后五位。
因此查看I帧p帧类型,遇到FU分片的,直接看第二个字节,即Fu header后五位,这个跟直接看NALU头并无差异,一般有:0x85,0x05,0x45等等。
取一段码流分析如下:
80 60 01 0f 00 0e 10 00 00 0000 00 7c 85 88 82 €`..........|???
00 0a 7f ca 94 05 3b7f 3e 7f fe 14 2b 27 26 f8 ...??.;.>.?.+'&?
89 88 dd 85 62 e1 6dfc 33 01 38 1a 10 35 f2 14 ????b?m?3.8..5?.
84 6e 21 24 8f 72 62f0 51 7e 10 5f 0d 42 71 12 ?n!$?rb?Q~._.Bq.
17 65 62 a1 f1 44 dc df 4b 4a 38 aa 96 b7 dd 24 .eb??D??KJ8????$
前12字节是RTP Header
7c是FU indicator
85是FU Header
FU indicator(0x7C)和FU Header(0x85)换成二进制如下
0111 1100 1000 0101
按顺序解析如下:
0 是F
11 是NRI
11100 是FU Type,这里是28,即FU-A
1 是S,Start,说明是分片的第一包
0 是E,End,如果是分片的最后一包,设置为1,这里不是
0 是R,Remain,保留位,总是0
00101 是NAl Type,这里是5,说明是关键帧
看代码实现:
int Rtp::RtpSendH264Frame(int socket, const char* ip, int16_t port,
struct RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize)
{
uint8_t naluHeader; // NALU头
#define NALU_TYPE 0x1F
#define NALU_F 0x80
#define NALU_NRI 0x60
int sendBytes = 0;
int ret;
naluHeader = frame[0];
/*
NALU长度小于RTP最大包长:单一NALU单元模式,一个NALU单元用一个RTP包发送出去。
只需要在RTP包头后去掉开始码的NALU数据即可。
RTP header(12bytes) + NALU header (1byte) + NALU payload
*/
if (frameSize <= RTP_MAX_PKT_SIZE)
{
/*NALU头,因为网络字节顺序NBO 和 主机字节顺序的原因,所以是高位在前
* 7 6 5 4 3 2 1 0
* +-+-+-+-+-+-+-+
* |F|NRI| Type |
* +-+-+-+-+-+-+-+
*/
memcpy(rtpPacket->rtppayload, frame, frameSize);
ret = RtpSendPacket(socket, ip, port, rtpPacket, frameSize);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
if ((naluHeader & NALU_TYPE) == 7 || (naluHeader & NALU_TYPE) == 8)
goto out;
}else {
/*
/nalu长度大于最大包长:分片模式,一个NALU分成几个RTP包(FUs模式)。
要在RTP头12自己后添加FU Indicator 和 FU Header 再添加NALU的数据:
RTP header (12bytes)+ FU Indicator (1byte) + FU header(1 byte) + NALU payload
* 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; // 完整的RTP包 个数
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
int i, pos = 1;
/* 发送完整的包 */
for (i = 0; i < pktNum; i++)
{
//前面的文章说过NAL头TYPE值28是FU-A分片,把原先nal头的 |F|NRI|赋值到FU Indicator的 |F|NRI|
rtpPacket->rtppayload[0] = (naluHeader & NALU_NRI) | 28;
//把原先nal头的type的5bits赋值到 FU Header的type的5bits
rtpPacket->rtppayload[1] = naluHeader & NALU_TYPE;
if (i == 0) //第一包数据
rtpPacket->rtppayload[1] |= 0x80; // 0x80: 1000s0000 ,S位为1是start
else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据
rtpPacket->rtppayload[1] |= 0x40; // 0x40:01000000,E位为1是end
//把NALU数据填充到RTP包数据段
memcpy(rtpPacket->rtppayload+2, frame+pos, RTP_MAX_PKT_SIZE);
ret = RtpSendPacket(socket, 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->rtppayload[0] = (naluHeader & NALU_NRI) | 28;
rtpPacket->rtppayload[1] = naluHeader & NALU_TYPE;
rtpPacket->rtppayload[1] |= 0x40; //end
memcpy(rtpPacket->rtppayload+2, frame+pos, remainPktSize+2);
ret = RtpSendPacket(socket, ip, port, rtpPacket, remainPktSize+2);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
}
}
out:
return sendBytes;
}
完整源码在:
“C++实现RTSP_RTP服务器的源码.zip”
执行程序 crl6@ubuntu:~/work/AudioVideo/rtsp$ ./rtspserver test.h264
rtsp://127.0.0.1:8554
打开vlc 在地址栏输入 rtsp://127.0.0.1:8554。
最后来张效果图:
到这里我们自己造出了rtsp这个轮子,但是这个轮子不够完善。实际项目使用我们可以参考ffmpeg 和 live555。
看看ffmpeg 的rtsp相关代码
// 截取部分代码。 主要是在rtsp开头的文件里。
const AVOption ff_rtsp_options[] = {
{ "initial_pause", "do not start playing the stream immediately", OFFSET(initial_pause), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DEC },
FF_RTP_FLAG_OPTS(RTSPState, rtp_muxer_flags),
{ "rtsp_transport", "set RTSP transport protocols", OFFSET(lower_transport_mask), AV_OPT_TYPE_FLAGS, {.i64 = 0}, INT_MIN, INT_MAX, DEC|ENC, "rtsp_transport" }, \
{ "udp", "UDP", 0, AV_OPT_TYPE_CONST, {.i64 = 1 << RTSP_LOWER_TRANSPORT_UDP}, 0, 0, DEC|ENC, "rtsp_transport" }, \
{ "tcp", "TCP", 0, AV_OPT_TYPE_CONST, {.i64 = 1 << RTSP_LOWER_TRANSPORT_TCP}, 0, 0, DEC|ENC, "rtsp_transport" }, \
{ "udp_multicast", "UDP multicast", 0, AV_OPT_TYPE_CONST, {.i64 = 1 << RTSP_LOWER_TRANSPORT_UDP_MULTICAST}, 0, 0, DEC, "rtsp_transport" },
{ "http", "HTTP tunneling", 0, AV_OPT_TYPE_CONST, {.i64 = (1 << RTSP_LOWER_TRANSPORT_HTTP)}, 0, 0, DEC, "rtsp_transport" },
{ "https", "HTTPS tunneling", 0, AV_OPT_TYPE_CONST, {.i64 = (1 << RTSP_LOWER_TRANSPORT_HTTPS )}, 0, 0, DEC, "rtsp_transport" },
RTSP_FLAG_OPTS("rtsp_flags", "set RTSP flags"),
{ "listen", "wait for incoming connections", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_LISTEN}, 0, 0, DEC, "rtsp_flags" },
{ "prefer_tcp", "try RTP via TCP first, if available", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_PREFER_TCP}, 0, 0, DEC|ENC, "rtsp_flags" },
{ "satip_raw", "export raw MPEG-TS stream instead of demuxing", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_SATIP_RAW}, 0, 0, DEC, "rtsp_flags" },
RTSP_MEDIATYPE_OPTS("allowed_media_types", "set media types to accept from the server"),
{ "min_port", "set minimum local UDP port", OFFSET(rtp_port_min), AV_OPT_TYPE_INT, {.i64 = RTSP_RTP_PORT_MIN}, 0, 65535, DEC|ENC },
{ "max_port", "set maximum local UDP port", OFFSET(rtp_port_max), AV_OPT_TYPE_INT, {.i64 = RTSP_RTP_PORT_MAX}, 0, 65535, DEC|ENC },
{ "listen_timeout", "set maximum timeout (in seconds) to wait for incoming connections (-1 is infinite, imply flag listen)", OFFSET(initial_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, INT_MIN, INT_MAX, DEC },
#if FF_API_OLD_RTSP_OPTIONS
{ "timeout", "set maximum timeout (in seconds) to wait for incoming connections (-1 is infinite, imply flag listen) (deprecated, use listen_timeout)", OFFSET(initial_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, INT_MIN, INT_MAX, DEC|AV_OPT_FLAG_DEPRECATED },
{ "stimeout", "set timeout (in microseconds) of socket TCP I/O operations", OFFSET(stimeout), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC },
#else
{ "timeout", "set timeout (in microseconds) of socket TCP I/O operations", OFFSET(stimeout), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC },
#endif
COMMON_OPTS(),
{ "user_agent", "override User-Agent header", OFFSET(user_agent), AV_OPT_TYPE_STRING, {.str = LIBAVFORMAT_IDENT}, 0, 0, DEC },
#if FF_API_OLD_RTSP_OPTIONS
{ "user-agent", "override User-Agent header (deprecated, use user_agent)", OFFSET(user_agent), AV_OPT_TYPE_STRING, {.str = LIBAVFORMAT_IDENT}, 0, 0, DEC|AV_OPT_FLAG_DEPRECATED },
#endif
{ NULL },
};
static const AVOption sdp_options[] = {
RTSP_FLAG_OPTS("sdp_flags", "SDP flags"),
{ "custom_io", "use custom I/O", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_CUSTOM_IO}, 0, 0, DEC, "rtsp_flags" },
{ "rtcp_to_source", "send RTCP packets to the source address of received packets", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_RTCP_TO_SOURCE}, 0, 0, DEC, "rtsp_flags" },
{ "listen_timeout", "set maximum timeout (in seconds) to wait for incoming connections", OFFSET(initial_timeout), AV_OPT_TYPE_INT, {.i64 = READ_PACKET_TIMEOUT_S}, INT_MIN, INT_MAX, DEC },
RTSP_MEDIATYPE_OPTS("allowed_media_types", "set media types to accept from the server"),
COMMON_OPTS(),
{ NULL },
};
static const AVClass rtsp_muxer_class = {
.class_name = "RTSP muxer",
.item_name = av_default_item_name,
.option = ff_rtsp_options,
.version = LIBAVUTIL_VERSION_INT,
};
AVOutputFormat ff_rtsp_muxer = {
.name = "rtsp",
.long_name = NULL_IF_CONFIG_SMALL("RTSP output"),
.priv_data_size = sizeof(RTSPState),
.audio_codec = AV_CODEC_ID_AAC,
.video_codec = AV_CODEC_ID_MPEG4,
.write_header = rtsp_write_header,
.write_packet = rtsp_write_packet,
.write_trailer = rtsp_write_close,
.flags = AVFMT_NOFILE | AVFMT_GLOBALHEADER,
.priv_class = &rtsp_muxer_class,
};
int ff_rtsp_connect(AVFormatContext *s)
{
av_dict_set_int(&options, "timeout", rt->stimeout, 0);
ff_url_join(httpname, sizeof(httpname), https_tunnel ? "https" : "http", auth, host, port, "%s", path);
snprintf(sessioncookie, sizeof(sessioncookie), "%08x%08x",
av_get_random_seed(), av_get_random_seed());
/* generate GET headers */
snprintf(headers, sizeof(headers),
"x-sessioncookie: %s\r\n"
"Accept: application/x-rtsp-tunnelled\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\r\n",
sessioncookie);
av_opt_set(rt->rtsp_hd->priv_data, "headers", headers, 0);
}