PR #25121 opened by Romain Beauxis (toots) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25121 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25121.patch
>From 422240ca421e62b35624f2a39c2c4b5d8ec39f7f Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Fri, 16 Oct 2026 19:11:44 +0200 Subject: [PATCH 1/3] libavformat/tests: add an HLS playlist parsing test program Parse a playlist, then optionally a Media Playlist into the first playlist it declared, as hls_read_header() does, and print the parsed playlists with their EXT-X-START offsets. --- libavformat/Makefile | 1 + libavformat/tests/.gitignore | 1 + libavformat/tests/hls.c | 99 ++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 libavformat/tests/hls.c diff --git a/libavformat/Makefile b/libavformat/Makefile index b707193eec..42c85dcca9 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -799,6 +799,7 @@ TESTPROGS-$(CONFIG_RTPDEC) += rtpdec FIFO-MUXER-TESTPROGS-$(CONFIG_NETWORK) += fifo_muxer TESTPROGS-$(CONFIG_FIFO_MUXER) += $(FIFO-MUXER-TESTPROGS-yes) TESTPROGS-$(CONFIG_FFRTMPCRYPT_PROTOCOL) += rtmpdh +TESTPROGS-$(CONFIG_HLS_DEMUXER) += hls TESTPROGS-$(CONFIG_HTTP_PROTOCOL) += http TESTPROGS-$(CONFIG_NETWORK) += noproxy TESTPROGS-$(CONFIG_SRTP) += srtp diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore index 40603e70a5..6e59411112 100644 --- a/libavformat/tests/.gitignore +++ b/libavformat/tests/.gitignore @@ -13,3 +13,4 @@ /seek_utils /http /ogg_pages +/hls diff --git a/libavformat/tests/hls.c b/libavformat/tests/hls.c new file mode 100644 index 0000000000..868bee143f --- /dev/null +++ b/libavformat/tests/hls.c @@ -0,0 +1,99 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "libavformat/hls.c" + +static void print_playlists(const HLSContext *c, int parse_ret) +{ + if (parse_ret < 0) { + printf("error: %s\n", av_err2str(parse_ret)); + return; + } + printf("%d variant(s), %d playlist(s)\n", c->n_variants, c->n_playlists); + for (int i = 0; i < c->n_playlists; i++) { + const struct playlist *pls = c->playlists[i]; + if (pls->time_offset_flag) + printf("%s: start offset %"PRId64"\n", + av_basename(pls->url), pls->start_time_offset); + else + printf("%s: no start offset\n", av_basename(pls->url)); + } +} + +/** + * Parse a playlist, then optionally a Media Playlist into the first + * playlist it declared, as hls_read_header() does, and print the result. + * Parsing errors are part of the output; only I/O and allocation errors + * make the program fail. + */ +int main(int argc, char **argv) +{ + AVIOContext *top_level = NULL, *first_media = NULL; + AVFormatContext *s = NULL; + HLSContext *c; + int ret, parse_ret; + + if (argc < 2 || argc > 3) { + fprintf(stderr, "Usage: %s <playlist> [<media playlist>]\n", argv[0]); + return 1; + } + + av_log_set_level(AV_LOG_QUIET); + + ret = avio_open(&top_level, argv[1], AVIO_FLAG_READ); + if (ret < 0) + goto end; + if (argc > 2) { + ret = avio_open(&first_media, argv[2], AVIO_FLAG_READ); + if (ret < 0) + goto end; + } + + s = avformat_alloc_context(); + if (!s) { + ret = AVERROR(ENOMEM); + goto end; + } + s->iformat = &ff_hls_demuxer.p; + s->priv_data = av_mallocz(ff_hls_demuxer.priv_data_size); + if (!s->priv_data) { + ret = AVERROR(ENOMEM); + goto end; + } + *(const AVClass **)s->priv_data = ff_hls_demuxer.p.priv_class; + av_opt_set_defaults(s->priv_data); + c = s->priv_data; + c->ctx = s; + + parse_ret = parse_playlist(c, argv[1], NULL, top_level); + if (parse_ret >= 0 && first_media && c->n_playlists) + parse_ret = parse_playlist(c, argv[2], c->playlists[0], first_media); + print_playlists(c, parse_ret); + +end: + if (s && s->priv_data) + hls_close(s); + avformat_free_context(s); + avio_closep(&top_level); + avio_closep(&first_media); + if (ret < 0) { + fprintf(stderr, "%s\n", av_err2str(ret)); + return 1; + } + return 0; +} -- 2.54.0 >From 44341fa389044f4088489707253c76d11aefb5df Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Fri, 16 Oct 2026 19:12:07 +0200 Subject: [PATCH 2/3] fate/libavformat: add hls-parse tests A Master Playlist carrying EXT-X-START before its variants is rejected with "Master Playlist tag found in a Media Playlist", as recorded in the master-start references. --- tests/fate/libavformat.mak | 22 +++++++++++++++++++ tests/ref/fate/hls-parse-master-start | 1 + .../ref/fate/hls-parse-master-start-override | 1 + tests/ref/fate/hls-parse-media-start | 2 ++ 4 files changed, 26 insertions(+) create mode 100644 tests/ref/fate/hls-parse-master-start create mode 100644 tests/ref/fate/hls-parse-master-start-override create mode 100644 tests/ref/fate/hls-parse-media-start diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak index 45878891fd..c40f1c9ba3 100644 --- a/tests/fate/libavformat.mak +++ b/tests/fate/libavformat.mak @@ -64,6 +64,28 @@ FATE_LIBAVFORMAT-yes += fate-url fate-url: libavformat/tests/url$(EXESUF) fate-url: CMD = run libavformat/tests/url$(EXESUF) +tests/data/hls-parse-master-start.m3u8: TAG = GEN +tests/data/hls-parse-master-start.m3u8: | tests/data + $(Q)printf '#EXTM3U\n#EXT-X-START:TIME-OFFSET=1.5\n#EXT-X-STREAM-INF:BANDWIDTH=64000\na.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=32000\nb.m3u8\n' > $@ + +tests/data/hls-parse-media-start.m3u8: TAG = GEN +tests/data/hls-parse-media-start.m3u8: | tests/data + $(Q)printf '#EXTM3U\n#EXT-X-START:TIME-OFFSET=4\n#EXT-X-TARGETDURATION:2\n#EXTINF:2,\nseg0.ts\n#EXTINF:2,\nseg1.ts\n#EXTINF:2,\nseg2.ts\n' > $@ + +FATE_HLS_PARSE = fate-hls-parse-media-start \ + fate-hls-parse-master-start \ + fate-hls-parse-master-start-override + +$(FATE_HLS_PARSE): libavformat/tests/hls$(EXESUF) +fate-hls-parse-media-start: tests/data/hls-parse-media-start.m3u8 +fate-hls-parse-media-start: CMD = run libavformat/tests/hls$(EXESUF) $(TARGET_PATH)/tests/data/hls-parse-media-start.m3u8 +fate-hls-parse-master-start: tests/data/hls-parse-master-start.m3u8 +fate-hls-parse-master-start: CMD = run libavformat/tests/hls$(EXESUF) $(TARGET_PATH)/tests/data/hls-parse-master-start.m3u8 +fate-hls-parse-master-start-override: tests/data/hls-parse-master-start.m3u8 tests/data/hls-parse-media-start.m3u8 +fate-hls-parse-master-start-override: CMD = run libavformat/tests/hls$(EXESUF) $(TARGET_PATH)/tests/data/hls-parse-master-start.m3u8 $(TARGET_PATH)/tests/data/hls-parse-media-start.m3u8 + +FATE_LIBAVFORMAT-$(CONFIG_HLS_DEMUXER) += $(FATE_HLS_PARSE) + FATE_LIBAVFORMAT-$(CONFIG_IMF_DEMUXER) += fate-imf fate-imf: libavformat/tests/imf$(EXESUF) fate-imf: CMD = run libavformat/tests/imf$(EXESUF) diff --git a/tests/ref/fate/hls-parse-master-start b/tests/ref/fate/hls-parse-master-start new file mode 100644 index 0000000000..401c7af49d --- /dev/null +++ b/tests/ref/fate/hls-parse-master-start @@ -0,0 +1 @@ +error: Invalid data found when processing input diff --git a/tests/ref/fate/hls-parse-master-start-override b/tests/ref/fate/hls-parse-master-start-override new file mode 100644 index 0000000000..401c7af49d --- /dev/null +++ b/tests/ref/fate/hls-parse-master-start-override @@ -0,0 +1 @@ +error: Invalid data found when processing input diff --git a/tests/ref/fate/hls-parse-media-start b/tests/ref/fate/hls-parse-media-start new file mode 100644 index 0000000000..d65a41fe3e --- /dev/null +++ b/tests/ref/fate/hls-parse-media-start @@ -0,0 +1,2 @@ +1 variant(s), 1 playlist(s) +hls-parse-media-start.m3u8: start offset 4000000 -- 2.54.0 >From 834a45a92e9d89357ee568a15cc9634e5ba58197 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Fri, 16 Oct 2026 17:29:58 +0200 Subject: [PATCH 3/3] avformat/hls: accept EXT-X-START in Master Playlists EXT-X-START may appear in both Master and Media Playlists (RFC 8216 4.3.5.2). Parsing it allocated a Media Playlist, so a Master Playlist carrying it before its EXT-X-STREAM-INF entries was rejected since 0e6eef3551. In a Master Playlist, apply the offset as the default for Media Playlists that do not carry their own. --- libavformat/hls.c | 27 ++++++++++++++----- tests/ref/fate/hls-parse-master-start | 4 ++- .../ref/fate/hls-parse-master-start-override | 4 ++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index 506c8c0f49..98ba5c4c07 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -852,6 +852,8 @@ static int parse_playlist(HLSContext *c, const char *url, int prev_n_segments = 0; int64_t prev_start_seq_no = -1; int64_t load_start = av_gettime_relative(); + int64_t start_time_offset = 0; + int time_offset_flag = 0; if (is_http && !in && c->http_persistent && c->playlist_pb) { in = c->playlist_pb; @@ -1010,15 +1012,11 @@ static int parse_playlist(HLSContext *c, const char *url, } else if (av_strstart(line, "#EXT-X-START:", &ptr)) { const char *time_offset_value = NULL; - ret = ensure_playlist(c, &pls, url); - if (ret < 0) { - goto fail; - } if (av_strstart(ptr, "TIME-OFFSET=", &time_offset_value)) { double offset = strtod(time_offset_value, NULL) * AV_TIME_BASE; if (offset >= -0x1p63 && offset < 0x1p63) { - pls->start_time_offset = offset; - pls->time_offset_flag = 1; + start_time_offset = offset; + time_offset_flag = 1; } else { av_log(c->ctx, AV_LOG_WARNING, "TIME-OFFSET value is" "invalid, it will be ignored"); @@ -1161,6 +1159,23 @@ static int parse_playlist(HLSContext *c, const char *url, } } + /* EXT-X-START is valid in both playlist types. In a Master Playlist, + * it is the default for Media Playlists that do not carry their own. */ + if (time_offset_flag) { + if (pls) { + pls->start_time_offset = start_time_offset; + pls->time_offset_flag = 1; + } else { + for (int i = 0; i < c->n_playlists; i++) { + struct playlist *p = c->playlists[i]; + if (!p->time_offset_flag) { + p->start_time_offset = start_time_offset; + p->time_offset_flag = 1; + } + } + } + } + fail: free_segment_dynarray(prev_segments, prev_n_segments); av_freep(&prev_segments); diff --git a/tests/ref/fate/hls-parse-master-start b/tests/ref/fate/hls-parse-master-start index 401c7af49d..6950b6de80 100644 --- a/tests/ref/fate/hls-parse-master-start +++ b/tests/ref/fate/hls-parse-master-start @@ -1 +1,3 @@ -error: Invalid data found when processing input +2 variant(s), 2 playlist(s) +a.m3u8: start offset 1500000 +b.m3u8: start offset 1500000 diff --git a/tests/ref/fate/hls-parse-master-start-override b/tests/ref/fate/hls-parse-master-start-override index 401c7af49d..4dd0f8b30c 100644 --- a/tests/ref/fate/hls-parse-master-start-override +++ b/tests/ref/fate/hls-parse-master-start-override @@ -1 +1,3 @@ -error: Invalid data found when processing input +2 variant(s), 2 playlist(s) +a.m3u8: start offset 4000000 +b.m3u8: start offset 1500000 -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
