2014年3月11日 星期二

live555 程式碼分析 -- testRTSPClient.cpp

testRTSPClient.cpp 提供了一個程式範例,介紹如何使用 live555 來建立一個 RTSP 連線。
以下整理個人對此程式碼的閱讀心得。程式碼備份於此

1. 定義 ourRTSPClient class
此 class 繼承 RTSPClient class,並且改寫 createNew(),以及新增一個用來記錄stream狀態的 StreamClientState scs,此處須注意的是 createNew() 修改了最後一個參數的值,固定此值 socketNumToServer=-1。



socketNumToServer 的用處
可讓使用者決定建立RTSP連線時,要使用既有的 socket,或是要建立一個新的socket。 
那麼什麼情況之下需要使用既有的 socket呢?
例如:系統內含有多張網路卡,你想要將此次連線限定在某張網路卡時,便可以自行建立 socket,bind到對應的網路卡後,再將此 socket number 交給 RTSPClient 處理。或者是透過 setsockopt() 設定各項參數。
     
修改前後,函數對照如下:
RTSPClient* RTSPClient::createNew(UsageEnvironment& env, char const* rtspURL,
                 int verbosityLevel,
                 char const* applicationName,
                 portNumBits tunnelOverHTTPPortNum,
                 int socketNumToServer)
             
ourRTSPClient* ourRTSPClient::createNew(UsageEnvironment& env, char const* rtspURL,
                  int verbosityLevel,
                  char const* applicationName,
                  portNumBits tunnelOverHTTPPortNum)

2. 定義 DummySink class
此 class 繼承 MediaSink class,實作了新的函數 afterGettingFrame(),並且實現虛擬函數 continuePlaying()

3. main()
依序進行下列步驟
  • 建立一個新的 TaskScheduler 與 UsageEnvironment
  • 建立一個 ourRTSPClient
  • 送出 RTSP Describe,並且設定 Describe Response 的 CallBack函數為continueAfterDESCRIBE。
  • 呼叫 doEventLoop() 接收網路端回傳的訊息並進行處理。
其中送出 RTSP 命令的流程如下:
  • rtspClient->sendDescribeCommand(continueAfterDESCRIBE);
  • sendRequest(new RequestRecord(++fCSeq, "DESCRIBE", responseHandler));
  • send() 
此處需要注意幾點
  • RequestRecord() 會紀錄 fCSeq 與 responseHandler,後續當收到網路來的封包時,便可以根據 fCSeq 找到正確的 responseHandler 進行處理。此處所指的 responseHandler 就是 continueAfterDESCRIBE()。 
  • sendRequest() 會呼叫 fRequestsAwaitingResponse.enqueue(),將 RequestRecord 放入 RTSPClient 的 RequestQueue 內,
   
4. continueAfterDESCRIBE()
針對此次RTSP連線,建立 MediaSession,同時根據 sdpDescription 內的 media,分別建立對應的 subsession,特別需要注意的是,此時會對每個 subsession 建立 fReadSource,後續程式碼中可能會有許多Source混用,因此需先了解各種 Source 之間的繼承關係,舉例如下: 
class MultiFramedRTPSource: public RTPSource
class RTPSource: public FramedSource
class FramedSource: public MediaSource
class MediaSource: public Medium
 
5. continueAfterSETUP()
a. 建立 DummySink,準備用來處理伺服器所送來的 audio 或 video stream。 
b. 呼叫 MediaSink::startPlaying,設定 MediaSink 的 fSource=*(scs.subsession->readSource()),以及兩個 callback 函數 fAfterFunc, fAfterClientData,可以想像是程式會將 fSource 所送來的資料交給 DummySink 處理。其原型如下:
scs.subsession->sink->startPlaying(*(scs.subsession->readSource()),
                                       subsessionAfterPlaying,
                                       scs.subsession);
此函數會檢查 source 是否正確,並註冊函數 fSource->fAfterFunc = subsessionAfterPlaying();,然後呼叫 continuePlaying()
continuePlaying() 實際行為是呼叫 fSource->getNextFrame(),此行為有點複雜,分階段分析 
1. 函數流程依序如下
getNextFrame()  // 設定 fAfterGettingFunc = afterGettingFrame
fSource->getNextFrame()
doGetNextFrame()afterGetting()
source->fAfterGettingFunc() 
2. 許多檔案內都有 doGetNextFrame()
由於 fSource 是根據 SDP 內的 codec name 而動態決定的。因此直接分析靜態程式碼是無法確認 doGetNextFrame() 位於哪個檔案內。 
3. 但無論哪個檔案,其運作原則皆相同
參考 MultiFramedRTPSource.cpp:doGetNextFrame()
其實際行為是turnOnBackgroundReadHandling(),將此 Stream 對應的 channel id 加入 fSubChannelHashTable 內。而當收到伺服器端回應的 SETUP 訊息時,RTSPClient就會先設定之後 RTP 連線需用到的資訊,並且在 MediaSubsession::initiate()建立 socket。  
其函數呼叫流程如下:
      handleResponseBytes()
          handleSETUPResponse()
              setStreamSocket()
接著會透過 setBackgroundHandling() 將建立好的 socket 設定到對應的 socket descriptor,後續會由 doEventLoop() 幫忙監聽並處理這些 socket 進來的資料。 
4. 當live555 core 收到一個完整的封包之後,便會呼叫使用者註冊的afterGettingFrame(),並將資料交由此函數進行處理。

6. continueAfterPLAY()
判斷是否成功建立連線

7. doEventLoop()
利用 select() 監聽 Read, Write, Expection socket,當收到資料後,就交給對應的處理函數,(*handler->handlerProc)(handler->clientData, resultConditionSet);此處 handlerProc 函數在創建 RTSPClient 時便已經指定RTSPClient::incomingDataHandler()

函數處理流程依序為
  • incomingDataHandler()
  • incomingDataHandler1()
  • handleResponseBytes() 
此處會檢查回應的資料 ResponseBytes 中是否有 CSeq
  • 若有 CSeq,則依序從 fRequestsAwaitingResponse() 中取出資料,若序號相同,表示找到 RequestRecord,將其指定給 foundRequest 
  • 若沒有 CSeq,則假設最接近的一筆 RequestRecord 就是我們要的foundRequest = fRequestsAwaitingResponse.dequeue(); 
  • *** 若 CSeq 與比預期的值小,則假設此封包已經不需要,因此不會進行處理??? 此處邏輯是否會有問題??
接著 RTSPClient 會檢查 Server 回應資料是否正確,並設定 resultCode 與resultString,然後呼叫使用者註冊的 CallBack 函數 (*foundRequest->handler())(this, resultCode, resultString) 
以 send DESCRIBE 為例,就是 continueAfterDESCRIBE
以 send SETUP 為例,就是 continueAfterSETUP
以 send PLAY 為例,就是 continueAfterPLAY

結論:
若要自行開發 RTSP Client,那麼只需要使用 testRTSPClient.cpp,然後在 afterGettingFrame() 加入對應的處理邏輯即可。