PR #25133 opened by Kacper Michajłow (kasper93)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25133
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25133.patch


From f2776bdf14ec2be32543ffa4c2d9658f99cc8794 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:54 +0200
Subject: [PATCH 1/7] tests/fate/demux: clean up only its own files after the
 HLS WebVTT test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The globs also matched the data of other tests named hls-webvtt-*.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 tests/fate/demux.mak | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/fate/demux.mak b/tests/fate/demux.mak
index d4ac666f81..a86110bc6c 100644
--- a/tests/fate/demux.mak
+++ b/tests/fate/demux.mak
@@ -204,7 +204,7 @@ tests/data/hls-webvtt.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | 
tests/data
 
 FATE_FFMPEG-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS WEBVTT, 
MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS NULL WEBVTT, LAVFI_INDEV MPEGAUDIO_PARSER 
PIPE_PROTOCOL) += fate-hls-webvtt-demux
 fate-hls-webvtt-demux: tests/data/hls-webvtt.m3u8
-fate-hls-webvtt-demux: CLEANFILES = tests/data/hls-webvtt*.m3u8 
tests/data/hls-webvtt-*.ts tests/data/hls-webvtt-*.vtt
+fate-hls-webvtt-demux: CLEANFILES = tests/data/hls-webvtt.m3u8 
tests/data/hls-webvtt-a.m3u8 tests/data/hls-webvtt-s.m3u8 
tests/data/hls-webvtt-a[0-9]*.ts tests/data/hls-webvtt-s[0-9]*.vtt
 fate-hls-webvtt-demux: CMD = fmtstdout webvtt -copyts -i 
$(TARGET_PATH)/tests/data/hls-webvtt.m3u8 -map 0:a -c copy -f null - -map 0:s 
-c copy
 
 tests/data/hls-self-variant.m3u8: TAG = GEN
-- 
2.54.0


From 42b7963407988e3b3005105c4939da308401676d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:54 +0200
Subject: [PATCH 2/7] tests/fate/demux: add HLS WebVTT timestamp map and seek
 tests

Audio from 10 s with WebVTT segments mapping cue time 0 to 10 s, a cue
that spans three segments and a seek into the second segment.
---
 tests/fate/demux.mak                 | 26 ++++++++++++++++++++++++++
 tests/ref/fate/hls-webvtt-map-demux  |  1 +
 tests/ref/fate/hls-webvtt-seek-demux |  1 +
 3 files changed, 28 insertions(+)
 create mode 100644 tests/ref/fate/hls-webvtt-map-demux
 create mode 100644 tests/ref/fate/hls-webvtt-seek-demux

diff --git a/tests/fate/demux.mak b/tests/fate/demux.mak
index a86110bc6c..5021a01fb7 100644
--- a/tests/fate/demux.mak
+++ b/tests/fate/demux.mak
@@ -207,6 +207,32 @@ fate-hls-webvtt-demux: tests/data/hls-webvtt.m3u8
 fate-hls-webvtt-demux: CLEANFILES = tests/data/hls-webvtt.m3u8 
tests/data/hls-webvtt-a.m3u8 tests/data/hls-webvtt-s.m3u8 
tests/data/hls-webvtt-a[0-9]*.ts tests/data/hls-webvtt-s[0-9]*.vtt
 fate-hls-webvtt-demux: CMD = fmtstdout webvtt -copyts -i 
$(TARGET_PATH)/tests/data/hls-webvtt.m3u8 -map 0:a -c copy -f null - -map 0:s 
-c copy
 
+define HLS_WEBVTT_MAP_DATA
+tests/data/$(1).m3u8: TAG = GEN
+tests/data/$(1).m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+       $$(M)$$(TARGET_EXEC) $$(TARGET_PATH)/$$< -nostdin \
+        -f lavfi -i "aevalsrc=0:d=6" -c:a mp2fixed -flags +bitexact -muxdelay 
0 -muxpreload 0 -output_ts_offset 10 \
+        -f hls -hls_time 2 -hls_list_size 0 \
+        -hls_segment_filename $$(TARGET_PATH)/tests/data/$(1)-a%d.ts \
+        $$(TARGET_PATH)/tests/data/$(1)-a.m3u8 2>/dev/null
+       $$(Q)printf 
'#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:2,\n$(1)-s0.vtt\n#EXTINF:2,\n$(1)-s1.vtt\n#EXTINF:2,\n$(1)-s2.vtt\n#EXT-X-ENDLIST\n'
 > tests/data/$(1)-s.m3u8
