2014年7月6日 星期日

live555 -- 運作原理

M最近把 live555 程式碼重頭看了一遍,以下整理個人的閱讀心得。

一、live555 主要由以下四個模組構成
  • Groupsock:此模組負責網路通訊
  • TaskScheduler:此模組負責工作排程
  • UsageEnvironment:此模組負責記錄各種變數與狀態
  • liveMedia:此模組會針對 client 或是 server 建立對應的 session 與 subsession

二、GroupSocket:此模組負責網路通訊
1. 針對 Unicast 溝通,GroupsockHelper.cpp 提供基本函數如下:
GroupsockHelper.cpp  setupStreamSocket()
create socket with SOCK_STREAM, set socket options with SO_REUSEADDR and SO_REUSEPORT
GroupsockHelper.cpp  setupDatagramSocket():
create socket with SOCK_DGRAM, set socket options with SO_REUSEADDR and SO_REUSEPORT
傳送 RTP 封包時可能會用到 setupStreamSocket() 或是 setupDatagramSocket()
傳送 RTSP 封包時會用到 setupDatagramSocket() 
其用法舉例如下:
參考 testRTSPClient.cpp
      main()
         openURL()
            sendDescribeCommand()
               RTSPClient::sendRequest()
                  RTSPClient::openConnection()
                     setupStreamSocket()    
                 
參考 testOnDemandRTSPServer.cpp
      main()
         RTSPServer::createNew()
            RTSPServer.cpp::setUpOurSocket()
               setupStreamSocket()
 
       
2. 針對 multicast 溝通
live555 將 GroupsockHelper.cpp 內的各項函數封裝成 Groupsock 物件,此物件同時提供 unicast 與 multicast 溝通。
因此不論 unicast 或 multicast,建議都使用 groupsock 物件。 
3. 使用方式 
  • RTSP:Server與Client都要設定一組 group socket
  • RTP:針對每個接收與傳送的 media, 都要設定對應的 group socket

三、TaskScheduler:此模組負責工作排程
1. BasicTaskScheduler.cpp  BasicTaskScheduler::setBackgroundHandling()
透過 setBackgroundHandling() 將建立好的 socket 設定到對應的 socket descriptor,並設定對應的 callback function 
2. BasicTaskScheduler.cpp  BasicTaskScheduler::schedulerTickTask
此函數會設定一個 AlarmHandler,其內容包含(proc, clientData, timeToDelay),之後將此 AlarmHandler 加入 fDelayQueue
3. BasicTaskScheduler.cpp  BasicTaskScheduler::SingleStep()
此函數會會做三件事情 
a. I/O handling
檢查 Read, Write, Expection 三種動作對應的接口(select()),當某一個收到資料後,就交給對應的處理函數,(*handler->handlerProc)(handler->;clientData, resultConditionSet); 這裡的函數固定為 incomingDataHandler(),實際處理資料的函數則須檢視內部的 request->hanlder()。
b. newly-triggered event
處理 triggerEvent,例如:當 Server 端已經取得一個完整frame時,你可以選擇設定 socket callback,或設定 fTriggeredEventHandlers 進行處理。   
建立 triggerEvent 方式舉例如下
DeviceSource::DeviceSource
         BasicTaskScheduler0::createEventTrigger // fTriggeredEventHandlers
   
DeviceSource() 原文說明如下
We arrange here for our "deliverFrame" member function to be called whenever the next frame of data becomes available from the device. 
If the device can be accessed as a readable socket, then one easy way to do this is using a call to envir().taskScheduler().turnOnBackgroundReadHandling( ... ) (See examples of this call in the "liveMedia" directory.)

