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]

Reply via email to