1. 程式人生 > >基於Jrtplib的流媒體技術解析

基於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埠了。