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中的代碼片段:
[code]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 代碼修改
[code]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的緩沖機制,缺陷就是只能針對專門的協議,相當於添加了很多硬編碼的代碼,當然還是那句話,看項目具體需求了,如果對延遲有苛刻要求,那麼就可以這麼做。
zz:https://jiya.io/archives/vlc_optimize_2.html