+       $$(Q)printf 
'WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n\n00:00.500 --> 
00:01.500\nfirst\n\n00:01.000 --> 00:04.500 line:1\nlong\n' > 
tests/data/$(1)-s0.vtt
+       $$(Q)printf 
'WEBVTT\nX-TIMESTAMP-MAP=LOCAL:00:00:02.000,MPEGTS:1080000\n\n00:01.000 --> 
00:04.500 line:1\nlong\n\n00:03.000 --> 00:03.900 align:start\nsecond\n' > 
tests/data/$(1)-s1.vtt
+       $$(Q)printf 
'WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n\n00:01.000 --> 
00:04.500 line:1\nlong\n\n00:04.500 --> 00:05.500\nthird\n' > 
tests/data/$(1)-s2.vtt
+       $$(Q)printf 
'#EXTM3U\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="s",NAME="en",URI="$(1)-s.m3u8"\n#EXT-X-STREAM-INF:BANDWIDTH=64000,SUBTITLES="s"\n$(1)-a.m3u8\n'
 > $$@
+
+fate-$(1)-demux: tests/data/$(1).m3u8
+fate-$(1)-demux: CLEANFILES = tests/data/$(1).m3u8 tests/data/$(1)-a.m3u8 
tests/data/$(1)-s.m3u8 tests/data/$(1)-a[0-9]*.ts tests/data/$(1)-s[0-9]*.vtt
+endef
+
+$(eval $(call HLS_WEBVTT_MAP_DATA,hls-webvtt-map))
+$(eval $(call HLS_WEBVTT_MAP_DATA,hls-webvtt-seek))
+
+FATE_HLS_WEBVTT_MAP-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS 
WEBVTT, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS NULL WEBVTT, LAVFI_INDEV 
MPEGAUDIO_PARSER PIPE_PROTOCOL) += fate-hls-webvtt-map-demux 
fate-hls-webvtt-seek-demux
+FATE_FFMPEG += $(FATE_HLS_WEBVTT_MAP-yes)
+fate-hls-webvtt-map-demux: CMD = fmtstdout webvtt -copyts -i 
$(TARGET_PATH)/tests/data/hls-webvtt-map.m3u8 -map 0:a -c copy -f null - -map 
0:s -c copy
+fate-hls-webvtt-seek-demux: CMD = fmtstdout webvtt -copyts -ss 2.9 -i 
$(TARGET_PATH)/tests/data/hls-webvtt-seek.m3u8 -map 0:s -c copy
+
 tests/data/hls-self-variant.m3u8: TAG = GEN
 tests/data/hls-self-variant.m3u8: | tests/data
        $(Q)printf 
'#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=64000\nhls-self-variant.m3u8\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="s",NAME="en",URI="hls-self-variant-missing.m3u8"\n#EXTINF:2,\nhls-self-variant.vtt\n'
 > $@
diff --git a/tests/ref/fate/hls-webvtt-map-demux 
b/tests/ref/fate/hls-webvtt-map-demux
new file mode 100644
index 0000000000..5ea461665c
--- /dev/null
+++ b/tests/ref/fate/hls-webvtt-map-demux
@@ -0,0 +1 @@
+WEBVTT
diff --git a/tests/ref/fate/hls-webvtt-seek-demux 
b/tests/ref/fate/hls-webvtt-seek-demux
new file mode 100644
index 0000000000..5ea461665c
--- /dev/null
+++ b/tests/ref/fate/hls-webvtt-seek-demux
@@ -0,0 +1 @@
+WEBVTT
-- 
2.54.0


From 20a862c6caeb39719e10269e0274dfacc15e7672 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:53 +0200
Subject: [PATCH 3/7] avformat/webvttdec: add an option to apply the
 X-TIMESTAMP-MAP header
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

HLS WebVTT segments carry an X-TIMESTAMP-MAP header that maps a cue time
to an MPEG-TS time of the other renditions (RFC 8216, 3.5). With
timestamp_map set, add the offset it gives to the cue timestamps. Skip
the header when it comes in a block of its own instead of ending the
parsing there.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 doc/demuxers.texi       | 17 ++++++++++++++++
 libavformat/webvttdec.c | 43 ++++++++++++++++++++++++++++++++++++++---
 2 files changed, 57 insertions(+), 3 deletions(-)

diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index 7610055193..f076710f04 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -1255,4 +1255,21 @@ color hint will be used, otherwise transparent black 
will be used for
 the background. Default value is false.
 @end table
 
