一、RTP 是进行实时流媒体传输的标准协议和关键技术
实时传输协议(Real-time Transport Protocol,PRT)是在 Internet 上处理多媒体数据流的一种网络协议,利用它能够在一对一(unicast,单播)或者一对多(multicast,多播)的网络环境中实现传流媒体数据的实时传输。RTP 通常使用 UDP 来进行多媒体数据的传输,但如果需要的话可以使用 TCP 或者 ATM 等其它协议。
协议分析 :每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前 12 个字节的含义是固定的,而负载则可以是音频或者视频数据。
RTP 是目前解决流媒体实时传输问题的最好办法,要在 Linux 平台上进行实时传送编程,可以考虑使用一些开放源代码的 RTP 库,如 LIBRTP、JRTPLIB 等。JRTPLIB 是一个面向对象的 RTP 库,它完全遵循 RFC 1889 设计,在很多场合下是一个非常不错的选择。JRTPLIB 是一个用 C++ 语言实现的 RTP 库,这个库使用socket 机制实现网络通讯 因此可以运行在 Windows、Linux、FreeBSD、Solaris、Unix和VxWorks 等多种操作系统上。
嵌入式版的环境搭建
嵌入式版上的环境搭建和PC机上有些不同,如不注意可能导致两个库都不能使用。
首先,必须先安装jthread库,再安装jrtplib库;其次,要交叉编译,需修改configure文件。具体步骤为:
[root@linuxgam src]# cd jthread-1.2.1
[root@linuxgam jthread-1.2.1]# ./configure -host=arm-linux –prefix=/usr/local/arm/2.95.3
[root@linuxgam src]# cd jrtplib-3.7.1
[root@linuxgam jrtplib-3.7.1]# ./configure -host=arm-linux -prefix=/usr/local/arm/2.95.3
二、JRTPLIB 库的使用方法及程序实现
a、在使用 JRTPLIB 进行实时流媒体数据传输之前,首先应该生成 RTPSession 类的一个实例来表示此次 RTP 会话,然后调用 Create() 方法来对其进行初始化操作。RTPSession 类的 Create() 方法只有一个参数,用来指明此次 RTP 会话所采用的端口号。
RTPSession sess;
RTPUDPv4TransmissionParams transparams;
RTPSessionParams sessparams;
// 创建RTP会话 sessparams.SetAcceptOwnPackets(true); transparams.SetPortbase(portbase); status = sess.Create(sessparams, &transparams); checkerror(status);
//函数原型 jrtplib-3.7.1/src/rtpsession.cpp(151) int RTPSession::Create(const RTPSessionParams &sessparams, RTPTransmitter *transmitter) { int status; if (created) return ERR_RTP_SESSION_ALREADYCREATED; usingpollthread = sessparams.IsUsingPollThread(); useSR_BYEifpossible = sessparams.GetSenderReportForBYE(); sentpackets = false; // Check max packet size if ((maxpacksize = sessparams.GetMaximumPacketSize()) < RTP_MINPACKETSIZE) return ERR_RTP_SESSION_MAXPACKETSIZETOOSMALL; rtptrans = transmitter; if ((status = rtptrans->SetMaximumPacketSize(maxpacksize)) < 0) return status; deletetransmitter = false; return InternalCreate(sessparams); }
b、设置恰当的时戳单元,是 RTP 会话初始化过程所要进行的另外一项重要工作,这是通过调用 RTPSession 类的 SetTimestampUnit() 方法来实现的,该方法同样也只有一个参数,表示的是以秒为单元的时戳单元。
sessparams.SetOwnTimestampUnit(1.0/8000.0);
int RTPSession::SetTimestampUnit(double u) { if (!created) return ERR_RTP_SESSION_NOTCREATED; int status; BUILDER_LOCK status = rtcpbuilder.SetTimestampUnit(u); BUILDER_UNLOCK return status; }
c、当 RTP 会话成功建立起来之后,接下去就可以开始进行流媒体数据的实时传输了。首先需要设置好数据发送的目标地址,RTP 协议允许同一会话存在多个目标地址,这可以通过调用 RTPSession 类的 AddDestination()、DeleteDestination() 和 ClearDestinations() 方法来完成。
// 指定RTP数据接收端
uint16_t portbase, destport;
uint32_t destip;
RTPIPv4Address addr(destip, destport);
status = sess.AddDestination(addr);
checkerror(status);
d、对于同一个RTP会话来讲,负载类型、标识和时戳增量通常来讲都是相同的,JRTPLIB允许将它们设置为会话的默认参数,这是通过调用 RTPSession类的SetDefaultPayloadType()、SetDefaultMark()和 SetDefaultTimeStampIncrement()方法来完成的。为RTP会话设置这些默认参数的好处是可以简化数据的发送,例如,如果为 RTP会话设置了默认参数:
// 设置RTP会话默认参数
sess.SetDefaultPayloadType(96);
sess.SetDefaultMark(false);
sess.SetDefaultTimestampIncrement(160);
//函数原型 jrtplib-3.7.1/src/rtppacketbuilder.h(215) inline int RTPPacketBuilder::SetDefaultPayloadType(uint8_t pt) { if (!init) return ERR_RTP_PACKBUILD_NOTINIT; defptset = true; defaultpayloadtype = pt; return 0; } inline int RTPPacketBuilder::SetDefaultMark(bool m) { if (!init) return ERR_RTP_PACKBUILD_NOTINIT; defmarkset = true; defaultmark = m; return 0; } inline int RTPPacketBuilder::SetDefaultTimestampIncrement(uint32_t timestampinc) { if (!init) return ERR_RTP_PACKBUILD_NOTINIT; deftsset = true; defaulttimestampinc = timestampinc; return 0; }
e、之后就可以调用 RTPSession 类的 SendPacket() 方法向所有的目标地址发送流媒体数据,进行数据发送时只需指明要发送的数据及其长度就可以了(SendPacket()最典型的用法是类似于下面的语句,其中第一个参数是要被发送的数据,而第二个参数则指明将要发送数据的长度,再往后依次是RTP负载类型、标识和时戳增量。至少要用到前两个参数):
status = sess.SendPacket(send_buf, sizeof(send_buf), 0, false, 10);
checkerror(status);
函数原型 jrtplib-3.7.1/src/rtpsession.cpp(552) int RTPSession::SendPacket(const void *data,size_t len) { int status; if (!created) return ERR_RTP_SESSION_NOTCREATED; BUILDER_LOCK if ((status = packetbuilder.BuildPacket(data,len)) < 0) { BUILDER_UNLOCK return status; } if ((status = rtptrans->SendRTPData(packetbuilder.GetPacket(),packetbuilder.GetPacketLength())) < 0) { BUILDER_UNLOCK return status; } BUILDER_UNLOCK SOURCES_LOCK sources.SentRTPPacket(); SOURCES_UNLOCK PACKSENT_LOCK sentpackets = true; PACKSENT_UNLOCK return 0; } int RTPSession::SendPacket(const void *data,size_t len, uint8_t pt,bool mark,uint32_t timestampinc) { int status; if (!created) return ERR_RTP_SESSION_NOTCREATED; BUILDER_LOCK if ((status = packetbuilder.BuildPacket(data,len,pt,mark,timestampinc)) < 0) { BUILDER_UNLOCK return status; } if ((status = rtptrans->SendRTPData(packetbuilder.GetPacket(),packetbuilder.GetPacketLength())) < 0) { BUILDER_UNLOCK return status; } BUILDER_UNLOCK SOURCES_LOCK sources.SentRTPPacket(); SOURCES_UNLOCK PACKSENT_LOCK sentpackets = true; PACKSENT_UNLOCK return 0; } int RTPSession::SendPacketEx(const void *data,size_t len, uint16_t hdrextID,const void *hdrextdata,size_t numhdrextwords) { int status; if (!created) return ERR_RTP_SESSION_NOTCREATED; BUILDER_LOCK if ((status = packetbuilder.BuildPacketEx(data,len,hdrextID,hdrextdata,numhdrextwords)) < 0) { BUILDER_UNLOCK return status; } if ((status = rtptrans->SendRTPData(packetbuilder.GetPacket(),packetbuilder.GetPacketLength())) < 0) { BUILDER_UNLOCK return status; } BUILDER_UNLOCK SOURCES_LOCK sources.SentRTPPacket(); SOURCES_UNLOCK PACKSENT_LOCK sentpackets = true; PACKSENT_UNLOCK return 0; } int RTPSession::SendPacketEx(const void *data,size_t len, uint8_t pt,bool mark,uint32_t timestampinc, uint16_t hdrextID,const void *hdrextdata,size_t numhdrextwords) { int status; if (!created) return ERR_RTP_SESSION_NOTCREATED; BUILDER_LOCK if ((status = packetbuilder.BuildPacketEx(data,len,pt,mark,timestampinc,hdrextID,hdrextdata,numhdrextwords)) < 0) { BUILDER_UNLOCK return status; } if ((status = rtptrans->SendRTPData(packetbuilder.GetPacket(),packetbuilder.GetPacketLength())) < 0) { BUILDER_UNLOCK return status; } BUILDER_UNLOCK SOURCES_LOCK sources.SentRTPPacket(); SOURCES_UNLOCK PACKSENT_LOCK sentpackets = true; PACKSENT_UNLOCK return 0; }
f、接收实时流媒体数据,同 一个RTP 会话中允许有多个参与者( 源),通 过调用GotoFirstSourceWithData()和 GotoNextSourceWithData()函数来实现源的遍历,通过 GetNextPacket()函数提取 RTP 数据包。数据包接受以RTPSession的成员函数BeginDataAccess开始、EndDataAccess结束,BeginDataAccess确保轮询(poll)线程不会在这期间访问source table 。EndDataAccess 调用完成后,轮询(poll)线程会得到锁而继续访问。
sess.BeginDataAccess();
// check incoming packets if (sess.GotoFirstSourceWithData()) { do { RTPPacket *pack; uint8_t *data; size_t length; while ((pack = sess.GetNextPacket()) != NULL) { data = pack->GetPayloadData(); length = pack->GetPayloadLength(); printf("Got %d-%d: %d/%d:%s Length:%d/n", portbase, destport, i, num, data, length); sess.DeletePacket(pack); } } while (sess.GotoNextSourceWithData()); } sess.EndDataAccess();
下面的函数要在BeginDataAccess 和EndDataAccess之间被调用:
int RTPSession::BeginDataAccess() {
if (!created)
return ERR_RTP_SESSION_NOTCREATED;
SOURCES_LOCK
return 0;
}
int RTPSession::EndDataAccess() {
if (!created)
return ERR_RTP_SESSION_NOTCREATED;
SOURCES_UNLOCK
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////Start
• bool GotoFirstSource()
开始递归参与者的第一个流,如果找到了,就返回tree,否则返回false。ps:我们通过这个函数和下面的GotoNextSource遍历source table中的每一个source。
bool RTPSources::GotoFirstSource(){
sourcelist.GotoFirstElement();
if (sourcelist.HasCurrentElement())
return true;
return false;
}
• bool GotoNextSource()
设置当前的源(source)为source table中的下一个源。如果已经到尾部了就返回false.
bool RTPSources::GotoNextSource(){
sourcelist.GotoNextElement();
if (sourcelist.HasCurrentElement())
return true;
return false;
}
• bool GotoPreviousSource()
设置当前的源(source)为source table中上一个源。如果已经到头部了就返回false.
bool RTPSources::GotoPreviousSource(){ sourcelist.GotoPreviousElement(); if (sourcelist.HasCurrentElement()) return true; return false; }
• bool GotoFirstSourceWithData()
开始递归参与者中第一个有RTP数据的流,如果找到了,就返回tree,否则返回false。PS:在接收数据是我们常用的是这套函数,因为如果没有数据要来都没用。
bool RTPSources::GotoFirstSourceWithData(){
bool found = false;
sourcelist.GotoFirstElement();
while (!found && sourcelist.HasCurrentElement()){
RTPInternalSourceData *srcdat;
srcdat = sourcelist.GetCurrentElement();
if (srcdat->HasData())
found = true;
else
sourcelist.GotoNextElement();
}
return found;
}
• bool GotoNextSourceWithData()
设置当前的源(source)为source table中有RTP数据的下一个源。如果已经到尾部了就返回false.
bool RTPSources::GotoNextSourceWithData(){
bool found = false;
sourcelist.GotoNextElement();
while (!found && sourcelist.HasCurrentElement()){
RTPInternalSourceData *srcdat;
srcdat = sourcelist.GetCurrentElement();
if (srcdat->HasData())
found = true;
else
sourcelist.GotoNextElement();
}
return found;
}
• bool GotoPreviousSourceWithData()
设置当前的源(source)为source table中有RTP数据的上一个源。如果已经到头部了就返回false.
bool RTPSources::GotoPreviousSourceWithData(){
bool found = false;
sourcelist.GotoPreviousElement();
while (!found && sourcelist.HasCurrentElement()){
RTPInternalSourceData *srcdat;
srcdat = sourcelist.GetCurrentElement();
if (srcdat->HasData())
found = true;
else
sourcelist.GotoNextElement();
}
return found;
}
• RTPSourceData *GetCurrentSourceInfo()
返回当前参与者的当前源(source)的RTPSourceData 实列。ps:返回的这个RTPSourceData 就是本进程从期它参与者的RTCP数据包中收集得到的信息,对我们来说其实很有用,只是作者的例程没有用上,国内的网络也没有提到。在RFC3550中有关RTCP的东西都在这了,看过RFC3550的人都知到,里头谈得最多的就是RTCP。这个类我们以后会专门说。
RTPSourceData *RTPSources::GetCurrentSourceInfo(){
if (!sourcelist.HasCurrentElement())
return 0;
return sourcelist.GetCurrentElement();
}
• RTPSourceData *GetSourceInfo(uint32 t ssrc)
返回由ssrc指定的RTPSourceData ,或都NULL(当这个条目不存在)。ps:这个函数也很有用。因为GetCurrentSourceInfo只有在GotoFirstSource等上下文当中才能用。如果我们是在RTPSource子类的成员函数中,我们没有这个上下文,就只能用这个函数。
RTPSourceData *RTPSources::GetSourceInfo(uint32_t ssrc){
if (sourcelist.GotoElement(ssrc) < 0)
return 0;
if (!sourcelist.HasCurrentElement())
return 0;
return sourcelist.GetCurrentElement();
}
• RTPPacket *GetNextPacket()
得到当前参与者当前媒体流的下一个RTP数据包。
inline RTPPacket *RTPSourceData::GetNextPacket() { if (!validated) return 0; RTPPacket *p; if (packetlist.empty()) return 0; p = *(packetlist.begin()); packetlist.pop_front(); return p; } ///// RTPPacket *RTPSources::GetNextPacket() { if (!sourcelist.HasCurrentElement()) return 0; RTPInternalSourceData *srcdat = sourcelist.GetCurrentElement(); RTPPacket *pack = srcdat->GetNextPacket(); return pack; } // RTPRawPacket *RTPUDPv4Transmitter::GetNextPacket() { if (!init) return 0; MAINMUTEX_LOCK RTPRawPacket *p; if (!created) { MAINMUTEX_UNLOCK return 0; } if (rawpacketlist.empty()) { MAINMUTEX_UNLOCK return 0; } p = *(rawpacketlist.begin()); rawpacketlist.pop_front(); MAINMUTEX_UNLOCK return p; } // RTPRawPacket *RTPUDPv6Transmitter::GetNextPacket() { if (!init) return 0; MAINMUTEX_LOCK RTPRawPacket *p; if (!created) { MAINMUTEX_UNLOCK return 0; } if (rawpacketlist.empty()) { MAINMUTEX_UNLOCK return 0; } p = *(rawpacketlist.begin()); rawpacketlist.pop_front(); MAINMUTEX_UNLOCK return p; }
/////////////////////////////////////////////////////////////////////////////////////////End
g、数据传输结束,释放资源
//释放资源最多等待十秒超时,然后释放所有占有资源
sess.BYEDestroy(RTPTime(10,0),0,0);
三、上面代码和媒体流没关,就是example里就发发一些数字把JRTPLIB测试过而已。