2012年6月8日 星期五

live555 程式碼分析 -- RTSP Client


Live555 是一套以 C++ 實作的Multimedia Streaming library,這套函數庫實做了RTP/RTCP/RTSP/SIP 等標準協議,支援MPEG, H.264, H.263+, DV, JPEG 等格式。

Live555 為GNU Lesser General Public License(LGPL)自由軟體,因此只要不更改其原始碼,
我們可以使用其函數庫進行商業產品開發,並且保留自己的程式碼。例如 VLC media player 就是使用 live555 來實作其 RTSP 協議。

使用方式可以參考 live555 的 testprogs 目錄,以下以 RTSP Client 為例說明其運作方式




一、RTSP Client  (參考playCommon.cpp)

RTSP協議定義了許多用來溝通的指令,如:OPTIONS, DESCRIBE, ANNOUNCE, SETUP, PLAY, TEARDOWN等等,RTSPClient 則根據這些定義,設計了各種函數用來送出對應的 RTSP request,並且在呼叫函數時,就直接告知將使用哪個函數來處理Server端所回應的資料。

例如:
送出 DESCRIBE 時,便指定要用來處理 response 的函數
sendDescribeCommand(responseHandler* responseHandler, Authenticator* authenticator = NULL);
對於所收到的 RTSP Response,RTSPClient 已經實作了一個預設的處理函數,但是使用者可以呼叫 changeResponseHandler(),更改設定要處理的callback函數。
對於所有 sendXXXXCommand() 的實作,都是先建立 RequestRecord 物件,再透過SendRequest()送出,若此時網路忙碌,無法立刻送出資料,則會先放入RequestQueue中。 
RTSPClient 並針對 blocking 與否分別提供兩組API,以Setup為例
  • nonblocking,呼叫 sendSetupCommand()
  • blocking,呼叫 setupMediaSubsession()

2. UsageEnvironment 與 TaskScheduler
這兩個模組用來排程那些已經被延遲的事件,針對各種非同步的事件指定其處理程序,並輸出錯誤或警告訊息。 
當 RTSPClient 呼叫 sendXXXXCommand(),送出要求之後,便會等待網路端伺服器的回應,當伺服器有回應時,TaskScheduler 就會交由已註冊的處理函數來處理資料。

TaskScheduler 主要的行為實作於 doEventLoop()。
原文:Delayed tasks, background I/O handling, and other events are handled,
sequentially (as a single thread of control). 
TaskScheduler 依序進行三件事情,其實作可以參考 SingleStep() 
2-1. I/O handling
檢查 Read, Write, Expection 三種動作對應的接口(select()),當某一個收到資料後,就交給對應的處理函數,(*handler->handlerProc)(handler->clientData, resultConditionSet); 這裡的函數固定為 incomingDataHandler(),實際處理資料的函數則須檢視內部的 request->hanlder()。可參考CallBack 的運作邏輯。
2-2. newly-triggered event
處理 triggerEvent,例如:當收到一個完整的frame時,便會觸發 triggerEvent,參考 signalNewFrameData()。這部分並不是很清楚其使用方法???
2-3. Delay tasks
針對已經加入 fDelayQueue 內的事件,更新其delay的時間,若已經過期,則刪除,可參考 handleAlarm()。若在時間內收到伺服器回應的封包,則會進行 step 2-1 I/O handling ,呼叫 handleResponseBytes()處理封包。


3. MediaSession 與 MediaSubSession
簡單的說就是根據所收到的 SDP 內容,分別建立對應的 session。 
針對一個SDP會建立一個 Media Session,SDP內若存在兩個 media description,就會分別建立兩個 Media subsession,並且在 subsession 內會用 fRTPSource 記錄此 media與伺服器連線的相關資訊,試舉一個包含兩個 media description 的 SDP 如下。
RTSP/1.0 200 OK
CSeq: 2
v=0
o=- 2890844526 2890842807 IN IP4 192.16.24.202
s=RTSP Session
m=audio 0 RTP/AVP 0
a=control:rtsp://audio.example.com/twister/audio.en
m=video 0 RTP/AVP 31
a=control:rtsp://video.example.com/twister/video

4.  RTSP 用戶端建立連線的方法 (參考playCommon.cpp  main())
4-1. createClient()
建立一個 RTSP Client,此時需要指定伺服器端的URL,TaskScheduler 與 UsageEnvironment 等資訊。
4-2. getOptions(continueAfterOPTIONS);
  • RTSPClient 送出第一個封包 OPTION,並且設定伺服器回應資料的處理函數。 
  • 當RTSP的基礎設定都透過網路送出後,才會呼叫 envir().taskScheduler().doEventLoop() ,由 TaskScheduler 負責接收伺服器回應的資料。 在doEventLoop() 執行前需要先完成的動作包含 "OPTIONS","DESCRIBE", "SETUP", 
  • TaskScheduler會將回應資料交給 continueAfterOPTIONS() 處理。 
  • continueAfterOPTIONS()  的內容就是呼叫 getSDPDescription(continueAfterDESCRIBE); 向伺服器取得 SDP。
  • continueAfterDESCRIBE 則會根據收到的SDP,分別建立 media session 與 media subsession,並且設定連線資訊,如 port number、socket buffer size,然後呼叫 setupStreams(),告知伺服器本端的設定。 
