C++实现RTSP/RTP服务器

前面介绍了rtsp,rtp,h264相关的知识,记不清的可以回顾一下。这篇我们来讲解如何用c++自己写一个简单的最基本的rtsp服务器。

 

一、RTSP代码实现

先来讲解RTSP代码实现,这个比较简单,直接参考前面讲的wireshark抓包内容,都是文本内容,就是一问一答的方式。

客户端发来的:

rtmp服务端 java c++ rtmp服务器_后端

服务器端回复:

rtmp服务端 java c++ rtmp服务器_rtmp服务端 java_02

有些字段不是必须的。看看代码实现:

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的一张图:

rtmp服务端 java c++ rtmp服务器_rtmp服务端 java_03

码流是一帧一帧的图片数据,所以传输的时候也是一帧帧来传输的,因此这里就会涉及到各种类型的帧处理了

 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);

看图:

rtmp服务端 java c++ rtmp服务器_TCP_04

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。

最后来张效果图:

rtmp服务端 java c++ rtmp服务器_TCP_05

到这里我们自己造出了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);

}