文章

记录排查直播流断流错误过程

FSPlayer 播放直播流时,出现以下错误,排查发现是 http_buf_read 函数在读取数据时,遇到了流结束的情况,导致返回了 AVERROR(EIO) 错误。具体错误日志如下:

1
2
3
4
5
6
7
8
[03-26 09:59:28.380] [FSPlayer] Stream ends prematurely at 2622243, should be 18446744073709551615
[03-26 09:59:28.380] [FSPlayer] av_read_frame error: Input/output error
[03-26 09:59:28.427] [FSPlayer] ffp_toggle_buffering: error: -5
[03-26 09:59:28.427] [FSPlayer] IO error: End of file
[03-26 09:59:28.427] [FSPlayer] Stream ends prematurely at 2622243, should be 18446744073709551615
[03-26 09:59:28.428] [FSPlayer] av_read_frame error: Input/output error
[03-26 09:59:28.428] [FSPlayer] ffp_toggle_buffering: eof
[03-26 09:59:28.432] [Live] live play err withCode -5

查到 Stream ends prematurely 的日志来自于 http_buf_read 函数,代码如下:

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
70
71
72
73
74
75
76
77
78
79
80
static int http_buf_read(URLContext *h, uint8_t *buf, int size)
{
    HTTPContext *s = h->priv_data;
    int len;

    if (s->chunksize != UINT64_MAX) {
        if (s->chunkend) {
            return AVERROR_EOF;
        }
        if (!s->chunksize) {
            char line[32];
            int err;

            do {
                if ((err = http_get_line(s, line, sizeof(line))) < 0)
                    return err;
            } while (!*line);    /* skip CR LF from last chunk */

            s->chunksize = strtoull(line, NULL, 16);

            av_log(h, AV_LOG_TRACE,
                   "Chunked encoding data size: %"PRIu64"\n",
                    s->chunksize);

            if (!s->chunksize && s->multiple_requests) {
                http_get_line(s, line, sizeof(line)); // read empty chunk
                s->chunkend = 1;
                return 0;
            }
            else if (!s->chunksize) {
                av_log(h, AV_LOG_DEBUG, "Last chunk received, closing conn\n");
                ffurl_closep(&s->hd);
                return 0;
            }
            else if (s->chunksize == UINT64_MAX) {
                av_log(h, AV_LOG_ERROR, "Invalid chunk size %"PRIu64"\n",
                       s->chunksize);
                return AVERROR(EINVAL);
            }
        }
        size = FFMIN(size, s->chunksize);
    }

    /* read bytes from input buffer first */
    len = s->buf_end - s->buf_ptr;
    if (len > 0) {
        if (len > size)
            len = size;
        memcpy(buf, s->buf_ptr, len);
        s->buf_ptr += len;
    } else {
        uint64_t target_end = s->end_off ? s->end_off : s->filesize;
        if ((!s->willclose || s->chunksize == UINT64_MAX) && s->off >= target_end)
            return AVERROR_EOF;
        len = size;
        if (s->filesize > 0 && s->filesize != UINT64_MAX && s->filesize != INT32_MAX) {
            int64_t unread = s->filesize - s->off;
            if (len > unread)
                len = (int)unread;
        }
        if (len > 0)
            len = ffurl_read(s->hd, buf, len);
        if ((!len || len == AVERROR_EOF) &&
            (!s->willclose || s->chunksize == UINT64_MAX) && s->off < target_end) {
            av_log(h, AV_LOG_ERROR,
                   "Stream ends prematurely at %"PRIu64", should be %"PRIu64"\n",
                   s->off, target_end
                  );
            return AVERROR(EIO);
        }
    }
    if (len > 0) {
        s->off += len;
        if (s->chunksize > 0 && s->chunksize != UINT64_MAX) {
            av_assert0(s->chunksize >= len);
            s->chunksize -= len;
        }
    }
    return len;
}

编译 debug 版本的 FFmpeg,跟踪调用栈,发现 http_buf_read 打印这个错误,是由于 ffurl_read 返回的 len 出错导致的,具体调用链如下:

ffurl_read -> ffurl_read2 -> retry_transfer_wrapper -> ffurl_read2 -> tls_read

完整代码:

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
70
71
72
73
static inline int ffurl_read(URLContext *h, uint8_t *buf, int size)
{
    return ffurl_read2(h, buf, size);
}

int ffurl_read2(void *urlcontext, uint8_t *buf, int size)
{
    URLContext *h = urlcontext;

    if (!(h->flags & AVIO_FLAG_READ))
        return AVERROR(EIO);
    return retry_transfer_wrapper(h, buf, NULL, size, 1, 1);
}

static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,
                                         const uint8_t *cbuf,
                                         int size, int size_min,
                                         int read)
{
    int ret, len;
    int fast_retries = 5;
    int64_t wait_since = 0;

    len = 0;
    while (len < size_min) {
        if (ff_check_interrupt(&h->interrupt_callback))
            return AVERROR_EXIT;
        ret = read ? h->prot->url_read (h, buf + len, size - len):
                     h->prot->url_write(h, cbuf + len, size - len);
        if (ret == AVERROR(EINTR))
            continue;
        if (h->flags & AVIO_FLAG_NONBLOCK)
            return ret;
        if (ret == AVERROR(EAGAIN)) {
            ret = 0;
            if (fast_retries) {
                fast_retries--;
            } else {
                if (h->rw_timeout) {
                    if (!wait_since)
                        wait_since = av_gettime_relative();
                    else if (av_gettime_relative() > wait_since + h->rw_timeout)
                        return AVERROR(EIO);
                }
                av_usleep(1000);
            }
        } else if (ret == AVERROR_EOF)
            return (len > 0) ? len : AVERROR_EOF;
        else if (ret < 0)
            return ret;
        if (ret) {
            fast_retries = FFMAX(fast_retries, 2);
            wait_since = 0;
        }
        len += ret;
    }
    return len;
}

static int tls_read(URLContext *h, uint8_t *buf, int size)
{
    TLSContext *c = h->priv_data;
    int ret;
    // Set or clear the AVIO_FLAG_NONBLOCK on c->tls_shared.tcp
    c->tls_shared.tcp->flags &= ~AVIO_FLAG_NONBLOCK;
    c->tls_shared.tcp->flags |= h->flags & AVIO_FLAG_NONBLOCK;
    ret = SSL_read(c->ssl, buf, size);
    if (ret > 0)
        return ret;
    if (ret == 0)
        return AVERROR_EOF;
    return print_tls_error(h, ret);
}

也就是说 tls 层读数据遇到问题了。这种情况抛给上层播放器,播放器就会认为流结束了,导致播放失败。

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