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]
