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(¶meter);
//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文件可以播放。