+@section webvtt
+
+WebVTT subtitle demuxer.
+
+It accepts the following options:
+
+@table @option
+@item kind
+Set the kind of the track: @samp{subtitles} (default), @samp{captions},
+@samp{descriptions} or @samp{metadata}.
+
+@item timestamp_map
+Apply the @code{X-TIMESTAMP-MAP} header of HLS WebVTT segments to the cue
+timestamps, which moves them onto the MPEG-TS timeline of the other
+renditions. Default value is false.
+@end table
+
 @c man end DEMUXERS
diff --git a/libavformat/webvttdec.c b/libavformat/webvttdec.c
index 4ca1e939b1..6ec316fd06 100644
--- a/libavformat/webvttdec.c
+++ b/libavformat/webvttdec.c
@@ -36,6 +36,7 @@ typedef struct {
     const AVClass *class;
     FFDemuxSubtitlesQueue q;
     int kind;
+    int timestamp_map;
 } WebVTTContext;
 
 static int webvtt_probe(const AVProbeData *p)
@@ -58,11 +59,40 @@ static int64_t read_ts(const char *s)
     return AV_NOPTS_VALUE;
 }
 
+/* X-TIMESTAMP-MAP=LOCAL:<cue time>,MPEGTS:<90 kHz time>, in either order.
+ * Sets *offset to the value to add to the cue times, in milliseconds. */
+static int parse_timestamp_map(AVFormatContext *s, const char *header,
+                               int64_t *offset)
+{
+    const char *p = strstr(header, "X-TIMESTAMP-MAP=");
+    int64_t local = AV_NOPTS_VALUE, mpegts = -1;
+
+    if (!p)
+        return 0;
+    p += strlen("X-TIMESTAMP-MAP=");
+    while (*p && *p != '\r' && *p != '\n') {
+        if (!strncmp(p, "LOCAL:", 6))
+            local = read_ts(p + 6);
+        else if (!strncmp(p, "MPEGTS:", 7))
+            sscanf(p + 7, "%"SCNd64, &mpegts);
+        p += strcspn(p, ",\r\n");
+        if (*p == ',')
+            p++;
+    }
+    if (local == AV_NOPTS_VALUE || mpegts < 0) {
+        av_log(s, AV_LOG_WARNING, "Invalid X-TIMESTAMP-MAP header\n");
+        return 0;
+    }
+    *offset = av_rescale(mpegts, 1000, 90000) - local;
+    return 1;
+}
+
 static int webvtt_read_header(AVFormatContext *s)
 {
     WebVTTContext *webvtt = s->priv_data;
     AVBPrint cue;
     int res = 0;
+    int64_t ts_offset = 0;
     AVStream *st = avformat_new_stream(s, NULL);
 
     if (!st)
@@ -92,10 +122,16 @@ static int webvtt_read_header(AVFormatContext *s)
         p = identifier = cue.str;
         pos = avio_tell(s->pb);
 
-        /* ignore header chunk */
+        /* header chunk */
         if (!strncmp(p, "\xEF\xBB\xBFWEBVTT", 9) ||
             !strncmp(p, "WEBVTT", 6) ||
-            !strncmp(p, "STYLE", 5) ||
+            !strncmp(p, "X-TIMESTAMP-MAP", 15)) {
+            if (webvtt->timestamp_map)
+                parse_timestamp_map(s, p, &ts_offset);
+            continue;
+        }
+
+        if (!strncmp(p, "STYLE", 5) ||
             !strncmp(p, "REGION", 6) ||
             !strncmp(p, "NOTE", 4))
             continue;
@@ -148,7 +184,7 @@ static int webvtt_read_header(AVFormatContext *s)
             goto end;
         }
         sub->pos = pos;
-        sub->pts = ts_start;
+        sub->pts = ts_start + ts_offset;
         sub->duration = ts_end - ts_start;
 
 #define SET_SIDE_DATA(name, type) do {                                  \
@@ -203,6 +239,7 @@ static const AVOption options[] = {
         { "captions",     "WebVTT captions kind",     0, AV_OPT_TYPE_CONST, { 
.i64 = AV_DISPOSITION_CAPTIONS },     INT_MIN, INT_MAX, KIND_FLAGS, .unit = 
"webvtt_kind" },
         { "descriptions", "WebVTT descriptions kind", 0, AV_OPT_TYPE_CONST, { 
.i64 = AV_DISPOSITION_DESCRIPTIONS }, INT_MIN, INT_MAX, KIND_FLAGS, .unit = 
"webvtt_kind" },
         { "metadata",     "WebVTT metadata kind",     0, AV_OPT_TYPE_CONST, { 
.i64 = AV_DISPOSITION_METADATA },     INT_MIN, INT_MAX, KIND_FLAGS, .unit = 
"webvtt_kind" },
+    { "timestamp_map", "Apply the X-TIMESTAMP-MAP header to the cue 
timestamps", OFFSET(timestamp_map), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, 
KIND_FLAGS },
     { NULL }
 };
 
