流媒體

 流媒體指的是在網絡中使用流技術傳輸的連續時基媒體,其特點是在播放前不需要下載整個文件,而是採用邊下載邊播放的方式,它是視頻會議、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 網絡有效地傳輸多媒體數據。

  2.1 RTP 數據協議

  RTP 數據協議負責對流媒體數據進行封包並實現媒體流的實時傳輸,每一個RTP數據報都由頭部(Header)和負載(Payload)兩個部分組成,其中頭部前 12 個字節的含義是固定的,而負載則可以是音頻或者視頻數據。RTP 數據報的頭部格式如圖 1 所示:


  
  圖 1 RTP 頭部格式


  其中比較重要的幾個域及其意義如下:

  ? 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 一般是在傳輸協議之上作爲應用程序的一部分加以實現的,如圖 2 所示:


  
  圖2 RTP與各種網絡協議的關係


  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 協議進行實時流媒體編程。

  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 會話的初始化工作,還不具備任何實際的功能。

  代碼清單 1:initial.cpp

#include "rtpsession.h" 
   
int main(void) 
{ 
 RTPSession sess; 
 sess.Create(5000); 
 return 0; 
}

  如果 RTP 會話創建過程失敗,Create() 方法將會返回一個負數,通過它雖然可以很容易地判斷出函數調用究竟是成功的還是失敗的,但卻很難明白出錯的原因到底什麼。JRTPLIB 採用了統一的錯誤處理機制,它提供的所有函數如果返回負數就表明出現了某種形式的錯誤,而具體的出錯信息則可以通過調用 RTPGetErrorString() 函數得到。RTPGetErrorString( )函數將錯誤代碼作爲參數傳入,然後返回該錯誤代碼所對應的錯誤信息。清單 2 給出了一個更加完整的初始化框架,它可以對 RTP 會話初始化過程中所產生的錯誤進行更好的處理:

  代碼清單 2:framework.cpp

#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 端口:

  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("[email protected]",19);
  

  在 RTP 會話過程中,不是所有的控制信息都需要被髮送,通過調用 RTPSession 類提供的 EnableSendName()、EnableSendEMail()、EnableSendLocation()、EnableSendPhone()、EnableSendTool() 和 EnableSendNote() 方法,可以爲當前 RTP 會話選擇將被髮送的控制信息。

  3.6 實際應用

  最後通過一個簡單的流媒體發送-接收實例,介紹如何利用 JRTPLIB 來進行實時流媒體的編程。清單 3 給出了數據發送端的完整代碼,它負責向用戶指定的 IP 地址和端口,不斷地發送 RTP 數據包:

  代碼清單 3:sender.cpp

#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 數據包:

  代碼清單 4:receiver.cpp

#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 平臺上的實時流媒體編程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章