VLC优化(2)修改VLC读缓冲机制

xiaoxiao2021-02-28  117

https://jiya.io/archives/vlc_optimize_2.html

0x00 前置信息

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

0x01 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帧数据即可处理,而该读缓冲机制会首先填满缓冲区再提供数据,这就导致先到的帧没有被及时的处理,造成了无谓的等待。

0x02 优化方法

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

这样做的目的就是:使得下层读到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的缓冲机制,缺陷就是只能针对专门的协议,相当于添加了很多硬编码的代码,当然还是那句话,看项目具体需求了,如果对延迟有苛刻要求,那么就可以这么做。

标签: vlc ffmpeg

转载请注明原文地址: https://www.6miu.com/read-40208.html

最新回复(0)