PR #24825 opened by Kacper Michajłow (kasper93)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24825
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24825.patch


From 62af4e891b57d0c454775cace61a0dd0c7f1410f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 01:15:57 +0200
Subject: [PATCH 01/11] avformat/libcurl: take the open result from the first
 reply
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Decide whether the open succeeded from the first reply only, so that a
follow-up request that the loop thread has already started cannot fail
the open after the first reply turned out usable.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/libcurl.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c
index 983b207722..519fcb6193 100644
--- a/libavformat/libcurl.c
+++ b/libavformat/libcurl.c
@@ -173,6 +173,7 @@ struct CurlContext {
 
     /* Probe result. Set by the loop thread, read by url_open() once probed. */
     int             probed;
+    int             probe_ok;    /* the first reply can be read */
     int             stream_ok;
     int             seekable;
     int64_t         content_size;
@@ -612,6 +613,8 @@ static size_t header_callback(char *ptr, size_t size, 
size_t nitems, void *userd
         c->stream_ok = 0;
         update_status_locked(c, ff_http_averror(status, AVERROR(EIO)), 
CURLE_OK);
     }
+    if (!c->probed)
+        c->probe_ok = c->stream_ok;
     c->probed = 1;
     pthread_cond_broadcast(&c->cond);
     pthread_mutex_unlock(&c->mutex);
@@ -1270,7 +1273,7 @@ static int wait_for_probe(CurlContext *c)
         curl_cond_wait(c);
     }
     if (!ret) {
-        if (!c->stream_ok)
+        if (!c->probe_ok)
             ret = c->status ? c->status : AVERROR(EIO);
     }
     pthread_mutex_unlock(&c->mutex);
-- 
2.52.0


From d47b6abad5bc96ee9289bc9b7ffae61b73b40013 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 01:17:11 +0200
Subject: [PATCH 02/11] avformat/libcurl: stop at a follow-up reply without
 data
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Report an error instead of issuing the same request again when a reply
that should continue the transfer delivered no data.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/libcurl.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c
index 519fcb6193..9aa5b807bf 100644
--- a/libavformat/libcurl.c
+++ b/libavformat/libcurl.c
@@ -747,6 +747,16 @@ static void on_done(CurlContext *c, CURLcode code)
         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 (!received) {
+                av_log(c->h, AV_LOG_ERROR, "Server sent back no data at offset 
"
+                       "%"PRId64"\n", c->request_start);
+                c->loop->num_errors++;
+                pthread_mutex_lock(&c->mutex);
+                update_status_locked(c, AVERROR(EIO), CURLE_OK);
+                pthread_cond_broadcast(&c->cond);
+                pthread_mutex_unlock(&c->mutex);
+                return;
+            }
             c->is_initial = 0;
             start_request(c);
             return;
-- 
2.52.0


From 8922ffef2bd3311b39f3da752858ea931a916d5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 26 Sep 2026 20:28:43 +0200
Subject: [PATCH 03/11] avformat/libcurl: don't issue requests past the end of
 the file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Report EOF without a request when the requested offset is at or past