If, however, the device *cannot* be accessed as a readable socket, then instead we can implement it using 'event triggers':
Create an 'event trigger' for this device (if it hasn't already been done):
 
c. Delay tasks
針對已經加入 fDelayQueue 內的事件,更新其delay的時間,若已經過期,則刪除,可參考 handleAlarm()。若在時間內收到伺服器回應的封包,則會呼叫 handleResponseBytes()處理封包。
4. BasicTaskScheduler0.cpp  BasicTaskScheduler0::doEventLoop
建立一個 while(1) 迴圈,迴圈內持續呼叫 SingleStep(),直到連線中斷。
5. 若程式流程控制中,需要定期觸發機制,可以直接使用scheduleDelayedTask
TaskToken BasicTaskScheduler0::scheduleDelayedTask(int64_t microseconds,
    TaskFunc* proc,
    void* clientData)  

四、UsageEnvironment:此模組負責記錄各種變數與狀態
1. UsageEnvironment.hh
主要包含幾部分
a. 定義多個 virtual function,提供使用者紀錄程式執行結果或錯誤原因。
b. 設計兩個變數,liveMediaPriv 與 groupsockPriv,用來記錄Client state。 
2. UsageEnvironment.cpp  UsageEnvironment(TaskScheduler &scheduler)
建立 UsageEnvironment 物件時,會指定內部變數 fScheduler = scheduler, 並設定 liveMediaPriv=NULL, groupsockPriv=NULL 
3. 建立 liveMediaPriv 的時機
testRTSPClient.cpp openURL()
     testRTSPClient.cpp ourRTSPClient::createNew
         testRTSPClient.cpp ourRTSPClient()
             RTSPClient.cpp RTSPClient()
                 RTSPClient.cpp Medium(env)
         
當建立 RTSPClient 或是 RTSPServer 時,會呼叫 Medium(env), 記住目前使用的 UsageEnvironment,設定 fEnviron = env,並建立MediaLookupTable,並且令env.liveMediaPriv=MediaLookupTable 
4. 建立 groupsockPriv 的時機
   testRTSPClient.cpp openURL()
      testRTSPClient.cpp sendDescribeCommand()
         RTSPClient.cpp sendRequest()
            GroupsockHelper.cpp sendRequest()
               GroupsockHelper.cpp openConnection()
                  GroupsockHelper.cpp setupStreamSocket()
                     GroupsockHelper.cpp groupsockPriv() 
5. 有了 liveMediaPriv 與 groupsockPriv 兩個 table,後續查找值會比較方便。

五、liveMedia:此模組會針對 client 或是 server 建立對應的 session 與 subsession
1. MediaSession 資料結構
MediaSession.hh 定義了三個物件: 
a. MediaSession
    用來記錄 SDP 的各種資訊 
b. MediaSubsession
    協助解析 SDP 內文中與 media 有關的各種資訊,並記錄Subsession中的各種資訊,提供各種 parse function 
c. MediaSubsessionIterator
     提供紀錄 MediaSession 與 MediaSubsession 的指標,方便切換各個 subsession。 
註:ServerMediaSession 也是類似觀念
2. MediaSession 的操作方式 
RTSP Client (參考 testRTSPClient.cpp) 
a. 當收到 RTSP Describe Response 之後,便會建立一個新的MediaSession,並依據 SDP內文進行處理 
MediaSession::createNew
//建立對應的 MediaSession  
MediaSession::initializeWithSDP
//逐行處理SDP內文,針對每一個 media "m=" 建立 MediaSubSession,並將 subsession 紀錄於 fSubsessionsHead  
MediaSubsession::initiate()
//建立連線時所需要使用的 socket,  fRTPSocket, fRTCPSocket,並綁定 RTP, RTCP 所需要使用的 port number (even: RTP; even+1: RTCP) 
b. 接著針對第一個 subsession 送出 RTSP Setup,
當收到 RTSP Setup Response 之後,針對每個 subsession 建立 MediaSink,準備存放接收到的資料 (若是此 subsession 需要發送資料,則需要在此時建立 MediaSource) 
呼叫 startPlaying(),指定此 sink 的資料來源(FramedSource), subsession 以及結束時的 callback function,scs.subsession->sink->startPlaying(*(scs.subsession->readSource()), subsessionAfterPlaying, scs.subsession);
此處資料來源為 FramedSource,可能是 BasicUDPSource,MPEG4GenericRTPSource,H264VideoRTPSource and etc
startPlaying() 內部實作會呼叫 virtual function continuePlaying() 
以此例而言
continuePlaying() 的內容便是從網路端取得下一個frame (fSource->getNextFrame()) 
c. 針對第一個 subsession 送出 RTSP Play,
此時 Server 便會開始傳送資料,
Client 則定期每100ms檢查是否網路有封包進入,每1秒檢查此 Session duration 是否逾期。 
d. 若 SDP 內包含多個 media, 
此時會重複 step b, c, 繼續處理下一個 subsession 
e. Client 端會持續收到封包,直到下列兩種情況,才結束
e1. 使用者所設定的 Session duration expired.
e2. Client 收到 RTCP Bye
3. ServerMediaSession 的操作方式
RTSP Server (參考 testOnDemandRTSPServer.cpp) 
a. 建立 RTSP Server時,便會同時建立新的 ServerMediaSession 與 ServerMediaSubSession,如下:
ServerMediaSession* sms = 
   ServerMediaSession::createNew(*env, streamName, streamName, descriptionString);
sms->addSubsession(MPEG4VideoFileServerMediaSubsession
   ::createNew(*env, inputFileName, reuseFirstSource));
rtspServer->addServerMediaSession(sms);
此處的 descriptionString  便是 SDP 的部分內容。 
b. RTSPServer.cpp 會協助處理所有 RTSP 流程。
當收到 Play Request 之後,會呼叫 virtual function startStream(), 開始發送封包

 1 RTSPServer::RTSPClientSession::handleCmd_PLAY
 2    OnDemandServerMediaSubsession::startStream()
 3       StreamState::startPlaying()
 4          fRTPSink->startPlaying()
 5             MultiFramedRTPSink:startPlaying() (MultiFramedRTPSink.cpp)
 6                continuePlaying()
 7                buildAndSendPacket()
 8                packFrame()
 9                afterGettingFrame1()
10                sendPacketIfNecessary()
11                sendPacket()
12                output()
13                write()
14                writeSocket()
15                sendto()
16          fUDPSink->startPlaying()
17             MediaSink::startPlaying() (BasicUDPSink.cpp)
18                continuePlaying()
19                continuePlaying1()
20                afterGettingFrame()
21                afterGettingFrame1()
22                output()
23                write()
24                writeSocket()
25                sendto()
c. 注意:Server 與 Client 最後都會呼叫 startPlaying,
MediaSink::startPlaying(MediaSource& source, afterPlayingFunc* afterFunc, void* afterClientData)
但其意義有所差異, 
RTSP Client:
從 FramedSource 取得來自網路的資料,之後呼叫使用者自行定義的 afterGettingFrame(),傳入收到的frame。
RTSP Server:
從 FramedSource 取得來自檔案的資料,之後呼叫系統定義的 afterGettingFrame(),送出資料。

4. Sink 與 Source  關係
Sink 就是消費資料的物件,例如:FileSink,把接收到的資料存儲到檔,這個檔就是一個Sink。 
Source 就是生產資料的物件,例如:RTPSource,就是通過RTP持續的從網路端/檔案取得資料。 
這兩個物件可能會同時存在,例如下面這個例子,
1. RTSP Server 先設定一個 MultiFramedRTPSink,準備持續消耗資料。
2. 然後設定 MultiFramedRTPSink 的資料來源為檔案 (ADTSAudioFileSource)
3. 當 RTSP Server 收到 Play request 之後,便會呼叫 continuePlaying(),持續的讀出資料並送至 RTSP Client。 
   RTP (MultiFramedRTPSink.cpp)
      continuePlaying()
         buildAndSendPacket()
            FramedSource.cpp::getNextFrame()
               FramedSource.cpp::doGetNextFrame()
                  ADTSAudioFileSource::doGetNextFrame()
                       nextTask() = envir().taskScheduler().cheduleDelayedTask(0,
                       (TaskFunc*)FramedSource::afterGetting, this);
                   
                 
以 RTSP Client 而言
收到 RTSP Setup Response 之後,針對一個SDP會建立一個 Media Session,SDP內若存在兩個 media description,就會分別建立兩個 Media subsession,並且在 subsession 內會用  fRTPSource 記錄此 media與伺服器連線的相關資訊
收到 RTSP Setup Response 之後,建立 dummy sink,準備收網路送來的資料,接著呼叫 startPlaying() 設定此 sink 的 source 來源是 fRTPSource 
scs.subsession->sink->startPlaying(*(scs.subsession->readSource()),subsessionAfterPlaying, scs.subsession);
以 RTSP Server 而言
針對每次連線會建立一個 Server Media Session,若 Client 要求傳送多組串流資訊(例如:vidoe+audio),就會分別建立多個 Server Media subsession,並且在 subsession 內會用  fRTPSink 記錄欲撥放的檔案

Sink 物件繼承關係如下
   
 Source物件繼承關係如下

六、以 rtsp client 為例,一個最簡單的使用方式如下(testRTSPClient.cpp)
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler); 
// 此處處理 RTSP
RTSPClient* rtspClient = ourRTSPClient::createNew(env, rtspURL, RTSP_CLIENT_VERBOSITY_LEVEL, progName);
rtspClient->sendDescribeCommand(continueAfterDESCRIBE); 
// .... 略 
// 此處處理 RTP, RTCP
env->taskScheduler().doEventLoop(&eventLoopWatchVariable); 
// 實作 afterGettingFrame 函數,在此處處理收到的 frame


