流媒體指的是在網絡中使用流技術傳輸的連續時基媒體,其特點是在播放前不需要下載整個文件,而是采用邊下載邊播放的方式,它是視頻會議、 IP電話等應用場合的技術基礎。RTP是進行實時流媒體傳輸的標准協議和關鍵技術,本文介紹如何在Linux下利用JRTPLIB進行實時流媒體編程。
隨着Internet的日益普及,在網絡上傳輸的數據已經不再局限於文字和圖形,而是逐漸向聲音和視頻等多媒體格式過渡。目前在網絡上傳輸音頻/視頻 (Audio/Video,簡稱A/V)等多媒體文件時,基本上只有下載和流式傳輸兩種選擇。通常說來,A/V文件占據的存儲空間都比較大,在帶寬受限的 網絡環境中下載可能要耗費數分鍾甚至數小時,所以這種處理方法的延遲很大。如果換用流式傳輸的話,聲音、影像、動畫等多媒體文件將由專門的流媒體服務器負 責向用戶連續、實時地發送,這樣用戶可以不必等到整個文件全部下載完畢,而只需要經過幾秒鍾的啟動延時就可以了,當這些多媒體數據在客戶機上播放時,文件 的剩余部分將繼續從流媒體服務器下載。
流(Streaming)是近年在Internet上出現的新概念,其定義非常廣泛,主要是指通過網絡傳輸多媒體數據的技術總稱。流媒體包含廣義和狹義兩 種內涵:廣義上的流媒體指的是使音頻和視頻形成穩定和連續的傳輸流和回放流的一系列技術、方法和協議的總稱,即流媒體技術;狹義上的流媒體是相對於傳統的 下載-回放方式而言的,指的是一種從Internet上獲取音頻和視頻等多媒體數據的新方法,它能夠支持多媒體數據流的實時傳輸和實時播放。通過運用流媒 體技術,服務器能夠向客戶機發送穩定和連續的多媒體數據流,客戶機在接收數據的同時以一個穩定的速率回放,而不用等數據全部下載完之后再進行回放。
由於受網絡帶寬、計算機處理能力和協議規范等方面的限制,要想從Internet上下載大量的音頻和視頻數據,無論從下載時間和存儲空間上來講都是不太現 實的,而流媒體技術的出現則很好地解決了這一難題。目前實現流媒體傳輸主要有兩種方法:順序流(progressive streaming)傳輸和實時流(realtime streaming)傳輸,它們分別適合於不同的應用場合。
順序流傳輸
順序流傳輸采用順序下載的方式進行傳輸,在下載的同時用戶可以在線回放多媒體數據,但給定時刻只能觀看已經下載的部分,不能跳到尚未下載的部分,也不能在 傳輸期間根據網絡狀況對下載速度進行調整。由於標准的HTTP服務器就可以發送這種形式的流媒體,而不需要其他特殊協議的支持,因此也常常被稱作HTTP 流式傳輸。順序流式傳輸比較適合於高質量的多媒體片段,如片頭、片尾或者廣告等。
實時流傳輸
實時流式傳輸保證媒體信號帶寬能夠與當前網絡狀況相匹配,從而使得流媒體數據總是被實時地傳送,因此特別適合於現場事件。實時流傳輸支持隨機訪問,即用戶 可以通過快進或者后退操作來觀看前面或者后面的內容。從理論上講,實時流媒體一經播放就不會停頓,但事實上仍有可能發生周期性的暫停現象,尤其是在網絡狀 況惡化時更是如此。與順序流傳輸不同的是,實時流傳輸需要用到特定的流媒體服務器,而且還需要特定網絡協議的支持。
實時傳輸協議(Real-time Transport Protocol,PRT)是在Internet上處理多媒體數據流的一種網絡協議,利用它能夠在一對一(unicast,單播)或者一對多 (multicast,多播)的網絡環境中實現傳流媒體數據的實時傳輸。RTP通常使用UDP來進行多媒體數據的傳輸,但如果需要的話可以使用TCP或者 ATM等其它協議,整個RTP協議由兩個密切相關的部分組成:RTP數據協議和RTP控制協議。實時流協議(Real Time Streaming Protocol,RTSP)最早由Real Networks和Netscape公司共同提出,它位於RTP和RTCP之上,其目的是希望通過IP網絡有效地傳輸多媒體數據。
RTSP,RTP,RTCP的區別:RTSP發起/終結流媒體、RTP傳輸流媒體數據 、RTCP對RTP進行控制,同步。
2.1 RTP數據協議
RTP數據協議負責對流媒體數據進行封包並實現媒體流的實時傳輸,每一個RTP數據報都由頭部(Header)和負載(Payload)兩個部分組成,其中頭部前12個字節的含義是固定的,而負載則可以是音頻或者視頻數據。RTP數據報的頭部格式如圖1所示:
其中比較重要的幾個域及其意義如下:
CSRC記數(CC) 表示CSRC標識的數目。CSRC標識緊跟在RTP固定頭部之后,用來表示RTP數據報的來源,RTP協議允許在同一個 會話中存在多個數據源,它們可以通過RTP混合器合並為一個數據源。例如,可以產生一個CSRC列表來表示一個電話會議,該會議通過一個 RTP混合器將所有講話者的語音數據組合為一個RTP數據源。
負載類型(PT) 標明RTP負載的格式,包括所采用的編碼算法、采樣頻率、承載通道等。例如,類型2表明該RTP數據包中承載的是用ITU G.721算法編碼的語音數據,采樣頻率為8000Hz,並且采用單聲道。
序列號 用來為接收方提供探測數據丟失的方法,但如何處理丟失的數據則是應用程序自己的事情,RTP協議本身並不負責數據的重傳。
時間戳 記錄了負載中第一個字節的采樣時間,接收方能夠時間戳能夠確定數據的到達是否受到了延遲抖動的影響,但具體如何來補償延遲抖動則是應用程序自己的事情。
從RTP 數據報的格式不難看出,它包含了傳輸媒體的類型、格式、序列號、時間戳以及是否有附加數據等信息,這些都為實時的流媒體傳輸提供了相應的基礎。RTP協議 的目的是提供實時數據(如交互式的音頻和視頻)的端到端傳輸服務,因此在RTP中沒有連接的概念,它可以建立在底層的面向連接或面向非連接的傳輸協議之 上;RTP也不依賴於特別的網絡地址格式,而僅僅只需要底層傳輸協議支持組幀(Framing)和分段(Segmentation)就足夠了;另外RTP 本身還不提供任何可靠性機制,這些都要由傳輸協議或者應用程序自己來保證。在典型的應用場合下,RTP 一般是在傳輸協議之上作為應用程序的一部分加以實現的。
參考:
version (V): 2 bits
標明RTP版本號。協議初始版本為0,RFC3550中規定的版本號為2。
padding (P): 1 bit
如果該位被設置,則在該packet末尾包含了額外的附加信息,附加信息的最后一個字節表示額外附加信息的長度(包含該字節本身)。該字段之所以存在是因 為一些加密機制需要固定長度的數據塊,或者為了在一個底層協議數據單元中傳輸多個RTP packets。
extension (X): 1 bit
如果該位被設置,則在固定的頭部后存在一個擴展頭部,格式定義在RFC3550 5.3.1節。
CSRC count (CC): 4 bits
在固定頭部后存在多少個CSRC標記。
marker (M): 1 bit
該位的功能依賴於profile的定義。profile可以改變該位的長度,但是要保持marker和payload type總長度不變(一共是8 bit)。
payload type (PT): 7 bits
標記着RTP packet所攜帶信息的類型,標准類型列出在RFC3551中。如果接收方不能識別該類型,必須忽略該packet。
sequence number: 16 bits
序列號,每個RTP packet發送后該序列號加1,接收方可以根據該序列號重新排列數據包順序。
timestamp: 32 bits
時間戳。反映RTP packet所攜帶信息包中第一個字節的采樣時間。
SSRC: 32 bits
數據源標識。在一個RTP Session其間每個數據流都應該有一個不同的SSRC。
CSRC list: 0 to 15 items, 每個源標識32 bits
貢獻數據源標識。只有存在Mixer的時候才有效。如一個將多聲道的語音流合並成一個單聲道的語音流,在這里就列出原來每個聲道的SSRC。
2.2 RTCP控制協議
RTCP 控制協議需要與RTP數據協議一起配合使用,當應用程序啟動一個RTP會話時將同時占用兩個端口,分別供RTP 和RTCP使用。RTP本身並不能為按序傳輸數據包提供可靠的保證,也不提供流量控制和擁塞控制,這些都由RTCP來負責完成。通常RTCP會采用與 RTP相同的分發機制,向會話中的所有成員周期性地發送控制信息,應用程序通過接收這些數據,從中獲取會話參與者的相關資料,以及網絡狀況、分組丟失概率 等反饋信息,從而能夠對服務質量進行控制或者對網絡狀況進行診斷。
RTCP協議的功能是通過不同的RTCP數據報來實現的,主要有如下幾種類型:
SR 發送端報告,所謂發送端是指發出RTP數據報的應用程序或者終端,發送端同時也可以是接收端。
RR 接收端報告,所謂接收端是指僅接收但不發送RTP數據報的應用程序或者終端。
SDES 源描述,主要功能是作為會話成員有關標識信息的載體,如用戶名、郵件地址、電話號碼等,此外還具有向會話成員傳達會話控制信息的功能。
BYE 通知離開,主要功能是指示某一個或者幾個源不再有效,即通知會話中的其他成員自己將退出會話。
APP 由應用程序自己定義,解決了RTCP的擴展性問題,並且為協議的實現者提供了很大的靈活性。
RTCP數據報攜帶有服務質量監控的必要信息,能夠對服務質量進行動態的調整,並能夠對網絡擁塞進行有效的控制。由於RTCP數據報采用的是多播方式,因此會話中的所有成員都可以通過RTCP數據報返回的控制信息,來了解其他參與者的當前情況。
在一個典型的應用場合下,發送媒體流的應用程序將周期性地產生發送端報告SR,該RTCP數據報含有不同媒體流間的同步信息,以及已經發送的數據報 和字節的計數,接收端根據這些信息可以估計出實際的數據傳輸速率。另一方面,接收端會向所有已知的發送端發送接收端報告RR,該RTCP數據報含有已接收 數據報的最大序列號、丟失的數據報數目、延時抖動和時間戳等重要信息,發送端應用根據這些信息可以估計出往返時延,並且可以根據數據報丟失概率和時延抖動 情況動態調整發送速率,以改善網絡擁塞狀況,或者根據網絡狀況平滑地調整應用程序的服務質量。
2.3 RTSP實時流協議
作為一個應用層協議,RTSP提供了一個可供擴展的框架,它的意義在於使得實時流媒體數據的受控和點播變得可能。總的說來,RTSP是一個流媒體表 示協議,主要用來控制具有實時特性的數據發送,但它本身並不傳輸數據,而是必須依賴於下層傳輸協議所提供的某些服務。RTSP 可以對流媒體提供諸如播放、暫停、快進等操作,它負責定義具體的控制消息、操作方法、狀態碼等,此外還描述了與RTP間的交互操作。
RTSP 在制定時較多地參考了HTTP/1.1協議,甚至許多描述與HTTP/1.1完全相同。RTSP之所以特意使用與HTTP/1.1類似的語法和操作,在很 大程度上是為了兼容現有的Web基礎結構,正因如此,HTTP/1.1的擴展機制大都可以直接引入到RTSP 中。
由RTSP 控制的媒體流集合可以用表示描述(Presentation Description)來定義,所謂表示是指流媒體服務器提供給客戶機的一個或者多個媒體流的集合,而表示描述則包含了一個表示中各個媒體流的相關信 息,如數據編碼/解碼算法、網絡地址、媒體流的內容等。
雖然RTSP服務器同樣也使用標識符來區別每一流連接會話(Session),但RTSP連接並沒有被綁定到傳輸層連接(如TCP等),也就是說在 整個 RTSP連接期間,RTSP用戶可打開或者關閉多個對RTSP服務器的可靠傳輸連接以發出RTSP 請求。此外,RTSP連接也可以基於面向無連接的傳輸協議(如UDP等)。
RTSP協議目前支持以下操作:
檢索媒體 允許用戶通過HTTP或者其它方法向媒體服務器提交一個表示描述。如表示是組播的,則表示描述就包含用於該媒體流的組播地址和端口號;如果表示是單播的,為了安全在表示描述中應該只提供目的地址。
邀請加入 媒體服務器可以被邀請參加正在進行的會議,或者在表示中回放媒體,或者在表示中錄制全部媒體或其子集,非常適合於分布式教學。
添加媒體 通知用戶新加入的可利用媒體流,這對現場講座來講顯得尤其有用。與HTTP/1.1類似,RTSP請求也可以交由代理、通道或者緩存來進行處理。
RTP 是目前解決流媒體實時傳輸問題的最好辦法,如果需要在Linux平台上進行實時流媒體編程,可以考慮使用一些開放源代碼的RTP庫,如LIBRTP、 JRTPLIB等。JRTPLIB是一個面向對象的RTP庫,它完全遵循RFC 1889設計,在很多場合下是一個非常不錯的選擇,下面就以JRTPLIB為例,講述如何在Linux平台上運用RTP協議進行實時流媒體編程。
資源預留協議RSVP
1) RSVP協議:
RSVP (Resorce reSerVation Protocol) 資源預留協議並不是一個路由協議,而是一種IP網絡中的信令協議,它與路由協議相結合來實現對網絡傳輸服務質量(QoS)的控制。RSVP是為支持因特網 綜合業務而提出的。這是解決IP通信中QoS(服務質量)問題的一種技術,用來保證點端到端的傳輸帶寬。
2) RSVP協議是如何工作:
RSVP使用控制數據報,這些數據報在向特定地址傳輸時包括了需要由路由器檢查(有些時候需要更新)的信息,如果路由器需要決定是不是要檢查數據報的內容 的時候對上層數據內容進行語法分析。這種分析的代價可不小。現在的情況是,網絡終端利用它向網絡申請資源,在這種表明“申請” 的信號中,包含着如下的信息:業務的種類? 使用者類型? 什么時間? 需要多大帶寬? 其他參考信息? 網絡在接收到上類信息后,會根據實際情況為此次連接分配一個優先代碼,用戶利用優先代碼進行信息傳遞時,網絡不需重新對業務進行分析與判別,從另外一個角 度來說,利用RSVP 能從一定程度上減少網絡對信息處理的時延,提高網絡節點的工作效率,改善信息傳輸的服務質量(QoS)。實時應用用RSVP是為了在傳輸路徑中保持必要的 資源以保證請求能確保到達。
RSVP是IP路由器為提供更好的服務質量向前邁進的具有深刻意義的一步。傳統上IP路由器只負責分組轉發,通過路由協議知道鄰近路由器的地址。而 RSVP則類似於電路交換系統的信令協議一樣,為一個數據流通知其所經過的每個節點(IP路由器),與端點協商為此數據流提供質量保證。
在前面我們討論了一些與實時數據傳輸相關的四個協議:
1)RTP是實時數據傳輸協議。它提供時間標志,序列號以及其它能夠保證在實時數據傳輸時處理時間的方法;它是依靠RVSP保證服務質量標准的。
2)RTCP是RTP的控制部分,是用來保證服務質量和成員管理的。
3)RTSP是開始和指引流媒體數據從流媒體服務器。它又可叫做"網上錄像機控制協議".它是提供遠程的控制,具體的數據傳輸是交給RTP的。
4)RSVP是Internet上的資源預訂協議,使用RSVP預留一部分網絡資源(即帶寬),能在一定程度上為流媒體的傳輸提供QoS。就像TCP的重發和滑動窗口等都是
3.1 環境搭建
JRTPLIB 是一個用C++語言實現的RTP庫,目前已經可以運行在Windows、Linux、FreeBSD、 Solaris、Unix和VxWorks等多種操作系統上。要為Linux 系統安裝JRTPLIB,首先從JRTPLIB的網站(http: //lumumba.luc.ac.be/jori/jrtplib/jrtplib.html)下載最新的源碼包,此處使用的是jrtplib- 2.7b.tar.bz2。假設下載后的源碼包保存在/usr/local/src目錄下,執行下面的命令可以對其進行解壓縮:
[root@linuxgam src]# bzip2 -dc jrtplib-2.7b.tar.bz2 | tar xvf -
接下去需要對JRTPLIB進行配置和編譯:
[root@linuxgam src]# cd jrtplib-2.7
[root@linuxgam jrtplib-2.7b]# ./configure
[root@linuxgam jrtplib-2.7b]# make
最后再執行如下命令就可以完成JRTPLIB的安裝:
[root@linuxgam jrtplib-2.7b]# make install
3.2 初始化
在使用JRTPLIB進行實時流媒體數據傳輸之前,首先應該生成RTPSession類的一個實例來表示此次RTP會話,然后調用Create() 方法來對其進行初始化操作。RTPSession類的Create()方法只有一個參數,用來指明此次RTP會話所采用的端口號。清單1給出了一個最簡單 的初始化框架,它只是完成了RTP會話的初始化工作,還不具備任何實際的功能。
#include "rtpsession.h"
int main(void)
{
RTPSession sess;
sess.Create(5000);
return 0;
}
如 果RTP會話創建過程失敗,Create()方法將會返回一個負數,通過它雖然可以很容易地判斷出函數調用究竟是成功的還是失敗的,但卻很難明白出錯的原 因到底什么。JRTPLIB采用了統一的錯誤處理機制,它提供的所有函數如果返回負數就表明出現了某種形式的錯誤,而具體的出錯信息則可以通過調用 RTPGetErrorString()函數得到。RTPGetErrorString()函數將錯誤代碼作為參數傳入,然后返回該錯誤代碼所對應的錯誤 信息。清單2給出了一個更加完整的初始化框架,它可以對RTP會話初始化過程中所產生的錯誤進行更好的處理:
#include <stdio.h>
#include "rtpsession.h"
int main(void)
{
RTPSession sess;
int status;
char* msg;
sess.Create(6000);
msg = RTPGetErrorString(status);
printf("Error String: %s\\n", msg);
return 0;
}
設 置恰當的時戳單元,是RTP會話初始化過程所要進行的另外一項重要工作,這是通過調用RTPSession類的 SetTimestampUnit()方法來實現的,該方法同樣也只有一個參數,表示的是以秒為單元的時戳單元。例如,當使用RTP會話傳輸8000Hz 采樣的音頻數據時,由於時戳每秒鍾將遞增8000,所以時戳單元相應地應該被設置成1/8000:
sess.SetTimestampUnit(1.0/8000.0);
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型的數值,表明該數據中的前面多少個字符將會被使用。例如下面的語句可以被用來設置控制信息中的電子郵件地址:
sess.SetLocalEMail("xiaowp@linuxgam.comxiaowp@linuxgam.com",19);
在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數據。
推薦一款STUN client/server 程序代碼,http://sourceforge.net/projects/stun/files/
這是一款開源軟件。在客戶端中的主要函數是下面這個:
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端口了。
5 jrtplib 移植中遇到的問題及解決方法
把jrtplib移植到arm11平台,遇到一些問題,如下。
5.1 字節序的問題
jrtplib中的報頭的字節序問題,網上可以搜到一些,但都是只言片語,沒有詳細的把解決方案說出來。ARM采用的是Big-Endian, 而X86采用的是Little-Endian。目前我所采用的解決方法是讓兩台互相通信的主機所使用的jrtplib的Endian格式一致,都為 Big-Endian或都為Little-Endian,這只需要修改jrtplib-version/src/rtpconfig_unix.h 文件,默認是采用的Little-Endian方式,里面的注釋部分有說若加
#define RTP_BIG_ENDIAN
表示采用Big-Endian的字節方式,否則默認為Little-Endian方式。至於原因還沒弄清楚。可以發郵件給作者問一下。
5.2 Can't retrieve login name的錯誤
上述都沒有問題了,又遇到另外的問題,在N800的客戶端建立RTPSession的過程中,報了Can't retrieve login name的錯誤,在網上搜索后,找到一篇博客講到嵌入式系統由於某些原因系統可能沒有login name, 而在RTPSession的Create->InternalCreate->CreateCNAME方法,其中的getlogin_r, getlogin和getenv操作會因為logname為空而返回ERR_RTP_SESSION_CANTGETLOGINNAME的錯誤。我在 N800的機器上做了個實驗,使用getlogin和getenv("LOGNAME")確實都不能得到登錄名。要解決上述問題,可以對jrtplib的 源代碼進行修改, 即修改RTPSession的CreateCNAME,即使getlogin_r, getlogin和getenv三個函數調用都不能得到登錄名,也要設置一個登錄名。