PR #25385 opened by danog URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25385 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25385.patch
`http_multiple` opens the next segment while the current one is read, but AES-128 encrypted streams never benefited, so live streams read in real time stall at every segment boundary while a new connection is set up. 1. **avformat/crypto: expose the wrapped protocol context as a child.** `http_multiple` auto-detection reads `http_version` through the segment input with `AV_OPT_SEARCH_CHILDREN`, which didn't reach through the crypto protocol. Other child lookups, e.g. cookies and `mime_type`, now reach the nested protocol too. 2. **avformat/hls: don't retry a failed prefetch on every read.** A failed early open used to be retried on every read of the current segment, and with encryption that repeats the key request each time. It's now left to the normal open at the segment boundary. 3. **avformat/hls: prefetch AES-128 encrypted segments.** Each AES-128 segment gets its own key and IV when opened, so these segments are prefetched too. SAMPLE-AES keeps the current behaviour, and AES-128 segments aren't prefetched while a SAMPLE-AES segment is being read, because opening one replaces the key that segment needs. Decoding was checked to be bit-exact against plaintext and unpatched master. The test streams used key rotation, a switch to unencrypted segments, byte ranges, encrypted fMP4 init sections, seeking and a missing key. FATE has no HTTP server, so there's no FATE test. Kodi carries this as a patch in https://github.com/xbmc/xbmc/pull/30047, where it fixes stalls on encrypted live channels; its maintainers asked for FFmpeg changes to go upstream. >From f3076b46c786ec5e30f62d94a9a943a2a0e9e14a Mon Sep 17 00:00:00 2001 From: Daniil Gentili <[email protected]> Date: Fri, 23 Oct 2026 12:06:42 +0200 Subject: [PATCH 1/3] avformat/crypto: expose the wrapped protocol context as a child AVOptions of the underlying protocol, such as http_version, could not be read through a crypto context with AV_OPT_SEARCH_CHILDREN. The hls demuxer relies on this to detect whether it can use multiple HTTP connections, so http_multiple could not be auto-detected while the current segment was AES-128 encrypted. This also lets other child lookups on crypto inputs reach the nested protocol, e.g. cookies set by encrypted segment responses are now picked up by the hls demuxer, and mime_type is found. Signed-off-by: Daniil Gentili <[email protected]> --- libavformat/crypto.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libavformat/crypto.c b/libavformat/crypto.c index 4bc47f01282..a23dc1f34d6 100644 --- a/libavformat/crypto.c +++ b/libavformat/crypto.c @@ -76,11 +76,18 @@ static const AVOption crypto_options[] = { { NULL } }; +static void *crypto_child_next(void *obj, void *prev) +{ + CryptoContext *c = obj; + return prev ? NULL : c->hd; +} + static const AVClass crypto_class = { .class_name = "crypto", .item_name = av_default_item_name, .option = crypto_options, .version = LIBAVUTIL_VERSION_INT, + .child_next = crypto_child_next, }; static int set_aes_arg(URLContext *h, uint8_t **buf, int *buf_len, -- 2.54.0 >From 669a3705bbc2b4bb548592c05bb0a42c0f2378ec Mon Sep 17 00:00:00 2001 From: Daniil Gentili <[email protected]> Date: Fri, 23 Oct 2026 12:06:49 +0200 Subject: [PATCH 2/3] avformat/hls: don't retry a failed prefetch on every read When opening the next segment ahead of time fails, it is attempted again on every read of the current segment, each time with a new request and a warning. Leave it to the normal open once the segment is reached. Signed-off-by: Daniil Gentili <[email protected]> --- libavformat/hls.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index c68b5e88cf5..cbbc40925fd 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -1925,13 +1925,14 @@ restart: if (ret) return ret; - if (c->http_multiple == 1 && v->input_next_requested) { + if (c->http_multiple == 1 && v->input_next) { 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 { + v->input_next_requested = 0; ret = open_input(c, v, seg, &v->input, &v->input_end); } if (ret < 0) { @@ -1978,9 +1979,9 @@ restart: av_log(v->parent, AV_LOG_WARNING, "Failed to open segment %"PRId64" of playlist %d\n", v->cur_seq_no + 1, v->index); - } else { - v->input_next_requested = 1; } + /* After a failure, the segment is opened again when it is reached */ + v->input_next_requested = 1; } if (v->init_sec_buf_read_offset < v->init_sec_data_len) { -- 2.54.0 >From f98269350d9c55d8dded912d251700443d39d2d1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili <[email protected]> Date: Fri, 23 Oct 2026 12:07:11 +0200 Subject: [PATCH 3/3] avformat/hls: prefetch AES-128 encrypted segments With http_multiple the next segment is opened while the current one is read, but only for unencrypted segments. AES-128 encrypted streams therefore open every segment from scratch at the segment boundary, which on live streams read in real time (e.g. with 10 s segments) stalls reading while a new connection is set up and the first byte arrives. AES-128 segments get their own key and IV when they are opened, so they can be prefetched as well. SAMPLE-AES is decrypted with the playlist key and keeps the current behaviour, and AES-128 segments are not prefetched while a SAMPLE-AES segment is read, as opening them replaces that key. Signed-off-by: Daniil Gentili <[email protected]> --- libavformat/hls.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index cbbc40925fd..09f65e740f2 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -1901,7 +1901,7 @@ static int read_data_continuous(void *opaque, uint8_t *buf, int buf_size) int ret; int just_opened = 0; int segment_retries = 0; - struct segment *seg; + struct segment *seg, *cur; if (c->http_persistent && v->input_read_done) { av_assert0(v->input); @@ -1969,9 +1969,15 @@ restart: } seg = next_segment(v); + cur = current_segment(v); + /* AES-128 segments get their own key and IV when opened, so they can be + * prefetched too, but not while a SAMPLE-AES segment, which is decrypted + * with the playlist key, is still being read. */ if (c->http_multiple == 1 && !v->input_next_requested && - seg && seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL) && - !segment_continues(current_segment(v), seg, v->input_end)) { + seg && (seg->key_type == KEY_NONE || + (seg->key_type == KEY_AES_128 && cur->key_type != KEY_SAMPLE_AES)) && + av_strstart(seg->url, "http", NULL) && + !segment_continues(cur, 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)) -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
