FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavcodec/webvttdec.c
Date: 2026-07-18 19:31:15
Exec Total Coverage
Lines: 72 95 75.8%
Functions: 5 5 100.0%
Branches: 38 68 55.9%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2012 Clément Bœsch
3 *
4 * This file is part of FFmpeg.
5 *
6 * FFmpeg is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * FFmpeg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with FFmpeg; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 /**
22 * @file
23 * WebVTT subtitle decoder
24 * @see https://www.w3.org/TR/webvtt1/
25 * @todo need to support extended markups and cue settings
26 */
27
28 #include "avcodec.h"
29 #include "ass.h"
30 #include "codec_internal.h"
31 #include "libavutil/bprint.h"
32 #include "libavutil/mathematics.h"
33
34 static const struct {
35 const char *from;
36 const char *to;
37 } webvtt_tag_replace[] = {
38 {"{", "\\{{}"}, {"\\", "\\\xe2\x81\xa0"}, // escape to avoid ASS markup conflicts
39 {"&gt;", ">"}, {"&lt;", "<"},
40 {"&lrm;", "\xe2\x80\x8e"}, {"&rlm;", "\xe2\x80\x8f"},
41 {"&amp;", "&"}, {"&nbsp;", "\\h"},
42 };
43 static const struct {
44 const char from[6];
45 const char to[6];
46 } webvtt_valid_tags[] = {
47 {"i", "{\\i1}"}, {"/i", "{\\i0}"},
48 {"b", "{\\b1}"}, {"/b", "{\\b0}"},
49 {"u", "{\\u1}"}, {"/u", "{\\u0}"},
50 };
51
52 /* parse a WebVTT timestamp string (HH:MM:SS.mmm or MM:SS.mmm).
53 * Returns milliseconds or -1 on failure. */
54 2 static int64_t parse_webvtt_timestamp(const char *buf)
55 {
56 2 int h = 0, m = 0, s = 0, ms = 0;
57
58
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (sscanf(buf, "%d:%2d:%2d.%3d", &h, &m, &s, &ms) == 4) {
59
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (m > 59 || s > 59)
60 return -1;
61 2 return (int64_t)h * 3600000 + m * 60000 + s * 1000 + ms;
62 }
63 if (sscanf(buf, "%2d:%2d.%3d", &m, &s, &ms) == 3) {
64 if (m > 59 || s > 59)
65 return -1;
66 return m * 60000 + s * 1000 + ms;
67 }
68
69 return -1;
70 }
71
72 /* validate a cue timestamp tag body: must be digits/colons/periods,
73 * parseable, strictly within (cue_start, cue_end), and after prev_ts.
74 * Returns 1 and writes to *ts_out on success, 0 on failure. */
75 49 static int read_cue_timestamp(const char *body, int len,
76 int64_t cue_start, int64_t cue_end,
77 int64_t prev_ts, int64_t *ts_out)
78 {
79 int64_t ts;
80
81
3/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✓ Branch 3 taken 2 times.
49 if (len < 1 || !av_isdigit(body[0]))
82 47 return 0;
83
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if ((int)strspn(body, "0123456789:.") != len)
84 return 0;
85
86 2 ts = parse_webvtt_timestamp(body);
87
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
2 if (ts <= cue_start || ts >= cue_end)
88 2 return 0;
89 if (prev_ts >= 0 && ts <= prev_ts)
90 return 0;
91
92 *ts_out = ts;
93 return 1;
94 }
95
96 /* Append the pending segment text, prefixed by a {\kf} karaoke override when
97 * dur_cs > 0. A {\kf} must precede the text it times, so segments are buffered
98 * in \seg and flushed once their duration (the span up to the next timestamp,
99 * or the cue end) is known. */
100 27 static void flush_segment(AVBPrint *buf, AVBPrint *seg, int64_t dur_cs)
101 {
102
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (dur_cs > 0)
103 av_bprintf(buf, "{\\kf%"PRId64"}", dur_cs);
104 27 av_bprintf(buf, "%s", seg->str);
105 27 av_bprint_clear(seg);
106 27 }
107
108 27 static int webvtt_event_to_ass(AVBPrint *buf, const char *p,
109 int64_t cue_start_ms, int64_t cue_end_ms)
110 {
111 27 int i, again = 0;
112 27 int64_t prev_ts = -1, ts;
113 27 int64_t start_cs = 0; /* cs from cue start where the pending segment begins */
114 AVBPrint seg;
115
116 27 av_bprint_init(&seg, 0, AV_BPRINT_SIZE_UNLIMITED);
117
118
2/2
✓ Branch 0 taken 1094 times.
✓ Branch 1 taken 27 times.
1121 while (*p) {
119
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 1045 times.
1094 if (*p == '<') {
120 49 const char *tag_end = strchr(p, '>');
121 ptrdiff_t len;
122
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 49 times.
49 if (!tag_end)
123 break;
124 49 len = tag_end - p + 1;
125
126 /* A cue timestamp ends the pending segment; flush it with its own
127 * duration (rounded against cue start, then differenced, so the
128 * durations telescope to the cue length without drift). */
129
2/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 49 times.
98 if (len > 2 &&
130 49 read_cue_timestamp(p + 1, (int)(len - 2),
131 cue_start_ms, cue_end_ms, prev_ts, &ts)) {
132 int64_t end_cs = (ts - cue_start_ms + 5) / 10;
133 flush_segment(buf, &seg, end_cs - start_cs);
134 start_cs = end_cs;
135 prev_ts = ts;
136 p += len;
137 again = 1;
138 continue;
139 }
140
141
2/2
✓ Branch 0 taken 262 times.
✓ Branch 1 taken 41 times.
303 for (i = 0; i < FF_ARRAY_ELEMS(webvtt_valid_tags); i++) {
142 262 const char *from = webvtt_valid_tags[i].from;
143
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 254 times.
262 if(!strncmp(p + 1, from, strlen(from))) {
144 8 av_bprintf(&seg, "%s", webvtt_valid_tags[i].to);
145 8 break;
146 }
147 }
148 49 p += len;
149 49 again = 1;
150 }
151
152
2/2
✓ Branch 0 taken 8729 times.
✓ Branch 1 taken 1084 times.
9813 for (i = 0; i < FF_ARRAY_ELEMS(webvtt_tag_replace); i++) {
153 8729 const char *from = webvtt_tag_replace[i].from;
154 8729 const size_t len = strlen(from);
155
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 8719 times.
8729 if (!strncmp(p, from, len)) {
156 10 av_bprintf(&seg, "%s", webvtt_tag_replace[i].to);
157 10 p += len;
158 10 again = 1;
159 10 break;
160 }
161 }
162
163
2/2
✓ Branch 0 taken 59 times.
✓ Branch 1 taken 1035 times.
1094 if (again) {
164 59 again = 0;
165 59 continue;
166 }
167
3/4
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1026 times.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
1035 if (p[0] == '\n' && p[1])
168 9 av_bprintf(&seg, "\\N");
169
2/2
✓ Branch 0 taken 1025 times.
✓ Branch 1 taken 1 times.
1026 else if (*p != '\r')
170 1025 av_bprint_chars(&seg, *p, 1);
171 1035 p++;
172 }
173
174 /* Flush the final segment. With no cue timestamp this is the whole cue,
175 * emitted untimed (duration 0 -> no {\kf}). */
176
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 flush_segment(buf, &seg,
177 prev_ts < 0 ? 0 : (cue_end_ms - cue_start_ms + 5) / 10 - start_cs);
178
179 27 av_bprint_finalize(&seg, NULL);
180 27 return 0;
181 }
182
183 27 static int webvtt_decode_frame(AVCodecContext *avctx, AVSubtitle *sub,
184 int *got_sub_ptr, const AVPacket *avpkt)
185 {
186 27 int ret = 0;
187 27 const char *ptr = avpkt->data;
188 27 FFASSDecoderContext *s = avctx->priv_data;
189 AVBPrint buf;
190 27 AVRational ms = { 1, 1000 };
191 27 int64_t start_ms = 0, end_ms = 0;
192
193 /* Inline cue timestamps are absolute milliseconds on the media timeline.
194 * Convert the packet timing to the same unit via pkt_timebase instead of
195 * assuming a 1/1000 time base. If the timing is unknown, leave both at 0
196 * so every inline timestamp is rejected and the cue is emitted verbatim. */
197
2/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27 times.
✗ Branch 3 not taken.
27 if (avpkt->pts != AV_NOPTS_VALUE && avctx->pkt_timebase.num) {
198 27 start_ms = av_rescale_q(avpkt->pts, avctx->pkt_timebase, ms);
199 27 end_ms = start_ms + av_rescale_q(avpkt->duration, avctx->pkt_timebase, ms);
200 }
201
202 27 av_bprint_init(&buf, 0, AV_BPRINT_SIZE_UNLIMITED);
203
3/6
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 27 times.
✗ Branch 5 not taken.
54 if (ptr && avpkt->size > 0 &&
204 27 !webvtt_event_to_ass(&buf, ptr, start_ms, end_ms))
205 27 ret = ff_ass_add_rect(sub, buf.str, s->readorder++, 0, NULL, NULL);
206 27 av_bprint_finalize(&buf, NULL);
207
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (ret < 0)
208 return ret;
209 27 *got_sub_ptr = sub->num_rects > 0;
210 27 return avpkt->size;
211 }
212
213 const FFCodec ff_webvtt_decoder = {
214 .p.name = "webvtt",
215 CODEC_LONG_NAME("WebVTT subtitle"),
216 .p.type = AVMEDIA_TYPE_SUBTITLE,
217 .p.id = AV_CODEC_ID_WEBVTT,
218 FF_CODEC_DECODE_SUB_CB(webvtt_decode_frame),
219 .init = ff_ass_subtitle_header_default,
220 .flush = ff_ass_decoder_flush,
221 .priv_data_size = sizeof(FFASSDecoderContext),
222 };
223