排查 FFmpeg avio buffer size 变化问题
AVIOContext 在 FFmpeg里有作用举足轻重的地位,负责按字节读取和写入,今天不看写入只看读取。
AVIOContext 的创建
下图调用堆栈展示了 AVIOContext 的创建过程:
当调用 avformat_open_input 时,内部会调用 init_input 进行初始化,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int avformat_open_input(AVFormatContext **ps, const char *filename,
const AVInputFormat *fmt, AVDictionary **options)
{
AVFormatContext *s = *ps;
FFFormatContext *si;
AVDictionary *tmp = NULL;
AVDictionary *tmp2 = NULL;
ID3v2ExtraMeta *id3v2_extra_meta = NULL;
int ret = 0;
if (!s && !(s = avformat_alloc_context()))
return AVERROR(ENOMEM);
si = ffformatcontext(s);
if (!s->av_class) {
av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
return AVERROR(EINVAL);
}
if (fmt)
s->iformat = fmt;
if (options)
av_dict_copy(&tmp, *options, 0);
if (s->pb) // must be before any goto fail
s->flags |= AVFMT_FLAG_CUSTOM_IO;
if ((ret = av_opt_set_dict(s, &tmp)) < 0)
goto fail;
if (!(s->url = av_strdup(filename ? filename : ""))) {
ret = AVERROR(ENOMEM);
goto fail;
}
if ((ret = init_input(s, filename, &tmp)) < 0)
goto fail;
s->probe_score = ret;
......
}
init_input 函数里会调用 s 的 io_open 函数,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
int ret;
AVProbeData pd = { filename, NULL, 0 };
int score = AVPROBE_SCORE_RETRY;
if (s->pb) {
s->flags |= AVFMT_FLAG_CUSTOM_IO;
if (!s->iformat)
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
else if (s->iformat->flags & AVFMT_NOFILE)
av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
"will be ignored with AVFMT_NOFILE format.\n");
return 0;
}
if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
(!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
return score;
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;
if (s->iformat)
return 0;
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
}
io_open 是创建 AVFormatContext 的时候赋值的:
1
2
3
4
5
6
7
8
9
10
11
12
13
AVFormatContext *avformat_alloc_context(void)
{
FFFormatContext *const si = av_mallocz(sizeof(*si));
AVFormatContext *s;
if (!si)
return NULL;
s = &si->pub;
s->av_class = &av_format_context_class;
s->io_open = io_open_default;
......
}
所以调用 io_open 实际上是调用 io_open_default 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int io_open_default(AVFormatContext *s, AVIOContext **pb,
const char *url, int flags, AVDictionary **options)
{
int loglevel;
if (!strcmp(url, s->url) ||
s->iformat && !strcmp(s->iformat->name, "image2") ||
s->oformat && !strcmp(s->oformat->name, "image2")
) {
loglevel = AV_LOG_DEBUG;
} else
loglevel = AV_LOG_INFO;
av_log(s, loglevel, "Opening \'%s\' for %s\n", url, flags & AVIO_FLAG_WRITE ? "writing" : "reading");
return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}
进一步调用 ffio_open_whitelist 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options,
const char *whitelist, const char *blacklist
)
{
URLContext *h;
int err;
*s = NULL;
err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
if (err < 0)
return err;
err = ffio_fdopen(s, h);
if (err < 0) {
ffurl_close(h);
return err;
}
return 0;
}
注意在 ffio_fdopen 函数里调用 avio_alloc_context 函数创建了 AVIOContext 对象,并且 buffer_size 默认是 IO_BUFFER_SIZE,也就是 32768
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define IO_BUFFER_SIZE 32768
int ffio_fdopen(AVIOContext **s, URLContext *h)
{
uint8_t *buffer = NULL;
int buffer_size, max_packet_size;
max_packet_size = h->max_packet_size;
if (max_packet_size) {
buffer_size = max_packet_size; /* no need to bufferize more than one packet */
} else {
buffer_size = IO_BUFFER_SIZE;
}
if (!(h->flags & AVIO_FLAG_WRITE) && h->is_streamed) {
if (buffer_size > INT_MAX/2)
return AVERROR(EINVAL);
buffer_size *= 2;
}
buffer = av_malloc(buffer_size);
if (!buffer)
return AVERROR(ENOMEM);
*s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, h,
ffurl_read2, ffurl_write2, ffurl_seek2);
if (!*s) {
av_freep(&buffer);
return AVERROR(ENOMEM);
}
(*s)->protocol_whitelist = av_strdup(h->protocol_whitelist);
if (!(*s)->protocol_whitelist && h->protocol_whitelist) {
avio_closep(s);
return AVERROR(ENOMEM);
}
(*s)->protocol_blacklist = av_strdup(h->protocol_blacklist);
if (!(*s)->protocol_blacklist && h->protocol_blacklist) {
avio_closep(s);
return AVERROR(ENOMEM);
}
(*s)->direct = h->flags & AVIO_FLAG_DIRECT;
(*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL;
(*s)->max_packet_size = max_packet_size;
(*s)->min_packet_size = h->min_packet_size;
if(h->prot) {
(*s)->read_pause = (int (*)(void *, int))h->prot->url_read_pause;
(*s)->read_seek =
(int64_t (*)(void *, int, int64_t, int))h->prot->url_read_seek;
if (h->prot->url_read_seek)
(*s)->seekable |= AVIO_SEEKABLE_TIME;
}
((FFIOContext*)(*s))->short_seek_get = ffurl_get_short_seek;
(*s)->av_class = &ff_avio_class;
return 0;
}
avio_alloc_context 函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AVIOContext *avio_alloc_context(
unsigned char *buffer,
int buffer_size,
int write_flag,
void *opaque,
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
#if FF_API_AVIO_WRITE_NONCONST
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
#else
int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size),
#endif
int64_t (*seek)(void *opaque, int64_t offset, int whence))
{
FFIOContext *s = av_malloc(sizeof(*s));
if (!s)
return NULL;
ffio_init_context(s, buffer, buffer_size, write_flag, opaque,
read_packet, write_packet, seek);
return &s->pub;
}
不用感到困惑,FFIOContext 和 AVIOContext 是一个对象:
1
2
3
4
static av_always_inline FFIOContext *ffiocontext(AVIOContext *ctx)
{
return (FFIOContext*)ctx;
}
可以看到 buffer_size 的值是这里记录下的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void ffio_init_context(FFIOContext *ctx,
unsigned char *buffer,
int buffer_size,
int write_flag,
void *opaque,
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
#if FF_API_AVIO_WRITE_NONCONST
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
#else
int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size),
#endif
int64_t (*seek)(void *opaque, int64_t offset, int whence))
{
AVIOContext *const s = &ctx->pub;
memset(ctx, 0, sizeof(*ctx));
s->buffer = buffer;
ctx->orig_buffer_size =
s->buffer_size = buffer_size;
s->buf_ptr = buffer;
s->buf_ptr_max = buffer;
s->opaque = opaque;
s->direct = 0;
url_resetbuf(s, write_flag ? AVIO_FLAG_WRITE : AVIO_FLAG_READ);
s->write_packet = write_packet;
s->read_packet = read_packet;
s->seek = seek;
s->pos = 0;
s->eof_reached = 0;
s->error = 0;
s->seekable = seek ? AVIO_SEEKABLE_NORMAL : 0;
s->min_packet_size = 0;
s->max_packet_size = 0;
s->update_checksum = NULL;
ctx->short_seek_threshold = SHORT_SEEK_THRESHOLD;
if (!read_packet && !write_flag) {
s->pos = buffer_size;
s->buf_end = s->buffer + buffer_size;
}
s->read_pause = NULL;
s->read_seek = NULL;
s->write_data_type = NULL;
s->ignore_boundary_point = 0;
ctx->current_type = AVIO_DATA_MARKER_UNKNOWN;
ctx->last_time = AV_NOPTS_VALUE;
ctx->short_seek_get = NULL;
}
读包时出现意外
当调用 av_read_frame() 后,会发起 avio_seek 将读的位置确定好,然后开始填充 buffer ,但奇怪的是AVIOContext 的 buffer_size 开始时是32768,但是读取一定的数据后就变成 16766384 了,我尝试寻找修改 buffer_size 值的位置,但这个神秘问题困扰了我几个小时,在网上也没能搜到答案,但我这有个 mp4 视频播放卡顿现在定位到问题就是这个 buffer_size 太大导致的 ,所以不得不硬着头皮去搞清楚这个值到底因何而变。
我遇到的问题是这样的,调用 av_read_frame,会读取 16MB的数据,这个数据量很大,网络层需要一定的加载时间,及时做了 cache 的情况下,在 av_read_frame 交错读取包时,会 seek 到另外一个位置,会导致刚才读取的大部分数据都丢弃掉, FFmpeg 并没有完全利用,一直重复这个操作,就导致虽然下载速度很快,可实际上读到的包却很少,大部分数据都浪费了,不卡顿才怪呢。
经过一番努力,最终发现在播放 mp4/mov 时才可能会改变AVIOContext 的buffer 大小,其他情况都使用默认的 32768。其具体逻辑是在 mov_read_header 函数里调用了 ff_configure_buffers_for_index 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void ff_configure_buffers_for_index(AVFormatContext *s, int64_t time_tolerance)
{
int64_t pos_delta = 0;
int64_t skip = 0;
//We could use URLProtocol flags here but as many user applications do not use URLProtocols this would be unreliable
const char *proto = avio_find_protocol_name(s->url);
FFIOContext *ctx;
av_assert0(time_tolerance >= 0);
if (!proto) {
av_log(s, AV_LOG_INFO,
"Protocol name not provided, cannot determine if input is local or "
"a network protocol, buffers and access patterns cannot be configured "
"optimally without knowing the protocol\n");
}
if (proto && !(strcmp(proto, "file") && strcmp(proto, "pipe") && strcmp(proto, "cache")))
return;
for (unsigned ist1 = 0; ist1 < s->nb_streams; ist1++) {
AVStream *const st1 = s->streams[ist1];
FFStream *const sti1 = ffstream(st1);
for (unsigned ist2 = 0; ist2 < s->nb_streams; ist2++) {
AVStream *const st2 = s->streams[ist2];
FFStream *const sti2 = ffstream(st2);
if (ist1 == ist2)
continue;
for (int i1 = 0, i2 = 0; i1 < sti1->nb_index_entries; i1++) {
const AVIndexEntry *const e1 = &sti1->index_entries[i1];
int64_t e1_pts = av_rescale_q(e1->timestamp, st1->time_base, AV_TIME_BASE_Q);
if (e1->size < (1 << 23))
skip = FFMAX(skip, e1->size);
for (; i2 < sti2->nb_index_entries; i2++) {
const AVIndexEntry *const e2 = &sti2->index_entries[i2];
int64_t e2_pts = av_rescale_q(e2->timestamp, st2->time_base, AV_TIME_BASE_Q);
int64_t cur_delta;
if (e2_pts < e1_pts || e2_pts - (uint64_t)e1_pts < time_tolerance)
continue;
cur_delta = FFABS(e1->pos - e2->pos);
if (cur_delta < (1 << 23))
pos_delta = FFMAX(pos_delta, cur_delta);
break;
}
}
}
}
pos_delta *= 2;
ctx = ffiocontext(s->pb);
/* XXX This could be adjusted depending on protocol*/
if (s->pb->buffer_size < pos_delta) {
av_log(s, AV_LOG_VERBOSE, "Reconfiguring buffers to size %"PRId64"\n", pos_delta);
/* realloc the buffer and the original data will be retained */
if (ffio_realloc_buf(s->pb, pos_delta)) {
av_log(s, AV_LOG_ERROR, "Realloc buffer fail.\n");
return;
}
ctx->short_seek_threshold = FFMAX(ctx->short_seek_threshold, pos_delta/2);
}
ctx->short_seek_threshold = FFMAX(ctx->short_seek_threshold, skip);
}
因为对 buffer 的扩容是调用了ffio_realloc_buf(s->pb, pos_delta) 函数,所以从代码上搜不到类似的 **buffer_size = ** 这样的关键字。这个代码是什么意思,需要结合提交记录分析:
这次改动作者的提到了之前,也就是说这次是一个优化,之前是这样的:
如果计算位置差值,计算完后再乘以 2,当其结果大于了16MB时就忽略,不做扩大buffer的操作,但这样会导致有些 mov 应该触发扩容但是跳过了。
于是作者改成了不跳过,而是限制最大扩容成 16MB。到此我这个问题的根源算是找到了,只能说作者当时的测试视频有这个问题,而我这边这个视频呢又是一种 case了,他的这个改动并不适用于我这个测试视频。
后记
查看这两次改动是 6 代 FFmpeg 提交的,之前的版本是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void ff_configure_buffers_for_index(AVFormatContext *s, int64_t time_tolerance)
{
int64_t pos_delta = 0;
int64_t skip = 0;
//We could use URLProtocol flags here but as many user applications do not use URLProtocols this would be unreliable
const char *proto = avio_find_protocol_name(s->url);
FFIOContext *ctx;
av_assert0(time_tolerance >= 0);
if (!proto) {
av_log(s, AV_LOG_INFO,
"Protocol name not provided, cannot determine if input is local or "
"a network protocol, buffers and access patterns cannot be configured "
"optimally without knowing the protocol\n");
}
if (proto && !(strcmp(proto, "file") && strcmp(proto, "pipe") && strcmp(proto, "cache")))
return;
for (unsigned ist1 = 0; ist1 < s->nb_streams; ist1++) {
AVStream *const st1 = s->streams[ist1];
FFStream *const sti1 = ffstream(st1);
for (unsigned ist2 = 0; ist2 < s->nb_streams; ist2++) {
AVStream *const st2 = s->streams[ist2];
FFStream *const sti2 = ffstream(st2);
if (ist1 == ist2)
continue;
for (int i1 = 0, i2 = 0; i1 < sti1->nb_index_entries; i1++) {
const AVIndexEntry *const e1 = &sti1->index_entries[i1];
int64_t e1_pts = av_rescale_q(e1->timestamp, st1->time_base, AV_TIME_BASE_Q);
skip = FFMAX(skip, e1->size);
for (; i2 < sti2->nb_index_entries; i2++) {
const AVIndexEntry *const e2 = &sti2->index_entries[i2];
int64_t e2_pts = av_rescale_q(e2->timestamp, st2->time_base, AV_TIME_BASE_Q);
if (e2_pts < e1_pts || e2_pts - (uint64_t)e1_pts < time_tolerance)
continue;
pos_delta = FFMAX(pos_delta, e1->pos - e2->pos);
break;
}
}
}
}
pos_delta *= 2;
ctx = ffiocontext(s->pb);
/* XXX This could be adjusted depending on protocol*/
if (s->pb->buffer_size < pos_delta && pos_delta < (1<<24)) {
av_log(s, AV_LOG_VERBOSE, "Reconfiguring buffers to size %"PRId64"\n", pos_delta);
/* realloc the buffer and the original data will be retained */
if (ffio_realloc_buf(s->pb, pos_delta)) {
av_log(s, AV_LOG_ERROR, "Realloc buffer fail.\n");
return;
}
ctx->short_seek_threshold = FFMAX(ctx->short_seek_threshold, pos_delta/2);
}
if (skip < (1<<23)) {
ctx->short_seek_threshold = FFMAX(ctx->short_seek_threshold, skip);
}
}
这两次提交的 diff :




