c++直接获取rtsp流

自述

有的时候在嵌入式环境,不能用库去拉取rtsp流,或者编译库比较麻烦,这个时候,就可以研究下rtsp协议和流传,自己拉取流。本文为记录我直接用c++(或c)拉取rtsp over udp 流。

抓包分析

首先需要区别需要拉取的rtsp流是基于udp还是tcp的,基于udp的rtsp就是rtsp的协议部分用tcp沟通,协商好后会定义一个端口,用udp接收rtp包;而tcp的就是全程用一个套接字,我没有研究tcp的。本文只讨论udp的。
安装wireshark抓包软件,过滤器为:ip.src==192.168.85.119 直接抓网络摄像头的ip数据,然后打开网络摄像头的上位机软件,先用软件收流(也可以用ffplay rtsp://192.168.85.119:554/554),这个时候就能在wireshark中抓到数据包,如下如所示:
在这里插入图片描述抓到的上面一部分就为tcp沟通rtsp协议的,下面就是udp接收数据,我们追踪tcp流,结果如下:

OPTIONS rtsp://192.168.85.119:554 RTSP/1.0
CSeq: 1
User-Agent: Lavf58.29.100

RTSP/1.0 200 OK
CSeq: 1
Date: Thu, 01 Jan 1970 01:42:20 GMT
Public: OPTIONS,DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN

DESCRIBE rtsp://192.168.85.119:554 RTSP/1.0
Accept: application/sdp
CSeq: 2
User-Agent: Lavf58.29.100

RTSP/1.0 200 OK
CSeq: 2
Server: sunshine/1.11
Date: Thu, 01 Jan 1970 01:42:20 GMT
Content-Type: application/sdp
Content-Base: rtsp://192.168.85.119/554/
Content-Length: 337

v=0
o=OnewaveUServerNG 2208994816 2208994816 IN IP4 192.168.85.126
s=Unnamed
i=N/A
c=IN IP4 192.168.85.126
t=0 0
a=recvonly
m=video 5004 RTP/AVP 96
b=RR:0
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42001f;sprop-parameter-sets=Z0IAH5Y1QKALdNwEBAQIAAAAAWjOPIAAAAABBuU=,aM48gAAA;
a=control:trackID=0
SETUP rtsp://192.168.85.119/554/trackID=0 RTSP/1.0
Transport: RTP/AVP/UDP;unicast;client_port=34802-34803
CSeq: 3
User-Agent: Lavf58.29.100

RTSP/1.0 200 OK
CSeq: 3
Server: sunshine/1.11
Date: Thu, 01 Jan 1970 01:42:20 GMT
Session: 141565
Transport: RTP/AVP/UDP;unicast;client_port=34802-34803;destination=192.168.85.126;source=192.168.85.119;server_port=-5004--5003

PLAY rtsp://192.168.85.119/554/ RTSP/1.0
Range: npt=0.000-
CSeq: 4
User-Agent: Lavf58.29.100
Session: 141565

RTSP/1.0 200 OK
CSeq: 4
Server: sunshine/1.11
Date: Thu, 01 Jan 1970 01:42:20 GMT
Session: 141565

这就是tcp沟通的rtsp协议内容,而我们要做的就是把对应的内容,用tcp发给相机。此处不详细介绍,可参考最后代码一起分析。

协议分析

rtsp的协议部分,不同相机可能不同,需要根据各自的情况调整,基本模仿着wireshark抓取客户端的沟通过程数据内容,基本没有问题,接收udp数据流时,端口就在rtsp协议里面Transport: RTP/AVP/UDP;unicast;client_port=34802-34803这是我们发过去的,请求把udp数据发到我们的34802端口,后面那个端口不用管他,应该是给音频的;而相机就会给我们回复Transport: RTP/AVP/UDP;unicast;client_port=34802-34803;destination=192.168.85.126;source=192.168.85.119;server_port=-5004--5003udp数据从相机的5004端口发出,发送到我们的34802端口,这样我们用udp去收取这个端口的数据即可。当然,这个端口我们可以修改的,前提是不能被占用。

收到数据还不算完成,因为这是rtp包,获取完整的一个264包才能送去解码,这时需要研究下rtp包的格式,提取出264码流。我就不详细介绍rtp包格式了,就说下我如何处理。
264码流最重要的就是sps pps I帧,有的相机他会把这些一起打包到rtp包里面,我们只要提取出来数据部分就可以了,但是有的严格按照rtp协议,我们就需要特殊处理了。
在这里插入图片描述
截图中data长度为27为sps帧,16的为pps帧,17的为 SEI 补充增强信息,后面的就是I帧。

  • sps帧
//sps帧
80 e0 00 19 20 f1 82 02 77 55 a8 c0 67 42 00 1f 96 35 40 a0 0b 74 dc 04 04 04 08
//解析:
//rtp头:80 e0 00 19 20 f1 82 02 77 55 a8 c0 67 42
//data:00 1f 96 35 40 a0 0b 74 dc 04 04 04 08

rtp头最后的67 42就是sps帧的标志。提取data后,在前面补充00 00 00 01 67 42

00 00 00 01 67 42 00 1f 96 35 40 a0 0b 74 dc 04 04 04 08
  • pps帧
//pps帧
80 e0 00 1a 20 f1 84 78 77 55 a8 c0 68 ce 3c 80
//解析:
//rtp头:80 e0 00 1a 20 f1 84 78 77 55 a8 c0 68 ce
//data:3c 80

rtp头最后的68 ce就是sps帧的标志。提取data后,在前面补充00 00 00 01 68 ce

00 00 00 01 68 ce 3c 80
  • SEI 补充增强信息
//SEI  补充增强信息
80 e0 00 1b 20 f1 87 fc 77 55 a8 c0 06 e5 01 47 80
//rtp头:80 e0 00 1b 20 f1 87 fc 77 55 a8 c0 06 e5
//data:01 47 80
//补全后的SEI  补充增强信息
00 00 00 01 06 e5 01 47 80
  • I帧和P帧

而p和I帧的提取就简单很多,只需要判断rtp头的第14位即可:

//        1                            是S,Start,说明是分片的第一包
//        0                            是E,End,如果是分片的最后一包,设置为1,这里不是
//        0                            是R,Remain,保留位,总是0
//        00101                    是NAL Type,这里是5,说明是关键帧(不知道为什么是关键帧请自行谷歌)
//I帧
8060001c20f18e507755a8c07c85//最后的0x85,代表I帧的第一个rtp包
80e0004520f18e507755a8c07c45//最后的0x45,代表I帧的最后一个rtp包

//P帧
8060001c20f18e507755a8c07c81//最后的0x81,代表P帧的第一个rtp包
80e0004520f18e507755a8c07c41//最后的0x41,代表P帧的最后一个rtp包

代码实现

此代码仅供参考,我也是从网上摘抄。

void camera_rtsp::open_rtsp(const char *rtsp)
{

    char buf[CMD_LEN];
    char rbuf[RECV_LEN];

    //tcp 用于连接rtsp协议
    int sock_fd=socket(AF_INET,SOCK_STREAM,0);
    if(sock_fd==-1)
    {
        printf("create the socket failed.\n");
        return;
    }

    //定义sockaddr_in
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr("192.168.85.119");
    servaddr.sin_port = htons(554);  //服务器端口

    if(connect(sock_fd,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
    {
        printf("connect the server failed.\n");
        close(sock_fd);
        return;
    }

    /***************** OPTIONS *************************/
    memset(buf,0,CMD_LEN);
    printf("3\n");
    sprintf(buf,GetRTSPCmd("OPTIONS"),RTSP_URL,1,"User-Agent: myrtsp");
    SendRTSPCmd(sock_fd, "OPTIONS",buf);
    ReadSocket(sock_fd,buf,CMD_LEN,10);

    /***************** DESCRIBE *************************/
    memset(buf,0,CMD_LEN);
    sprintf(buf,GetRTSPCmd("DESCRIBE"),RTSP_URL,"Accept: application/sdp", 2, "User-Agent: myrtsp");
    SendRTSPCmd(sock_fd, "DESCRIBE", buf);
    memset(buf,0,CMD_LEN);
    ReadSocket(sock_fd,buf,CMD_LEN,10);

    /***************** SETUP *************************/
    memset(buf,0,CMD_LEN);
    sprintf(buf,
            "SETUP rtsp://%s:%d/554\r\n"
            "CSeq: 1\r\n"
            "Transport: RTP/AVP/UDP;unicast;client_port=57104-57105\r\n\r\n",SERVER_IP,SERVER_PORT);
    SendRTSPCmd(sock_fd, "SETUP", buf);
    ReadSocket(sock_fd,buf,CMD_LEN,10);


    //get the session and server port
    char psession[50],pserverport[10];
    char* ptr;
    memset(psession,0,50);
    memset(pserverport,0,10);
    ptr=strstr(buf,"Session:");
    if(ptr==0)
    {
        printf("cannot find the session.\n");
        return 0;
    }
    memcpy(psession,ptr+strlen("Session:"),strstr(ptr,"\r\n")-ptr-strlen("Session:"));
    printf("Session:%s\n",psession);
    ptr=strstr(buf,"server_port=");
    memcpy(pserverport,ptr+strlen("server_port="),strstr(ptr,"-")-ptr-strlen("server_port="));
    printf("Server port: %s\n",pserverport);
    printf("\n");

    /***************** PLAY *************************/
    memset(buf,0,CMD_LEN);
    sprintf(buf,GetRTSPCmd("PLAY"),RTSP_URL,1,psession,"Range: npt=0.000-\r\n","","","");
    SendRTSPCmd(sock_fd, "PLAY", buf);
    ReadSocket(sock_fd,buf,CMD_LEN,10);

    /*    接收数据用udp       */
    struct sockaddr_in addr_stream;
    struct sockaddr_in addr_local;
    int recv_fd=socket(AF_INET,SOCK_DGRAM,0);
    if(recv_fd==-1)
    {
        printf("create the stream socket failed.\n");
        return 0;
    }

    addr_local.sin_family=AF_INET;
    // addr_local.sin_addr.s_addr=inet_addr("192.168.1.126");
    addr_local.sin_addr.s_addr=INADDR_ANY;
    //监听本地端口号
    addr_local.sin_port=htons(57104);
    int len_addr_local=sizeof(addr_local);
    if(bind(recv_fd,(struct sockaddr*)&addr_local,len_addr_local)==-1)
    {
        printf("bind the local stream socket failed.\n");
        return 0;
    }

    addr_stream.sin_family=AF_INET;
    addr_stream.sin_addr.s_addr=inet_addr(SERVER_IP);
    addr_stream.sin_port=htons(atoi(pserverport));
    int len_recv;
    int len_addr=sizeof(addr_stream);
    int count = 0;


    //    FILE *save_fd = fopen("/tmp/test.264", "wb");
    while(1)
    {
        memset(rbuf, 0, RECV_LEN);

        len_recv = recvfrom(recv_fd, rbuf, RECV_LEN, 0, (struct sockaddr*)&addr_stream, &len_addr);
        // printf("len_recv:%d\n", len_recv);
        if(len_recv == -1)
        {
            printf("recvfrom the stream failed.\n");
            return 0;
        }

        char *p = rbuf;
        char *data = h264_data;
        unsigned char frame_end = rbuf[1] >> 7;
        unsigned char frame_start_flag = rbuf[13] & 0x80;
        unsigned char frame_end_flag = rbuf[13] & 0x40;
        #if 0
        memcpy(data + h264_data_len, p + 14, len_recv - 14);
        h264_data_len += len_recv - 14;

        // printf("rbuf[1]:%x\n", rbuf[1]);
        // printf("rbuf[13]:%x\n", rbuf[13]);
        // printf("frame_end_flag:%x\n", frame_end_flag);
//        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 w_ret = fwrite(p + 14, len_recv - 14, 1, save_fd);
        if(frame_end_flag == 0x40)
        {
        //    printf("h264_data_len:%d\n", h264_data_len);
            _data_callback->push_data((uint8_t *)(h264_data), (int16_t)1920, (int16_t)1080, (int32_t)h264_data_len);
           int w_ret = fwrite(h264_data, h264_data_len, 1, save_fd);

            h264_data_len = 0;
            memset(h264_data, 0, 102400);
        }
        #else
        // printf("rbuf[12]:%x rbuf[13]:%x\n", rbuf[12], rbuf[13]);
        if((rbuf[12] == 0x7c) && (frame_end_flag == 0x40))//如果是尾就发送
        {
            memcpy(data + h264_data_len, p + 14, len_recv - 14);
            h264_data_len += len_recv - 14;
                    //    printf("pack h264_data_len:%d\n", h264_data_len);
            _data_callback->push_data((uint8_t *)(h264_data), (int16_t)1280, (int16_t)720, (int32_t)h264_data_len);
            // int w_ret = fwrite(h264_data, h264_data_len, 1, save_fd);

            h264_data_len = 0;
            memset(h264_data, 0, 102400);
            continue;
        }
        if(rbuf[13] == 0x81)  //P帧
        {
            h264_data[h264_data_len] = 0x00;
            h264_data[h264_data_len + 1] = 0x00;
            h264_data[h264_data_len + 2] = 0x00;
            h264_data[h264_data_len + 3] = 0x01;
            h264_data[h264_data_len + 4] = 0x61;
            h264_data_len = h264_data_len + 5;
        }

        if(rbuf[13] == 0x85)  //I帧
        {
            h264_data[h264_data_len] = 0x00;
            h264_data[h264_data_len + 1] = 0x00;
            h264_data[h264_data_len + 2] = 0x00;
            h264_data[h264_data_len + 3] = 0x01;
            h264_data[h264_data_len + 4] = 0x65;
            h264_data_len = h264_data_len + 5;
        }

        if((rbuf[12] == 0x67) && (rbuf[13] == 0x42))  //SPS帧
        {
            h264_data[h264_data_len] = 0x00;
            h264_data[h264_data_len + 1] = 0x00;
            h264_data[h264_data_len + 2] = 0x00;
            h264_data[h264_data_len + 3] = 0x01;
            h264_data[h264_data_len + 4] = 0x67;
            h264_data[h264_data_len + 5] = 0x42;
            h264_data_len = h264_data_len + 6;
        }

        if((rbuf[12] == 0x68) && (rbuf[13] == 0xce))  //PPS帧
        {
            h264_data[h264_data_len] = 0x00;
            h264_data[h264_data_len + 1] = 0x00;
            h264_data[h264_data_len + 2] = 0x00;
            h264_data[h264_data_len + 3] = 0x01;
            h264_data[h264_data_len + 4] = 0x68;
            h264_data[h264_data_len + 5] = 0xce;
            h264_data_len = h264_data_len + 6;
        }

        if((rbuf[12] == 0x06))  //补全后的SEI  补充增强信息
        {
            h264_data[h264_data_len] = 0x00;
            h264_data[h264_data_len + 1] = 0x00;
            h264_data[h264_data_len + 2] = 0x00;
            h264_data[h264_data_len + 3] = 0x01;
            h264_data[h264_data_len + 4] = rbuf[12];
            h264_data[h264_data_len + 5] = rbuf[13];
            h264_data_len = h264_data_len + 6;
        }

        memcpy(data + h264_data_len, p + 14, len_recv - 14);
        h264_data_len += len_recv - 14;
        #endif
    }

    return;
}

char * camera_rtsp::GetRTSPCmd( const char * szName)
{
 char *str = NULL;
 char const*  cmdFmt = NULL;
 if(!strcmp(szName, "OPTIONS"))
 {
  cmdFmt =
   "OPTIONS %s554 RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "%s"
   "\r\n";
 }
 else if(!strcmp(szName, "ANNOUNCE"))
 {
   cmdFmt =
   "ANNOUNCE %s RTSP/1.0\r\n"
   "CSeq: %d/r/n"
   "Content-Type: application/sdp\r\n"
   "%s"
   "Content-length: %d\r\n\r\n"
   "%s";
 }
 else if(!strcmp(szName, "PLAY"))
 {
   cmdFmt =
   "PLAY %s RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "Session: %s\r\n"
   "%s"
   "%s"
   "%s"
   "%s"
   "\r\n";

 }
 else if(!strcmp(szName, "PAUSE"))
 {
   cmdFmt =
   "PAUSE %s RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "Session: %s\r\n"
   "%s"
   "%s"
   "/r/n";
 }
 else if(!strcmp(szName, "RECORD"))
 {
  cmdFmt =
   "RECORD %s%s%s RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "Session: %s\r\n"
   "Range: npt=0-\r\n"
   "%s"
   "%s"
   "\r\n";
 }
 else if(!strcmp(szName, "SET_PARAMETER"))
 {
   cmdFmt =
   "SET_PARAMETER %s RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "Session: %s\r\n"
   "%s"
   "%s"
   "Content-length: %d\r\n\r\n"
   "%s: %s\r\n";
 }
 else if(!strcmp(szName, "GET_PARAMETER"))
 {
   cmdFmt =
   "GET_PARAMETER %s RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "Session: %s\r\n"
   "%s"
   "%s"
   "Content-type: text/parameters\r\n"
   "Content-length: %d\r\n\r\n"
   "%s\r\n";
 }
 else if(!strcmp(szName, "TEARDOWN"))
 {
   cmdFmt =
   "TEARDOWN %s RTSP/1.0\r\n"
   "CSeq: %d\r\n"
   "Session: %s\r\n"
   "%s"
   "%s"
   "\r\n";
 }
 else if(!strcmp(szName, "DESCRIBE"))
 {
   cmdFmt =
   "DESCRIBE %s554 RTSP/1.0\r\n"
   "%s\r\n"
   "CSeq: %d\r\n"
   "%s\r\n"
   "\r\n";
 }
 else if(!strcmp(szName, "ANNOUNCE"))
 {
   cmdFmt =
  "ANNOUNCE %s RTSP/1.0\r\n"
  "CSeq: %d\r\n"
  "Content-Type: application/sdp\r\n"
  "%s"
  "Content-length: %d\r\n\r\n"
  "%s";
 }
 else if(!strcmp(szName, "SETUP"))
 {
    cmdFmt =
    "%s"
    "CSeq: %d\r\n"
    "%s"
    "%s"
    "%s"
    "%s"
    "\r\n";
 }
 str = (char*)cmdFmt;
 return str;
}


long camera_rtsp::ReadSocket(int sock,  char *buf, int len, int timeout )
{
    long lret ;

    lret = recv(sock, buf, len, 0);
    if(lret == -1)
    {
        printf("recv failed.\n");
    }
    else if(lret > 0)
    {
        printf("recv:%s\n",buf);
    }
    else
    {
        printf("recv 0.\n");
    }

    return lret;
}

long camera_rtsp::SendRTSPCmd( int sock, const char *cmd, const char *szparam )
{
    long lret;
    int ilen;
    ilen = strlen(szparam);
    lret = send(sock, szparam, ilen,0);
//    lret = sendto(sock, szparam, ilen,0, (struct sockaddr* )&addr_server, sizeof(addr_server));
    if(lret == -1)
    {
        printf("send %s failed.\n",cmd);
    }
    else
    {
        printf("send:%d\n", lret);
        printf("send:\n%s\n",szparam);
    }
    return lret;
}

效果展示

最后附上效果图,在rv1126上收取了网络摄像头的rtsp流,硬件解码、编码为265、发送给上位机显示
rv1126上直接收取rtsp流


版权声明:本文为xian0gang原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。