PR #25383 opened by danog
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25383
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25383.patch

Players that only ever play one variant of a Master Playlist still pay for 
downloading the Media Playlist and first segment of every variant when the hls 
demuxer opens it. Opening a live stream with five variants took 24 requests and 
about 4 MB in testing; with only the selected variant it takes 12 requests and 
1.9 MB.

This adds two options:
- `load_all_variants` (default 1): when disabled, only one variant and the 
renditions of its groups are loaded.
- `max_bandwidth` (default 0, no limit): the variant with the highest 
RESOLUTION, then BANDWIDTH, among those within the limit is kept, or the lowest 
BANDWIDTH one if none fits.

The first commit is a no-functional-change refactor. Default behaviour is 
unchanged. FATE tests cover the default selection, the bandwidth limit and the 
resolution preference.

Kodi carries this as a patch in https://github.com/xbmc/xbmc/pull/30046 (Kodi 
already passed `load_all_variants=0`, an option from its former FFmpeg fork); 
its maintainers asked for it to be submitted here instead.



>From e8009d42d51672dbbc3787161510b6fca6f1aeee Mon Sep 17 00:00:00 2001
From: Daniil Gentili <[email protected]>
Date: Thu, 22 Oct 2026 23:42:44 +0200
Subject: [PATCH 1/2] avformat/hls: factor out freeing a playlist

No functional change.

Signed-off-by: Daniil Gentili <[email protected]>
---
 libavformat/hls.c | 49 +++++++++++++++++++++++++----------------------
 1 file changed, 26 insertions(+), 23 deletions(-)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index c68b5e88cf5..6ea5a982b4a 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -314,32 +314,35 @@ static void free_init_section_list(struct playlist *pls)
     pls->n_init_sections = 0;
 }
 
