https://jiya.io/archives/vlc_optimize_2.html
为进一步降低延迟,采用极端方法修改VLC读缓冲机制。
对于一个rtmp流的读取,发起端在Demux module中,具体在该模块的Demux方法中调用ffmepg的接口av_read_frame读取每一帧数据。但是这个read的接口实在不清晰,经过了多个抽象层的封装,最后真正指向了rtmp_read接口。还是通过一个图来看会比较清晰:
上图描述了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中数据的状态像如下图所示:
Packet 4可能不是一个完整的Packet。
现在回到最初的Demux layer,Demux这层的Cache大小默认是16k,相当于从IORead接口以16k的大小向Stream layer层请求数据,最终Stream layer层向IORead接口返回16k的数据。Stream layer层有一个4M的缓冲区,缓存从Access layer读到的数据,可借助下图理解这三个缓冲的关系:
如图所示,初始状态时,三个缓冲区都为空。然后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帧数据即可处理,而该读缓冲机制会首先填满缓冲区再提供数据,这就导致先到的帧没有被及时的处理,造成了无谓的等待。
为了降低延迟,现修改VLC的读缓冲机制,我的方法比较极端,直接去掉了Stream layer这层的缓冲,初始化阶段不预读,也不在Stream layer做peek操作,直到Demux layer第一次读,向下请求数据时,读到一个packet就返回给上层,不做任何缓存。 对应的状态图如下:
这样做的目的就是:使得下层读到packet迅速被上层获取并处理。
经过如上修改,绕过了VLC的缓冲机制,缺陷就是只能针对专门的协议,相当于添加了很多硬编码的代码,当然还是那句话,看项目具体需求了,如果对延迟有苛刻要求,那么就可以这么做。
标签: vlc ffmpeg
