Commit d9242028a6 for ffmpeg

commit d9242028a6362b86dffa254b696c36bc41fac620
Author: Kacper Michajłow <kasper93@gmail.com>
Date:   Sun Oct 11 00:59:20 2026 +0200

    avformat/hls: fix reading byte-range segments after the first one

    Seeking an input opened with the http or libcurl "offset" option to the
    next segment's offset does not work, because its AVIOContext counts from
    0 while the protocol seeks to file offsets, so the seek skipped as many
    bytes as the request started at. A seek also cannot extend a request past
    its end offset. Keep reading from the same input when the next segment
    follows within the requested range, and open it with a new request
    otherwise. Keep the requested end with each input, extend prefetched
    requests over their run too, and reuse a connection only after its reply
    was read to the end.

    Only hls.c and dashdec.c pass "offset". The AVIOContext starts its
    position at 0 and never learns the protocol's start offset. Initializing
    the position from the protocol in ffio_fdopen would fix that for
    everyone, but the crypto and cache protocols and probing with an offset
    report positions in their own coordinates and break with it, so avoid
    the seek here instead.

    Note that external users that pass "offset" themselves see the same
    thing, positions and seeks on such an input are relative to the request
    start rather than to the file, and this commit only avoids the seek in
    hls.c instead of fixing that accounting.

    Fixes: 0616685b1e
    Fixes: #25000
    Signed-off-by: Kacper Michajłow <kasper93@gmail.com>

diff --git a/libavformat/hls.c b/libavformat/hls.c
index 2c7121a217..506c8c0f49 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -106,9 +106,10 @@ struct playlist {
     FFIOContext pb;
     AVIOContext *input;
     int input_read_done;
-    int input_reuse;
+    int64_t input_end; /* end of the byte range requested on input, or -1 */
     AVIOContext *input_next;
     int input_next_requested;
+    int64_t input_next_end;
     AVFormatContext *parent;
     int index;
     AVFormatContext *ctx;
@@ -298,7 +299,6 @@ static void free_playlist_list(HLSContext *c)
         av_freep(&pls->pb.pub.buffer);
         ff_format_io_close(c->ctx, &pls->input);
         pls->input_read_done = 0;
-        pls->input_reuse = 0;
         ff_format_io_close(c->ctx, &pls->input_next);
         pls->input_next_requested = 0;
         if (pls->ctx) {
@@ -1203,21 +1203,36 @@ static struct segment *next_segment(struct playlist *pls)
     return pls->segments[n];
 }

-/* True if 'next' can be reached by seeking the open 'in' instead of reopening.
- * An unencrypted, contiguous byte range directly following 'cur' in the same
- * seekable resource (i.e. consecutive EXT-X-BYTERANGE segments). */
-static int segment_reusable(AVIOContext *in, const struct segment *cur,
-                            const struct segment *next)
+/* True if 'next' is an unencrypted byte range that directly follows 'cur' in
+ * the same resource and ends within the range requested up to 'end', so that
+ * it is read on from the input of 'cur' (i.e. consecutive EXT-X-BYTERANGE
+ * segments). */
+static int segment_continues(const struct segment *cur,
+                             const struct segment *next, int64_t end)
 {
-    return in && cur && next &&
-           (in->seekable & AVIO_SEEKABLE_NORMAL) &&
+    return cur && next &&
            cur->size >= 0 && next->size >= 0 &&
            next->url_offset == cur->url_offset + cur->size &&
+           next->url_offset + next->size <= end &&
            cur->key_type == KEY_NONE && next->key_type == KEY_NONE &&
            next->init_section == cur->init_section &&
            !strcmp(next->url, cur->url);
 }

+static int64_t range_end(struct playlist *pls, const struct segment *seg)
+{
+    int64_t n = pls->cur_seq_no - pls->start_seq_no + 1;
+
+    if (seg == next_segment(pls))
+        n++;
+    else if (seg != current_segment(pls))
+        return seg->url_offset + seg->size;
+    for (; n < pls->n_segments &&
+           segment_continues(seg, pls->segments[n], INT64_MAX); n++)
+        seg = pls->segments[n];
+    return seg->url_offset + seg->size;
+}
+
 static int read_from_url(struct playlist *pls, struct segment *seg,
                          uint8_t *buf, int buf_size)
 {
@@ -1467,54 +1482,23 @@ static int read_key(HLSContext *c, struct playlist *pls, struct segment *seg)
     return 0;
 }

-static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg, AVIOContext **in)
+static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
+                      AVIOContext **in, int64_t *end)
 {
     AVDictionary *opts = NULL;
     int ret;
     int is_http = 0;

-    /* Reuse a kept-open connection to the same resource by seeking instead of
-     * reopening. For contiguous ranges the seek is a no-op and issues no request. */
-    if (*in && pls->input_reuse && seg->key_type == KEY_NONE &&
-        ((*in)->seekable & AVIO_SEEKABLE_NORMAL)) {
-        int64_t seek_ret = avio_seek(*in, seg->url_offset, SEEK_SET);
-        pls->input_reuse = 0;
-        if (seek_ret >= 0) {
-            av_log(pls->parent, AV_LOG_VERBOSE,
-                   "HLS reusing connection for url '%s', offset %"PRId64", playlist %d\n",
-                   seg->url, seg->url_offset, pls->index);
-            pls->input_read_done = 0;
-            pls->cur_seg_offset = 0;
-            return 0;
-        }
-        /* Seek failed: drop the connection and reopen. */
-        ff_format_io_close(pls->parent, in);
-    }
-    pls->input_reuse = 0;
-
     if (c->http_persistent)
         av_dict_set(&opts, "multiple_requests", "1", 0);

+    *end = -1;
     if (seg->size >= 0) {
-        /* Restrict the request to the wanted byte range. The end is extended
-         * across following contiguous segments of the same resource so one
-         * connection serves the whole run. */
-        int64_t end_offset = seg->url_offset + seg->size;
-        if (seg == current_segment(pls)) {
-            int64_t n = pls->cur_seq_no - pls->start_seq_no + 1;
-            while (n < pls->n_segments) {
-                struct segment *ns = pls->segments[n];
-                if (ns->size < 0 || ns->url_offset != end_offset ||
-                    ns->key_type != KEY_NONE ||
-                    ns->init_section != seg->init_section ||
-                    strcmp(ns->url, seg->url))
-                    break;
-                end_offset = ns->url_offset + ns->size;
-                n++;
-            }
-        }
+        /* Restrict the request to the wanted byte range, extended so that one
+         * request serves the whole run of segments that continue it. */
+        *end = range_end(pls, seg);
         av_dict_set_int(&opts, "offset", seg->url_offset, 0);
-        av_dict_set_int(&opts, "end_offset", end_offset, 0);
+        av_dict_set_int(&opts, "end_offset", *end, 0);
     }

     av_log(pls->parent, AV_LOG_VERBOSE, "HLS request for url '%s', offset %"PRId64", playlist %d\n",
@@ -1590,7 +1574,7 @@ static int update_init_section(struct playlist *pls, struct segment *seg)
     if (!seg->init_section)
         return 0;

-    ret = open_input(c, pls, seg->init_section, &pls->input);
+    ret = open_input(c, pls, seg->init_section, &pls->input, &pls->input_end);
     if (ret < 0) {
         av_log(pls->parent, AV_LOG_WARNING,
                "Failed to open an initialization section in playlist %d\n",
@@ -1794,11 +1778,12 @@ restart:

         if (c->http_multiple == 1 && v->input_next_requested) {
             FFSWAP(AVIOContext *, v->input, v->input_next);
+            FFSWAP(int64_t, v->input_end, v->input_next_end);
             v->cur_seg_offset = 0;
             v->input_next_requested = 0;
             ret = 0;
         } else {
-            ret = open_input(c, v, seg, &v->input);
+            ret = open_input(c, v, seg, &v->input, &v->input_end);
         }
         if (ret < 0) {
             if (ff_check_interrupt(c->interrupt_callback))
@@ -1819,6 +1804,7 @@ restart:
         }
         segment_retries = 0;
         just_opened = 1;
+        v->input_read_done = 0;
         if (v->first_read_seq_no < 0)
             v->first_read_seq_no = v->cur_seq_no;
     }
@@ -1835,8 +1821,8 @@ restart:
     seg = next_segment(v);
     if (c->http_multiple == 1 && !v->input_next_requested &&
         seg && seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL) &&
-        !segment_reusable(v->input, current_segment(v), seg)) {
-        ret = open_input(c, v, seg, &v->input_next);
+        !segment_continues(current_segment(v), seg, v->input_end)) {
+        ret = open_input(c, v, seg, &v->input_next, &v->input_next_end);
         if (ret < 0) {
             if (ff_check_interrupt(c->interrupt_callback))
                 return AVERROR_EXIT;
@@ -1867,16 +1853,21 @@ restart:

         return ret;
     }
-    if (ret == 0 && segment_reusable(v->input, seg, next_segment(v))) {
-        /* Clean boundary, and the next segment continues this resource. Keep
-         * the connection open and read it as a whole. Note that splitting
-         * segments in these cases is useful for dynamic variant/quality
-         * switching, but this is not supported by our HLS demuxer, so we
-         * join the ranges. */
-        v->input_reuse = 1;
-        v->input_read_done = 1;
-    } else if (c->http_persistent && is_native_http(v->input) &&
-        seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL)) {
+    if (ret == 0 && segment_continues(seg, next_segment(v), v->input_end)) {
+        /* The next segment follows within the requested range and is read
+         * on from the same input. */
+        v->cur_seq_no++;
+        c->cur_seq_no = v->cur_seq_no;
+        v->cur_seg_offset = 0;
+        just_opened = 1;
+        goto restart;
+    }
+    /* Keep the connection for the next request only if its reply was read
+     * to the end. */
+    if (c->http_persistent && is_native_http(v->input) &&
+        seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL) &&
+        (seg->size >= 0 ? ret == 0 && seg->url_offset + seg->size == v->input_end
+                        : ret == AVERROR_EOF)) {
         v->input_read_done = 1;
     } else {
         ff_format_io_close(v->parent, &v->input);
@@ -1902,7 +1893,7 @@ static int read_data_subtitle_segment(void *opaque, uint8_t *buf, int buf_size)
     }

     if (!v->input) {
-        ret = open_input(c, v, seg, &v->input);
+        ret = open_input(c, v, seg, &v->input, &v->input_end);
         if (ret < 0) {
             if (ff_check_interrupt(c->interrupt_callback))
                 return AVERROR_EXIT;
@@ -2482,7 +2473,6 @@ static int hls_read_header(AVFormatContext *s)
             ff_format_io_close(pls->parent, &pls->input);
             pls->input = NULL;
             pls->input_read_done = 0;
-            pls->input_reuse = 0;
             ff_format_io_close(pls->parent, &pls->input_next);
             pls->input_next = NULL;
             pls->input_next_requested = 0;
@@ -2656,7 +2646,6 @@ static int recheck_discard_flags(AVFormatContext *s, int first)
         } else if (first && !cur_needed && pls->needed) {
             ff_format_io_close(pls->parent, &pls->input);
             pls->input_read_done = 0;
-            pls->input_reuse = 0;
             ff_format_io_close(pls->parent, &pls->input_next);
             pls->input_next_requested = 0;
             if (pls->is_subtitle)
@@ -3043,7 +3032,6 @@ static int hls_read_seek(AVFormatContext *s, int stream_index,
         ff_format_io_close(pls->parent, &pls->input);
         pls->input_read_done = 0;
         pls->first_read_seq_no = -1;
-        pls->input_reuse = 0;
         ff_format_io_close(pls->parent, &pls->input_next);
         pls->input_next_requested = 0;
         av_packet_unref(pls->pkt);