+static void free_playlist(HLSContext *c, struct playlist *pls)
+{
+    free_segment_list(pls);
+    free_init_section_list(pls);
+    av_freep(&pls->main_streams);
+    av_freep(&pls->renditions);
+    av_freep(&pls->id3_buf);
+    av_dict_free(&pls->id3_initial);
+    av_dict_free(&pls->timed_id3_metadata);
+    ff_id3v2_free_extra_meta(&pls->id3_deferred_extra);
+    av_freep(&pls->init_sec_buf);
+    av_packet_free(&pls->pkt);
+    av_freep(&pls->pb.pub.buffer);
+    ff_format_io_close(c->ctx, &pls->input);
+    pls->input_read_done = 0;
+    ff_format_io_close(c->ctx, &pls->input_next);
+    pls->input_next_requested = 0;
+    if (pls->ctx) {
+        pls->ctx->pb = NULL;
+        avformat_close_input(&pls->ctx);
+    }
+    av_free(pls);
+}
+
 static void free_playlist_list(HLSContext *c)
 {
     int i;
-    for (i = 0; i < c->n_playlists; i++) {
-        struct playlist *pls = c->playlists[i];
-        free_segment_list(pls);
-        free_init_section_list(pls);
-        av_freep(&pls->main_streams);
-        av_freep(&pls->renditions);
-        av_freep(&pls->id3_buf);
-        av_dict_free(&pls->id3_initial);
-        av_dict_free(&pls->timed_id3_metadata);
-        ff_id3v2_free_extra_meta(&pls->id3_deferred_extra);
-        av_freep(&pls->init_sec_buf);
-        av_packet_free(&pls->pkt);
-        av_freep(&pls->pb.pub.buffer);
-        ff_format_io_close(c->ctx, &pls->input);
-        pls->input_read_done = 0;
-        ff_format_io_close(c->ctx, &pls->input_next);
-        pls->input_next_requested = 0;
-        if (pls->ctx) {
-            pls->ctx->pb = NULL;
-            avformat_close_input(&pls->ctx);
-        }
-        av_free(pls);
-    }
+    for (i = 0; i < c->n_playlists; i++)
+        free_playlist(c, c->playlists[i]);
     av_freep(&c->playlists);
     c->n_playlists = 0;
     av_tree_destroy(c->playlists_by_url);
-- 
2.54.0


>From 431e9dc60f5f79016d5e58639767156598922886 Mon Sep 17 00:00:00 2001
From: Daniil Gentili <[email protected]>
Date: Thu, 22 Oct 2026 23:42:44 +0200
Subject: [PATCH 2/2] avformat/hls: add an option to load a single variant

When opening a Master Playlist, the demuxer fetches the Media Playlist
and the first segment of every variant before returning, although a
player usually plays only one of them. For example opening a live
stream with five variants took 24 requests and about 4 MB in testing;
with only the selected variant it takes 12 requests and 1.9 MB.

Add load_all_variants: when disabled, only the variant with the highest
RESOLUTION, then BANDWIDTH, not above the new max_bandwidth option (or
the lowest BANDWIDTH one if none fits) is loaded, together with the
renditions of its groups. The default behaviour is unchanged.

Signed-off-by: Daniil Gentili <[email protected]>
---
 doc/demuxers.texi                             |  14 +++
 libavformat/hls.c                             | 103 ++++++++++++++++++
 tests/fate/demux.mak                          |  29 +++++
 tests/ref/fate/hls-single-variant-demux       |   5 +
 .../hls-single-variant-max-bandwidth-demux    |   5 +
 .../fate/hls-single-variant-resolution-demux  |   5 +
 6 files changed, 161 insertions(+)
 create mode 100644 tests/ref/fate/hls-single-variant-demux
 create mode 100644 tests/ref/fate/hls-single-variant-max-bandwidth-demux
 create mode 100644 tests/ref/fate/hls-single-variant-resolution-demux

diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index 76100551931..b550324ddce 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -619,6 +619,20 @@ Set options for the demuxer of media segments using a list 
of key=value pairs se
 @item seg_max_retry
 Maximum number of times to reload a segment on error, useful when segment skip 
on network error is not desired.
 Default value is 0.
+
+@item load_all_variants
+Load all variants of a Master Playlist. If disabled, only the variant
+selected by @option{max_bandwidth} and the renditions it references are
+loaded, so the playlists and segments of the other variants are never
+downloaded. @code{CODECS} is not taken into account, and opening fails if
+the selected variant cannot be loaded. Default is enabled.
+
+@item max_bandwidth
+When @option{load_all_variants} is disabled, load the variant with the
+highest @code{RESOLUTION}, then @code{BANDWIDTH}, among those with a
+@code{BANDWIDTH} not above this value in bits per second, or the variant
+with the lowest @code{BANDWIDTH} if none fits. 0 means no limit, which is
+the default.
 @end table
 
 @section image2
diff --git a/libavformat/hls.c b/libavformat/hls.c
index 6ea5a982b4a..ed3636821b6 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -37,6 +37,7 @@
 #include "libavutil/mathematics.h"
 #include "libavutil/mem.h"
 #include "libavutil/opt.h"
+#include "libavutil/parseutils.h"
 #include "libavutil/dict.h"
 #include "libavutil/time.h"
 #include "libavutil/tree.h"
@@ -215,6 +216,7 @@ struct rendition {
 
 struct variant {
     int bandwidth;
+    int64_t pixels;
 
     /* every variant contains at least the main Media Playlist in index 0 */
     int n_playlists;
@@ -281,6 +283,8 @@ typedef struct HLSContext {
     int http_multiple;
     int http_seekable;
     int seg_max_retry;
+    int load_all_variants;
+    int64_t max_bandwidth;
     AVIOContext *playlist_pb;
     HLSCryptoContext  crypto_ctx;
 } HLSContext;
@@ -441,6 +445,7 @@ fail:
 
 struct variant_info {
     char bandwidth[20];
+    char resolution[24];
     /* variant group ids: */
     char audio[MAX_FIELD_LEN];
     char video[MAX_FIELD_LEN];
@@ -462,7 +467,11 @@ static struct variant *new_variant(HLSContext *c, struct 
variant_info *info,
         return NULL;
 
     if (info) {
+        int w = 0, h = 0;
+
         var->bandwidth = atoi(info->bandwidth);
+        if (info->resolution[0] && av_parse_video_size(&w, &h, 
info->resolution) >= 0)
+            var->pixels = (int64_t)w * h;
         strcpy(var->audio_group, info->audio);
         strcpy(var->video_group, info->video);
         strcpy(var->subtitles_group, info->subtitles);
@@ -480,6 +489,9 @@ static void handle_variant_args(void *context, const char 
*key,
     if (!strncmp(key, "BANDWIDTH=", key_len)) {
         *dest     =        info->bandwidth;
         *dest_len = sizeof(info->bandwidth);
+    } else if (!strncmp(key, "RESOLUTION=", key_len)) {
+        *dest     =        info->resolution;
+        *dest_len = sizeof(info->resolution);
     } else if (!strncmp(key, "AUDIO=", key_len)) {
         *dest     =        info->audio;
         *dest_len = sizeof(info->audio);
@@ -2455,6 +2467,89 @@ static void update_noheader_flag(AVFormatContext *s)
         s->ctx_flags &= ~AVFMTCTX_NOHEADER;
 }
 
+/* Remove a playlist that is not part of the selected variant */
+static void drop_playlist(HLSContext *c, struct playlist *pls)
+{
+    struct AVTreeNode *node = NULL;
+
+    av_tree_insert(&c->playlists_by_url, pls, cmp_playlist_url, &node);
+    av_free(node);
+
+    for (int i = 0; i < c->n_renditions; i++)
+        if (c->renditions[i]->playlist == pls)
+            c->renditions[i]->playlist = NULL;
+
+    for (int i = 0; i < c->n_rendition_groups; i++) {
+        struct rendition_group *group = c->rendition_groups[i];
+        int n = 0;
+
+        for (int j = 0; j < group->n_playlists; j++)
+            if (group->playlists[j] != pls)
+                group->playlists[n++] = group->playlists[j];
+        group->n_playlists = n;
+    }
+
+    free_playlist(c, pls);
+}
+
+static int playlist_in_group(HLSContext *c, const struct playlist *pls,
+                             enum AVMediaType type, const char *id)
+{
+    const struct rendition_group *group;
+
+    if (!id[0] || !(group = find_rendition_group(c, type, id)))
+        return 0;
+    for (int i = 0; i < group->n_playlists; i++)
+        if (group->playlists[i] == pls)
+            return 1;
+    return 0;
+}
+
+/*
+ * Keep only the variant with the highest resolution, then bandwidth, not
+ * above max_bandwidth (or the one with the lowest bandwidth if none fits)
+ * and the playlists of its renditions, so the other variants are never
+ * downloaded.
+ */
+static void select_single_variant(HLSContext *c)
+{
+    struct variant *sel = NULL;
+    int n = 0;
+
+    for (int i = 0; i < c->n_variants; i++) {
+        struct variant *var = c->variants[i];
+        int fits     = !c->max_bandwidth || var->bandwidth <= c->max_bandwidth;
+        int sel_fits = sel && (!c->max_bandwidth || sel->bandwidth <= 
c->max_bandwidth);
+
+        if (!sel || (fits && (!sel_fits || var->pixels > sel->pixels ||
+                              (var->pixels == sel->pixels && var->bandwidth > 
sel->bandwidth))) ||
+            (!fits && !sel_fits && var->bandwidth < sel->bandwidth))
+            sel = var;
+    }
+
+    for (int i = 0; i < c->n_playlists; i++) {
+        struct playlist *pls = c->playlists[i];
+
+        if (pls == sel->playlists[0] ||
+            playlist_in_group(c, pls, AVMEDIA_TYPE_AUDIO,    sel->audio_group) 
||
+            playlist_in_group(c, pls, AVMEDIA_TYPE_VIDEO,    sel->video_group) 
||
+            playlist_in_group(c, pls, AVMEDIA_TYPE_SUBTITLE, 
sel->subtitles_group))
+            c->playlists[n++] = pls;
+        else
+            drop_playlist(c, pls);
+    }
+    c->n_playlists = n;
+
+    for (int i = 0; i < c->n_variants; i++) {
+        if (c->variants[i] != sel) {
+            av_freep(&c->variants[i]->playlists);
+            av_freep(&c->variants[i]);
+        }
+    }
+    c->variants[0] = sel;
+    c->n_variants  = 1;
+}
+
 static int hls_close(AVFormatContext *s)
 {
     HLSContext *c = s->priv_data;
@@ -2518,6 +2613,10 @@ static int hls_read_header(AVFormatContext *s)
         av_log(s, AV_LOG_WARNING, "Empty playlist\n");
         return AVERROR_EOF;
     }
+
+    if (!c->load_all_variants)
+        select_single_variant(c);
+
     /* If the playlist only contained playlists (Master Playlist),
      * parse each individual playlist. */
     if (c->n_playlists > 1 || c->playlists[0]->n_segments == 0) {
@@ -3355,6 +3454,10 @@ static const AVOption hls_options[] = {
         OFFSET(seg_format_opts), AV_OPT_TYPE_DICT, {.str = NULL}, 0, 0, FLAGS},
     {"seg_max_retry", "Maximum number of times to reload a segment on error.",
      OFFSET(seg_max_retry), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, FLAGS},
+    {"load_all_variants", "Load all variants of a Master Playlist, otherwise 
only the one selected by max_bandwidth",
+        OFFSET(load_all_variants), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS},
+    {"max_bandwidth", "Highest variant bandwidth to load when 
load_all_variants is disabled (0 = no limit)",
+        OFFSET(max_bandwidth), AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX, 
FLAGS},
     {NULL}
 };
 
diff --git a/tests/fate/demux.mak b/tests/fate/demux.mak
index d4ac666f810..4fa4b72ee64 100644
--- a/tests/fate/demux.mak
+++ b/tests/fate/demux.mak
@@ -233,6 +233,35 @@ fate-hls-shared-renditions-demux: 
tests/data/hls-shared-renditions.m3u8
 fate-hls-shared-renditions-demux: CLEANFILES = 
tests/data/hls-shared-renditions*.m3u8 tests/data/hls-shared-renditions-*.ts 
tests/data/hls-shared-renditions-*.vtt
 fate-hls-shared-renditions-demux: CMD = ffprobe_demux 
$(TARGET_PATH)/tests/data/hls-shared-renditions.m3u8 -show_programs
 
+tests/data/hls-single-variant.m3u8: TAG = GEN
+tests/data/hls-single-variant.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+       $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+        -f lavfi -i "aevalsrc=0:s=16000:d=2" -c:a mp2fixed -flags +bitexact 
-muxdelay 0 -muxpreload 0 \
+        -f hls -hls_time 2 -hls_list_size 0 \
+        -hls_segment_filename 
$(TARGET_PATH)/tests/data/hls-single-variant-lo%d.ts \
+        $(TARGET_PATH)/tests/data/hls-single-variant-lo.m3u8 2>/dev/null
+       $(Q)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+        -f lavfi -i "aevalsrc=0:s=32000:d=2" -c:a mp2fixed -flags +bitexact 
-muxdelay 0 -muxpreload 0 \
+        -f hls -hls_time 2 -hls_list_size 0 \
+        -hls_segment_filename 
$(TARGET_PATH)/tests/data/hls-single-variant-hi%d.ts \
+        $(TARGET_PATH)/tests/data/hls-single-variant-hi.m3u8 2>/dev/null
+       $(Q)printf 
'#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:2,\nhls-single-variant-s0.vtt\n#EXT-X-ENDLIST\n'
 > tests/data/hls-single-variant-slo.m3u8
+       $(Q)printf 
'#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:2,\nhls-single-variant-s0.vtt\n#EXT-X-ENDLIST\n'
 > tests/data/hls-single-variant-shi.m3u8
+       $(Q)printf 'WEBVTT\n\n00:00.500 --> 00:01.500\nfirst\n' > 
tests/data/hls-single-variant-s0.vtt
+       $(Q)printf 
'#EXTM3U\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="lo",NAME="lo",URI="hls-single-variant-slo.m3u8"\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="hi",NAME="hi",URI="hls-single-variant-shi.m3u8"\n#EXT-X-STREAM-INF:BANDWIDTH=64000,SUBTITLES="lo"\nhls-single-variant-lo.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=128000,SUBTITLES="hi"\nhls-single-variant-hi.m3u8\n'
 > $@
+
+tests/data/hls-single-variant-res.m3u8: tests/data/hls-single-variant.m3u8
+       $(Q)printf 
'#EXTM3U\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="lo",NAME="lo",URI="hls-single-variant-slo.m3u8"\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="hi",NAME="hi",URI="hls-single-variant-shi.m3u8"\n#EXT-X-STREAM-INF:BANDWIDTH=64000,RESOLUTION=1920x1080,SUBTITLES="lo"\nhls-single-variant-lo.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=128000,RESOLUTION=1280x720,SUBTITLES="hi"\nhls-single-variant-hi.m3u8\n'
 > $@
+
+HLS_SINGLE_VARIANT_PROBE = run ffprobe$(PROGSSUF)$(EXESUF) -bitexact 
-load_all_variants 0 -show_entries 
program=program_id:program_tags=variant_bitrate:stream=codec_type,sample_rate:stream_tags=comment
 -of compact
+FATE_HLS_SINGLE_VARIANT-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS 
MPEGTS WEBVTT, MP3FLOAT PCM_F64LE, MP2FIXED, HLS MPEGTS, LAVFI_INDEV 
MPEGAUDIO_PARSER) += fate-hls-single-variant-demux 
fate-hls-single-variant-max-bandwidth-demux 
fate-hls-single-variant-resolution-demux
+FATE_FFMPEG_FFPROBE += $(FATE_HLS_SINGLE_VARIANT-yes)
+fate-hls-single-variant-demux fate-hls-single-variant-max-bandwidth-demux: 
tests/data/hls-single-variant.m3u8
+fate-hls-single-variant-demux: CMD = $(HLS_SINGLE_VARIANT_PROBE) 
$(TARGET_PATH)/tests/data/hls-single-variant.m3u8
+fate-hls-single-variant-max-bandwidth-demux: CMD = $(HLS_SINGLE_VARIANT_PROBE) 
-max_bandwidth 100000 $(TARGET_PATH)/tests/data/hls-single-variant.m3u8
+fate-hls-single-variant-resolution-demux: 
tests/data/hls-single-variant-res.m3u8
+fate-hls-single-variant-resolution-demux: CMD = $(HLS_SINGLE_VARIANT_PROBE) 
$(TARGET_PATH)/tests/data/hls-single-variant-res.m3u8
+
 FATE_SAMPLES_DEMUX-$(call PARSERDEM, JPEGXS, IMAGE_JPEGXS_PIPE, 
CONCAT_PROTOCOL) += fate-jxs-concat-demux
 fate-jxs-concat-demux: CMD = framecrc "-i 
concat:$(TARGET_SAMPLES)/jxs/lena.jxs|$(TARGET_SAMPLES)/jxs/lena.jxs -c:v copy"
 
diff --git a/tests/ref/fate/hls-single-variant-demux 
b/tests/ref/fate/hls-single-variant-demux
new file mode 100644
index 00000000000..314a89c69d2
--- /dev/null
+++ b/tests/ref/fate/hls-single-variant-demux
@@ -0,0 +1,5 @@
+program|program_id=0|tag:variant_bitrate=128000
+stream|codec_type=subtitle
+stream|codec_type=audio|sample_rate=32000
+stream|codec_type=subtitle|tag:comment=hi
+stream|codec_type=audio|sample_rate=32000
diff --git a/tests/ref/fate/hls-single-variant-max-bandwidth-demux 
b/tests/ref/fate/hls-single-variant-max-bandwidth-demux
new file mode 100644
index 00000000000..384543b20c0
--- /dev/null
+++ b/tests/ref/fate/hls-single-variant-max-bandwidth-demux
@@ -0,0 +1,5 @@
+program|program_id=0|tag:variant_bitrate=64000
+stream|codec_type=subtitle
+stream|codec_type=audio|sample_rate=16000
+stream|codec_type=subtitle|tag:comment=lo
+stream|codec_type=audio|sample_rate=16000
diff --git a/tests/ref/fate/hls-single-variant-resolution-demux 
b/tests/ref/fate/hls-single-variant-resolution-demux
new file mode 100644
index 00000000000..384543b20c0
--- /dev/null
+++ b/tests/ref/fate/hls-single-variant-resolution-demux
@@ -0,0 +1,5 @@
+program|program_id=0|tag:variant_bitrate=64000
+stream|codec_type=subtitle
+stream|codec_type=audio|sample_rate=16000
+stream|codec_type=subtitle|tag:comment=lo
+stream|codec_type=audio|sample_rate=16000
-- 
2.54.0

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

Reply via email to