rtsp server(live555)詳細設計

/*

*本文基於LIVE555的嵌入式的RTSP流媒體服務器一個設計文檔,箇中細節現剖於此,有需者可參考指正,同時也方便後期自己查閱。(本版本是基於2011年的live555)

作者:[email protected]

*/

RTSP SERVER(基於live555)詳細設計

 

 

這個server的最終情況如下:

性能:D1數據時:

1.      8路全開udp

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175root      20   0 65712 23m 3112 R 29.0 34.5 285:13.05 dvrapp_SN6108 

2.      8路全開tcp

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175root      20   0 65612 23m 3112 R 31.0 34.4 289:03.86 dvrapp_SN6108

 

文件:

靜態庫大小:Live555.a  1,585KB

文件個數:  150左右

 

 

 


目錄

1.代碼移植... 3

1.1代碼獲取... 3

1.2文件初步裁剪... 3

1.3修改Makefile. 3

1.4live555生成的靜態庫鏈接到我司的可執行程序中... 3

2.功能添加... 4

2.1 數據的輸入... 4

2.2實時視頻(H264)的輸入和輸出... 5

2.3實時音頻(G711.a)的輸入和輸出... 6

2.4 H264視頻離散nal單元輸入的實現... 6

2.5單播的實現... 7

2.6多路連接的實現... 7

2.7最大連接數的限制... 9

2.8服務端主動斷開連接的實現... 9

2.9 sdp信息的添加修改... 10

2.10重定向的實現... 10

3. 代碼裁剪... 11

4. 效率優化... 12

4.1Ring buffer雙隊列的修改... 12

4.2內存拷貝的去除及socketwritev 的實現... 13

5. 部分函數說明:... 16

6. 部分實驗結果... 17

 


1.代碼移植

live555代碼移植到我司嵌入式平臺上。

1.1代碼獲取

http://www.live555.com/liveMedia/public/

本次移植使用的版本是2011.12.23.

1.2文件初步裁剪

Live555爲跨平臺庫,本移移植旨在arm linux上運行,所以需先裁剪掉其它無關文件。

a.刪除之前先生成用於linuxmakefile.進入live文件夾,運行./genMakefiles linux.此時生成了用於linuxMakefile.

b.刪除冗餘文件和文件夾。

每個文件夾下只保留*.cpp,*.hh, *.c, *.h, Makefile.其餘全部刪除。

刪除文件夾:WindowsAudioInputDevicemediaServer

1.3修改Makefile

Makefile變量主要做以下修改:

C_COMPILER =              arm-hismall-linux-gcc

CPLUSPLUS_COMPILER =    arm-hismall-linux-g++

LINK =                    arm-hismall-linux-g++-o

LINK_OPTS =          -L.-lpthread  

C_FLAGS =              $(COMPILE_OPTS)     $(CFLAGSARM)

CPLUSPLUS_FLAGS =    $(COMPILE_OPTS)$(CFLAGSARM)

1.4live555生成的靜態庫鏈接到我司的可執行程序中