-- 
2.54.0


From 760db079127643b5ae269d05eaa95bae11165648 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:53 +0200
Subject: [PATCH 4/7] avformat/hls: apply the X-TIMESTAMP-MAP header of WebVTT
 segments
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Set the timestamp_map option of the webvtt demuxer for the subtitle
segments in place of prefer_hls_mpegts_pts, which no demuxer has, so the
cue times land on the MPEG-TS timeline of the audio and video.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/hls.c                    |  2 +-
 tests/ref/fate/hls-webvtt-map-demux  | 18 ++++++++++++++++++
 tests/ref/fate/hls-webvtt-seek-demux |  9 +++++++++
 3 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index 766f9a6274..c05e0d50dc 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -2568,7 +2568,7 @@ static int hls_read_header(AVFormatContext *s)
         highest_cur_seq_no = FFMAX(highest_cur_seq_no, pls->cur_seq_no);
     }
 
-    av_dict_set(&c->seg_format_opts, "prefer_hls_mpegts_pts", "1", 0);
+    av_dict_set(&c->seg_format_opts, "timestamp_map", "1", 0);
 
     /* Open the demuxer for each playlist */
     for (i = 0; i < c->n_playlists; i++) {
diff --git a/tests/ref/fate/hls-webvtt-map-demux 
b/tests/ref/fate/hls-webvtt-map-demux
index 5ea461665c..b82bd1435f 100644
--- a/tests/ref/fate/hls-webvtt-map-demux
+++ b/tests/ref/fate/hls-webvtt-map-demux
@@ -1 +1,19 @@
 WEBVTT
+
+00:10.500 --> 00:11.500
+first
+
+00:11.000 --> 00:14.500 line:1
+long
+
+00:11.000 --> 00:14.500 line:1
+long
+
+00:13.000 --> 00:13.900 align:start
+second
+
+00:13.000 --> 00:16.500 line:1
+long
+
+00:14.500 --> 00:15.500
+third
diff --git a/tests/ref/fate/hls-webvtt-seek-demux 
b/tests/ref/fate/hls-webvtt-seek-demux
index 5ea461665c..3314f620ac 100644
--- a/tests/ref/fate/hls-webvtt-seek-demux
+++ b/tests/ref/fate/hls-webvtt-seek-demux
@@ -1 +1,10 @@
 WEBVTT
+
+00:13.000 --> 00:13.900 align:start
+second
+
+00:13.000 --> 00:16.500 line:1
+long
+
+00:14.500 --> 00:15.500
+third
-- 
2.54.0


From a357b1d49cb0edc22bad457cf59ccf14f7645ea4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:53 +0200
Subject: [PATCH 5/7] avformat/hls: keep subtitle playlists on the shared
 timeline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The cue times of a subtitle playlist are on the MPEG-TS timeline, and
the first cue of a segment says nothing about where the segment starts,
so use an offset of 0 instead of deriving one from the first cue, which
made a seek skip the cues within that offset.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/hls.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index c05e0d50dc..985f2e2928 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -2922,7 +2922,11 @@ static int hls_read_packet(AVFormatContext *s, AVPacket 
*pkt)
                     fill_timing_for_id3_timestamped_stream(pls);
                 }
 