the known file size or the end offset.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/libcurl.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c
index 9aa5b807bf..06cfca6d91 100644
--- a/libavformat/libcurl.c
+++ b/libavformat/libcurl.c
@@ -637,6 +637,17 @@ static int xferinfo_callback(void *userdata, curl_off_t 
dltotal, curl_off_t dlno
  * thread only. */
 static void start_request(CurlContext *c)
 {
+    /* At or past the end of the file, report EOF without a request. */
+    if (c->probed && c->seekable &&
+        ((c->content_size >= 0 && c->request_start >= c->content_size) ||
+         (c->end_off > 0 && c->request_start >= c->end_off))) {
+        pthread_mutex_lock(&c->mutex);
+        update_status_locked(c, AVERROR_EOF, CURLE_OK);
+        pthread_cond_broadcast(&c->cond);
+        pthread_mutex_unlock(&c->mutex);
+        return;
+    }
+
     if (!c->probed || c->seekable) {
         int64_t start = c->request_start;
         char range[48];
-- 
2.52.0


From a52ed56f6a9f9ad350c51809bbe552d4ba18b881 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 26 Sep 2026 20:28:44 +0200
Subject: [PATCH 04/11] avformat/libcurl: keep requesting when the file size is
 unknown
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Keep requesting from where each reply stopped when the file size is
unknown ("Content-Range: bytes S-E/*"). Treat a 416 on such a follow-up
request as EOF and take the size from its "bytes */N" unless N is past
the requested offset.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/libcurl.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c
index 06cfca6d91..05a47458dc 100644
--- a/libavformat/libcurl.c
+++ b/libavformat/libcurl.c
@@ -608,6 +608,13 @@ static size_t header_callback(char *ptr, size_t size, 
size_t nitems, void *userd
             c->seekable     = 0;
             c->content_size = -1;
         }
+    } else if (status == 416 && c->probed && c->content_size < 0) {
+        /* A 416 on a follow-up request of a file of unknown size is EOF. Take
+         * the size from the reply unless it is past the requested offset. */
+        if (c->hdr_content_total >= 0 && c->hdr_content_total <= 
c->request_start)
+            c->content_size = c->hdr_content_total;
+        c->stream_ok = 0;
+        update_status_locked(c, AVERROR_EOF, CURLE_OK);
     } else {
         c->loop->num_errors++;
         c->stream_ok = 0;
@@ -754,7 +761,8 @@ static void on_done(CurlContext *c, CURLcode code)
     }
 
     if (code == CURLE_OK && c->stream_ok) {
-        int64_t file_end = c->content_size > 0 ? c->content_size - 1 : -1;
+        /* Without a known size, continue until the server reports the end. */
+        int64_t file_end = c->content_size >= 0 ? c->content_size - 1 : 
INT64_MAX;
         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) {
-- 
2.52.0


From a3b4cd7f3a5a6e8c54cbec4cefb813c8ee1ec4f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 01:40:30 +0200
Subject: [PATCH 05/11] avformat/http: keep the file size when a request fails
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Restore the file size together with the offset whenever opening a
connection fails, also after too many redirects, so that the
Content-Length of an error or redirect reply does not become the size of
the resource.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/http.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index b7ae56f723..6eb9a29143 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -434,15 +434,18 @@ static int http_open_cnx(URLContext *h, AVDictionary 
**options)
     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;
     char *cached;
 
 redo:
 
     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);
@@ -515,8 +518,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;
@@ -541,6 +546,7 @@ redo:
 
 fail:
     s->off = off;
+    s->filesize = filesize;
     if (s->hd)
         ffurl_closep(&s->hd);
     if (ret < 0)
-- 
2.52.0


From 93af5b49f8e6f7ed869df836c967d3a08705b827 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 01:03:38 +0200
Subject: [PATCH 06/11] avformat/http: keep the state of the old reply when a
 seek fails
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Keep the offset after a failed seek, and restore the range end, chunk
state and connection mode as well when reading goes on over the old
connection. Refuse a seek on a streamed resource before it moves the
offset.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/http.c | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index 6eb9a29143..45b52c10e2 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -2204,6 +2204,9 @@ 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;
     uint8_t old_buf[BUFFER_SIZE];
     int old_buf_size, ret;
     AVDictionary *options = NULL;
@@ -2224,10 +2227,9 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
         return AVERROR(EINVAL);
     if (!force_reconnect && off == s->off)
         return s->off;
-    s->off = off;
-
-    if (s->off && h->is_streamed)
+    if (off && h->is_streamed)
         return AVERROR(ENOSYS);
+    s->off = off;
 
     /* do not try to make a new connection if seeking past the end of the file 
*/
     if (s->end_off || s->filesize != UINT64_MAX) {
@@ -2293,8 +2295,11 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
             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;
         }
+        s->off = old_off;
         av_dict_free(&options);
         return ret;
     }
-- 
2.52.0


From 8a5b47b70545a92c7c84ffb88d1b5fb0dcfd0d5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 01:44:53 +0200
Subject: [PATCH 07/11] avformat/http: stop at the end offset or the end of the
 file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Take the end of the data to read as the end offset or the file size,
whichever comes first, when reading, when telling a broken connection
from the end of the data, and when seeking past the end. Reading up to
the end offset then ends with EOF instead of an I/O error, and an end
offset past the end of the file no longer leads to a request past it.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/http.c | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index 45b52c10e2..e7db233803 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1843,6 +1843,11 @@ done:
     return err;
 }
 
