FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/mccdec.c
Date: 2026-05-01 13:04:54
Exec Total Coverage
Lines: 150 191 78.5%
Functions: 10 11 90.9%
Branches: 74 114 64.9%

Line Branch Exec Source
1 /*
2 * MCC subtitle demuxer
3 * Copyright (c) 2020 Paul B Mahol
4 * Copyright (c) 2025 Jacob Lifshay
5 *
6 * This file is part of FFmpeg.
7 *
8 * FFmpeg is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 of the License, or (at your option) any later version.
12 *
13 * FFmpeg is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with FFmpeg; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 */
22
23 #include "avformat.h"
24 #include "demux.h"
25 #include "internal.h"
26 #include "libavcodec/bytestream.h"
27 #include "libavcodec/codec_id.h"
28 #include "libavcodec/smpte_436m.h"
29 #include "libavutil/attributes_internal.h"
30 #include "libavutil/avstring.h"
31 #include "libavutil/avutil.h"
32 #include "libavutil/error.h"
33 #include "libavutil/internal.h"
34 #include "libavutil/log.h"
35 #include "libavutil/macros.h"
36 #include "libavutil/opt.h"
37 #include "libavutil/rational.h"
38 #include "libavutil/timecode.h"
39 #include "subtitles.h"
40 #include <inttypes.h>
41 #include <stdbool.h>
42 #include <string.h>
43
44 typedef struct MCCContext {
45 const AVClass *class;
46 int eia608_extract;
47 FFDemuxSubtitlesQueue q;
48 } MCCContext;
49
50 7480 static int mcc_probe(const AVProbeData *p)
51 {
52 char buf[28];
53 FFTextReader tr;
54
55 7480 ff_text_init_buf(&tr, p->buf, p->buf_size);
56
57
4/4
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 7483 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 7480 times.
7487 while (ff_text_peek_r8(&tr) == '\r' || ff_text_peek_r8(&tr) == '\n')
58 7 ff_text_r8(&tr);
59
60 7480 ff_text_read(&tr, buf, sizeof(buf));
61
62
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7480 times.
7480 if (!memcmp(buf, "File Format=MacCaption_MCC V", 28))
63 return AVPROBE_SCORE_MAX;
64
65 7480 return 0;
66 }
67
68 4864 static int convert(uint8_t x)
69 {
70
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4864 times.
4864 if (x >= 'a')
71 x -= 87;
72
2/2
✓ Branch 0 taken 1900 times.
✓ Branch 1 taken 2964 times.
4864 else if (x >= 'A')
73 1900 x -= 55;
74 else
75 2964 x -= '0';
76 4864 return x;
77 }
78
79 typedef struct alias {
80 uint8_t key;
81 int len;
82 const char *value;
83 } alias;
84
85 #define CCPAD "\xFA\x0\x0"
86 #define CCPAD3 CCPAD CCPAD CCPAD
87
88 static attribute_nonstring const char cc_pad[27] = CCPAD3 CCPAD3 CCPAD3;
89
90 static const alias aliases[20] = {
91 // clang-format off
92 { .key = 16, .len = 3, .value = cc_pad, },
93 { .key = 17, .len = 6, .value = cc_pad, },
94 { .key = 18, .len = 9, .value = cc_pad, },
95 { .key = 19, .len = 12, .value = cc_pad, },
96 { .key = 20, .len = 15, .value = cc_pad, },
97 { .key = 21, .len = 18, .value = cc_pad, },
98 { .key = 22, .len = 21, .value = cc_pad, },
99 { .key = 23, .len = 24, .value = cc_pad, },
100 { .key = 24, .len = 27, .value = cc_pad, },
101 { .key = 25, .len = 3, .value = "\xFB\x80\x80", },
102 { .key = 26, .len = 3, .value = "\xFC\x80\x80", },
103 { .key = 27, .len = 3, .value = "\xFD\x80\x80", },
104 { .key = 28, .len = 2, .value = "\x96\x69", },
105 { .key = 29, .len = 2, .value = "\x61\x01", },
106 { .key = 30, .len = 3, .value = "\xFC\x80\x80", },
107 { .key = 31, .len = 3, .value = "\xFC\x80\x80", },
108 { .key = 32, .len = 4, .value = "\xE1\x00\x00\x00", },
109 { .key = 33, .len = 0, .value = NULL, },
110 { .key = 34, .len = 0, .value = NULL, },
111 { .key = 35, .len = 1, .value = "\x0", },
112 // clang-format on
113 };
114
115 typedef struct TimeTracker {
116 int64_t last_ts;
117 int64_t twenty_four_hr;
118 AVTimecode timecode;
119 } TimeTracker;
120
121 8 static int time_tracker_init(TimeTracker *tt, AVStream *st, AVRational rate, void *log_ctx)
122 {
123 8 *tt = (TimeTracker){ .last_ts = 0 };
124 8 int ret = av_timecode_init(&tt->timecode, rate, rate.den == 1001 ? AV_TIMECODE_FLAG_DROPFRAME : 0, 0, log_ctx);
125
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (ret < 0)
126 return ret;
127 // wrap pts values at 24hr ourselves since they can be bigger than fits in an int
128 AVTimecode twenty_four_hr;
129 8 ret = av_timecode_init_from_components(&twenty_four_hr, rate, tt->timecode.flags, 24, 0, 0, 0, log_ctx);
130
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (ret < 0)
131 return ret;
132 8 tt->twenty_four_hr = twenty_four_hr.start;
133 // timecode uses reciprocal of timebase
134 8 avpriv_set_pts_info(st, 64, rate.den, rate.num);
135 8 return 0;
136 }
137
138 typedef struct MCCTimecode {
139 unsigned hh, mm, ss, ff, field, line_number;
140 } MCCTimecode;
141
142 192 static int time_tracker_set_time(TimeTracker *tt, const MCCTimecode *tc, void *log_ctx)
143 {
144 192 AVTimecode last = tt->timecode;
145 192 int ret = av_timecode_init_from_components(&tt->timecode, last.rate, last.flags, tc->hh, tc->mm, tc->ss, tc->ff, log_ctx);
146
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0) {
147 tt->timecode = last;
148 return ret;
149 }
150 192 tt->last_ts -= last.start;
151 192 tt->last_ts += tt->timecode.start;
152
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (tt->timecode.start < last.start)
153 tt->last_ts += tt->twenty_four_hr;
154 192 return 0;
155 }
156
157 struct ValidTimeCodeRate {
158 AVRational rate;
159 char str[5];
160 };
161
162 static const struct ValidTimeCodeRate valid_time_code_rates[] = {
163 { .rate = { .num = 24, .den = 1 }, .str = "24" },
164 { .rate = { .num = 25, .den = 1 }, .str = "25" },
165 { .rate = { .num = 30000, .den = 1001 }, .str = "30DF" },
166 { .rate = { .num = 30, .den = 1 }, .str = "30" },
167 { .rate = { .num = 50, .den = 1 }, .str = "50" },
168 { .rate = { .num = 60000, .den = 1001 }, .str = "60DF" },
169 { .rate = { .num = 60, .den = 1 }, .str = "60" },
170 };
171
172 4 static int parse_time_code_rate(AVFormatContext *s, AVStream *st, TimeTracker *tt, const char *time_code_rate)
173 {
174
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 for (size_t i = 0; i < FF_ARRAY_ELEMS(valid_time_code_rates); i++) {
175 const char *after;
176
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 8 times.
12 if (av_stristart(time_code_rate, valid_time_code_rates[i].str, &after) != 0) {
177 4 bool bad_after = false;
178
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 for (; *after; after++) {
179 if (!av_isspace(*after)) {
180 bad_after = true;
181 break;
182 }
183 }
184
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (bad_after)
185 continue;
186 4 return time_tracker_init(tt, st, valid_time_code_rates[i].rate, s);
187 }
188 }
189 av_log(s, AV_LOG_FATAL, "invalid mcc time code rate: %s", time_code_rate);
190 return AVERROR_INVALIDDATA;
191 }
192
193 896 static int mcc_parse_time_code_part(char **line_left, unsigned *value, unsigned max, const char *after_set)
194 {
195 896 *value = 0;
196
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 896 times.
896 if (!av_isdigit(**line_left))
197 return AVERROR_INVALIDDATA;
198
2/2
✓ Branch 0 taken 1728 times.
✓ Branch 1 taken 896 times.
2624 while (av_isdigit(**line_left)) {
199 1728 unsigned digit = **line_left - '0';
200 1728 *value = *value * 10 + digit;
201 1728 ++*line_left;
202
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1728 times.
1728 if (*value > max)
203 return AVERROR_INVALIDDATA;
204 }
205 896 unsigned char after = **line_left;
206
2/4
✓ Branch 0 taken 896 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 896 times.
896 if (!after || !strchr(after_set, after))
207 return AVERROR_INVALIDDATA;
208 896 ++*line_left;
209 896 return after;
210 }
211
212 192 static int mcc_parse_time_code(char **line_left, MCCTimecode *tc)
213 {
214 192 *tc = (MCCTimecode){ .field = 0, .line_number = 9 };
215 192 int ret = mcc_parse_time_code_part(line_left, &tc->hh, 23, ":");
216
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0)
217 return ret;
218 192 ret = mcc_parse_time_code_part(line_left, &tc->mm, 59, ":");
219
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0)
220 return ret;
221 192 ret = mcc_parse_time_code_part(line_left, &tc->ss, 59, ":;");
222
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0)
223 return ret;
224 192 ret = mcc_parse_time_code_part(line_left, &tc->ff, 59, ".\t");
225
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0)
226 return ret;
227
2/2
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 128 times.
192 if (ret == '.') {
228 64 ret = mcc_parse_time_code_part(line_left, &tc->field, 1, ",\t");
229
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (ret < 0)
230 return ret;
231
1/2
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
64 if (ret == ',') {
232 64 ret = mcc_parse_time_code_part(line_left, &tc->line_number, 0xFFFF, "\t");
233
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (ret < 0)
234 return ret;
235 }
236 }
237
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret != '\t')
238 return AVERROR_INVALIDDATA;
239 192 return 0;
240 }
241
242 4 static int mcc_read_header(AVFormatContext *s)
243 {
244 4 MCCContext *mcc = s->priv_data;
245 4 AVStream *st = avformat_new_stream(s, NULL);
246 int64_t pos;
247 4 AVSmpte436mCodedAnc coded_anc = {
248 .payload_sample_coding = AV_SMPTE_436M_PAYLOAD_SAMPLE_CODING_8BIT_LUMA,
249 };
250 char line[4096];
251 FFTextReader tr;
252 4 int ret = 0;
253
254 4 ff_text_init_avio(s, &tr, s->pb);
255
256
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!st)
257 return AVERROR(ENOMEM);
258
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (mcc->eia608_extract) {
259 2 st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
260 2 st->codecpar->codec_id = AV_CODEC_ID_EIA_608;
261 } else {
262 2 st->codecpar->codec_type = AVMEDIA_TYPE_DATA;
263 2 st->codecpar->codec_id = AV_CODEC_ID_SMPTE_436M_ANC;
264 2 av_dict_set(&st->metadata, "data_type", "vbi_vanc_smpte_436M", 0);
265 }
266
267 TimeTracker tt;
268 4 ret = time_tracker_init(&tt, st, (AVRational){ .num = 30, .den = 1 }, s);
269
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (ret < 0)
270 return ret;
271
272
2/2
✓ Branch 1 taken 356 times.
✓ Branch 2 taken 4 times.
360 while (!ff_text_eof(&tr)) {
273 356 pos = ff_text_pos(&tr);
274 356 ff_subtitles_read_line(&tr, line, sizeof(line));
275
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 352 times.
356 if (!strncmp(line, "File Format=MacCaption_MCC V", 28))
276 356 continue;
277
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 212 times.
352 if (!strncmp(line, "//", 2))
278 140 continue;
279
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 208 times.
212 if (!strncmp(line, "Time Code Rate=", 15)) {
280 4 ret = parse_time_code_rate(s, st, &tt, line + 15);
281
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (ret < 0)
282 return ret;
283 4 continue;
284 }
285
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 192 times.
208 if (strchr(line, '='))
286 16 continue; // skip attributes
287 192 char *line_left = line;
288
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 while (av_isspace(*line_left))
289 line_left++;
290
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (!*line_left) // skip empty lines
291 continue;
292 MCCTimecode tc;
293 192 ret = mcc_parse_time_code(&line_left, &tc);
294
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0) {
295 av_log(s, AV_LOG_ERROR, "can't parse mcc time code");
296 continue;
297 }
298
299 192 int64_t last_pts = tt.last_ts;
300 192 ret = time_tracker_set_time(&tt, &tc, s);
301
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (ret < 0)
302 continue;
303 192 bool merge = last_pts == tt.last_ts;
304
305 192 coded_anc.line_number = tc.line_number;
306
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 160 times.
192 coded_anc.wrapping_type = tc.field ? AV_SMPTE_436M_WRAPPING_TYPE_VANC_FIELD_2 : AV_SMPTE_436M_WRAPPING_TYPE_VANC_FRAME;
307
308 PutByteContext pb;
309 192 bytestream2_init_writer(&pb, coded_anc.payload, AV_SMPTE_436M_CODED_ANC_PAYLOAD_CAPACITY);
310
311
2/2
✓ Branch 0 taken 2944 times.
✓ Branch 1 taken 192 times.
3136 while (*line_left) {
312 2944 uint8_t v = convert(*line_left++);
313
314
3/4
✓ Branch 0 taken 1024 times.
✓ Branch 1 taken 1920 times.
✓ Branch 2 taken 1024 times.
✗ Branch 3 not taken.
3968 if (v >= 16 && v <= 35) {
315 1024 int idx = v - 16;
316 1024 bytestream2_put_buffer(&pb, aliases[idx].value, aliases[idx].len);
317 } else {
318 uint8_t vv;
319
320
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1920 times.
1920 if (!*line_left)
321 break;
322 1920 vv = convert(*line_left++);
323 1920 bytestream2_put_byte(&pb, vv | (v << 4));
324 }
325 }
326
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (pb.eof)
327 continue;
328 // remove trailing ANC checksum byte (not to be confused with the CDP checksum byte),
329 // it's not included in 8-bit sample encodings. see section 6.2 (page 14) of:
330 // https://pub.smpte.org/latest/st436/s436m-2006.pdf
331 192 bytestream2_seek_p(&pb, -1, SEEK_CUR);
332 192 coded_anc.payload_sample_count = bytestream2_tell_p(&pb);
333
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (coded_anc.payload_sample_count == 0)
334 continue; // ignore if too small
335 // add padding to align to 4 bytes
336
3/4
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 64 times.
✓ Branch 4 taken 192 times.
256 while (!pb.eof && bytestream2_tell_p(&pb) % 4)
337 64 bytestream2_put_byte(&pb, 0);
338
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 192 times.
192 if (pb.eof)
339 continue;
340 192 coded_anc.payload_array_length = bytestream2_tell_p(&pb);
341
342 AVPacket *sub;
343
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 96 times.
192 if (mcc->eia608_extract) {
344 AVSmpte291mAnc8bit anc;
345
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 96 times.
96 if (av_smpte_291m_anc_8bit_decode(
346 96 &anc, coded_anc.payload_sample_coding, coded_anc.payload_sample_count, coded_anc.payload, s)
347 < 0)
348 32 continue;
349 // reuse line
350 96 int cc_count = av_smpte_291m_anc_8bit_extract_cta_708(&anc, line, s);
351
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 64 times.
96 if (cc_count < 0) // continue if error or if it's not a closed captions packet
352 32 continue;
353 64 int len = cc_count * 3;
354
355 64 sub = ff_subtitles_queue_insert(&mcc->q, line, len, merge);
356
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (!sub)
357 return AVERROR(ENOMEM);
358 } else {
359 96 sub = ff_subtitles_queue_insert(&mcc->q, NULL, 0, merge);
360
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 96 times.
96 if (!sub)
361 return AVERROR(ENOMEM);
362
363 96 ret = av_smpte_436m_anc_append(sub, 1, &coded_anc);
364
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 96 times.
96 if (ret < 0)
365 return ret;
366 }
367
368 160 sub->pos = pos;
369 160 sub->pts = tt.last_ts;
370 160 sub->duration = 1;
371 160 continue;
372 }
373
374 4 ff_subtitles_queue_finalize(s, &mcc->q);
375
376 4 return ret;
377 }
378
379 132 static int mcc_read_packet(AVFormatContext *s, AVPacket *pkt)
380 {
381 132 MCCContext *mcc = s->priv_data;
382 132 return ff_subtitles_queue_read_packet(&mcc->q, pkt);
383 }
384
385 static int mcc_read_seek(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags)
386 {
387 MCCContext *mcc = s->priv_data;
388 return ff_subtitles_queue_seek(&mcc->q, s, stream_index, min_ts, ts, max_ts, flags);
389 }
390
391 4 static int mcc_read_close(AVFormatContext *s)
392 {
393 4 MCCContext *mcc = s->priv_data;
394 4 ff_subtitles_queue_clean(&mcc->q);
395 4 return 0;
396 }
397
398 #define OFFSET(x) offsetof(MCCContext, x)
399 #define SD AV_OPT_FLAG_SUBTITLE_PARAM | AV_OPT_FLAG_DECODING_PARAM
400 // clang-format off
401 static const AVOption mcc_options[] = {
402 { "eia608_extract", "extract EIA-608/708 captions from VANC packets", OFFSET(eia608_extract), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, SD },
403 { NULL },
404 };
405 // clang-format on
406
407 static const AVClass mcc_class = {
408 .class_name = "mcc demuxer",
409 .item_name = av_default_item_name,
410 .option = mcc_options,
411 .version = LIBAVUTIL_VERSION_INT,
412 .category = AV_CLASS_CATEGORY_DEMUXER,
413 };
414
415 const FFInputFormat ff_mcc_demuxer = {
416 .p.name = "mcc",
417 .p.long_name = NULL_IF_CONFIG_SMALL("MacCaption"),
418 .p.extensions = "mcc",
419 .p.priv_class = &mcc_class,
420 .priv_data_size = sizeof(MCCContext),
421 .flags_internal = FF_INFMT_FLAG_INIT_CLEANUP,
422 .read_probe = mcc_probe,
423 .read_header = mcc_read_header,
424 .read_packet = mcc_read_packet,
425 .read_seek2 = mcc_read_seek,
426 .read_close = mcc_read_close,
427 };
428