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.c

int avformat_open_input()
{
/*
Edit by sparktend.
*/
/*
    if (s->pb)
        ff_id3v2_read(s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
*/
}

0x04 总结

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