-                if (pls->pkt->dts != AV_NOPTS_VALUE &&
+                if (pls->is_subtitle) {
+                    /* the cues are on the shared timeline */
+                    if (pls->ts_offset == AV_NOPTS_VALUE)
+                        pls->ts_offset = 0;
+                } else if (pls->pkt->dts != AV_NOPTS_VALUE &&
                     (pls->ts_offset == AV_NOPTS_VALUE ||
                      (!c->first_timestamp_locked &&
                       c->first_timestamp_pls == pls &&
-- 
2.54.0


From 0308ef236142933d7e55c0a72b38c1d862c9f9d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:54 +0200
Subject: [PATCH 6/7] avformat/hls: keep the subtitle cues still shown at the
 seek target
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Discard the cues of a subtitle playlist by their end time when seeking
or when the playlist is enabled during playback, so a cue that is on
screen at the target is delivered instead of the next one starting after
it.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/hls.c                    | 8 +++++++-
 tests/ref/fate/hls-webvtt-seek-demux | 3 +++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index 985f2e2928..7177d6faba 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -2994,8 +2994,14 @@ static int hls_read_packet(AVFormatContext *s, AVPacket 
*pkt)
                         break;
                     }
 
+                    int64_t pkt_ts = pls->pkt->dts;
+
+                    /* a cue stays on screen until its end */
+                    if (pls->is_subtitle && pls->pkt->duration > 0)
+                        pkt_ts += pls->pkt->duration;
+
                     tb = get_timebase(pls);
-                    ts_diff = av_rescale_q_rnd(pls->pkt->dts, tb,
+                    ts_diff = av_rescale_q_rnd(pkt_ts, tb,
                                                AV_TIME_BASE_Q, AV_ROUND_DOWN) -
                             pls->seek_timestamp;
                     if (ts_diff >= 0 && (pls->seek_flags  & AVSEEK_FLAG_ANY ||
diff --git a/tests/ref/fate/hls-webvtt-seek-demux 
b/tests/ref/fate/hls-webvtt-seek-demux
index 3314f620ac..cfd97a413b 100644
--- a/tests/ref/fate/hls-webvtt-seek-demux
+++ b/tests/ref/fate/hls-webvtt-seek-demux
@@ -1,5 +1,8 @@
 WEBVTT
 
+00:11.000 --> 00:14.500 line:1
+long
+
 00:13.000 --> 00:13.900 align:start
 second
 
-- 
2.54.0


From 9f74bcaf6c8e5eecf92a49357adb7cbd41731c38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]>
Date: Sat, 17 Oct 2026 01:37:54 +0200
Subject: [PATCH 7/7] avformat/hls: drop WebVTT cues repeated across segment
 boundaries
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A WebVTT segment contains every cue shown during its period (RFC 8216,
3.5), so a cue that reaches into the next segment appears in both. Keep
the cues of the previous segment and drop the ones the current segment
repeats, so each cue is delivered once.

Signed-off-by: Kacper Michajłow <[email protected]>
---
 libavformat/hls.c                    | 77 ++++++++++++++++++++++++++--
 tests/ref/fate/hls-webvtt-map-demux  |  6 ---
 tests/ref/fate/hls-webvtt-seek-demux |  3 --
 3 files changed, 72 insertions(+), 14 deletions(-)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index 7177d6faba..a9c87ca6c5 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -117,6 +117,10 @@ struct playlist {
     int index;
     AVFormatContext *ctx;
     AVPacket *pkt;
+    AVPacket **prev_cues;
+    int n_prev_cues;
+    AVPacket **cur_cues;
+    int n_cur_cues;
     int has_noheader_flag;
 
     /* main demuxer streams associated with this playlist
@@ -313,6 +317,20 @@ static void free_init_section_list(struct playlist *pls)
     pls->n_init_sections = 0;
 }
 
+static void free_cue_list(AVPacket ***cues, int *n_cues)
+{
+    for (int i = 0; i < *n_cues; i++)
+        av_packet_free(&(*cues)[i]);
+    av_freep(cues);
+    *n_cues = 0;
+}
+
+static void free_subtitle_cues(struct playlist *pls)
+{
+    free_cue_list(&pls->prev_cues, &pls->n_prev_cues);
+    free_cue_list(&pls->cur_cues, &pls->n_cur_cues);
+}
+
 static void free_playlist_list(HLSContext *c)
 {
     int i;
@@ -320,6 +338,7 @@ static void free_playlist_list(HLSContext *c)
         struct playlist *pls = c->playlists[i];
         free_segment_list(pls);
         free_init_section_list(pls);
+        free_subtitle_cues(pls);
         av_freep(&pls->main_streams);
         av_freep(&pls->renditions);
         av_freep(&pls->id3_buf);
@@ -2087,6 +2106,38 @@ static int init_subtitle_context(struct playlist *pls)
     return ret;
 }
 
+static int cue_repeated(const struct playlist *v, const AVPacket *pkt)
+{
+    for (int i = 0; i < v->n_prev_cues; i++) {
+        const AVPacket *prev = v->prev_cues[i];
+        const uint8_t *settings, *prev_settings;
+        size_t size, prev_size;
+
+        if (prev->pts != pkt->pts || prev->duration != pkt->duration ||
+            prev->size != pkt->size || memcmp(prev->data, pkt->data, 
pkt->size))
+            continue;
+        settings = av_packet_get_side_data(pkt, AV_PKT_DATA_WEBVTT_SETTINGS, 
&size);
+        prev_settings = av_packet_get_side_data(prev, 
AV_PKT_DATA_WEBVTT_SETTINGS,
+                                                &prev_size);
+        if (size == prev_size && (!size || !memcmp(settings, prev_settings, 
size)))
+            return 1;
+    }
+    return 0;
+}
+
+static int remember_cue(struct playlist *v, const AVPacket *pkt)
+{
+    AVPacket *cue = av_packet_clone(pkt);
+
+    if (!cue)
+        return AVERROR(ENOMEM);
+    if (av_dynarray_add_nofree(&v->cur_cues, &v->n_cur_cues, cue) < 0) {
+        av_packet_free(&cue);
+        return AVERROR(ENOMEM);
+    }
+    return 0;
+}
+
 static int read_subtitle_packet(struct playlist *v, AVPacket *pkt)
 {
     HLSContext *c = v->parent->priv_data;
@@ -2106,9 +2157,17 @@ restart:
             return ret;
     }
 
-    ret = av_read_frame(v->ctx, v->pkt);
-    if (!ret) {
-        return ret;
+    while (!(ret = av_read_frame(v->ctx, v->pkt))) {
+        int repeated = cue_repeated(v, v->pkt);
+
+        ret = remember_cue(v, v->pkt);
+        if (ret < 0) {
+            av_packet_unref(v->pkt);
+            return ret;
+        }
+        if (!repeated)
+            return 0;
+        av_packet_unref(v->pkt);
     }
     ff_format_io_close(v->parent, &v->input);
     v->cur_seq_no++;
@@ -2116,6 +2175,10 @@ restart:
 
     avformat_close_input(&v->ctx);
 
+    free_cue_list(&v->prev_cues, &v->n_prev_cues);
+    FFSWAP(AVPacket **, v->prev_cues, v->cur_cues);
+    FFSWAP(int, v->n_prev_cues, v->n_cur_cues);
+
     goto restart;
 }
 
@@ -2814,8 +2877,10 @@ static int recheck_discard_flags(AVFormatContext *s, int 
first)
             pls->input_read_done = 0;
             ff_format_io_close(pls->parent, &pls->input_next);
             pls->input_next_requested = 0;
-            if (pls->is_subtitle)
+            if (pls->is_subtitle) {
                 avformat_close_input(&pls->ctx);
+                free_subtitle_cues(pls);
+            }
             pls->needed = 0;
             changed = 1;
             av_log(s, AV_LOG_INFO, "No longer receiving playlist %d\n", i);
@@ -3222,8 +3287,10 @@ static int hls_read_seek(AVFormatContext *s, int 
stream_index,
             for (j = 0; j < (int)pls->ctx->nb_streams; j++)
                 ffstream(pls->ctx->streams[j])->pts_wrap_reference = 
AV_NOPTS_VALUE;
         }
-        if (pls->is_subtitle)
+        if (pls->is_subtitle) {
             avformat_close_input(&pls->ctx);
+            free_subtitle_cues(pls);
+        }
 
         /* Reset the init segment so it's re-fetched and served appropriately 
*/
         pls->cur_init_section = NULL;
diff --git a/tests/ref/fate/hls-webvtt-map-demux 
b/tests/ref/fate/hls-webvtt-map-demux
index b82bd1435f..284413facf 100644
--- a/tests/ref/fate/hls-webvtt-map-demux
+++ b/tests/ref/fate/hls-webvtt-map-demux
@@ -6,14 +6,8 @@ first
 00:11.000 --> 00:14.500 line:1
 long
 
-00:11.000 --> 00:14.500 line:1
-long
-
 00:13.000 --> 00:13.900 align:start
 second
 
-00:13.000 --> 00:16.500 line:1
-long
-
 00:14.500 --> 00:15.500
 third
diff --git a/tests/ref/fate/hls-webvtt-seek-demux 
b/tests/ref/fate/hls-webvtt-seek-demux
index cfd97a413b..12af5c285e 100644
--- a/tests/ref/fate/hls-webvtt-seek-demux
+++ b/tests/ref/fate/hls-webvtt-seek-demux
@@ -6,8 +6,5 @@ long
 00:13.000 --> 00:13.900 align:start
 second
 
-00:13.000 --> 00:16.500 line:1
-long
-
 00:14.500 --> 00:15.500
 third
-- 
2.54.0

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

Reply via email to