当前位置: 首页 > 工具软件 > jrtplib > 使用案例 >

使用jrtplib库收发视频流

南门展
2023-12-01

 jrtplib开源库可以用于设备端发视频流, 也可用于服务器端收视频流,在使用的过程也有几项需要注意。
1. 编译使用环境
1.1 客户端:Cortex-A9 开发板,,g++交叉编译,插上摄像头。
       这里jrtplib需要交叉编译,前面文章有讲,且编译的时候,要将服务端和客户端都设置为相同的大端或者小端,默认的是采用大端模式,那么我这里约定使用大端模式,这点需要特别注意,否则对应不上,收发不成功。具体怎么配置,参见cmake文档。
如果漏掉配置了,就手动在rtpconfig.h 加一句:
#define RTP_BIG_ENDIAN
     
1.2 服务端:Windows 10,vc++ 2012。
    jrtplib编译成静态库,文件名:jrtplib_d.lib
    另外,同样也需要编译成大端模式,在rtpconfig.h中加上:
    #define RTP_BIG_ENDIAN
 
       这里说一下,我是如何发现这个问题的呢?其实也花费了我一点精力,这就是开源的好处,可以深入代码调试,如果不是开源的,估计就耗死在这里,在此再一次佩服开源的伟大。我们做开发,不是只知道会用就行了,重要的是要知道怎么分析问题解决问题。
    下面就深入代码分析一下:
    找到解析rtp包的地方:rtppacket.cpp,找到函数:int RTPPacket::ParseRawPacket(RTPRawPacket &rawpack),
    这其中有如下一段代码:
    packetbytes = (uint8_t *)rawpack.GetData();
    rtpheader = (RTPHeader *)packetbytes;
    
    // The version number should be correct
    if (rtpheader->version != RTP_VERSION)
        return ERR_RTP_PACKET_INVALIDPACKET;    
    在这儿判断版本号:rtpheader->version
    我一看这儿rtpheader->version==0,而RTP_VERSION是2,再看rtpheader的内存内容:
    0x00BEB7F8  02 c1 5e 58 9c b3 ff d3 61 a8 24 ec ff d8 ff ...
    0x00BEB7F8为rtpheader的内存地址,这样来看,明明头部是等于2呀,为什么解析出来不对呢?
    再看看头的定义:
    struct RTPHeader
    {
    #ifdef RTP_BIG_ENDIAN
        uint8_t version:2;
        uint8_t padding:1;
        uint8_t extension:1;
        uint8_t csrccount:4;
        
        uint8_t marker:1;
        uint8_t payloadtype:7;
    #else // little endian
        uint8_t csrccount:4;
        uint8_t extension:1;
        uint8_t padding:1;
        uint8_t version:2;
        
        uint8_t payloadtype:7;
        uint8_t marker:1;
    #endif // RTP_BIG_ENDIAN
        
        uint16_t sequencenumber;
        uint32_t timestamp;
        uint32_t ssrc;
    };
    
    看这儿,如果是RTP_BIG_ENDIAN,数就不就对了吗?这样问题解决。
    
2. 客户端发送视频流
       视频流是抓取的MPEG4数据,所以动态图片比较大。这里需要说明的是,jrtplib不会帮你分包,一次发送的数据包不能超过大约1440字节,最好比1440字节小一点,否则认为数据包太大,发送失败。所以只能自己分包发送,其它方法暂时没有试过。另外一点就是需要特别注意,既然自己分包,那么如何表示开始,如何表示结束呢?这里需要用到头部的marker字段,对于marker的定义,是这么说的:对于视频,标记一帧的结束;对于音频,标记会话的开始。
