[PR] avformat/libcurl: various improvements (PR #23934)
Niklas Haas via ffmpeg-devel <[email protected]> Mon, 27 Jul 2026 22:34:49 -0000
| Newsgroups | gmane.comp.video.ffmpeg.devel |
|---|---|
| Message-ID | <178519169062.51.14086580488374677820@29965ddac10e> |
PR #23934 opened by Niklas Haas (haasn) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23934 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23934.patch I can split this up into smaller PRs if that makes it easier to review, just let me know. By the way, I double-checked the `c->is_initial` flow but it matches the intent exactly - once we hit the end of a given request and start the next, initial is set to 0. Note that this relies on the seek happening *before* the request was fully read, which triggers either a stream reset, a reconnect or (as of this PR) a short seek. So there's no change to be made there. >From efa65806965525df43a795c61dec89444c637304 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Wed, 24 Jun 2026 19:21:41 +0200 Subject: [PATCH 01/16] avformat/libcurl: implement -short_seek_size Since libcurl.c doesn't have to care about the exact number of bytes read, we just perform the seek but defer the actual `start_request()` call until the next on_done() callback, while simply discarding all data read in the meantime. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- doc/protocols.texi | 8 ++++++++ libavformat/libcurl.c | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/doc/protocols.texi b/doc/protocols.texi index f7e65b1c9d..1650cbf983 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -1166,9 +1166,17 @@ read request larger than this size (without a seek in between), after which the implementation will continue using requests as usual. Disabled (set to 0) by default. +Note that if enabling this option, it's strongly recommended to also set +@option{short_seek_size} to the same value or higher. + @item max_retries Maximum number of retries after a recoverable error on a seekable transfer. Default is @code{5}. + +@item short_seek_size +Set the threshold, in bytes, for when a readahead should be preferred over a seek and +new HTTP request. This is useful, for example, to make sure the same connection +is used for reading large video packets with small audio packets in between. @end table For more information see: @url{https://curl.se/libcurl/}. diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index bb5fa0f002..e3ae42558c 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -120,6 +120,7 @@ struct CurlContext { int64_t buffer_size; int64_t request_size; int64_t initial_request_size; + int64_t short_seek_size; int max_retries; int64_t logical_pos; /* next byte url_read() will return, caller side */ @@ -131,6 +132,7 @@ struct CurlContext { int64_t request_end; /* expected end of request, or -1 if unknown */ int retry_count; /* consecutive recoverable failures */ int is_initial; /* using reduced request size */ + int seek_queued; /* soft seeking; drain remaining bytes until done */ /* Per-response-block header scratch, loop thread only. */ int hdr_accept_ranges; @@ -212,6 +214,11 @@ static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdat return CURL_WRITEFUNC_ERROR; } + if (c->seek_queued) { + pthread_mutex_unlock(&c->mutex); + return bytes; /* discard */ + } + space = av_fifo_can_write(c->fifo); if (space < bytes) { /* pause the transfer and wait for the consumer to drain. */ @@ -482,6 +489,13 @@ static void on_done(CurlContext *c, CURLcode code) return; } + if (c->seek_queued && !aborted) { + /* previous soft seek drain finished; can start new request now */ + c->seek_queued = 0; + start_request(c); + return; + } + if (code == CURLE_OK && !aborted && c->stream_ok) { c->retry_count = 0; int64_t file_end = c->content_size > 0 ? c->content_size - 1 : -1; @@ -545,7 +559,15 @@ static void execute_command(CurlLoop *loop, CurlCmd *cmd) curl_easy_pause(c->easy, CURLPAUSE_CONT); break; case CMD_SEEK: - if (c->active) { + if (c->active && c->request_end >= 0 && c->short_seek_size > 0 && + c->request_end - c->request_start + 1 - c->request_received <= c->short_seek_size) + { + c->seek_queued = 1; + if (c->paused) { + curl_easy_pause(c->easy, CURLPAUSE_CONT); + c->paused = 0; + } + } else if (c->active && !c->seek_queued) { curl_multi_remove_handle(loop->multi, c->easy); c->active = 0; } @@ -555,9 +577,11 @@ static void execute_command(CurlLoop *loop, CurlCmd *cmd) c->eof = 0; c->error = 0; pthread_mutex_unlock(&c->mutex); - c->request_start = cmd->pos; - c->retry_count = 0; - start_request(c); + c->request_start = cmd->pos; + c->request_received = 0; + c->retry_count = 0; + if (!c->seek_queued) + start_request(c); break; } } @@ -1168,6 +1192,14 @@ static int libcurl_close(URLContext *h) return 0; } +static int libcurl_get_short_seek(URLContext *h) +{ + CurlContext *c = h->priv_data; + if (c->short_seek_size >= 1) + return c->short_seek_size; + return AVERROR(ENOSYS); +} + #define OFFSET(x) offsetof(CurlContext, x) #define D AV_OPT_FLAG_DECODING_PARAM #define E AV_OPT_FLAG_ENCODING_PARAM @@ -1201,6 +1233,7 @@ static const AVOption options[] = { { "2-prior-knowledge", "HTTP/2 without an upgrade handshake", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE }, 0, 0, D, .unit = "http_version" }, { "3", "HTTP/3, fall back to earlier versions", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3 }, 0, 0, D, .unit = "http_version" }, { "3only", "HTTP/3 only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3ONLY }, 0, 0, D, .unit = "http_version" }, + { "short_seek_size", "threshold to favor readahead over seek", OFFSET(short_seek_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { NULL } }; @@ -1217,6 +1250,7 @@ const URLProtocol ff_libcurl_protocol = { .url_read = libcurl_read, .url_seek = libcurl_seek, .url_close = libcurl_close, + .url_get_short_seek = libcurl_get_short_seek, .priv_data_size = sizeof(CurlContext), .priv_data_class = &libcurl_context_class, .flags = URL_PROTOCOL_FLAG_NETWORK, -- 2.52.0 >From 7fb2ced3c00390e0e69b1e920169a132d025ab87 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 25 Jun 2026 13:54:35 +0200 Subject: [PATCH 02/16] avformat/libcurl: consolidate s->error and s->eof s->eof can just be AVERROR_EOF. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 44 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index e3ae42558c..11cf6418d2 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -152,8 +152,7 @@ struct CurlContext { pthread_cond_t cond; AVFifo *fifo; int paused; /* write callback paused, FIFO was full */ - int eof; /* producer delivered all data */ - int error; /* AVERROR for an unrecoverable failure, or 0 */ + int status; /* current stream status (AVERROR code) */ int aborted; /* transfer should stop (open was interrupted) */ }; @@ -316,8 +315,8 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd "with offset %"PRId64" (expected %"PRId64")\n", content_start, c->request_start); c->stream_ok = 0; - if (!c->error) - c->error = AVERROR(EIO); + if (!c->status) + c->status = AVERROR(EIO); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return len; @@ -367,8 +366,8 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd c->seekable = c->seekable_opt; } else { c->stream_ok = 0; - if (!c->error) - c->error = ff_http_averror(status, AVERROR(EIO)); + if (!c->status) + c->status = ff_http_averror(status, AVERROR(EIO)); } c->probed = 1; pthread_cond_broadcast(&c->cond); @@ -424,8 +423,8 @@ static void start_request(CurlContext *c) curl_multi_strerror(res)); c->active = 0; pthread_mutex_lock(&c->mutex); - if (!c->error) - c->error = AVERROR(EIO); + if (!c->status) + c->status = AVERROR(EIO); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); } @@ -466,8 +465,8 @@ static void on_done(CurlContext *c, CURLcode code) received = c->request_received; /* Advance past delivered bytes so a retry or seek resumes at the right offset. */ if (received > INT64_MAX - c->request_start) { - if (!c->error) - c->error = AVERROR(EIO); + if (!c->status) + c->status = AVERROR(EIO); received = 0; aborted = 1; pthread_cond_broadcast(&c->cond); @@ -482,8 +481,8 @@ static void on_done(CurlContext *c, CURLcode code) pthread_mutex_lock(&c->mutex); c->probed = 1; c->stream_ok = 0; - if (!c->error) - c->error = curlcode_to_averror(code); + if (!c->status) + c->status = curlcode_to_averror(code); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; @@ -507,7 +506,7 @@ static void on_done(CurlContext *c, CURLcode code) return; } pthread_mutex_lock(&c->mutex); - c->eof = 1; + c->status = AVERROR_EOF; pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; @@ -526,8 +525,8 @@ static void on_done(CurlContext *c, CURLcode code) if (!aborted) { pthread_mutex_lock(&c->mutex); - if (!c->error) - c->error = curlcode_to_averror(code); + if (!c->status) + c->status = curlcode_to_averror(code); pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); } @@ -574,8 +573,7 @@ static void execute_command(CurlLoop *loop, CurlCmd *cmd) pthread_mutex_lock(&c->mutex); av_fifo_reset2(c->fifo); c->paused = 0; - c->eof = 0; - c->error = 0; + c->status = 0; pthread_mutex_unlock(&c->mutex); c->request_start = cmd->pos; c->request_received = 0; @@ -980,7 +978,7 @@ static int wait_for_probe(CurlContext *c) int ret = 0; pthread_mutex_lock(&c->mutex); - while (!c->probed && !c->error) { + while (!c->probed && !c->status) { if (ff_check_interrupt(&h->interrupt_callback)) { c->aborted = 1; ret = AVERROR_EXIT; @@ -990,7 +988,7 @@ static int wait_for_probe(CurlContext *c) } if (!ret) { if (!c->stream_ok) - ret = c->error ? c->error : AVERROR(EIO); + ret = c->status ? c->status : AVERROR(EIO); } pthread_mutex_unlock(&c->mutex); @@ -1096,12 +1094,8 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) curl_dispatch(c->loop, CMD_UNPAUSE, c, 0, 0); return n; } - if (c->error) { - ret = c->error; - break; - } - if (c->eof) { - ret = AVERROR_EOF; + if (c->status) { + ret = c->status; break; } if (nonblock) { -- 2.52.0 >From 42a961a23590e7e83920e3e3143ed0bdf783f791 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 28 Jul 2026 00:32:05 +0200 Subject: [PATCH 03/16] avutil/error: add HTTP 416 Range Not Satisfiable This is returned by HTTPds for files with unknown/growing filesize, when trying to read past what's already available. Needed to implement support for such streaming-file / incremental scenarios, both internally, and as a user-facing failure condition when e.g. the maximum retry delay is reached. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavutil/error.c | 1 + libavutil/error.h | 1 + libavutil/version.h | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libavutil/error.c b/libavutil/error.c index 2c9f0028bd..0e6395d5c4 100644 --- a/libavutil/error.c +++ b/libavutil/error.c @@ -55,6 +55,7 @@ E(HTTP_FORBIDDEN, "Server returned 403 Forbidden (access denied)") \ E(HTTP_NOT_FOUND, "Server returned 404 Not Found") \ E(HTTP_TOO_MANY_REQUESTS, "Server returned 429 Too Many Requests") \ + E(HTTP_RANGE_NOT_SATISFIABLE,"Server returned 416 Range Not Satisfiable") \ E(HTTP_OTHER_4XX, "Server returned 4XX Client Error, but not one of 40{0,1,3,4}") \ E(HTTP_SERVER_ERROR, "Server returned 5XX Server Error reply") \ diff --git a/libavutil/error.h b/libavutil/error.h index 1efa86c4c1..148717a907 100644 --- a/libavutil/error.h +++ b/libavutil/error.h @@ -79,6 +79,7 @@ #define AVERROR_HTTP_UNAUTHORIZED FFERRTAG(0xF8,'4','0','1') #define AVERROR_HTTP_FORBIDDEN FFERRTAG(0xF8,'4','0','3') #define AVERROR_HTTP_NOT_FOUND FFERRTAG(0xF8,'4','0','4') +#define AVERROR_HTTP_RANGE_NOT_SATISFIABLE FFERRTAG(0xF8,'4','1','6') #define AVERROR_HTTP_TOO_MANY_REQUESTS FFERRTAG(0xF8,'4','2','9') #define AVERROR_HTTP_OTHER_4XX FFERRTAG(0xF8,'4','X','X') #define AVERROR_HTTP_SERVER_ERROR FFERRTAG(0xF8,'5','X','X') diff --git a/libavutil/version.h b/libavutil/version.h index d5bf20cf89..4f6fa46314 100644 --- a/libavutil/version.h +++ b/libavutil/version.h @@ -80,7 +80,7 @@ #define LIBAVUTIL_VERSION_MAJOR 61 #define LIBAVUTIL_VERSION_MINOR 5 -#define LIBAVUTIL_VERSION_MICRO 100 +#define LIBAVUTIL_VERSION_MICRO 101 #define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ LIBAVUTIL_VERSION_MINOR, \ -- 2.52.0 >From 6cce72be42912dd86120eb9f3719383c789795ca Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 16:23:43 +0200 Subject: [PATCH 04/16] avformat/libcurl: early-exit on_done() when aborted Simplifies the following logic by not having to embed a check for !aborted in every conditional branch. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 11cf6418d2..32fbb0ddb8 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -488,14 +488,17 @@ static void on_done(CurlContext *c, CURLcode code) return; } - if (c->seek_queued && !aborted) { + if (aborted) + return; + + if (c->seek_queued) { /* previous soft seek drain finished; can start new request now */ c->seek_queued = 0; start_request(c); return; } - if (code == CURLE_OK && !aborted && c->stream_ok) { + if (code == CURLE_OK && c->stream_ok) { c->retry_count = 0; int64_t file_end = c->content_size > 0 ? c->content_size - 1 : -1; if (c->end_off > 0) @@ -513,7 +516,7 @@ static void on_done(CurlContext *c, CURLcode code) } /* Resume seekable transfers after a recoverable error. */ - if (!aborted && c->seekable && is_recoverable(code) && + if (c->seekable && is_recoverable(code) && c->retry_count < c->max_retries) { c->retry_count++; c->loop->num_retries++; @@ -523,13 +526,12 @@ static void on_done(CurlContext *c, CURLcode code) return; } - if (!aborted) { - pthread_mutex_lock(&c->mutex); - if (!c->status) - c->status = curlcode_to_averror(code); - pthread_cond_broadcast(&c->cond); - pthread_mutex_unlock(&c->mutex); - } + /* Unhandled generic curl error */ + pthread_mutex_lock(&c->mutex); + if (!c->status) + c->status = curlcode_to_averror(code); + pthread_cond_broadcast(&c->cond); + pthread_mutex_unlock(&c->mutex); } /* ------------------------------------------------------------------------- */ -- 2.52.0 >From d7e99a2d91bb2a7ce6d8431f126b948d38654132 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 16:19:55 +0200 Subject: [PATCH 05/16] avformat/libcurl: log all unexpected curl errors Excludes the case of !c->stream_ok (e.g. the result of an HTTP error), since in this case the actual curl errore code is likely just CURLE_WRITE_ERROR. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 32fbb0ddb8..77d4935d53 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -515,13 +515,16 @@ static void on_done(CurlContext *c, CURLcode code) return; } + if (c->stream_ok) + av_log(c->h, AV_LOG_WARNING, "%s\n", curl_easy_strerror(code)); + /* Resume seekable transfers after a recoverable error. */ if (c->seekable && is_recoverable(code) && c->retry_count < c->max_retries) { c->retry_count++; c->loop->num_retries++; - av_log(c->h, AV_LOG_WARNING, "%s, retrying (#%d) from %"PRId64"\n", - curl_easy_strerror(code), c->retry_count, c->request_start); + av_log(c->h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64"\n", + c->retry_count, c->request_start); start_request(c); return; } -- 2.52.0 >From 8f1a209f3c4b74d9896521908d7f4ff03c553418 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 16:45:58 +0200 Subject: [PATCH 06/16] avformat/libcurl: log number of errors, not retries This is arguably more informative, and also easier to account for, than the retries (which are already indirectly reflected in the number of requests and connections). More importantly, this avoids getting in the way of us refactoring the retry mechanism. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 77d4935d53..cdee8fb2d5 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -87,7 +87,7 @@ typedef struct CurlLoop { int num_connections; int num_redirects; int num_requests; - int num_retries; + int num_errors; } CurlLoop; struct CurlContext { @@ -314,6 +314,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd av_log(c->h, AV_LOG_ERROR, "Server sent back unexpected reply " "with offset %"PRId64" (expected %"PRId64")\n", content_start, c->request_start); + c->loop->num_errors++; c->stream_ok = 0; if (!c->status) c->status = AVERROR(EIO); @@ -365,6 +366,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd if (c->seekable_opt >= 0) c->seekable = c->seekable_opt; } else { + c->loop->num_errors++; c->stream_ok = 0; if (!c->status) c->status = ff_http_averror(status, AVERROR(EIO)); @@ -483,6 +485,7 @@ static void on_done(CurlContext *c, CURLcode code) c->stream_ok = 0; if (!c->status) c->status = curlcode_to_averror(code); + c->loop->num_errors++; pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); return; @@ -515,14 +518,15 @@ static void on_done(CurlContext *c, CURLcode code) return; } - if (c->stream_ok) + if (c->stream_ok) { av_log(c->h, AV_LOG_WARNING, "%s\n", curl_easy_strerror(code)); + c->loop->num_errors++; + } /* Resume seekable transfers after a recoverable error. */ if (c->seekable && is_recoverable(code) && c->retry_count < c->max_retries) { c->retry_count++; - c->loop->num_retries++; av_log(c->h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64"\n", c->retry_count, c->request_start); start_request(c); @@ -736,8 +740,8 @@ static void print_statistics(CurlLoop *loop) loop->total_bytes, time * 1e3, avg / 1e3); av_log(avfc, AV_LOG_VERBOSE, - "libcurl: %d connections, %d redirects, %d requests, %d retries\n", - loop->num_connections, loop->num_redirects, loop->num_requests, loop->num_retries); + "libcurl: %d connections, %d redirects, %d requests, %d errors\n", + loop->num_connections, loop->num_redirects, loop->num_requests, loop->num_errors); } static void curl_loop_destroy(CurlLoop *loop) -- 2.52.0 >From f032afb41abe11c193fb77398d2387966b04d0ce Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 17:08:27 +0200 Subject: [PATCH 07/16] avformat/libcurl: move retry handling to the URL thread Instead of the loop worker immediately retrying requests, this change moves the retry accounting logic to the calling thread instead. The worker thread signals AVERROR(EAGAIN) to indicate that the request should be retried by the caller. It's worth pointing out that this value can never reach wait_for_probe() in libcurl_open(), because that function only returns if !c->stream_ok, at which point we would already have bailed out with some other error. I note this simply to point out why this commit does not need to add any retry handling to libcurl_open() yet. The main motivation here is that it allows us to add a configurable delay between retries, e.g. for exponential backoff or respecting server advertised Retry-After headers. There is some ambiquity about when exactly we want to reset retry_count, with the options roughly being: 1. After fully reading a successful request to the end (old behavior) 2. After successfully reading any number of bytes (new behavior) 3. Never / only on explicit ffurl_seek() I think that both 2 and 3 are defensible, but 1 is arguably the only one that's not really useful; so this patch also represents a mild improvement in that regard - the retry counter only starts ticking up if the request fails several times in a row. The reason I think the old behavior is not useful here is because the rate of *requests* is a bit arbitrary and depends on options like -request_size. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 54 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index cdee8fb2d5..8bcc060bc7 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -123,14 +123,15 @@ struct CurlContext { int64_t short_seek_size; int max_retries; - int64_t logical_pos; /* next byte url_read() will return, caller side */ + /* URL thread bookkeeping, not touched by loop thread */ + int64_t logical_pos; /* next byte url_read() will return, caller side */ + int retry_count; /* consecutive recoverable failures */ /* Producer bookkeeping, touched only by the loop thread. */ int active; /* currently added to the multi */ int64_t request_start; /* absolute offset the current request began at */ int64_t request_received;/* bytes delivered in the current request */ int64_t request_end; /* expected end of request, or -1 if unknown */ - int retry_count; /* consecutive recoverable failures */ int is_initial; /* using reduced request size */ int seek_queued; /* soft seeking; drain remaining bytes until done */ @@ -502,7 +503,6 @@ static void on_done(CurlContext *c, CURLcode code) } if (code == CURLE_OK && c->stream_ok) { - c->retry_count = 0; int64_t file_end = c->content_size > 0 ? c->content_size - 1 : -1; if (c->end_off > 0) file_end = FFMIN(file_end, c->end_off - 1); @@ -524,12 +524,12 @@ static void on_done(CurlContext *c, CURLcode code) } /* Resume seekable transfers after a recoverable error. */ - if (c->seekable && is_recoverable(code) && - c->retry_count < c->max_retries) { - c->retry_count++; - av_log(c->h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64"\n", - c->retry_count, c->request_start); - start_request(c); + if (c->seekable && is_recoverable(code)) { + pthread_mutex_lock(&c->mutex); + if (!c->status) + c->status = AVERROR(EAGAIN); + pthread_cond_broadcast(&c->cond); + pthread_mutex_unlock(&c->mutex); return; } @@ -586,7 +586,6 @@ static void execute_command(CurlLoop *loop, CurlCmd *cmd) pthread_mutex_unlock(&c->mutex); c->request_start = cmd->pos; c->request_received = 0; - c->retry_count = 0; if (!c->seek_queued) start_request(c); break; @@ -1004,6 +1003,32 @@ static int wait_for_probe(CurlContext *c) return ret; } +static int retry_request(URLContext *h) +{ + CurlContext *c = h->priv_data; + + if (c->retry_count >= c->max_retries) { + av_log(h, AV_LOG_ERROR, "Maximum number of retries (%d) reached\n", + c->max_retries); + return AVERROR(EIO); + } + + c->retry_count++; + av_log(h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64"\n", + c->retry_count, c->logical_pos); + + /** + * Use a synchronous request to ensure that the seek is registered, and + * the reset of c->state is observable, before the next libcurl_read() + * call, otherwise this might hit the exact same retry path a second time. + */ + int ret = curl_dispatch(c->loop, CMD_SEEK, c, c->logical_pos, 1); + if (ret < 0) + return ret; + + return AVERROR(EAGAIN); /* allow caller to handle interrupts and retry */ +} + static int libcurl_open(URLContext *h, const char *url, int flags, AVDictionary **options) { @@ -1067,6 +1092,8 @@ static int libcurl_open(URLContext *h, const char *url, int flags, goto fail; ret = wait_for_probe(c); + if (ret == AVERROR(EAGAIN)) + ret = 0; /* this will be handled by the next libcurl_read() call */ if (ret < 0) goto fail; @@ -1097,13 +1124,17 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) av_fifo_read(c->fifo, buf, n); /* Resume a paused transfer once the FIFO is at least half empty. */ unpause = c->paused && av_fifo_can_write(c->fifo) * 2 >= c->buffer_size; + c->retry_count = 0; c->logical_pos += n; pthread_mutex_unlock(&c->mutex); if (unpause) curl_dispatch(c->loop, CMD_UNPAUSE, c, 0, 0); return n; } - if (c->status) { + if (c->status == AVERROR(EAGAIN)) { + pthread_mutex_unlock(&c->mutex); + return retry_request(h); + } else if (c->status) { ret = c->status; break; } @@ -1165,6 +1196,7 @@ static int64_t libcurl_seek(URLContext *h, int64_t pos, int whence) * surfaces on the following url_read(). */ curl_dispatch(c->loop, CMD_SEEK, c, newpos, 1); c->logical_pos = newpos; + c->retry_count = 0; return newpos; } -- 2.52.0 >From 84cea92c0e671f1c104b811fb25dbba8e129dec7 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:21:19 +0200 Subject: [PATCH 08/16] avformat/libcurl: allow retrying initial request Relaxes the retry logic and adds a retry loop to libcurl_open() as well, in case the *initial* request fails (e.g. due to unexpected network event before receiving all headers). This does require a bit more strictness on what type of errors we allow recovering from during the initial connection attempt. In particular, we probably don't want to retry on resolve/connect failure. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 8bcc060bc7..0f3173d03b 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -179,19 +179,23 @@ static int curlcode_to_averror(CURLcode code) } } -static int is_recoverable(CURLcode code) +static int is_recoverable(CurlContext *c, CURLcode code) { + if (!c->seekable && c->request_start > 0) + return 0; + switch (code) { case CURLE_RECV_ERROR: case CURLE_SEND_ERROR: case CURLE_PARTIAL_FILE: case CURLE_OPERATION_TIMEDOUT: case CURLE_GOT_NOTHING: - case CURLE_COULDNT_CONNECT: - case CURLE_COULDNT_RESOLVE_HOST: case CURLE_HTTP2: case CURLE_HTTP2_STREAM: return 1; + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + return c->request_start > 0; default: return 0; } @@ -484,8 +488,12 @@ static void on_done(CurlContext *c, CURLcode code) pthread_mutex_lock(&c->mutex); c->probed = 1; c->stream_ok = 0; - if (!c->status) - c->status = curlcode_to_averror(code); + if (!c->status) { + if (is_recoverable(c, code)) + c->status = AVERROR(EAGAIN); + else + c->status = curlcode_to_averror(code); + } c->loop->num_errors++; pthread_cond_broadcast(&c->cond); pthread_mutex_unlock(&c->mutex); @@ -524,7 +532,7 @@ static void on_done(CurlContext *c, CURLcode code) } /* Resume seekable transfers after a recoverable error. */ - if (c->seekable && is_recoverable(code)) { + if (is_recoverable(c, code)) { pthread_mutex_lock(&c->mutex); if (!c->status) c->status = AVERROR(EAGAIN); @@ -1091,11 +1099,14 @@ static int libcurl_open(URLContext *h, const char *url, int flags, if (ret < 0) goto fail; - ret = wait_for_probe(c); - if (ret == AVERROR(EAGAIN)) - ret = 0; /* this will be handled by the next libcurl_read() call */ - if (ret < 0) - goto fail; + do { + ret = wait_for_probe(c); + if (ret == AVERROR(EAGAIN)) { + c->probed = 0; + ret = retry_request(h); + } else if (ret < 0) + goto fail; + } while (ret == AVERROR(EAGAIN)); pthread_mutex_lock(&c->mutex); h->is_streamed = !c->seekable; -- 2.52.0 >From 6cf5a8174356430848bc8ba5b097ac00b56d8791 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:27:52 +0200 Subject: [PATCH 09/16] avformat/libcurl: also log errors without successful data transfer Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 0f3173d03b..f537d46d38 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -737,18 +737,21 @@ fail: static void print_statistics(CurlLoop *loop) { AVFormatContext *avfc = loop->avfc; - if (!loop->total_bytes) - return; - double time = (double) loop->total_time_us / 1000000.0; - double avg = time ? loop->total_bytes / time : 0; - av_log(avfc, AV_LOG_VERBOSE, - "libcurl: Overall %"PRId64" bytes received in %.0f ms = %.0f kB/s\n", - loop->total_bytes, time * 1e3, avg / 1e3); + if (loop->total_bytes) { + double time = (double) loop->total_time_us / 1000000.0; + double avg = time ? loop->total_bytes / time : 0; + av_log(avfc, AV_LOG_VERBOSE, + "libcurl: Overall %"PRId64" bytes received in %.0f ms = %.0f kB/s\n", + loop->total_bytes, time * 1e3, avg / 1e3); + } - av_log(avfc, AV_LOG_VERBOSE, - "libcurl: %d connections, %d redirects, %d requests, %d errors\n", - loop->num_connections, loop->num_redirects, loop->num_requests, loop->num_errors); + if (loop->num_connections || loop->num_errors) { + av_log(avfc, AV_LOG_VERBOSE, + "libcurl: %d connections, %d redirects, %d requests, %d errors\n", + loop->num_connections, loop->num_redirects, loop->num_requests, + loop->num_errors); + } } static void curl_loop_destroy(CurlLoop *loop) -- 2.52.0 >From 71fd25db06e6f3c3f6ead840c6862f2fcaa3f48e Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:29:08 +0200 Subject: [PATCH 10/16] avformat/libcurl: add -reconnect_on_http_error option Parity with http.c. This piggy-backs off the previously added mechanism for triggering a retry by just setting the stream status to AVERROR(EAGAIN) and letting the caller deal with it. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- doc/protocols.texi | 4 ++++ libavformat/libcurl.c | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/doc/protocols.texi b/doc/protocols.texi index 1650cbf983..6990d60000 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -1177,6 +1177,10 @@ Default is @code{5}. Set the threshold, in bytes, for when a readahead should be preferred over a seek and new HTTP request. This is useful, for example, to make sure the same connection is used for reading large video packets with small audio packets in between. + +@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'. @end table For more information see: @url{https://curl.se/libcurl/}. diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index f537d46d38..b237bc9319 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -122,6 +122,7 @@ struct CurlContext { int64_t initial_request_size; int64_t short_seek_size; int max_retries; + char *reconnect_on_http_error; /* URL thread bookkeeping, not touched by loop thread */ int64_t logical_pos; /* next byte url_read() will return, caller side */ @@ -179,7 +180,27 @@ static int curlcode_to_averror(CURLcode code) } } -static int is_recoverable(CurlContext *c, CURLcode code) +static int http_error_is_recoverable(CurlContext *c, long http_status) +{ + if (!c->reconnect_on_http_error || !http_status) + return 0; + if (!c->seekable && c->request_start > 0) + return 0; + + const char *status_group = NULL; + if (http_status >= 400 && http_status < 500) + status_group = "4xx"; + else if (http_status >= 500 && http_status < 600) + status_group = "5xx"; + if (status_group && av_match_list(status_group, c->reconnect_on_http_error, ',') > 0) + return 1; + + char http_code[4]; + snprintf(http_code, sizeof(http_code), "%ld", http_status); + return av_match_list(http_code, c->reconnect_on_http_error, ',') > 0; +} + +static int curl_error_is_recoverable(CurlContext *c, CURLcode code) { if (!c->seekable && c->request_start > 0) return 0; @@ -373,8 +394,12 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd } else { c->loop->num_errors++; c->stream_ok = 0; - if (!c->status) - c->status = ff_http_averror(status, AVERROR(EIO)); + if (!c->status) { + if (http_error_is_recoverable(c, status)) + c->status = AVERROR(EAGAIN); + else + c->status = ff_http_averror(status, AVERROR(EIO)); + } } c->probed = 1; pthread_cond_broadcast(&c->cond); @@ -489,7 +514,7 @@ static void on_done(CurlContext *c, CURLcode code) c->probed = 1; c->stream_ok = 0; if (!c->status) { - if (is_recoverable(c, code)) + if (curl_error_is_recoverable(c, code)) c->status = AVERROR(EAGAIN); else c->status = curlcode_to_averror(code); @@ -532,7 +557,7 @@ static void on_done(CurlContext *c, CURLcode code) } /* Resume seekable transfers after a recoverable error. */ - if (is_recoverable(c, code)) { + if (curl_error_is_recoverable(c, code)) { pthread_mutex_lock(&c->mutex); if (!c->status) c->status = AVERROR(EAGAIN); @@ -1283,6 +1308,7 @@ static const AVOption options[] = { { "3", "HTTP/3, fall back to earlier versions", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3 }, 0, 0, D, .unit = "http_version" }, { "3only", "HTTP/3 only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3ONLY }, 0, 0, D, .unit = "http_version" }, { "short_seek_size", "threshold to favor readahead over seek", OFFSET(short_seek_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, + { "reconnect_on_http_error", "list of http status codes to reconnect on", OFFSET(reconnect_on_http_error), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D }, { NULL } }; -- 2.52.0 >From 8363b1a03cb3ca26f3003317532a055aaf24a0ff Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:31:27 +0200 Subject: [PATCH 11/16] avformat/libcurl: add -reconnect_streamed Parity with http.c. This will just result in a non-range request, which is analogous to the situation in http.c. It's up to the server to send meaningful data in this case. It's worth pointing out that c->request_start will still continue ticking up in this case, which only affects the position reported in the reconnection message since all other code paths are additionally guarded by c->seekable. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- doc/protocols.texi | 3 +++ libavformat/libcurl.c | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/protocols.texi b/doc/protocols.texi index 6990d60000..9ae0fbc936 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -1181,6 +1181,9 @@ is used for reading large video packets with small audio packets in between. @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'. + +@item reconnect_streamed +If set then even streamed/non seekable streams will be reconnected on errors. @end table For more information see: @url{https://curl.se/libcurl/}. diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index b237bc9319..5227b29fe3 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -123,6 +123,7 @@ struct CurlContext { int64_t short_seek_size; int max_retries; char *reconnect_on_http_error; + int reconnect_streamed; /* URL thread bookkeeping, not touched by loop thread */ int64_t logical_pos; /* next byte url_read() will return, caller side */ @@ -184,7 +185,7 @@ static int http_error_is_recoverable(CurlContext *c, long http_status) { if (!c->reconnect_on_http_error || !http_status) return 0; - if (!c->seekable && c->request_start > 0) + if (!c->seekable && c->request_start > 0 && !c->reconnect_streamed) return 0; const char *status_group = NULL; @@ -202,7 +203,7 @@ static int http_error_is_recoverable(CurlContext *c, long http_status) static int curl_error_is_recoverable(CurlContext *c, CURLcode code) { - if (!c->seekable && c->request_start > 0) + if (!c->seekable && c->request_start > 0 && !c->reconnect_streamed) return 0; switch (code) { @@ -1309,6 +1310,7 @@ static const AVOption options[] = { { "3only", "HTTP/3 only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3ONLY }, 0, 0, D, .unit = "http_version" }, { "short_seek_size", "threshold to favor readahead over seek", OFFSET(short_seek_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "reconnect_on_http_error", "list of http status codes to reconnect on", OFFSET(reconnect_on_http_error), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D }, + { "reconnect_streamed", "auto reconnect streamed / non seekable streams", OFFSET(reconnect_streamed), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, D }, { NULL } }; -- 2.52.0 >From b742f8fac70d8117f1a82419fc3bfe2f3ae6cc8c Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:32:05 +0200 Subject: [PATCH 12/16] avformat/libcurl: add delay before subsequent retries Note that unlike http.c, we do not add a specific reconnect_delay_max or reconnect_delay_total_max option, because URLProtocol already has a common -rw_timeout option that may just as well use to limit the total time we're willing to wait for a transfer. There is already no expectation that a general URL protocol request will complete in any reasonable amount of time, and the -rw_timeout option is the mechanism explicitly designed to limit this. Note that we need to explicitly cache the next retry timestamp the first time the retry() function is called, and continuously compare it against the current timestamp; otherwise we would end up in an infinite loop if the caller set AVIO_FLAG_NONBLOCK, with no way to observe the result of the caller sleeping before retrying. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 50 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 5227b29fe3..ea91c87045 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -41,6 +41,7 @@ #include "avformat.h" #include "http.h" #include "internal.h" +#include "network.h" #include "url.h" #include "version.h" @@ -51,6 +52,12 @@ * callback. */ #define CURL_WAIT_US 100000 +/* How many microseconds to wait before retrying a failed request; grows + * exponentially (2^) with each consecutive failure up to MAX_US. Note that the + * first retry is always immediate. */ +#define CURL_RETRY_BASE_US 1000000 +#define CURL_RETRY_MAX_US 60000000 + typedef struct CurlContext CurlContext; enum cmd_kind { @@ -128,6 +135,7 @@ struct CurlContext { /* URL thread bookkeeping, not touched by loop thread */ int64_t logical_pos; /* next byte url_read() will return, caller side */ int retry_count; /* consecutive recoverable failures */ + int64_t retry_time; /* timestamp of next retry */ /* Producer bookkeeping, touched only by the loop thread. */ int active; /* currently added to the multi */ @@ -1040,7 +1048,18 @@ static int wait_for_probe(CurlContext *c) return ret; } -static int retry_request(URLContext *h) +/* Scales by the recurrence relationship x := 2x + 1, i.e. 2^n - 1 */ +static int64_t retry_delay(CurlContext *c) +{ + if (c->retry_count >= 64) + return CURL_RETRY_MAX_US; + int64_t factor = (1LL << c->retry_count) - 1; + if (factor >= CURL_RETRY_MAX_US / CURL_RETRY_BASE_US) + return CURL_RETRY_MAX_US; + return factor * CURL_RETRY_BASE_US; +} + +static int retry_request(URLContext *h, int nonblock) { CurlContext *c = h->priv_data; @@ -1050,16 +1069,35 @@ static int retry_request(URLContext *h) return AVERROR(EIO); } + const int64_t now = av_gettime_relative(); + if (!c->retry_time) + c->retry_time = now + retry_delay(c); + + const int64_t sleep_us = c->retry_time - now; + if (sleep_us > 0 && nonblock) + return AVERROR(EAGAIN); + else if (h->rw_timeout && sleep_us >= h->rw_timeout) + return AVERROR(EIO); + c->retry_count++; - av_log(h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64"\n", - c->retry_count, c->logical_pos); + c->retry_time = 0; + av_log(h, AV_LOG_WARNING, "Retrying (#%d) from %"PRId64" in %.3fs\n", + c->retry_count, c->logical_pos, sleep_us * 1e-6); + + int ret = ff_network_sleep_interruptible(sleep_us, &h->interrupt_callback); + if (ret != AVERROR(ETIMEDOUT)) { + pthread_mutex_lock(&c->mutex); + c->aborted = 1; + pthread_mutex_unlock(&c->mutex); + return ret; + } /** * Use a synchronous request to ensure that the seek is registered, and * the reset of c->state is observable, before the next libcurl_read() * call, otherwise this might hit the exact same retry path a second time. */ - int ret = curl_dispatch(c->loop, CMD_SEEK, c, c->logical_pos, 1); + ret = curl_dispatch(c->loop, CMD_SEEK, c, c->logical_pos, 1); if (ret < 0) return ret; @@ -1132,7 +1170,7 @@ static int libcurl_open(URLContext *h, const char *url, int flags, ret = wait_for_probe(c); if (ret == AVERROR(EAGAIN)) { c->probed = 0; - ret = retry_request(h); + ret = retry_request(h, 0); } else if (ret < 0) goto fail; } while (ret == AVERROR(EAGAIN)); @@ -1173,7 +1211,7 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) } if (c->status == AVERROR(EAGAIN)) { pthread_mutex_unlock(&c->mutex); - return retry_request(h); + return retry_request(h, nonblock); } else if (c->status) { ret = c->status; break; -- 2.52.0 >From 7ecd5109b687bca00a0adb8e3ab98fba0128f80e Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:38:55 +0200 Subject: [PATCH 13/16] avformat/libcurl: add -respect_retry_after option Feature parity with http.c. This overrides the built-in retry delay if set. It's worth pointing out that c->retry_after does not require lock on read, because it is not modified by the loop thread after the request has already failed. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- doc/protocols.texi | 5 +++++ libavformat/libcurl.c | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/protocols.texi b/doc/protocols.texi index 9ae0fbc936..33e8a103b9 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -1184,6 +1184,11 @@ include specific status codes (e.g. '503') or the strings '4xx' / '5xx'. @item reconnect_streamed If set then even streamed/non seekable streams will be reconnected on errors. + +@item respect_retry_after +If enabled, and a Retry-After header is encountered, its requested reconnection +delay will be honored, rather than using exponential backoff. Useful for 429 and +503 errors. Default enabled. @end table For more information see: @url{https://curl.se/libcurl/}. diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index ea91c87045..3cc1935921 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -131,6 +131,7 @@ struct CurlContext { int max_retries; char *reconnect_on_http_error; int reconnect_streamed; + int respect_retry_after; /* URL thread bookkeeping, not touched by loop thread */ int64_t logical_pos; /* next byte url_read() will return, caller side */ @@ -152,11 +153,12 @@ struct CurlContext { int64_t hdr_content_end; /* inclusive end, or -1 */ int64_t hdr_content_total; /* if known, or -1 */ - /* Probe result. Set by the loop thread, read by url_open() once probed. */ + /* Probe result. Set by the loop thread, read by URL thread once probed. */ int probed; int stream_ok; int seekable; int64_t content_size; + int64_t retry_after; /* Shared transfer state, guarded by mutex. */ pthread_mutex_t mutex; @@ -409,6 +411,10 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd else c->status = ff_http_averror(status, AVERROR(EIO)); } + + curl_off_t retry_after = 0; + curl_easy_getinfo(c->easy, CURLINFO_RETRY_AFTER, &retry_after); + c->retry_after = retry_after; } c->probed = 1; pthread_cond_broadcast(&c->cond); @@ -457,6 +463,7 @@ static void start_request(CurlContext *c) c->loop->num_requests++; c->request_received = 0; c->request_end = -1; + c->retry_after = 0; c->active = 1; CURLMcode res = curl_multi_add_handle(c->loop->multi, c->easy); if (res != CURLM_OK) { @@ -1051,6 +1058,12 @@ static int wait_for_probe(CurlContext *c) /* Scales by the recurrence relationship x := 2x + 1, i.e. 2^n - 1 */ static int64_t retry_delay(CurlContext *c) { + int64_t retry_after = c->retry_after; + if (c->respect_retry_after && retry_after) { + retry_after = FFMIN(retry_after, INT64_MAX / 1000000); + return retry_after * 1000000; + } + if (c->retry_count >= 64) return CURL_RETRY_MAX_US; int64_t factor = (1LL << c->retry_count) - 1; @@ -1349,6 +1362,7 @@ static const AVOption options[] = { { "short_seek_size", "threshold to favor readahead over seek", OFFSET(short_seek_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "reconnect_on_http_error", "list of http status codes to reconnect on", OFFSET(reconnect_on_http_error), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D }, { "reconnect_streamed", "auto reconnect streamed / non seekable streams", OFFSET(reconnect_streamed), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, D }, + { "respect_retry_after", "respect the Retry-After header when retrying connections", OFFSET(respect_retry_after), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, { NULL } }; -- 2.52.0 >From 9e131fa455deb24321e97f9dd7577e5a3639ed6f Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:44:20 +0200 Subject: [PATCH 14/16] avformat/libcurl: also continue sending requests on unknown size Treat files with unknown size as having infinite size, at least until we see a 416 Range Not Satisfiable. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 3cc1935921..e7a4daa90b 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -555,7 +555,9 @@ static void on_done(CurlContext *c, CURLcode code) int64_t file_end = c->content_size > 0 ? c->content_size - 1 : -1; if (c->end_off > 0) file_end = FFMIN(file_end, c->end_off - 1); - if (c->seekable && c->request_end >= 0 && c->request_end < file_end) { + if (c->seekable && c->request_end >= 0 && + (c->request_end < file_end || file_end < 0)) + { c->is_initial = 0; start_request(c); return; -- 2.52.0 >From 21b34bcd5d8db6d873e17fab890e7b17a47e6aea Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Thu, 9 Jul 2026 19:48:37 +0200 Subject: [PATCH 15/16] avformat/libcurl: don't tear down connection on HTTP error This otherwise defeats the purpose of keep-alive / -multiple_requests, especially when hitting a temporary error (429, 416, 503, etc). The CURL_WRITEFUNC_ERROR is used to interrupt curl mid-transfer, which is only really needed for c->aborted anyways. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- libavformat/libcurl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index e7a4daa90b..1911152e52 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -245,12 +245,12 @@ static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdat pthread_mutex_lock(&c->mutex); - if (c->aborted || !c->stream_ok) { + if (c->aborted) { pthread_mutex_unlock(&c->mutex); return CURL_WRITEFUNC_ERROR; } - if (c->seek_queued) { + if (c->seek_queued || !c->stream_ok) { pthread_mutex_unlock(&c->mutex); return bytes; /* discard */ } -- 2.52.0 >From 8657b843658afc5015989e93b5811f404354bff4 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Tue, 21 Jul 2026 12:08:20 +0200 Subject: [PATCH 16/16] avformat/libcurl: retry 429 and 503 errors by default These are explicitly considered temporary failures that should be retried, and a conforming server would advertise a Retry-After header that we respect. Sponsored-by: nxtedition AB Signed-off-by: Niklas Haas <[email protected]> --- doc/protocols.texi | 1 + libavformat/libcurl.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/protocols.texi b/doc/protocols.texi index 33e8a103b9..18c392d725 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -1181,6 +1181,7 @@ is used for reading large video packets with small audio packets in between. @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'. +Defaults to @code{429,503}. @item reconnect_streamed If set then even streamed/non seekable streams will be reconnected on errors. diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 1911152e52..c763b49bb8 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -1362,7 +1362,7 @@ static const AVOption options[] = { { "3", "HTTP/3, fall back to earlier versions", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3 }, 0, 0, D, .unit = "http_version" }, { "3only", "HTTP/3 only", 0, AV_OPT_TYPE_CONST, { .i64 = CURL_HTTP_VERSION_3ONLY }, 0, 0, D, .unit = "http_version" }, { "short_seek_size", "threshold to favor readahead over seek", OFFSET(short_seek_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, - { "reconnect_on_http_error", "list of http status codes to reconnect on", OFFSET(reconnect_on_http_error), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, D }, + { "reconnect_on_http_error", "list of http status codes to reconnect on", OFFSET(reconnect_on_http_error), AV_OPT_TYPE_STRING, { .str = "429,503" }, 0, 0, D }, { "reconnect_streamed", "auto reconnect streamed / non seekable streams", OFFSET(reconnect_streamed), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, D }, { "respect_retry_after", "respect the Retry-After header when retrying connections", OFFSET(respect_retry_after), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, { NULL } -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]