4-3. setupStreams()
此時會送出 SETUP 命令給 RTSP Server,並且針對每種要接收的媒體創建一個新的檔案(sink),準備用來儲存從伺服器端接收的封包。此處 live555設計了幾種格式
  • QuickTimeFileSink:儲存 QuickTime
  • AVIFileSink: 儲存 AVI
  • FileSink:可以用來儲存  AMRAudioFileSink ( AMR, AMR-NB ) ,  H264VideoFileSink (H264), MP4V-ES 等格式。 呼叫addData()進行儲存動作。 
根據媒體的型別建立 sink 後,便呼叫 getNextFrame(),嘗試取得網路端送來的封包。 
注意: 
參考 MultiFramedRTPSource.cpp:doGetNextFrame(),其實際行為是致能背景執行程序 turnOnBackgroundReadHandling(),將此Stream對應的channel id 加入 fSubChannelHashTable 內。而當收到伺服器端回應的SETUP訊息時,RTSPClient就會先設定之後 RTP 連線需用到的資訊,並且在MediaSubsession::initiate()建立socket()。
handleResponseBytes()
    handleSETUPResponse()
        setStreamSocket()
4-4. startPlayingSession() 與 continueAfterPLAY()
要求伺服器開始發送封包,當送出"PLAY"後,伺服器就會透過 RTP 送出用戶所要求的資料,此時用戶端的 RTSP 需要
  1. 設定每一秒執行一次 sessionAfterPlaying(),檢查是否session已經中斷。若session仍存在,則重新設定此 session 的狀態。
  2. 設定每100毫秒檢查網路, 檢查是否已經收到封包,並記錄總共收到封包的數量,checkForPacketArrival(), checkInterPacketGaps()
4-5. RTP 封包的接收
4-1~4-4都是屬於 RTSP 的行為,但實際上要用來撥放的媒體則是透過RTP傳送,在伺服器收到"PLAY"之後,就會透過之前協商好的 port number,開始傳送 RTP 封包(MediaSubsession),用戶端可以透過 doEventLoop() 接收網路端來的封包並撥放。
其接收與播放的流程就是
  • doGetNextFrame():設定 networkReadHandler() 為處理網路接收的 背景程序。設定至 TaskScheduler class 中的 handler->handlerProc,由TaskScheduler負責排程,等待網路端封包進入。其擷取封包的流程如下:
fillInData()
    handleRead()
        readSocket()
  • doGetNextFrame1(): 判斷封包是否可以使用,是否已經可以構成一個frame, 若可用,則呼叫對應的afterGettingFrame()。
  • 根據 streams格式,分別執行存檔或播放的動作,以FileSink為例, 其  afterGettingFrame() 的動作就是呼叫 addData() 將接收到的封包存檔。以AVISink 為例,則是呼叫 useFrame() 將封包改成 AVI 的格式寫入檔案。

5. CallBack 的運作邏輯是,發出一個request,同時註冊處理response的函數,藉此完成整個
    RTSP前期的設定過程,然後串流的接收與撥放則由一個無窮環圈來處理。舉例如下:
發出需求時,便註冊callback函數
  sendOptionsCmd()
sendRequest() // 此時要檢查是否為 multicast
fRequestsAwaitingConnection.enqueue() // 將 request->handler() 加入 queue
發出需求後,由callback負責處理伺服器回應的資料
doEventLoop() {
while(1) {
SingleStep(); // 此處會呼叫 handler->handlerProc()
}
}

handler() 與 handlerProc() 這兩種 callback 的關係:
建立連線時,就會設定預設的response處理函數 handlerProc()
openConnection() //
 setBackgroundHandling(incomingDataHandler)
assignHandler()
 HandlerDescriptor()
handler->handlerProc = incomingDataHandler;  
根據呼叫的函數,使用對應的callback處理。
incomingDataHandler()
incomingDataHandler1() : readSocket()
handleResponseBytes()
(*foundRequest->handler()) // 取出 sendRequest() 註冊的 callback




常見問題

此處參考官網的FAQ與個人心得,摘錄一些值得注意的實作細節

1. 最大同時連線數
live555並沒有限制,但不同平台上對於一個行程可以同時開啟檔案個數的大小可能有限制。例如:windows定義的"FD_SETSIZE"=64,表示同時最多開啟64個file descriptor,因此同時最多只有32個連線。
2. 接收與播放的時間差
 live555接收到封包之後,會保留一段時間重新排序封包,之後才進行播放,這之間的時間差可以透過subsession->rtpSource()->setPacketReorderingThresholdTime() 設定,預設值為 1 秒。
3. 是否為 Thread-Safe?
No,若有一個thread正在執行,則其他的thread能做的只有設定某些全域變數,例如:such as event loop 'watch variables', or by calling 'event triggers'.

4. 若封包遺失,可以怎樣改善?
  • 首先,確認網路的頻寬是否足夠。 
  • 加大系統定義的 socket reception buffers ,可能是 live555 的 buffer 太小,或是 作業系統預設的 buffer 限制太小。
  • 判斷 IP MTU 或 TCP MSS,避免傳送過程中IP封包多餘的分片重組。以TCP而言,可以利用 3-way handshake的 SYN和SYN+ACK 封包所帶的 MTU 來作判斷。
  • live555只使用一個 thread 來處理socket的讀寫,,使用TCP傳送資料時應該與packet loss無關,若發生 packet loss,則應朝著網路與作業系統這兩方面作改善。
   

參考資料