diff --git a/doc/protocols.texi b/doc/protocols.texi index 1d43fa6327..7812d154db 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -599,6 +599,10 @@ Reconnect automatically when disconnected before EOF is hit. @item reconnect_at_eof If set then eof is treated like an error and causes reconnection, this is useful for live / endless streams. +This does not apply to the deliberately established end of a resource whose +complete size is unknown, i.e. when a ranged request past the end is answered +with @code{416}. Such an end is reported as a clean EOF and is not retried, +unless @option{reconnect_on_http_error} lists 416 by number. @item reconnect_on_network_error Reconnect automatically in case of TCP/TLS errors during connect. @@ -606,6 +610,10 @@ Reconnect automatically in case of TCP/TLS errors during connect. @item reconnect_on_http_error A comma separated list of HTTP status codes to reconnect on. The list can include specific status codes (e.g. '503') or the strings '4xx' / '5xx'. +A @code{416} that ends a read or a seek past the end of a resource whose +complete size is unknown is reported as EOF. It is retried first only if 416 +is listed by number, for example to read a resource that is still being +written. '4xx' does not cover it. @item reconnect_streamed If set then even streamed/non seekable streams will be reconnected on errors. diff --git a/libavformat/http.c b/libavformat/http.c index 3245f47d01..675bd81ea6 100644 --- a/libavformat/http.c +++ b/libavformat/http.c @@ -122,6 +122,8 @@ typedef struct HTTPContext { HTTPAuthState auth_state; /* auth_state.auth_type is also a config option */ HTTPAuthState proxy_auth_state; uint64_t filesize; + uint64_t oc_filesize; /* Complete size (200 Content-Length or Content-Range total) */ + int end_of_resource; /* The end of an unknown-size resource was reached */ int is_akamai; int is_mediagateway; /* A dictionary containing cookies keyed by cookie name */ @@ -171,6 +173,8 @@ typedef struct HTTPContext { /* Temporary during header parsing */ uint64_t filesize_from_content_range; + /* Reply is a partial response with unknown total ("bytes A-B/" + '*'). */ + int content_range_unknown_size; int line_count; /****************** @@ -426,23 +430,41 @@ static int redirect_cache_set(HTTPContext *s, const char *source, const char *de return 0; } -/* return non zero if error */ -static int http_open_cnx(URLContext *h, AVDictionary **options) +/* A 416 reply for a resource of unknown size marks its end, unless it states + * a length past the refused offset. */ +static int is_end_of_resource(HTTPContext *s, int err, uint64_t off) +{ + return err == AVERROR_HTTP_OTHER_4XX && s->http_code == 416 && + s->oc_filesize == UINT64_MAX && + (s->filesize_from_content_range == UINT64_MAX || + s->filesize_from_content_range <= off); +} + +/* Return non zero if error. With end_is_eof, the caller reports the end of a + * resource of unknown size as EOF, and the 416 that marks it is retried only + * if reconnect_on_http_error lists 416 by number. */ +static int http_open_cnx(URLContext *h, AVDictionary **options, int end_is_eof) { HTTPAuthType cur_auth_type, cur_proxy_auth_type; HTTPContext *s = h->priv_data; int ret, conn_attempts = 1, auth_attempts = 0, redirects = 0; int reconnect_delay = 0; int reconnect_delay_total = 0; - uint64_t off; + uint64_t off = s->off; + uint64_t filesize = s->filesize; + uint64_t oc_filesize = s->oc_filesize; char *cached; redo: + /* every attempt starts with the complete size known before the call */ + s->oc_filesize = oc_filesize; cached = redirect_cache_get(s); if (cached) { - if (redirects++ >= s->max_redirects) - return AVERROR(EIO); + if (redirects++ >= s->max_redirects) { + ret = AVERROR(EIO); + goto fail; + } av_free(s->location); s->location = av_strdup(cached); @@ -463,6 +485,8 @@ redo: ret = http_open_cnx_internal(h, options); if (ret < 0) { if (!http_should_reconnect(s, ret) || + (end_is_eof && is_end_of_resource(s, ret, off) && + !av_match_list("416", s->reconnect_on_http_error, ',')) || reconnect_delay > s->reconnect_delay_max || (s->reconnect_max_retries >= 0 && conn_attempts > s->reconnect_max_retries) || reconnect_delay_total > s->reconnect_delay_total_max) @@ -516,8 +540,10 @@ redo: s->new_location) { /* url moved, get next */ ffurl_closep(&s->hd); - if (redirects++ >= s->max_redirects) - return AVERROR(EIO); + if (redirects++ >= s->max_redirects) { + ret = AVERROR(EIO); + goto fail; + } if (!s->expires) { s->expires = (s->http_code == 301 || s->http_code == 308) ? INT64_MAX : -1; @@ -542,6 +568,8 @@ redo: fail: s->off = off; + s->filesize = filesize; + s->oc_filesize = oc_filesize; if (s->hd) ffurl_closep(&s->hd); if (ret < 0) @@ -598,6 +626,8 @@ int ff_http_do_new_request2(URLContext *h, const char *uri, AVDictionary **opts) s->chunkend = 0; s->range_end = 0; s->off = 0; + s->oc_filesize = UINT64_MAX; + s->end_of_resource = 0; s->icy_data_read = 0; av_free(s->location); @@ -614,7 +644,7 @@ int ff_http_do_new_request2(URLContext *h, const char *uri, AVDictionary **opts) return ret; av_log(s, AV_LOG_INFO, "Opening \'%s\' for %s\n", uri, h->flags & AVIO_FLAG_WRITE ? "writing" : "reading"); - ret = http_open_cnx(h, &options); + ret = http_open_cnx(h, &options, 0); av_dict_free(&options); return ret; } @@ -791,6 +821,8 @@ static int http_open(URLContext *h, const char *uri, int flags, s->initial_requests = s->seekable != 0 && s->initial_request_size > 0; s->filesize = UINT64_MAX; + s->oc_filesize = UINT64_MAX; + s->end_of_resource = 0; s->location = av_strdup(uri); if (!s->location) @@ -820,7 +852,7 @@ static int http_open(URLContext *h, const char *uri, int flags, if (s->listen) { return http_listen(h, uri, flags, options); } - ret = http_open_cnx(h, options); + ret = http_open_cnx(h, options, 0); bail_out: if (ret < 0) { av_dict_free(&s->chained_options); @@ -985,8 +1017,12 @@ static int parse_content_range(URLContext *h, const char *p) return AVERROR_INVALIDDATA; } } - if ((slash = strchr(p, '/')) && strlen(slash) > 0) - s->filesize_from_content_range = strtoull(slash + 1, NULL, 10); + if ((slash = strchr(p, '/')) && strlen(slash) > 0) { + if (av_isdigit(slash[1])) + s->filesize_from_content_range = strtoull(slash + 1, NULL, 10); + else + s->content_range_unknown_size = 1; /* unknown total size */ + } } if (s->seekable == -1 && (!s->is_akamai || s->filesize != 2147483647)) h->is_streamed = 0; /* we _can_ in fact seek */ @@ -1559,6 +1595,7 @@ static int http_read_header(URLContext *h) s->expires = 0; s->chunksize = UINT64_MAX; s->filesize_from_content_range = UINT64_MAX; + s->content_range_unknown_size = 0; for (;;) { int parsed_http_code = 0; @@ -1593,6 +1630,14 @@ static int http_read_header(URLContext *h) // filesize from Content-Range can always be used, even if using chunked Transfer-Encoding if (s->filesize_from_content_range != UINT64_MAX) s->filesize = s->filesize_from_content_range; + else if (s->content_range_unknown_size) + s->filesize = s->oc_filesize; + + /* Remember a concrete complete size for later unknown-total ("*") replies. */ + if (s->filesize_from_content_range != UINT64_MAX) + s->oc_filesize = s->filesize_from_content_range; + else if (s->http_code == 200 && s->filesize != UINT64_MAX) + s->oc_filesize = s->filesize; if (s->seekable == -1 && s->is_mediagateway && s->filesize == 2000000000) h->is_streamed = 1; /* we can in fact _not_ seek */ @@ -1962,6 +2007,17 @@ static int http_buf_read_compressed(URLContext *h, uint8_t *buf, int size) } #endif /* CONFIG_ZLIB */ +/* Record the end of the resource if err is the 416 that marks it. */ +static int http_end_of_resource(HTTPContext *s, int err) +{ + if (!is_end_of_resource(s, err, s->off)) + return 0; + s->oc_filesize = s->filesize_from_content_range; + s->filesize = s->oc_filesize; + s->end_of_resource = 1; + return 1; +} + static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int force_reconnect); static int http_read_stream(URLContext *h, uint8_t *buf, int size) @@ -1973,6 +2029,9 @@ static int http_read_stream(URLContext *h, uint8_t *buf, int size) int reconnect_delay_total = 0; int conn_attempts = 1; + if (s->end_of_resource) + return AVERROR_EOF; + if (!s->hd) return s->off < requested_end(s) ? AVERROR(EIO) : AVERROR_EOF; @@ -2002,10 +2061,14 @@ retry: if (s->willclose) ffurl_closep(&s->hd); s->initial_requests = 0; /* continue streaming uninterrupted from now on */ - read_ret = http_open_cnx(h, &options); + read_ret = http_open_cnx(h, &options, 1); av_dict_free(&options); if (read_ret == 0) goto retry; + if (http_end_of_resource(s, read_ret)) { + read_ret = AVERROR_EOF; + break; + } } if (h->is_streamed && !s->reconnect_streamed) @@ -2037,6 +2100,10 @@ retry: av_log(h, AV_LOG_ERROR, "Failed to reconnect at %"PRIu64".\n", target); return read_ret; } + if (s->end_of_resource) { + read_ret = AVERROR_EOF; + break; + } read_ret = http_buf_read(h, buf, size); } @@ -2211,6 +2278,13 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo HTTPContext *s = h->priv_data; URLContext *old_hd = NULL; uint64_t old_off = s->off; + uint64_t old_range_end = s->range_end; + uint64_t old_chunksize = s->chunksize; + int old_willclose = s->willclose; +#if CONFIG_ZLIB + int old_compressed = s->compressed; +#endif + int old_end_of_resource = s->end_of_resource; uint8_t old_buf[BUFFER_SIZE]; int old_buf_size, ret; AVDictionary *options = NULL; @@ -2229,12 +2303,12 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo return AVERROR(EINVAL); if (off < 0) return AVERROR(EINVAL); - if (!force_reconnect && off == s->off) + if (!force_reconnect && off == s->off && s->hd) return s->off; - s->off = off; - - if (s->off && h->is_streamed) + if (off && h->is_streamed) return AVERROR(ENOSYS); + s->off = off; + s->end_of_resource = 0; /* do not try to make a new connection if seeking past the end of the file */ if (s->end_off || s->filesize != UINT64_MAX) { @@ -2283,8 +2357,8 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo remaining -= ret; } - ret = http_open_cnx(h, &options); - if (ret >= 0) { + ret = http_open_cnx(h, &options, 1); + if (ret >= 0 || http_end_of_resource(s, ret)) { goto done; } else { /* fall back to normal reconnection */ @@ -2297,15 +2371,23 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo s->hd = NULL; } - if ((ret = http_open_cnx(h, &options)) < 0) { + if ((ret = http_open_cnx(h, &options, 1)) < 0 && + !http_end_of_resource(s, ret)) { /* if it fails, continue on old connection if possible */ if (old_hd) { memcpy(s->buffer, old_buf, old_buf_size); s->buf_ptr = s->buffer; s->buf_end = s->buffer + old_buf_size; s->hd = old_hd; - s->off = old_off; + s->range_end = old_range_end; + s->chunksize = old_chunksize; + s->willclose = old_willclose; +#if CONFIG_ZLIB + s->compressed = old_compressed; +#endif } + s->off = old_off; + s->end_of_resource = old_end_of_resource; av_dict_free(&options); return ret; }