+static uint64_t requested_end(HTTPContext *s)
+{
+    return s->end_off ? FFMIN(s->end_off, s->filesize) : s->filesize;
+}
+
 static int http_buf_read(URLContext *h, uint8_t *buf, int size)
 {
     HTTPContext *s = h->priv_data;
@@ -1897,7 +1902,7 @@ static int http_buf_read(URLContext *h, uint8_t *buf, int 
size)
         memcpy(buf, s->buf_ptr, len);
         s->buf_ptr += len;
     } else {
-        uint64_t file_end   = s->end_off   ? s->end_off   : s->filesize;
+        uint64_t file_end   = requested_end(s);
         uint64_t target_end = s->range_end ? s->range_end : file_end;
         if ((!s->willclose || s->chunksize == UINT64_MAX) && s->off >= 
file_end)
             return AVERROR_EOF;
@@ -1968,7 +1973,7 @@ static int http_read_stream(URLContext *h, uint8_t *buf, 
int size)
     int conn_attempts = 1;
 
     if (!s->hd)
-        return s->off < s->filesize ? AVERROR(EIO) : AVERROR_EOF;
+        return s->off < requested_end(s) ? AVERROR(EIO) : AVERROR_EOF;
 
     if (s->end_chunked_post && !s->end_header) {
         err = http_read_header(h);
@@ -1985,7 +1990,8 @@ retry:
     read_ret = http_buf_read(h, buf, size);
     while (read_ret < 0) {
         uint64_t target = h->is_streamed ? 0 : s->off;
-        bool is_premature = s->filesize > 0 && s->off < s->filesize;
+        uint64_t file_end = requested_end(s);
+        bool is_premature = file_end > 0 && s->off < file_end;
 
         if (read_ret == AVERROR_EXIT)
             break;
@@ -2232,11 +2238,8 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
     s->off = off;
 
     /* do not try to make a new connection if seeking past the end of the file 
*/
-    if (s->end_off || s->filesize != UINT64_MAX) {
-        uint64_t end_pos = s->end_off ? s->end_off : s->filesize;
-        if (s->off >= end_pos)
-            return s->off;
-    }
+    if (s->off >= requested_end(s))
+        return s->off;
 
     /* if the location changed (redirect), revert to the original uri */
     if (strcmp(s->uri, s->location)) {
-- 
2.52.0


From eddbfadc3fc5bbd8d60ca2b48f6d4b3fa5e6a084 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 01:45:44 +0200
Subject: [PATCH 08/11] avformat/http: drop the old reply when seeking past the
 end
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Close the connection and empty the buffer when a seek past the end is
answered without a request, so that neither the buffered bytes nor the
connection of the old reply are used at the new offset.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/http.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index e7db233803..2702679788 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -2238,8 +2238,12 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
     s->off = off;
 
     /* do not try to make a new connection if seeking past the end of the file 
*/
-    if (s->off >= requested_end(s))
+    if (s->off >= requested_end(s)) {
+        /* nothing of the old reply belongs to this offset */
+        ffurl_closep(&s->hd);
+        s->buf_ptr = s->buf_end = s->buffer;
         return s->off;
+    }
 
     /* if the location changed (redirect), revert to the original uri */
     if (strcmp(s->uri, s->location)) {
-- 
2.52.0


From f6575357a85c460ec3b7a654e1095c42dc50091d Mon Sep 17 00:00:00 2001
From: Andreas Menze <[email protected]>
Date: Fri, 11 Sep 2026 10:35:29 +0200
Subject: [PATCH 09/11] avformat/http: keep complete size for Content-Range
 "bytes A-B/*"

"bytes A-B/*" means the complete length is unknown (RFC 9110 14.4).
There were two issues here. strtoull("*") returned 0 and clobbered a
known filesize, and the 206 reply's Content-Length was published as
s->filesize, so avio_size() reported a partial size and http_buf_read()
hit EOF after the first part.

Never derive the complete size from a partial reply. Remember it
once learned from a 200 Content-Length or a "bytes A-B/N" total and
reuse it for later "*" replies. If the complete size was never learned
it stays unknown.

Fixes #24210
---
 doc/protocols.texi |  4 ++++
 libavformat/http.c | 56 +++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 57 insertions(+), 3 deletions(-)

diff --git a/doc/protocols.texi b/doc/protocols.texi
index 606ce07f3f..db2646a703 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -565,6 +565,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} or with a range that does not advance: such an end is reported
+as a clean EOF and is not retried.
 
 @item reconnect_on_network_error
 Reconnect automatically in case of TCP/TLS errors during connect.
diff --git a/libavformat/http.c b/libavformat/http.c
index 2702679788..d0473728eb 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;
 
     /******************
@@ -603,6 +607,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);
@@ -796,6 +802,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)
@@ -955,6 +963,7 @@ static int check_http_code(URLContext *h, int http_code, 
const char *end)
     if (http_code >= 400 && http_code < 600 &&
         (http_code != 401 || s->auth_state.auth_type != HTTP_AUTH_NONE) &&
         (http_code != 407 || s->proxy_auth_state.auth_type != HTTP_AUTH_NONE)) 
{
+        end += strspn(end, SPACE_CHARS);
         av_log(h, AV_LOG_WARNING, "HTTP error %d %s\n", http_code, end);
         return ff_http_averror(http_code, AVERROR(EIO));
     }
@@ -990,8 +999,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 (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 */
@@ -1564,6 +1577,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;
@@ -1598,6 +1612,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 */
@@ -1972,6 +1994,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;
 
@@ -2003,8 +2028,32 @@ retry:
             s->initial_requests = 0; /* continue streaming uninterrupted from 
now on */
             read_ret = http_open_cnx(h, &options);
             av_dict_free(&options);
-            if (read_ret == 0)
+            if (read_ret == 0) {
+                /* Don't request the same offset again. If the complete size is
+                 * known, this is a truncation, which also is not expected. */
+                if (s->range_end && s->range_end <= s->off) {
+                    if (s->filesize != UINT64_MAX && s->off < s->filesize) {
+                        av_log(h, AV_LOG_ERROR, "Non-advancing range at "
+                               "%"PRIu64".\n", s->off);
+                        read_ret = AVERROR(EIO);
+                    } else {
+                        s->end_of_resource = 1;
+                        read_ret = AVERROR_EOF;
+                    }
+                    break;
+                }
                 goto retry;
+            }
+            if (read_ret == AVERROR_EXIT)
+                break;
+             /* 416 for unknown-size resource is EOF, otherwise it is an error 
*/
+            if (read_ret == AVERROR_HTTP_OTHER_4XX && s->http_code == 416 &&
+                s->oc_filesize == UINT64_MAX) {
+                s->filesize = s->oc_filesize; /* the 416 reply is not a size */
+                s->end_of_resource = 1;
+                read_ret = AVERROR_EOF;
+                break;
+            }
         }
 
         if (h->is_streamed && !s->reconnect_streamed)
@@ -2236,6 +2285,7 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
     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->off >= requested_end(s)) {
-- 
2.52.0


From 20a326d76c4aff86b76439626253ac85d24a2446 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 00:29:08 +0200
Subject: [PATCH 10/11] avformat/http: treat a 416 on seek past the end as EOF
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Move the conversion of a 416 reply at the end of a resource of unknown
size into a helper and use it for seeks too.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/http.c | 28 +++++++++++++++++++++-------
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index d0473728eb..3ea1f37b85 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1983,6 +1983,17 @@ static int http_buf_read_compressed(URLContext *h, 
uint8_t *buf, int size)
 }
 #endif /* CONFIG_ZLIB */
 
+/* A 416 reply for a resource of unknown size marks its end. */
+static int http_end_of_resource(HTTPContext *s, int err)
+{
+    if (err != AVERROR_HTTP_OTHER_4XX || s->http_code != 416 ||
+        s->oc_filesize != UINT64_MAX)
+        return 0;
+    s->filesize = s->oc_filesize; /* the 416 reply is not a size */
+    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)
@@ -2046,11 +2057,7 @@ retry:
             }
             if (read_ret == AVERROR_EXIT)
                 break;
-             /* 416 for unknown-size resource is EOF, otherwise it is an error 
*/
-            if (read_ret == AVERROR_HTTP_OTHER_4XX && s->http_code == 416 &&
-                s->oc_filesize == UINT64_MAX) {
-                s->filesize = s->oc_filesize; /* the 416 reply is not a size */
-                s->end_of_resource = 1;
+            if (http_end_of_resource(s, read_ret)) {
                 read_ret = AVERROR_EOF;
                 break;
             }
@@ -2085,6 +2092,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);
     }
