This is an automated email from the git hooks/post-receive script. Git pushed a commit to branch master in repository ffmpeg.
commit 039335d9bc9ee8f8108f2768528cfe2657ecff4f Author: Kacper Michajłow <[email protected]> AuthorDate: Sat Oct 17 01:37:53 2026 +0200 Commit: Kacper Michajłow <[email protected]> CommitDate: Wed Nov 4 02:28:56 2026 +0000 avformat/webvttdec: add an option to apply the X-TIMESTAMP-MAP header 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, and use the 90 kHz MPEG-TS time base for the stream so that the mapping stays exact. 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 | 18 ++++++++++ libavformat/webvttdec.c | 60 ++++++++++++++++++++++++++++++--- tests/fate/subtitles.mak | 3 ++ tests/ref/fate/sub-webvtt-timestamp-map | 26 ++++++++++++++ 4 files changed, 102 insertions(+), 5 deletions(-) diff --git a/doc/demuxers.texi b/doc/demuxers.texi index bc8116d9d2..c1edd448cc 100644 --- a/doc/demuxers.texi +++ b/doc/demuxers.texi @@ -1279,4 +1279,22 @@ 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, and use the 90 kHz MPEG-TS time base for the stream. Default +value is false. +@end table + @c man end DEMUXERS diff --git a/libavformat/webvttdec.c b/libavformat/webvttdec.c index 4ca1e939b1..8b58466382 100644 --- a/libavformat/webvttdec.c +++ b/libavformat/webvttdec.c @@ -28,6 +28,7 @@ #include "demux.h" #include "internal.h" #include "subtitles.h" +#include "libavutil/avstring.h" #include "libavutil/bprint.h" #include "libavutil/intreadwrite.h" #include "libavutil/opt.h" @@ -36,6 +37,7 @@ typedef struct { const AVClass *class; FFDemuxSubtitlesQueue q; int kind; + int timestamp_map; } WebVTTContext; static int webvtt_probe(const AVProbeData *p) @@ -58,16 +60,55 @@ static int64_t read_ts(const char *s) return AV_NOPTS_VALUE; } +/* X-TIMESTAMP-MAP=LOCAL:<cue time>,MPEGTS:<MPEG-2 time>, in either order, + * on a header line of its own */ +static void parse_timestamp_map(AVFormatContext *s, const char *p, + AVRational time_base, int64_t *offset) +{ + int64_t local = AV_NOPTS_VALUE, mpegts = AV_NOPTS_VALUE; + + while (*p && !av_strstart(p, "X-TIMESTAMP-MAP=", NULL)) { + p += strcspn(p, "\r\n"); + p += strspn(p, "\r\n"); + } + if (!*p) + return; + p += strlen("X-TIMESTAMP-MAP="); + while (*p && *p != '\r' && *p != '\n') { + const char *val; + if (av_strstart(p, "LOCAL:", &val)) { + local = read_ts(val); + } else if (av_strstart(p, "MPEGTS:", &val)) { + char *end; + mpegts = strtoll(val, &end, 10); + if (end == val || (*end && !strchr(",\r\n", *end)) || + mpegts < 0 || mpegts >= (1LL << 33)) + mpegts = AV_NOPTS_VALUE; + } + p += strcspn(p, ",\r\n"); + if (*p == ',') + p++; + } + if (local == AV_NOPTS_VALUE || mpegts == AV_NOPTS_VALUE) { + av_log(s, AV_LOG_WARNING, "Invalid X-TIMESTAMP-MAP header\n"); + return; + } + + *offset = av_rescale_q(mpegts, (AVRational){ 1, 90000 }, time_base) - + av_rescale_q(local, (AVRational){ 1, 1000 }, time_base); +} + 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) return AVERROR(ENOMEM); - avpriv_set_pts_info(st, 64, 1, 1000); + avpriv_set_pts_info(st, 64, 1, webvtt->timestamp_map ? 90000 : 1000); st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE; st->codecpar->codec_id = AV_CODEC_ID_WEBVTT; st->disposition |= webvtt->kind; @@ -92,10 +133,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=", 16) && !strstr(p, "-->"))) { + if (webvtt->timestamp_map) + parse_timestamp_map(s, p, st->time_base, &ts_offset); + continue; + } + + if (!strncmp(p, "STYLE", 5) || !strncmp(p, "REGION", 6) || !strncmp(p, "NOTE", 4)) continue; @@ -148,8 +195,10 @@ static int webvtt_read_header(AVFormatContext *s) goto end; } sub->pos = pos; - sub->pts = ts_start; - sub->duration = ts_end - ts_start; + sub->pts = av_rescale_q(ts_start, (AVRational){ 1, 1000 }, st->time_base) + + ts_offset; + sub->duration = av_rescale_q(ts_end - ts_start, (AVRational){ 1, 1000 }, + st->time_base); #define SET_SIDE_DATA(name, type) do { \ if (name##_len) { \ @@ -203,6 +252,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, in the 90 kHz MPEG-TS time base", OFFSET(timestamp_map), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, KIND_FLAGS }, { NULL } }; diff --git a/tests/fate/subtitles.mak b/tests/fate/subtitles.mak index 3ded96e044..200125f718 100644 --- a/tests/fate/subtitles.mak +++ b/tests/fate/subtitles.mak @@ -109,6 +109,9 @@ fate-sub-webvtt: CMD = fmtstdout ass -i $(TARGET_SAMPLES)/sub/WebVTT_capability_ FATE_SUBTITLES_ASS-$(call DEMDEC, WEBVTT, WEBVTT) += fate-sub-webvtt2 fate-sub-webvtt2: CMD = fmtstdout ass -i $(TARGET_SAMPLES)/sub/WebVTT_extended_tester.vtt +FATE_SUBTITLES-$(call ALLYES, WEBVTT_DEMUXER FRAMECRC_MUXER) += fate-sub-webvtt-timestamp-map +fate-sub-webvtt-timestamp-map: CMD = framecrc -copyts -timestamp_map 1 -i $(TARGET_SAMPLES)/sub/WebVTT_timestamp_map.vtt -timestamp_map 1 -i $(TARGET_SAMPLES)/sub/WebVTT_timestamp_map_block.vtt -timestamp_map 1 -i $(TARGET_SAMPLES)/sub/WebVTT_timestamp_map_invalid.vtt -timestamp_map 1 -i $(TARGET_SAMPLES)/sub/WebVTT_timestamp_map_nolocal.vtt -timestamp_map 1 -i $(TARGET_SAMPLES)/sub/WebVTT_timestamp_map_range.vtt -i $(TARGET_SAMPLES)/sub/WebVTT_timestamp_map.vtt -map 0 -map 1 -map 2 -map [...] + FATE_SUBTITLES-$(call ALLYES, SRT_DEMUXER SUBRIP_DECODER WEBVTT_ENCODER WEBVTT_MUXER) += fate-sub-webvttenc fate-sub-webvttenc: CMD = fmtstdout webvtt -i $(TARGET_SAMPLES)/sub/SubRip_capability_tester.srt diff --git a/tests/ref/fate/sub-webvtt-timestamp-map b/tests/ref/fate/sub-webvtt-timestamp-map new file mode 100644 index 0000000000..4b084961d4 --- /dev/null +++ b/tests/ref/fate/sub-webvtt-timestamp-map @@ -0,0 +1,26 @@ +#tb 0: 1/90000 +#media_type 0: subtitle +#codec_id 0: webvtt +#tb 1: 1/90000 +#media_type 1: subtitle +#codec_id 1: webvtt +#tb 2: 1/90000 +#media_type 2: subtitle +#codec_id 2: webvtt +#tb 3: 1/90000 +#media_type 3: subtitle +#codec_id 3: webvtt +#tb 4: 1/90000 +#media_type 4: subtitle +#codec_id 4: webvtt +#tb 5: 1/1000 +#media_type 5: subtitle +#codec_id 5: webvtt +2, 90000, 90000, 90000, 4, 0x044101bc +3, 90000, 90000, 90000, 4, 0x042401aa +4, 90000, 90000, 90000, 3, 0x02a30154 +5, 1000, 1000, 1000, 3, 0x028e0142, S=1, WebVTT ID, 15, 0x226c0444 +5, 3000, 3000, 500, 3, 0x02b9015a, S=1, WebVTT Settings, 6, 0x08210213 +0, 990000, 990000, 90000, 3, 0x028e0142, S=1, WebVTT ID, 15, 0x226c0444 +1, 1125000, 1125000, 45000, 5, 0x06690218 +0, 1170000, 1170000, 45000, 3, 0x02b9015a, S=1, WebVTT Settings, 6, 0x08210213 -- To stop receiving notification emails like this one, please contact [email protected]. _______________________________________________ ffmpeg-cvslog mailing list -- [email protected] To unsubscribe send an email to [email protected]
