文章

排查 FFmpeg avio buffer size 变化问题

排查 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 :

本文由作者按照 CC BY 4.0 进行授权