@@ -2262,6 +2273,7 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
     uint64_t old_range_end = s->range_end;
     uint64_t old_chunksize = s->chunksize;
     int old_willclose = s->willclose;
+    int old_end_of_resource = s->end_of_resource;
     uint8_t old_buf[BUFFER_SIZE];
     int old_buf_size, ret;
     AVDictionary *options = NULL;
@@ -2332,7 +2344,7 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
         }
 
         ret = http_open_cnx(h, &options);
-        if (ret >= 0) {
+        if (ret >= 0 || http_end_of_resource(s, ret)) {
             goto done;
         } else {
             /* fall back to normal reconnection */
@@ -2345,7 +2357,8 @@ 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)) < 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);
@@ -2357,6 +2370,7 @@ static int64_t http_seek_internal(URLContext *h, int64_t 
off, int whence, int fo
             s->willclose = old_willclose;
         }
         s->off = old_off;
+        s->end_of_resource = old_end_of_resource;
         av_dict_free(&options);
         return ret;
     }
-- 
2.52.0


From 615de714c2ebc014b7924457e3291b6060b0bb62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Thu, 1 Oct 2026 00:31:13 +0200
Subject: [PATCH 11/11] avformat/http: take the complete length from a 416
 reply
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Take the complete length from the Content-Range of a 416 reply at the
end of a resource of unknown size ("bytes */N") when it does not exceed
the refused offset, so that later reads and seeks end at the known size
without another request.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/http.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/libavformat/http.c b/libavformat/http.c
index 3ea1f37b85..a86018753a 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1989,7 +1989,10 @@ static int http_end_of_resource(HTTPContext *s, int err)
     if (err != AVERROR_HTTP_OTHER_4XX || s->http_code != 416 ||
         s->oc_filesize != UINT64_MAX)
         return 0;
-    s->filesize = s->oc_filesize; /* the 416 reply is not a size */
+    if (s->filesize_from_content_range != UINT64_MAX &&
+        s->filesize_from_content_range <= s->off)
+        s->oc_filesize = s->filesize_from_content_range;
+    s->filesize = s->oc_filesize;
     s->end_of_resource = 1;
     return 1;
 }
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to