七、以 rtsp server 為例,一個最簡單的使用方式如下(testOnDemandRTSPServer.cpp)

      1. 建立RTSP Server時,需要建立 ServerMediaSession
 
      2. 針對每一個要做串流的檔案,都建立對應的 ServerMediaSubSession
         根據串流的檔案類型,會交給不同 xxxServerMediaSubsession class 處理
         例如:
            ADTSAAC -> ADTSAudioFileServerMediaSubsession class
            其流程如下:
            ADTSAudioFileServerMediaSubsession::ADTSAudioFileServerMediaSubsession
               FileServerMediaSubsession::FileServerMediaSubsession
                  OnDemandServerMediaSubsession::OnDemandServerMediaSubsession
                     ServerMediaSubsession::ServerMediaSubsession
                        Medium::Medium(UsageEnvironment& env)
                   
      3. 呼叫 doEventLoop(),由 live555 core 負責處理所有 RTSP 相關流程
         env->taskScheduler().doEventLoop();

      4. 當 live555 core 收到 RTSP Play,會呼叫對應的 startPlaying(),程式便會自動開始傳送檔案,如下:
         RTSPServer::RTSPClientSession::handleCmd_PLAY
            OnDemandServerMediaSubsession::startStream()
               StreamState::startPlaying()
                  fRTPSink->startPlaying()
                     MultiFramedRTPSink:startPlaying()
                        continuePlaying()
                           afterGettingFrame()
                              doGetNextFrame1()
                                 doGetNextFrame()
                                    fread()


八、擴充 live555 功能

功能 1: 自建影音串流伺服器的方法
可以直接參考 TestOnDemandServer,若沒有適合的檔案可用,可以自行新增一個 FramedSource class,並在其 doGetNextFrame() 函數內直接返回 video or audio frame。
功能 2: 支援 ONVIF Backchannel
live555論壇 建議的做法如下 
you would need to reimplement the "MediaSubsession::initiate()" function. For 'backchannel' subsessions, you would need to create an appropriate "RTPSink" subclass, rather than a "RTPSource" subclass that the existing code always does. 
實際試了一下,按照此方法實作支援 Backchannel 的 RTSP Client挺容易的。若要實作支援 Backchannel 的 RTSP Server,也是類似的方式,在 XXXServerMediaSubsession.cpp中同時支援 RTPSink 與 RTPSource即可。

參考資料:
  1. ONVIF Back Channel RTSP example 
  2. live555 程式碼分析 -- testRTSPClient.cpp
  3. live555 程式碼分析 -- RTSP Server on demand
  4. live555 程式碼分析 -- RTSP Client
  5. live555原始程式碼簡介