下面是客户端关键代码:

    /*Create rtp session*/
    RTPSession rtpsess;
    unsigned long server_ip = inet_addr("192.168.1.7");  //服务端地址
    
    //convert the ip address to be network byte order.
    server_ip = htonl(server_ip);
    
    //here port is not required.
    unsigned short port = 8888;  //服务端接收地址,注意,必须是偶数,因为靠下一位奇数端口(这儿是8889)为rtcp使用。
    
    int port_base = 6000;//本端地址,也必须是偶数,6001为rtcp使用
    RTPSessionParams rtppara;
    rtppara.SetOwnTimestampUnit(1.0/10.0);    
    rtppara.SetAcceptOwnPackets(true);
    
    //这个参数好像与分包发送的最大包没有关系,但是最大不能超过65535.
    rtppara.SetMaximumPacketSize(60000+128);   
    
    
    RTPUDPv4TransmissionParams transparams;
    transparams.SetPortbase(port_base);
    
    int status = rtpsess.Create(rtppara,&transparams);
    checkerror(status);
    
    RTPIPv4Address rtpaddr(server_ip,port);    
    status = rtpsess.AddDestination(rtpaddr);
    checkerror(status);
    
    //Add here 2 statements can cause sending data to be failing.
    rtpsess.SetDefaultPayloadType(96);
    
    /*这里设置为true或false都是没有关系的,但是必须要调用,和后面marker没有关系,
    看定义:
    inline int RTPPacketBuilder::SetDefaultMark(bool m)
    {
        if (!init)
            return ERR_RTP_PACKBUILD_NOTINIT;
        defmarkset = true;
        defaultmark = m;
        return 0;
    }
    
    如果调用了这个函数,defmarkset为置为true,这和后面的组包是有关系的,看组包定义:
    int RTPPacketBuilder::BuildPacket(const void *data,size_t len)
    {
        if (!init)
            return ERR_RTP_PACKBUILD_NOTINIT;
        if (!defptset)
            return ERR_RTP_PACKBUILD_DEFAULTPAYLOADTYPENOTSET;
        if (!defmarkset)
            return ERR_RTP_PACKBUILD_DEFAULTMARKNOTSET;  //这儿,未定义直接返回错误了。
        if (!deftsset)
            return ERR_RTP_PACKBUILD_DEFAULTTSINCNOTSET;
        return PrivateBuildPacket(data,len,defaultpayloadtype,defaultmark,defaulttimestampinc,false);
    }
    
    */
       rtpsess.SetDefaultMark(false);  
    unsigned char databuffer[60000] = {0};    
    rtpsess.SetDefaultTimestampIncrement(10);
    for(;;)
    {
        if(count>500)//抓满500张退出
        {
            break;
        }
         
            //抓取视频,MPEG4格式
            ret = avobj.GetFrame(&parameter);                
            //split buffer into many small packets.
            int packsize = 1000; //一包1000字节
            int leftsize = parameter.v4l2Info.length%packsize;   //整包后余下的字节
            int intcnt = parameter.v4l2Info.length/packsize;    //整数包数               
        
            //begin to send a frame 
            for(int i=0;i<intcnt;i++)
            {
                memset((void*)databuffer,0,packsize);
                memcpy((void*)databuffer,((unsigned char*)parameter.v4l2Info.buffer)+i*packsize,packsize);    

                //这儿是我的图片尾部是空白,其实数据没有这么大,如果下一包头部全是0,其实图片数据已经结束了,应该
                //告诉服务端我已发完一帧图片
                if(databuffer[0] == 0 && databuffer[1] == 0 && databuffer[2] == 0) 
                {                
                    //this is the last frame,the marker must be true.
                    //注意这儿的第4个参数,true表示发完一帧图片了。
                    status = rtpsess.SendPacket((void*)databuffer,packsize,96,true,10);
                    checkerror(status);                    
                    break;
                }
                else
                {
                    //print(databuffer,packsize);
                    //pay attention to the marker,here it must be false.
                    
                    //注意这儿的第4个参数,false表示未发完一帧,还会继续发数据体。
                    status = rtpsess.SendPacket((void*)databuffer,packsize,96,false,10);    
                    checkerror(status);        
                }
            }
            
            //send left data
            printf("the left size is:%d\n",leftsize);
            if(leftsize>0)
            {
                memset((void*)databuffer,0,packsize);
                memcpy((void*)databuffer,((unsigned char*)parameter.v4l2Info.buffer)+packsize*intcnt,leftsize);
                if(databuffer[0] != 0 && databuffer[1] != 0 && databuffer[2] != 0)
                {
                    
                    //this is the last frame,the marker must be true.
                    status = rtpsess.SendPacket((void*)databuffer,leftsize,96,true,10);
                    checkerror(status);        
                }
                else
                {
                    printf("the last data buffer is 0\n");                    
                }
            }
            RTPTime::Wait(RTPTime(0,100));    
        }        
    
        count++;
    }
    
3. 服务端接收视频流
   服务端接收关键代码,放在一个线程中执行:
   UINT CVideoMonitorDlg::RTPServerProc(LPVOID lParam)
   {
        CVideoMonitorDlg* pThis = (CVideoMonitorDlg*)lParam;

        RTPSession sess;
        uint16_t portbase = 8888;
        int status;
        bool done = false;

        RTPUDPv4TransmissionParams transparams;
        RTPSessionParams sessparams;
        sessparams.SetOwnTimestampUnit(1.0 / 10.0);

        sessparams.SetAcceptOwnPackets(true);

        transparams.SetPortbase(portbase);
        status = sess.Create(sessparams, &transparams);
        pThis->checkerror(status);

        sess.SetDefaultTimestampIncrement(10);
        sess.SetDefaultMark(true);

        int count = 1;
        //sess.BeginDataAccess();
        RTPTime delay(0.020);
        RTPTime starttime = RTPTime::CurrentTime();
        
        //打开视频存储文件,这里我存储为avi文件。
        pThis->OpenVideoFile();

        while (!done)
        {
            //如果没有使用jthread,那就要调用这个函数,这里我不使用jthread.
            //这一步,其实质是从socket取出数据并放入缓冲区。
            status = sess.Poll();
            pThis->checkerror(status);
            
            //这里是加锁
            sess.BeginDataAccess();

            //开始处理缓冲区中的数据
            if (sess.GotoFirstSourceWithData())
            {
                do
                {
                    RTPPacket *pack;

                    while ((pack = sess.GetNextPacket()) != NULL)
                    {        
                        //取出数据加入我的缓存区                    
                        pThis->AddDataPacket(pack->GetPayloadData(),pack->GetPayloadLength());
                        
                        //注意了,这儿就是前面的SendPacket的第4个参数,
                        bool hasmarker = pack->HasMarker();

                        if(hasmarker)
                        {
                            //表示收完一帧图片,写入文件
                            pThis->WriteVideoFile(count);                        

                            count++;
                        }
                        sess.DeletePacket(pack);
                    }
                
                } while (sess.GotoNextSourceWithData());
            }

            sess.EndDataAccess();

            RTPTime::Wait(delay);
            RTPTime t = RTPTime::CurrentTime();
            t -= starttime;
            if (t > RTPTime(60000.0))
                done = true;

            if(count>490)
            {
                //收完490帧图片,关闭视频文件.
                pThis->CloseVideoFile();        
            }
        }
        delay = RTPTime(10.0);
        sess.BYEDestroy(delay, 0, 0);
        
        return 0;
    }
    
    经过实测,接收存储的avi文件可以播放。


  

 类似资料: