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);