如:dvrapp_sn6108

       裁剪到此時的Live555編譯時會生成所有的庫,不可能將所有庫鏈進可執行程序中。我司只用到了視頻:H264,音頻 G711.a.其餘不用。故只需將這幾個有關庫鏈接進即可。

       我司鏈接靜態庫到可執行程序的做法是,先成一個xx.a,然後在生成可執行程序時,鏈接所有.a文件。所以我們這裏只需將需要的.o文件裝進live555.a即可。

       具體做法:在live555文件夾下新建文件夾liveLib用於存放其它文件生成的.a. 此文件夾用於生成live555.a最終庫。(將所有.a先打散成.o,再合成一個live555.a.每個文件夾下在生成.a時都拷貝一份.aliveLib中。最後將生成的live555.a拷貝到master\LIB\ARM\SN6108中。可執行程序鏈接時在此文件夾下可找到live555.a

 

 

2.功能添加

       Live555源碼的功能要用到我司具體項目中還需做一定的修改,不是拿來就能用的。

原本對於開源項目,尤其是c++項目,最好不要改動原有的類,所以修改應該是繼承父類,在子類中修改。這樣有利於代碼的升級,和維護。但是由於考慮到c++的繼承的層數以及虛函數的運行時綁定對性能的影響,以及編譯出文件的大小,所以本修改中只對一部分類作了繼承,另一些直接在原有類中添加新方法。

2.1 數據的輸入

       數據從ringbufferserver模塊的傳輸,使用了通知機制,即當ringbuf有數據時,數據發出通道,告知某一通道有數據可用,則server在需要的時候會來這個通道來取數據。

spacer.gif     數據輸入框圖

 

通知機制主要由以下函數實現。

void signalNewFrameData(int mediaType, int chanel,int trans_mode,int buf_len)

       signalNewFrameData爲一個全局函數,可被外部線程調用(注意,整個live555是一個單線程程序)。signalnewFrameData會調用virtual void triggerEvent(EventTriggerId eventTriggerId, void*clientData = NULL), 這個函數共兩個參數,一個是觸發的事件id,另一個是此id對應的事件處理函數所在的類實例指針,這裏具體是各個輸入的videoSourceaudioSource類實例指針。

       數據的寫入ringbuffer由以下函數實現:

       write_unicast_data_live(只用於單播),每當一個數據到來後,先判斷是音頻還是視頻,然後再裝入各自對應的ringbuffer,接着調用 signalnewFrameData通知相應的server. 通知時刻落在server剛好需要數據的時刻區間間的概率較小,大部分情況是(經實驗證明了的)server正在處理其它數據; 或已經取完數據,正在等下次取數據時刻的到來(此時可能正停留在Eventloop 裏的sigleStepselect)。所以通知後都會把事件記入一個bitmask類型的變量fTriggersAwaitingHandling(最多可累計掛入32個待處理事件), 然後在select結束後,再處理每個TriggerNum所對應的事件(調用Source中的deliverFrame將數據向後傳送)。處理完一個事件,則將fTriggersAwaitingHandling中對應的bitmask位清0,singleStep每一次循環中TriggerEvent只處理一個事件(如有未處理完事件,等下一循環再處理)

 

2.2實時視頻(H264)的輸入和輸出

       Live555提供的示例裏面有直接讀文件的類和使用方法,但沒有實時輸入的類及其實現。

       Live555中數據流基本路線是:

       SourceàFilter1àFilter2…àSink

       Filter可能有多個,也可能一個也沒有。對於h264filter有兩個,對於音頻g711.a,實現中則沒有Filter.

       本設計具體實現視頻實時輸入方法如下:      

1.視頻輸入

       live555中,輸入爲Source類,輸出爲 Sink類。中間處理環節類稱爲Filter.

SNDeviceSource類繼承於FramedSource,用於實時輸入h264視頻。該類的實現參考了DeviceSource

SNDeviceSource::deliverFrame()中實現數據的輸入。Memmove 將數據拷貝到fTo.(最後優化後已經改爲傳指針了,沒有了內存拷貝).

2 .Filter

       對於H264視頻Source不是直接到Sink, 而是經過如下:

       SNDeviceSourceàH264VideoStreamDiscreteFrameràH264FUAFragmenteràH264VideoRTPSink

       中間兩個類稱爲Filter,是對視頻數據的進一步處理。

       H264VideoStreamDiscreteFramer繼承於H264VideoStreamFramer, H264VideoStreamFramer主要是提取sps, ppsH264VideoStreamDiscreteFramer 主要是用於輸入 離散的NAL單元,H264FUAFragmenter主要是對H264nal進行分片打包,根據rfc3984FU-A規則進行分片打包。

3 .視頻輸出

       H264VideoRTPSink爲原有類, 實現了h264 rtp包的輸出.

       視頻的打包和發送操作都在H264VideoRTPSink的父類multiFramedRTPSink.其打包發送的流程如下:

sendNextàbuildandSendPacketàpackFrameàgetnextFrameààafterGettingFrameàsendPacketIfnessaryàscheduleDelayedTask àsendNextà。。。

       buildandSendPacket會將rtp包頭打好(timestamps and sequence num先預留,等取到幀數據後再填充)。

       packFrame即將幀數據往rtp包頭後面掛。所以其任務就是要取得幀數據,所以調用getnextFrame來從上一遊來獲取幀數據。對於h264來說,它的上一級由2.12可看出是H264FUAFragmenter, 這個類會將幀數據分好片(<1448字節), 交給multiFramedRTP Sink。好,獲取到幀數據後,就是afterGettingFrame了,這裏面主要做一些檢查工作(檢查數據是否正確,buffer是否溢出等各種檢查)和補充工作(填充前面所說的timestampsrtp sequence number)。之後便是sendPakcetIfnessary, 這裏面會使用tcpudp將數據發送出去。發送完後,需將下一次任務準備一下,即調用scheduleDelayedTasksendNext放入延遲隊列中,等待其在singleStep中被調用,再開始下一次發包過程。

       提示下,tiemstampssequence num是在doSpecialFrameHandling中做的,這是個虛函數,其具體實現在H264VideoRTPSink中實現。

       上面說到: H264FUAFragmenter, 這個類會將幀數據分好片(<1448字節), 交給multiFramedRTP Sink。但是它的數據是從哪來的。其所套路一樣,它也是通過getNextFrame從它的上游獲H264VideoStreamDiscreteFramer取的,而H264VideoStreamDiscreteFramer又是通過getNextFrame從它的上游SNDeviceSource來獲取。形式有些多餘,效率上會打折,但有很明顯的組件思想,這種設計,典型 c++思想,有利於擴展,像堆積木一樣,可堆出更多的功能。當然對於我們只使用其中h264和音頻功能來說,這樣設計框架有些多餘。

2.3實時音頻(G711.a)的輸入和輸出

1、輸入

       SNAudioDeviceSource繼承於DeviceSource.用於音頻輸入。類似於視頻輸入,也是能過DeliverFrame()將數據向後傳輸的。

2、輸出

       SNG711RTPSink繼承於SimpleRTPSink,用於音頻rtp包的輸出。

simpleRTPSink只用於簡單的發包處理,不進行分片,聚合等操作,而我司g711.a音頻包大小爲320bytes,符合簡單打包的條件。

       音頻流程中沒有Filter,直接是:

SNAudioDeviceSourceàSimpleRTPSinkàMultiFramedRTPSink

和視頻一樣的取數據發送方法,只是環節更少,G711.a音頻不需要分片,以及PPS,SPS等提取。MultiFramedRTPSink中的getNextFrame直接從SNAudioDeviceSource中取得幀數據。

2.4 H264視頻離散nal單元輸入的實現

       Live555H264數據的輸入有兩種方式,一種所謂ByteStream,讀文件時採用的主是這種方式,每次讀入的不是整數個幀,這些數據還要被解析成獨立的幀數據才能進一步操作,這種方法使用的filterH264VideoStreamFramer,它利用H264VideoStreamParser :: parse()從讀入的數據中解析出一個個nal單元 。另一種是所謂DiscreteFramer,即輸入是一幀幀離散的 nal單元,這就省去了再解析的環節。

       對於本公司的數據格式來說,是介於這兩種之間。因爲對於p幀本司是一個個離散的nal單元,但對於I幀來說,則是四個nal單元(pps,sps,sei,I)拼起來作爲一個整體進行輸入的。這種不“純粹性”導致只能把每個輸入都當成byteStream,顯然對於p幀來說,多餘了。

       所以,設計將I幀解析成單獨的NAL單元后再進行輸入,這樣所有的輸入都是離散的NAL單元,即可以使用DiscreteFramer方式進行輸入了。

spacer.gif

       設計SNGetNextNalU函數用來實現對I 幀數據的解析。如果不是視頻I幀,則直接通過deliverframe將數據向後傳,如果是I幀,則要先從中解析出單獨的NAL單元,再通過deliverFrame向後傳。

 

2.5單播的實現

Live555具有單播和多播功能,將單播具體應用到我司環境中,如下:

需繼承OnDemandServerMediaSubsession類,重寫與自己特定source相關的OnDemandServerMediaSubsession子類。本設計中

class SNG711OnDemandServerMediaSubsession:public OnDemandServerMediaSubsession

classSNH264OnDemandServerMediaSubsession:public OnDemandServerMediaSubsession

其中G711*類用於輸入音頻子會話,H264對應視頻子會話。

其中最主要的是需重寫createNewStreamSource createNewRTPSink這兩個虛函數,使*subsessionsourcesink關聯起來。Soure類和Sink類的實例化就是在這兩個函數調用時創建的。

       SNH264OnDemandServerMediaSubsession可參考H264VideoFileOnDemandServer MediaSubsession來實現。

       SNG711OnDemandServerMediaSubsession有些特別。參考實現文件是WAVAudioFile ServerMediaSubsession.其中音頻的位寬(816)和編碼方式(a lawu law)都會影響到具體實現方法。所以這些參數一定要提前確定好。Ps:之前由於參數認識錯誤,導致改了很久還是錯的。

2.6多路連接的實現

從單路到多路的實現,主要是將一個輸入流輸出流改爲多個。

spacer.gif多路輸入輸出圖

 

       SNonDemandRTSPServer(相當於本線程的main函數)中,用數組來實現多個ServerMediaSessin,再向每個servermdiaSesson添加各自的subsession (視頻:SNH264OnDemandServerMediaSubsession,音頻: SNG711OnDemandServerMediaSub session),每個subsession 會管理屬於自己的sourcesink

另外,還有一個需注意的是輸入的事件通知也要改成多個通道的通知。

多路連接的示意圖如下:

多路輸入事件的觸發有兩種方式:

方法1:

使用多個triggerEventId來代表每一路的音/視頻輸入,本設計使用了這種方法。

但這種法有侷限性。即通道數的限制。Live555使用一個32位的bitMask來代表每一個EventTriggerId, 也就是說最多可使用32 tirggerId, 本設計爲8通道,每路音視頻各需一個triggerId,所以共要8*2=16id. 如果通道數超過16路,按這種方法則有問題。當然也可以將bitMask改成64位的,應該可以解決此問題。

方法2:官方推薦

只用一個eventTriggerId,所有通道數據到達事件用各個voidSNDeviceSource::deliverFrame0(void* clientData)clientData來區分。

這種該去方法在應用中出現問題:數據輸入緩慢。初步分析是因爲使用了虛函數,而像數據輸入這種很頻繁的操作,如果每次都要通過查虛函數表來確定具體運行哪個函數的話,就會耗費不少時間,導致輸入緩慢,ringbuffer總是滿。當然也可能是我方法不對頭,後續開發者應該可能會想到其它更好的實現方法。

spacer.gif

多路連接類關係示意圖

通道數由宏CHANNEL_NUM_LIVE來設置。

2.7最大連接數的限制

本設計爲8通道rtspserver,爲避免server上連接上超過八路的客戶端,對平均每路性能造成影響,需對最大連接數做限制。

先要知道一個server擁有的client在代碼中對應的是RTSPClientSession類。每一連接對應一個rtspClientSession實例。而這個實例由RTSPServer::createNewClientSession函數產生。所以應當限制作其產生的實例個數不超過8個。

設計:在rtspServer類中添加私有成員變量fNumConnectedClients。在每次調用RTSPServer::createNewClientSessionfNumConnectedClients值做相應的增加。當計數達到8時,在createNewClientSession中直接反回,而不產生rtspClientSession實例。

最大連接數可通過SN_CLIENT_LIMIT來設置。

2.8服務端主動斷開連接的實現

Server端斷開某一通道連接的正常過程一般是:

Client請求斷開(teardownàServer響應請求,關閉相應資源à斷開連接

半閉資源時,最重要的是關閉順序要對,而且要關閉完全,否則會造成崩潰或內存泄露。如果要找到每一個資源的關閉地方,自己再寫一個函數內,按正確的時間和順序去關閉難度較大,容易出錯。

一個簡單的辦法是,只能能從服務器端觸發clienttearDown請求,則剩下的關閉流程會自動走完,這樣做也較安全,本設計用的就是此法。服務器裏的rtcp “BYE” packet可以實現此功能。流程是:

Server發送rtcp”BYE”àClientteardownàServer關閉相應資源à斷開連接

void DSND_DisConnect(intchn)實現了這個功能。

 

2.9 sdp信息的添加修改

Sdpsession descript的縮寫,會話描述息。是流媒體在會話開始時交互信息的一種方式。一般流媒體使用的都是sdp格式來描述。

Live555sdp信息由char* ServerMediaSession::generateSDPDescription()生成。此函數先生成session級別的sdp信息,如“v=…,o=…,s=…”,然後再補充subsession級別的sdp信息,如“a=fmtp:。。。,a=rtpmap:。。。”.subsession級別的sdp信息填充是在各個rtpSink中實現的。如H264char* H264VideoRTPSink::rtpmapLine(),char const*H264VideoRTPSink::auxSDPLine().

需注意的是subSession級別SDP信息,一般是由rtp/avptype來確定的。

0=<type<96時,一個號碼確定唯一的編碼及rtpmap參數,也就是說這種情況下,參數是死的,只能按國際標準來,自己不能隨便改(當然你非要改成自己想像的那種形式或參數,也可以,只是別人識別不了你時,就麻煩了)。

type>=96時,屬於動態範圍,號碼和參數內容允許不同的公司有不同的參數。H264一般取這個範圍的起始數字96,當然你要改爲97也沒人攔你。

對於我們的音頻編碼類型G711.apcma類型,其typertp/avp 裏規定爲8.所以它的採樣率,編碼率,你不能隨便改。

如果非要添加音頻sdp非標準化信息到sdp中(命令如山倒,做吧,不管合不合理),可在voidOnDemandServerMediaSubsession::setSDPLinesFromRTPSink中先進行音頻/視頻的判斷,當判斷出音頻時,填充auxSDPLine字符串,內容便是你想要添加的sdp內容。

關於在OnDemandServerMediaSubsession中進行音視頻的判斷方法,可通過在Source中設計一個虛函數實現,在運行時,再根據綁定的實例判斷出是音頻Source 還是視頻Source,。本設計中使用sourceType()來實現。

RTP/AVP types可參考下面網址:

http://en.wikipedia.org/wiki/RTP_audio_video_profile#RTP.2FAVP_audio_and_video_payload_types

2.10重定向的實現

爲了減輕服務器的壓力,有時需將數據重定向到其它服務器,如IPC

重定向功能是rtsp 協議裏的內容,但在live555裏並沒有實現,所以只能自己實現。

實現函數void getOneRedirectUrl(char*dst,const int chid,const int streamType)來對某一通道的請求進行URL重定向,即在RTSP 信令走到DISCRIBE命令時,服務器返回一個重定向後的url, 並結束此次會話。客戶端只需連接到新的url即可。

       voidRTSPServer::RTSPClientSession::handleRequestBytes(int newBytesRead) 中當收到DESCRIBE請求時,先返回一個錯誤碼

RTSP/1.0 305 UseProxy
再調用getOneRedirectUrl來獲取一個指定的url, 在發送了返回碼之後,再發送此url.

 客戶端需在此後發送TEARDWON來結束此次會話。

 

3. 代碼裁剪

       1、媒體轉發功能下的裁剪: 

裁剪後爲150多個文件。   

Live555代碼原有360個以上的文件,裏面有各種我們用不到的媒體類型的流媒體實現,如aac,mpeg1,mepg2,mp3,mkv等,我們只需要其中的H264 和有關簡單音頻的實現方法。所以需對源碼進行裁剪。

       首先刪除明顯用不到的媒體類型文件。

liveMedia文件夾:

刪除以下列字母開頭的文件:AAC, AC3, ADT, AMR, DVGSM, H261, H263, JPEG, Matroska, MP3 ,MPEG1or2, MPEG2,MPEG4ES,QuickTime, Sip, T140text, vob, Vorbis, VP8, WAV, 以及各種文件各中帶有*file*的文件(因爲我們這是實時流媒體,用不着file)。

然後在Boolean MediaSubsession::createSourceObjects(intuseSpecialRTPoffset) 中做如下修改:這個函數中包含了各種媒體類型,我們將除H264pcma類外的代碼內容都刪除,否則會編譯不過。

       Source文件夾(testProgs改名):

       保留:SNOnDemandRTSPServer用於serverplayCommontestRTSPClient(這兩個用於client), Makefile。除此外,其它全部刪除。

       其它文件夾下不作修改。

       再修改MakefileMakefile中將對應已經刪除的媒體對應的語句刪除,同時將每個makfile中的文件依賴刪除。如:

Media.$(CPP):              include/Media.hh

include/Media.hh:   include/liveMedia_version.hh刪除掉也不影響。

       2、僅保留重定向功能的裁剪

       相對於具有媒體轉發功能下的裁剪進一步做了以下裁剪:

       刪除所有媒體相關的文件。

       liveMedia文件夾:刪除以下文件

MPEGVideoStreamFramer.$(OBJ)MPEG4VideoStreamFramer.$(OBJ) MPEG4VideoStreamDiscreteFramer.$(OBJ)H264VideoStreamFramer.$(OBJ) H264VideoStreamDiscreteFramer.$(OBJ)MPEGVideoStreamParser.$(OBJ)      

H264VideoRTPSink.$(OBJ)

FramedFilter.$(OBJ)  BasicUDPSource.$(OBJ)    $(MPEG_SOURCE_OBJS) SNDeviceSource.$(OBJ)SNAudioDeviceSource.$(OBJ)

BasicUDPSink.$(OBJ)  $(H264_SINK_OBJS)  SimpleRTPSink.$(OBJ) SNG711RTPSink.$(OBJ)

MultiFramedRTPSink.$(OBJ)  VideoRTPSink.$(OBJ)

OnDemandServerMediaSubsession.$(OBJ)SNH264OnDemandServerMediaSubsession.$(OBJ)SNG711OnDemandServerMediaSubsession.$(OBJ)

BitVector.$(OBJ) StreamParser.$(OBJ)

       其它文件夾保持不變。

4. 效率優化

剛添加完功能時, 一路跑起來的cpu利用率爲5~6%

優化完之後的CPU利用率,一路爲4%左右。8D1全開時爲:

//最終性能 8路全開udp

  PIDUSER      PR  NI VIRT  RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175root      20   0 65712 23m 3112 R 29.0 34.5 285:13.05 dvrapp_SN6108 

//最終性能 8路全開tcp

  PIDUSER      PR  NI VIRT  RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175root      20   0 65612 23m 3112 R 31.0 34.4 289:03.86 dvrapp_SN6108     

主要做了以下兩點的優化:

4.1Ring buffer雙隊列的修改

先了解下背景:

基於live555的數據輸入模型是模式,即server需要發送什麼數據時,server再去索取,對於server(假定他是一個人),他關注的是sendPacket,sendnextPacket, sendnextnext Packet…., 其它所有的過程如取數據,打RTP包等都是sendPacket的輔助過程。所以數據的應該是以輸出爲主導而不是輸入。

對於Ringbuf而言,當server 輸入(對於server而言,ringbufserver外的模塊)主導模式時,我Rinbuf可以來一個幀,我要求你server發一個幀。

server是輸出主導時,我不管你ringbuf有沒有來幀,當我server需發送數據時,再向你rinbuf要,你ringbuf給我塞,我只會暫時記下,但並不取數據,一直等到需要時再取。

我們的rinbuffer 是單隊列模式,即音頻視頻混合在一個隊列中。

個人分析(可能不對,後來者可做參考),當單隊列rinbufferlive555配合時,存在這種情況:server需要視頻數據時,去隊列裏取時,而隊列尾部恰好是個音頻數據,這會導致此番數據取不到,而取數據之前,此rtp包的包頭已經打好,就只等幀數據到來後就可發送。但因沒有取到,等吧。。。等到延遲隊列觸發了下一次sendNextPacket後,RTP包頭會重新打,因爲這次要發的是另外一幀數據,所以上次只打了包頭而沒有發送的數據便被覆蓋了,沒有發。。丟包。。。費時。.反之亦然。

改:改成音視頻分開的雙隊列,保證每次去取時,都進入各自的媒體隊列,只有隊列裏有數據,就一定會取到,而不會出現要音頻確碰見視頻幀,要視頻確碰到音頻的情況。

先說一下改動後的結果:本來是衝着減小cpu佔用率去的,但沒什麼效果。然而性能上有一個改觀就是:基本上不會發生隊列滿的情況。隊列不滿,在發送前便不會丟包(試驗結果:八路全開,udp, 跑了兩天,沒有出現過一次ringbuf滿的情況)。

單隊列改雙隊列的基本隊列的存取邏輯還和單隊列一樣,只是來一個數據時,先判音視頻,再分別送入各自的隊列。雙隊列rinbuffer如下圖所示:

spacer.gif

主要的邏輯不同處在於,隊列滿時,該怎麼判斷,怎麼處理。因爲有2個隊列,便 有以下4種情況:

a .Video full, audio not full

b. video full, audio full

c. video not full, audio full

d. video not full, audio not full

首先確立video主導地位,即videofull,情況a,b ,清空所有隊列,不管audio 是否full.

情況d爲正常情況。

情況c , audio不做清空,video不做清空。爲添加新結點,Audio隊列需刪除最後(最old)一個結點,再添加新結點。

情況以c發生的概率較小,因爲音頻很小,很快就發送完畢業,而大概率的事件應該是視頻滿,因爲視頻節點數據大,發送比較費時。(然而這2種滿的情況都還沒有碰到過).

4.2內存拷貝的去除及socket writev 的實現

爲了減小cpu利用率,內存拷貝,首當其衝被想到要被去掉。

Live555中至少有一次內存拷貝,對於H264來說有兩次,對於g711.a來說有一次。

一次內存都不要則要用socketwirtev函數來將分散的各個內存塊發送出去。

首先定義3個結構體:

typedef struct _ptr{

unsigned char* nalBufStart;

unsigned char* qBufDataPtr;

int theLastNalu;//1(is the last).or 0(notthe last) , becase SN capasulate 4 nal in one Frame called I framed, free mustafter all 4 nal have passed

}BufPtr;

 

typedef struct _VecPtr{

unsigned char* firstPtr;

unsigned char* secndPtr;

unsigned char* qBufDataPtr;

int firstSize;//will be 2 or 0

int secndSize;

int theLastNaluOrFrame;

int theLastPacketOfOneFrame;

 }VecPtr;

 

typedef struct{

unsigned char* bufStart;

unsigned curOffset;

unsigned TotSize;

unsigned type;

}BufferType;

BufPtr用於從Source::DeliverFrame中進行指針的賦值.

VecPtr fFrameData定義在multFramedRTPSink中,用於存放wirtev所需的參數指針。

BufferType用於h264視頻分片打包時的數據指針存儲。

 

 

1、視頻流程的修改

       H264視頻傳輸經過的類如下:

SNDeviceSourceàH264videoDiscreteFrameràH264FUAFragmentàH264VideoRTPSinkà MuitiFramedRTPSink

要把數據流經路線上的所有類中有關數據的操作都操作掉,利用傳指針。

BufferTypefNalu;

BufPtr fBufptr;

以上兩個變量定義在H264FuaFragment中,fBufptr從前方 Source中獲取數據指針(指向ringbuffer中的數據)。fNalu用於記錄nal單元在分片打包時,各分片的指針狀態。

各個類中的修改如下:

SNDeviceSource::

修改deliverFrame, 去除內存拷貝,給傳入的指針賦值,此時的fTo即傳入的結構體指針fBufptr

H264videoDiscreteFramer::

修改afterGettingFrame1,需將sps, pps 拷貝下來,以備後用,這個類中的指針也要做相應的修改。

H264FUAFragment::

1.afterGettingFrame1fNalu進行賦值。

2.修改doGetnextFrame, 將各個分片內存拷貝取消。並將指針傳向後面流程(multframedRTPSink

3.重寫構造函數,對FinputBufSize做修改,改成2bytes, 只用來存放分片頭和分片指示單元。關於這個可參考rfc3984 fua 分片打包規則。

MultiFramedRTPSink::

1,afterGettingFrame1中對fVecBuf[3~5]進行填充。fVecbuf[3]指向RTP包頭,fVecbuf[4]指向分片頭或分片對+分片指示單元,fVecbuf[5]指向除分片和指示單元外的幀數據部分。

fVecbuf[0~2], 用來存入TCP方式發送時的三塊頭字節。

       關於wirtev這裏說一下,fVecbuf[i]可能NULL,這時,writev會動跳過 ,接着發fVecbuf[i+1], 因此設6vecbuf, 並不意味着6塊內存一定要都有數據。

       2.void MultiFramedRTPSink::sendPacketIfNecessary()

       修改fRTPInterface.sendPacket

RTPInterface::

1.重載BooleanRTPInterface::sendPacket(struct iovec *vecBuf,int vecCnt,unsigned packetSize)

2. 添加函數Boolean sendRTPOverTCP2(structiovec *vecBuf, int vecCnt,unsigned packetSize, int socketNum, unsigned charstreamChannelId)

此時實現了視頻的TCP 方式下的writev操作。

經測試,性能未見明顯提升,悲劇。。。

2、音頻流程的修改

音頻相對較簡單。其數據流程如下:

SNAudioDeviceSourceà SNG711RTPSinkàMuitiFramedRTPSink

SNAudioDeviceSource:

修改deliverFrame, 對傳入VecPtrfTo)賦值。

MuitiFramedRTPSink

afterGettingFrame1中對fVecBuf[3~5]進行填充。

3 UDPsendtotcp witev混合實現。

       因爲udp 使用writev時,會先進行connect,而《unix網絡編程》中關於udp 使用connect方法指出,它只能與一個對端進行數據報的交互,所以當UDP使用了WIRTEV後,對於某一個特定的通道,它只能有一個客戶端,當有多個客戶端去連接此通道時,只有最後一個連接能成功,前面的即使剛開始成功,也會被後連接的客戶端給擠掉。

       權衡考慮,決定tcp writev, udp仍用sendto.

       其實這個有個簡單的方法就是將writev參數裏的各個vecBuf[i] 裏的數據再拷貝到一整塊內存中,然後再sendto發送。本設計即用這種方法實現。也就是說,UDP最終還是用了一次內存拷貝。

       UDP改動中,需做以下修改:

       1.添加 BooleanwriteSocket2(UsageEnvironment& env,

                  int socket, struct in_addr address, Portport,

                  u_int8_t ttlArg,

                  struct iovec *vecBuf, int vecCnt,unsignedbufferSize)

2.重載Boolean OutputSocket::write

3.重載Boolean Groupsock::output

4 FREE_BUF的調用

       當指針指向的數據使用完之後應調用FREE_BUF來減小數據節點的引用計數。

       兩個判斷是否該FREE_BUF

a.是否是最後一個RTP包發掉了(一個視頻幀節點可分多個RTP包來發送)。用FrameData.theLastPacketOfOneFrame==1判斷。

b.是否是最後節點的最後一部分。對於視頻P幀和音頻幀,一幀永遠是節點的最後一部分,因爲節點只有這一個幀。而對於我司H264視頻來說,key frame節點裏麪包含了sps,pps,sei,I 4NAL單元,所以對於這個節點,一幀發完了仍然不能free這個節點,而要到這4個幀都發完了才能free此節點。用fFrameData.theLastNaluOrFrame== 1判斷。

 


5.部分函數說明:

void write_unicast_data_live(int chid,  unsigned char *data, int datalen, unsignedchar *header, int StreamIdx)

功能:

       將數據寫入ringbuffer. 只用於單播放.

參數:

       Chid:數據來源通道

       Data:QBUFFER_DATA類型的數據指針

       Datalen:數據的長度

       Header:ringbuffer結點頭,用來保存該節點的某些信息,如:video or audio,I or p frame

       StreamIdx:沒有用到

返回值:無

 

void signalNewFrameData(int mediaType, int chanel, inttrans_mode,int buf_len)

功能:

通知某一通道數據可用。

參數:

mediaType:媒體類型,VIDEO_LIVE AUDIO_LIVE

chanel: 通道號 1~8

trans_mode: 單播或多播。UNICAT_LIVEMULTICAST_LIVE

buf_len: 當前ringbuf的長度,這個參數用處不大。

返回值:無

 

SNDeviceSource::SNGetNextNalU(unsigned&lenNal,unsigned char &type)

功能:

查找出一個NALu, 從我司的I frame中。需配合IFrameStatus結構體使用。

參數:

lenNal: 用來保存找到的NAL單元的長度,包括startcode

type: 查找到的NALU的類型

返回值:

已經找到下一個startCode 00000001

       1  沒有找到下一個startCode(這是這一塊中的最後一個nal單元)

   -1 錯誤,解析對象不正確,非我司I.

 

void DSND_DisConnect(int chn)

功能:     

       斷開某一通道(1~max)的連接.包播放已經建立在此通道上的所有連接(一個通道連接了八路客戶端也是有可能的)。

參數:

       Chn,通道號,url中指定的那個通道號

返回值:

       .

注意:

       一個通道對應一個ServermediaSession實例。

       一個連接對應一個RTSPClientSession實例。

       一個session包含有2subsession,一個音頻,一個視頻。在關閉時要都關閉掉。

 

int FramedSource::sourceType()父類

int SNDeviceSource::sourceType()視頻子類

int SNAudioDeviceSource::sourceType()音頻子類

功能:

返回此實例的種類。用於父子類,兄弟類之間的身份確認。

返回值:在SNDeviceSource中返回VIDEO_LIVE

              SNAudioDeviceSource中返回AUDIO_LIVE

              FramedSource中返回0

注意:運行時任何一個FramedSource類型的對象調用sourceType便可知道它是哪個實例。

 

void getOneRedirectUrl(char *dst,const int chid,const intstreamType)

功能:

獲取一個重定向URL, dst指針指向新的url.

參數:

dst:目標URL指針

        chid:通道號 1~8

       streamType:MAINSTREAM SUBSTREAM

返回值:無

6. 部分實驗結果

cpu and memory rate:

測試環境:SN6000 D1

以下爲 top –p pid的顯示結果

 

//兩次內存拷貝省去, TCP writev

tcp writev 1channel  connected

 1167 root     20   0 65552  23m 3092 R 5.7 34.3   0:32.48 dvrapp_SN6108

tcp writev 4channel  connected

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1167 root     20   0 66944  24m 3096 R 22.3 36.3   4:49.36 dvrapp_SN6108     

 

//兩次內存拷貝省去, UDP writev(只支持單客戶端連接)

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1167 root     20   0 65272  22m 3128 R 4.3 33.9   1:22.50 dvrapp_SN6108     

 

//udp sendto, iovec 數組再合成一個完整的內存區域供udp sendto使用

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1167 root     20   0 64900  22m 3124 R 4.3 33.4   0:13.06dvrapp_SN6108   

 

//媒體走通後, 信號量阻塞方式。

Tcp writev ,1channle connected

PID USER      PR NI  VIRT  RES SHR S %CPU %MEM    TIME+  COMMAND           

 1193 root     20   0 65008  22m 3092 S 5.0 33.5   0:08.13dvrapp_SN6108 

 

//改雙隊列後1通道  UDP

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175 root     20   0 65900  23m 3120 R 4.3 34.8   0:03.82 dvrapp_SN6108

//改雙隊列後 8通道全開  UDP

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1201 root     20   0 64576  22m 3144 R 29.7 32.9   0:48.49 dvrapp_SN6108 

 

//改雙隊列後 8通道全開  TCP

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175 root     20   0 66252  23m 3120 R 32.3 35.3   0:53.46 dvrapp_SN6108  

//改雙隊列後1通道  TCP

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175 root     20   0 64084  21m 3120 R 4.0 32.2   1:14.59dvrapp_SN6108 

 

//4UDP,去掉SNservermediaSession  SNh264videortpsink snrtspserversnrtspclientsession

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175 root     20   0 63464  21m 3092 R 13.7 31.3   0:30.24 dvrapp_SN6108

 

//最終性能 8路全開udp

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175 root     20   0 65712  23m 3112 R 29.0 34.5 285:13.05dvrapp_SN6108 

//  最終性能 8路全開tcp

  PID USER     PR  NI  VIRT RES  SHR S %CPU %MEM    TIME+ COMMAND           

 1175 root     20   0 65612  23m 3112 R 31.0 34.4 289:03.86 dvrapp_SN6108     


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