基於Jrtplib的流媒體技術解析
3.3 資料傳送
當RTP 會話成功建立起來之後,接下去就可以開始進行流媒體資料的實時傳輸了。首先需要設定好資料傳送的目標地址, RTP協議允許同一會話存在多個目標地址,這可以通過呼叫RTPSession類的AddDestination()、 DeleteDestination()和ClearDestinations()方法來完成。例如,下面的語句表示的是讓RTP會話將資料傳送到本地主機的6000埠(注意:如果是需要發到另一個NAT裝置後面終端,則需要通過NAT穿透,見後):
unsigned long addr = ntohl(inet_addr("127.0.0.1"));
sess.AddDestination(addr, 6000);
目標地址全部指定之後,接著就可以呼叫RTPSession類的SendPacket()方法,向所有的目標地址傳送流媒體資料。SendPacket()是RTPSession類提供的一個過載函式,它具有下列多種形式:
int SendPacket(void *data,int len)
int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc)
int SendPacket(void *data,int len,unsigned short hdrextID,void *hdrextdata,int numhdrextwords)
int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc,
unsigned short hdrextID,void *hdrextdata,int numhdrextwords)
SendPacket()最典型的用法是類似於下面的語句,其中第一個引數是要被髮送的資料,而第二個引數則指明將要傳送資料的長度,再往後依次是RTP負載型別、標識和時戳增量。
sess.SendPacket(buffer, 5, 0, false, 10);
對於同一個RTP會話來講,負載型別、標識和時戳增量通常來講都是相同的,JRTPLIB允許將它們設定為會話的預設引數,這是通過呼叫 RTPSession類的SetDefaultPayloadType()、SetDefaultMark()和 SetDefaultTimeStampIncrement()方法來完成的。為RTP會話設定這些預設引數的好處是可以簡化資料的傳送,例如,如果為 RTP會話設定了預設引數:
sess.SetDefaultPayloadType(0);
sess.SetDefaultMark(false);
sess.SetDefaultTimeStampIncrement(10);
之後在進行資料傳送時只需指明要傳送的資料及其長度就可以了:
sess.SendPacket(buffer, 5);
3.4 資料接收
對於流媒體資料的接收端,首先需要呼叫RTPSession類的PollData()方法來接收發送過來的RTP或者 RTCP資料報。由於同一個RTP會話中允許有多個參與者(源),你既可以通過呼叫RTPSession類的GotoFirstSource()和 GotoNextSource()方法來遍歷所有的源,也可以通過呼叫RTPSession類的GotoFirstSourceWithData()和 GotoNextSourceWithData()方法來遍歷那些攜帶有資料的源。在從RTP會話中檢測出有效的資料來源之後,接下去就可以呼叫 RTPSession類的GetNextPacket()方法從中抽取RTP資料報,當接收到的RTP資料報處理完之後,一定要記得及時釋放。下面的程式碼示範了該如何對接收到的RTP資料報進行處理:
if (sess.GotoFirstSourceWithData()) {
do {
RTPPacket *pack;
pack = sess.GetNextPacket();
// 處理接收到的資料
delete pack;
} while (sess.GotoNextSourceWithData());
}
JRTPLIB為RTP資料報定義了三種接收模式,其中每種接收模式都具體規定了哪些到達的RTP資料報將會被接受,而哪些到達的RTP資料報將會被拒絕。通過呼叫RTPSession類的SetReceiveMode()方法可以設定下列這些接收模式:
RECEIVEMODE_ALL 預設的接收模式,所有到達的RTP資料報都將被接受;
RECEIVEMODE_IGNORESOME 除了某些特定的傳送者之外,所有到達的RTP資料報都將被接受,而被拒絕的傳送者列表可以通過呼叫AddToIgnoreList()、DeleteFromIgnoreList()和ClearIgnoreList()方法來進行設定;
RECEIVEMODE_ACCEPTSOME 除了某些特定的傳送者之外,所有到達的RTP資料報都將被拒絕,而被接受的傳送者列表可以通過呼叫AddToAcceptList ()、DeleteFromAcceptList和ClearAcceptList ()方法來進行設定。
3.5 控制資訊
JRTPLIB 是一個高度封裝後的RTP庫,程式設計師在使用它時很多時候並不用關心RTCP資料報是如何被髮送和接收的,因為這些都可以由JRTPLIB自己來完成。只要 PollData()或者SendPacket()方法被成功呼叫,JRTPLIB就能夠自動對到達的 RTCP資料報進行處理,並且還會在需要的時候傳送RTCP資料報,從而能夠確保整個RTP會話過程的正確性。
而另一方面,通過呼叫RTPSession類提供的SetLocalName()、SetLocalEMail()、 SetLocalLocation()、SetLocalPhone()、SetLocalTool()和SetLocalNote()方法, JRTPLIB又允許程式設計師對RTP會話的控制資訊進行設定。所有這些方法在呼叫時都帶有兩個引數,其中第一個引數是一個char型的指標,指向將要被設定的資料;而第二個引數則是一個int型的數值,表明該資料中的前面多少個字元將會被使用。例如下面的語句可以被用來設定控制資訊中的電子郵件地址:
在RTP 會話過程中,不是所有的控制資訊都需要被髮送,通過呼叫RTPSession類提供的 EnableSendName()、EnableSendEMail()、EnableSendLocation()、EnableSendPhone ()、EnableSendTool()和EnableSendNote()方法,可以為當前RTP會話選擇將被髮送的控制資訊。
3.6 實際應用
最後通過一個簡單的流媒體傳送-接收例項,介紹如何利用JRTPLIB來進行實時流媒體的程式設計。清單3給出了資料傳送端的完整程式碼,它負責向用戶指定的IP地址和埠,不斷地傳送RTP資料包:
#include <stdio.h>
#include <string.h>
#include "rtpsession.h"
// 錯誤處理函式
void checkerror(int err)
{
if (err < 0) {
char* errstr = RTPGetErrorString(err);
printf("Error:%s\\n", errstr);
exit(-1);
}
}
int main(int argc, char** argv)
{
RTPSession sess;
unsigned long destip;
int destport;
int portbase = 6000;
int status, index;
char buffer[128];
if (argc != 3) {
printf("Usage: ./sender destip destport\\n");
return -1;
}
// 獲得接收端的IP地址和埠號
destip = inet_addr(argv[1]);
if (destip == INADDR_NONE) {
printf("Bad IP address specified.\\n");
return -1;
}
destip = ntohl(destip);
destport = atoi(argv[2]);
// 建立RTP會話
status = sess.Create(portbase);
checkerror(status);
// 指定RTP資料接收端
status = sess.AddDestination(destip, destport);
checkerror(status);
// 設定RTP會話預設引數
sess.SetDefaultPayloadType(0);
sess.SetDefaultMark(false);
sess.SetDefaultTimeStampIncrement(10);
// 傳送流媒體資料
index = 1;
do {
sprintf(buffer, "%d: RTP packet", index ++);
sess.SendPacket(buffer, strlen(buffer));
printf("Send packet !\\n");
} while(1);
return 0;
}
清單4則給出了資料接收端的完整程式碼,它負責從指定的埠不斷地讀取RTP資料包:
#include <stdio.h>
#include "rtpsession.h"
#include "rtppacket.h"
// 錯誤處理函式
void checkerror(int err)
{
if (err < 0) {
char* errstr = RTPGetErrorString(err);
printf("Error:%s\\n", errstr);
exit(-1);
}
}
int main(int argc, char** argv)
{
RTPSession sess;
int localport;
int status;
if (argc != 2) {
printf("Usage: ./sender localport\\n");
return -1;
}
// 獲得使用者指定的埠號
localport = atoi(argv[1]);
// 建立RTP會話
status = sess.Create(localport);
checkerror(status);
do {
// 接受RTP資料
status = sess.PollData();
// 檢索RTP資料來源
if (sess.GotoFirstSourceWithData()) {
do {
RTPPacket* packet;
// 獲取RTP資料報
while ((packet = sess.GetNextPacket()) != NULL) {
printf("Got packet !\\n");
// 刪除RTP資料報
delete packet;
}
} while (sess.GotoNextSourceWithData());
}
} while(1);
return 0;
}
隨著多媒體資料在Internet上所承擔的作用變得越來越重要,需要實時傳輸音訊和視訊等多媒體資料的場合也將變得越來越多,如IP電話、視訊點播、線上會議等。RTP是用來在Internet上進行實時流媒體傳輸的一種協議,目前已經被廣泛地應用在各種場合,JRTPLIB是一個面向物件的RTP封裝庫,利用它可以很方便地完成Linux平臺上的實時流媒體程式設計。
4 基於jrtplib的NAT穿透
4.1 NAT穿透的基礎只是
4.2 rtp/rtcp傳輸涉及到的NAT穿透
rtp/rtcp傳輸資料的時候,需要兩個埠支援。即rtp埠用於傳輸rtp資料,即傳輸的多媒體資料;rtcp埠用於傳輸rtcp控制協議資訊。rtp/rtcp協議預設的埠是rtcp port = rtp port + 1 。詳細的說,比如A終端和B終端之間通過rtp/rtcp進行通訊,
如上圖,
本地IP:PORT NAT對映後IP:PORT
UACA RTP的傳送和接收IP:PORT : 192.168.1.100:8000 61.144.174.230:1597
UACA RTCP的傳送和接收IP:PORT:192.168.1.100:8001 61.144.174.230:1602
UACB RTP的傳送和接收IP:PORT : 192.168.1.10:8000 61.144.174.12:8357
UACB RTCP的傳送和接收IP:PORT:192.168.1.10:8001 61.144.174.12:8420
上圖可以得到一下一些資訊:
(一) 本地埠 RTCP PORT = RTP PORT + 1;但是經過NAT對映之後也有可能滿足這個等式,但是並不一定有這個關係。
(二)在NAT裝置後面的終端的本地IP:PORT並不被NAT外的設定可知,也就無法通過終端的本地IP:PORT與之通訊。而必須通過NAT對映之後的公網IP:PORT作為目的地址進行通訊。
如上圖的終端A如果要傳送RTP資料到終端B,UACA傳送的目的地址只能是:61.144.174.12:8357。同理,UACB傳送RTP資料給UACA,目的地址只能是: 61.144.174.230:1597 。
(三)也許看到這裡,如何得到自己的外網IP:PORT呢?如何得到對方的外網IP:PORT呢?這就是NAT IP:PORT轉換和穿孔(puncture),下回分解。
4.3 NAT 地址轉換
如上所述,終端需要知道自己的外網IP:port,可以通過STUN、STUNT、TURN、Full Proxy等方式。這裡介紹通過STUN方式實現NAT穿透。
STUN: Simple Traversal of UDP Through NAT。即通過UDP對NAT進行穿透。
STUNT:Simple Traversal of UDP Through NATs and TCP too.可以通過TCP對NAT進行穿透。
STUN是一個標準協議,具體的協議內容網路上很多。在此不累述了。
為了通過STUN實現NAT穿透,得到自己的公網IP:PORT,必須有一個公網STUN伺服器,以及我們的客戶端必須支援STUN Client功能。STUN Client 通過UDP傳送一個request到STUN伺服器,該請求通過NAT裝置的時候會把資料報頭中的本地IP:PORT換成該本地IP:PORT對應的公網IP:PORT,STUN伺服器接收到該資料包後就可以把該公網IP:PORT 傳送給STUN Client。這樣我們就得到了自己的公網IP:PORT。這樣別的終端就可以把該公網IP:PORT最為傳送UDP資料的目的地址傳送UDP資料。
這是一款開源軟體。在客戶端中的主要函式是下面這個:
NatType
stunNatType( StunAddress4& dest, //in 公網STUN伺服器地址,如stun.xten.net
bool verbose, //in 除錯時是否輸出除錯資訊
bool* preservePort=0, //out if set, is return for if NAT preservers ports or not
bool* hairpin=0 , //out if set, is the return for if NAT will hairpin packetsNAT裝置是否支援迴環
int port=0, // in 本地測試埠port to use for the test, 0 to choose random port
StunAddress4* sAddr=0 // out NIC to use ,返回STUN返回的本地地址的公網IP:PORT
);
輸入StunAddress和測試埠port,得到本地IP:PORT對應的公網IP:PORT.
4.4 對jrtplib 的改造
jrtplib中對rtp rtcp埠的處理關係是:rtcp port = rtp port + 1 。這就有問題,本地埠可以按照這個等式來設定埠,但是經過NAT對映之後的公網埠是隨機的,有可能並不滿足上述等式。
int portbase = 6000; //設定本地rtp埠為6000
transparams.SetPortbase(portbase);//預設的本地rtcp埠為6001.因為這裡是本地設定,所一這樣設定OK
status = sess.Create(sessparams,&transparams);
checkerror(status);
RTPIPv4Address addr(destip,destport); //設定目的地的rtp接收IP:PORT,公網傳輸的話就要設定為對方的rtp公網IP:PORT
// AddDestination()的內部處理是把addr.ip和addr.port+1賦給rtcp。這樣如果對方在公網上,就有問題了。因為對方的rtcp port 可能不等於rtp port +1;這就導致對方收不到rtcp資料包。
status = sess.AddDestination(addr);
通過跟蹤AddDestination()函式的實現,發現在class RTPIPv4Destination的建構函式中是這樣構造一個傳送目的地址的:
RTPIPv4Destination(uint32_t ip,uint16_t rtpportbase)
{
memset(&rtpaddr,0,sizeof(struct sockaddr_in));
memset(&rtcpaddr,0,sizeof(struct sockaddr_in));
rtpaddr.sin_family = AF_INET;
rtpaddr.sin_port = htons(rtpportbase);
rtpaddr.sin_addr.s_addr = htonl(ip);
rtcpaddr.sin_family = AF_INET;
rtcpaddr.sin_port = htons(rtpportbase+1);//預設把rtp的埠+1賦給目的rtcp埠。
rtcpaddr.sin_addr.s_addr = htonl(ip);
RTPIPv4Destination::ip = ip;
}
為了實現:可以自定義目的IP地址和目的rtp port和rtcp port。為了實現這麼目標,自己動手改造下面幾個函式:建構函式RTPIPv4Destination() 、RTPSession::AddDestination(),思路是在目的地址設定相關函式中增加一個rtcp ip 和port引數。
RTPIPv4Destination(uint32_t ip,uint16_t rtpportbase,uint32_t rtcpip,uint16_t rtcpport)
{
memset(&rtpaddr,0,sizeof(struct sockaddr_in));
memset(&rtcpaddr,0,sizeof(struct sockaddr_in));
rtpaddr.sin_family = AF_INET;
rtpaddr.sin_port = htons(rtpportbase);
rtpaddr.sin_addr.s_addr = htonl(ip);
/**If rtcport has not been set separately, use the default rtcpport*/
if ( 0 == rtcpport )
{
rtcpaddr.sin_family = AF_INET;
rtcpaddr.sin_port = htons(rtpportbase+1);
rtcpaddr.sin_addr.s_addr = htonl(ip);
}else
{
rtcpaddr.sin_family = AF_INET;
rtcpaddr.sin_port = htons(rtcpport);
rtcpaddr.sin_addr.s_addr = htonl(ip);
}
RTPIPv4Destination::ip = ip;
}
int RTPSession::AddDestination(const RTPAddress &addr,const RTPIPv4Address &rtcpaddr)
{
if (!created)
return ERR_RTP_SESSION_NOTCREATED;
return rtptrans->AddDestination(addr,rtcpaddr);
}
在呼叫RTPSession::AddDestination、定義RTPIPv4Destination的時候實參也相應增加目的rtcp引數。
這樣改造之後就可以自定義獨立的設定目的地址rtp ,rtcp埠了。