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