0x00 前置信息

为进一步降低延迟,采用极端方法修改VLC读缓冲机制。

0x01 VLC读缓冲机制

对于一个rtmp流的读取,发起端在Demux module中,具体在该模块的Demux方法中调用ffmepg的接口av_read_frame读取每一帧数据。但是这个read的接口实在不清晰,经过了多个抽象层的封装,最后真正指向了rtmp_read接口。还是通过一个图来看会比较清晰:

vlc_demux_read_analysis.png

上图描述了read指针的指向,由read的指向可以看出VLC中抽象层次的关系。
Demux layer 开始调用av_read_frame接口,在ffmpeg中经过层层调用,s->read_packet实际指向了Demux layer中IORead接口,然后再进一步指向Stream layer;Stream layer的AReadStream指向了Access layer的Read接口,最终Read接口调用了ffmpeg的接口avio_read,至进一步指向了rtmp_read接口。

在清楚了调用关系之后,现在详细分析上图中2个缓冲区是如何协调读缓冲的。

首先明确一个问题,rtmp_read接口向下调用librtmp的RTMP_Read接口时,一次调用向上层返回一个rtmp packet,正常情况下一个rtmp packet是不会大于16k的。那么先从avio_read接口入手,avio_read接口是针对AVIOContext结构体使用的,它内部有一个缓冲区buffer,默认大小为16k,avio_read接口在被调用时,如果缓冲区中的数据大小小于请求的大小,则调用fill_buffer接口填满缓冲区。fill_buffer相当于以16k的大小向下请求数据,这里进一步看retry_transfer_wrapper中的代码片段:

static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,                                         int size, int size_min,                                         int (*transfer_func)(URLContext *h,                                                              uint8_t *buf,                                                              int size)){  ...  while (len < size_min) {        ret = transfer_func(h, buf + len, size - len);        if (ret == AVERROR(EINTR))            continue;        if (h->flags & AVIO_FLAG_NONBLOCK)            return ret;  ...  }  ...}

如果没有设置AVIO_FLAG_NONBLOCK标志,那么会一直读到16k的数据才返回,这时候buffer中数据的状态像如下图所示:

vlc_demux_read_analysis4.png

Packet 4可能不是一个完整的Packet。

现在回到最初的Demux layer,Demux这层的Cache大小默认是16k,相当于从IORead接口以16k的大小向Stream layer层请求数据,最终Stream layer层向IORead接口返回16k的数据。Stream layer层有一个4M的缓冲区,缓存从Access layer读到的数据,可借助下图理解这三个缓冲的关系:
vlc_demux_read_analysis2.png

如图所示,初始状态时,三个缓冲区都为空。然后VLC在创建Stream layer后会执行一次AStreamPrebufferStream,预读1024个字节数据。该预读操作会使得pb->buffer被充满,然后移动current ptr(pb->buf_ptr),向Stream layer返回1024个字节,这时候的状态是tk->buffer中有1024个字节数据。接着,VLC在加载模块的时候,需要通过预读一定大小的数据来判断具体加载哪个模块,即要执行Stream layer的peek操作,一系列的peek操作结束后,缓冲区的状态如上图第三部分所示,pb->buffer的缓冲区数据未变,current ptr(pb->buf_ptr)指针移动,tk->buffer数据大小变大,大约在10000个字节左右,p_sys->io_buffer依旧为空。在Demux中开始第一次read之后,pb->buffer中的数据被全部读到tk->buffer中,然后再全部被读到p_sys->io_buffer中。因为Stream layer层并不是每次以16k的大小向下请求数据,所以三层缓冲区的数据并不是完全对齐的,比如此时tk->buffer中有15k的数据,尚不够16k,然后Stream layer再一次以6k大小向下请求数据,会使得pb->buffer缓冲被再一次填满,而tk->buffer中的数据大小为21k。

现在来分析该读缓冲机制产生延迟的原因,在Stream layer被创建的时候,pb->buffer中已经存在了3个完整的数据包,而这个数据包直到Demux layer去read的时候才被上层获取,这时Pkt 1已经等待了一段时间,产生了延迟。另外av_read_frame接口只需要获得1帧数据即可处理,而该读缓冲机制会首先填满缓冲区再提供数据,这就导致先到的帧没有被及时的处理,造成了无谓的等待。

0x02 优化方法

为了降低延迟,现修改VLC的读缓冲机制,我的方法比较极端,直接去掉了Stream layer这层的缓冲,初始化阶段不预读,也不在Stream layer做peek操作,直到Demux layer第一次读,向下请求数据时,读到一个packet就返回给上层,不做任何缓存。
对应的状态图如下:

vlc_demux_read_analysis3.png

这样做的目的就是:使得下层读到packet迅速被上层获取并处理。

0x03 代码修改

src/input/stream_filter.c(1)/*Change by sparktend.Note the next line. for not find "stream_filter" module, for no peeking.*/    //s->p_module = module_need( s, "stream_filter", psz_stream_filter, true );---------------------------------------------------------------------------------------------------------------------------------------------------modules/access/avio.c(1)/*Change by sparktend.Add 'AVIO_FLAG_NONBLOCK' flag. for no buffering.*/    ret = avio_open2(&sys->context, url, AVIO_FLAG_READ | AVIO_FLAG_NONBLOCK, &cb, &options);(2)    //int r = avio_read(access->p_sys->context, data, size);    int r = 0;     AVIOContext* s = access->p_sys->context;     if( s->read_packet )     {          r = s->read_packet(s->opaque, data, size);     }---------------------------------------------------------------------------------------------------------------------------------------------------src/input/demux.c(1)/*Edit by sparktend.I note the 'while()'.Because I use acc/h264, no ID3 and APE. */        /*        while (SkipID3Tag( p_demux ))          ;        SkipAPETag( p_demux );          */(2)/*Edit by sparktend.change module name from "demux" to "demux_rtmp".I want just to find a module, not every module that name "demux".*/        p_demux->p_module =            module_need( p_demux, "demux_rtmp", psz_module,                         !strcmp( psz_module, p_demux->psz_demux ) );---------------------------------------------------------------------------------------------------------------------------------------------------modules/demux/avformat/avformat.c/*Edit by sparktend.change "demux" to "demux_rtmp".because I only use this module to demux,set the individual name.connect to  "input/demux.c" module_need().*/    set_capability( "demux_rtmp", 2 )---------------------------------------------------------------------------------------------------------------------------------------------------src/input/stream.c(1)/*Change by sparktend.Note the next info for no Prebuffer.*/      /*            AStreamPrebufferStream( s );        if( p_sys->stream.tk[p_sys->stream.i_tk].i_end <= 0 )        {            msg_Err( s, "cannot pre fill buffer" );            goto error;        }*/ (2)static int AStreamReadNoSeekStream( stream_t *s, void *p_read, unsigned int i_read ){.../*Change by sparktend.Note the next info, for no buffering.*/  /*    if( tk->i_start >= tk->i_end )        return 0;*/.../*Change by sparktend.Do AReadStream, for no buffering. do return before while*/    return AReadStream( s, p_read, i_read );    while( i_data < i_read )} ---------------------------------------------------------------------------------------------------------------------------------------------------modules/demux/avformat/demux.c(1)     /*     Edit by sparktend.     I note the stream*, because I avoid the peek.     */     if( strcmp( p_demux->psz_access, "rtmp" )  )     {          pd.filename = psz_url;         if( ( pd.buf_size = stream_Peek( p_demux->s, (const uint8_t**)&pd.buf, 2048 + 213 ) ) <= 0 )         {             free( psz_url );             msg_Warn( p_demux, "cannot peek" );             return VLC_EGENERIC;         }     }//stream_Control( p_demux->s, STREAM_CAN_SEEK, &b_can_seek );(2)/*Edit by sparktend.I add 'flv' for format.*//*    //char *psz_format = var_InheritString( p_this, "avformat-format" );    char *psz_format = "flv";    if( psz_format )    {        if( (fmt = av_find_input_format(psz_format)) )            msg_Dbg( p_demux, "forcing format: %s", fmt->name );         //free( psz_format );    }*/注释之后的内容一直到msg_Dbg( p_demux, "detected format: %s", fmt->name );(4)如果ffmpeg version 低于 2.3.3需要设置  p_sys->ic->pb->max_packet_size = 32768;具体见aviobuf.c 中fill_buffer的实现异同。 ---------------------------------------------------------------------------------------------------------------------------------------------------ffmpeglibavformatutils.cint avformat_open_input(){/*Edit by sparktend.*//*    if (s->pb)        ff_id3v2_read(s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);*/}

0x04 总结

经过如上修改,绕过了VLC的缓冲机制,缺陷就是只能针对专门的协议,相当于添加了很多硬编码的代码,当然还是那句话,看项目具体需求了,如果对延迟有苛刻要求,那么就可以这么做。