zoukankan      html  css  js  c++  java
  • 【视频开发】RTSP SERVER(基于live555)详细设计

    /*

    *本文基于LIVE555的嵌入式的RTSP流媒体服务器一个设计文档,个中细节现剖于此,有需者可参考指正,同时也方便后期自己查阅。(本版本是基于2011年的live555)

    作者:llf_17@qq.com

    */

    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修改Makefile3

    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拷贝到masterLIBARMSN6108中。可执行程序链接时在此文件夹下可找到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帧,则直接通过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的类型

    返回值:

    0  已经找到下一个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

  • 相关阅读:
    linux下golang的配置
    为什么有闭包?
    分布式之高性能IO组件
    ECMAScript 5.1 Edition DOC 学习笔记
    直线光栅画法
    【计算机基础】三、指令与指令执行过程
    ThreadLocal的使用
    【设计模式】单例模式
    问题记录
    【Java基础】- 泛型
  • 原文地址:https://www.cnblogs.com/huty/p/8517062.html
Copyright © 2011-2022 走看看