1
0
mirror of https://github.com/FFmpeg/FFmpeg.git synced 2025-11-23 21:54:53 +02:00
Files
FFmpeg/libavformat/mccenc.c
James Almer 660983b7f3 avformat/mccenc: use Lavf version string instead of FFmpeg
The muxed subtitle is created by libavformat, and as such that's what should be
reported. This is in line with the string we write for every other muxer.
After this change, the muxer will no longer be recompiled every time a commit
is made.

Signed-off-by: James Almer <jamrial@gmail.com>
2025-10-06 11:51:50 -03:00

490 lines
19 KiB
C

/*
* MCC subtitle muxer
* Copyright (c) 2025 Jacob Lifshay
* Copyright (c) 2017 Paul B Mahol
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "avformat.h"
#include "internal.h"
#include "mux.h"
#include "version.h"
#include "libavcodec/codec_id.h"
#include "libavcodec/smpte_436m.h"
#include "libavutil/avassert.h"
#include "libavutil/avstring.h"
#include "libavutil/error.h"
#include "libavutil/log.h"
#include "libavutil/macros.h"
#include "libavutil/opt.h"
#include "libavutil/parseutils.h"
#include "libavutil/rational.h"
#include "libavutil/time_internal.h" // for localtime_r
#include "libavutil/timecode.h"
typedef struct MCCContext {
const AVClass *class;
AVTimecode timecode;
int64_t twenty_four_hr;
char *override_time_code_rate;
int use_u_alias;
unsigned mcc_version;
char *creation_program;
char *creation_time;
} MCCContext;
typedef enum MCCVersion
{
MCC_VERSION_1 = 1,
MCC_VERSION_2 = 2,
MCC_VERSION_MIN = MCC_VERSION_1,
MCC_VERSION_MAX = MCC_VERSION_2,
} MCCVersion;
#define MCC_HEADER \
"File Format=MacCaption_MCC V%c.0\n" \
"\n" \
"///////////////////////////////////////////////////////////////////////////////////\n" \
"// Computer Prompting and Captioning Company\n" \
"// Ancillary Data Packet Transfer File\n" \
"//\n" \
"// Permission to generate this format is granted provided that\n" \
"// 1. This ANC Transfer file format is used on an as-is basis and no warranty is given, and\n" \
"// 2. This entire descriptive information text is included in a generated .mcc file.\n" \
"//\n" \
"// General file format:\n" \
"// HH:MM:SS:FF(tab)[Hexadecimal ANC data in groups of 2 characters]\n" \
"// Hexadecimal data starts with the Ancillary Data Packet DID (Data ID defined in S291M)\n" \
"// and concludes with the Check Sum following the User Data Words.\n" \
"// Each time code line must contain at most one complete ancillary data packet.\n" \
"// To transfer additional ANC Data successive lines may contain identical time code.\n" \
"// Time Code Rate=[24, 25, 30, 30DF, 50, 60%s]\n" \
"//\n" \
"// ANC data bytes may be represented by one ASCII character according to the following schema:\n" \
"// G FAh 00h 00h\n" \
"// H 2 x (FAh 00h 00h)\n" \
"// I 3 x (FAh 00h 00h)\n" \
"// J 4 x (FAh 00h 00h)\n" \
"// K 5 x (FAh 00h 00h)\n" \
"// L 6 x (FAh 00h 00h)\n" \
"// M 7 x (FAh 00h 00h)\n" \
"// N 8 x (FAh 00h 00h)\n" \
"// O 9 x (FAh 00h 00h)\n" \
"// P FBh 80h 80h\n" \
"// Q FCh 80h 80h\n" \
"// R FDh 80h 80h\n" \
"// S 96h 69h\n" \
"// T 61h 01h\n" \
"// U E1h 00h 00h 00h\n" \
"// Z 00h\n" \
"//\n" \
"///////////////////////////////////////////////////////////////////////////////////\n"
#define MCC_HEADER_PRINTF_ARGS(mcc_version) (mcc_version) + '0', \
(mcc_version) == MCC_VERSION_1 ? "" : ", 60DF"
/**
* generated with the bash command:
* ```bash
* URL="https://code.ffmpeg.org/FFmpeg/FFmpeg/src/branch/master/libavformat/mccenc.c"
* python3 -c "from uuid import *; print(str(uuid5(NAMESPACE_URL, '$URL')).upper())"
* ```
*/
static const char mcc_ffmpeg_uuid[] = "0087C4F6-A6B4-5469-8C8E-BBF44950401D";
static const AVRational valid_time_code_rates[] = {
{ .num = 24, .den = 1 },
{ .num = 25, .den = 1 },
{ .num = 30000, .den = 1001 },
{ .num = 30, .den = 1 },
{ .num = 50, .den = 1 },
{ .num = 60000, .den = 1001 },
{ .num = 60, .den = 1 },
};
static int mcc_write_header(AVFormatContext *avf)
{
MCCContext *mcc = avf->priv_data;
const char *creation_program = mcc->creation_program;
if (!creation_program) {
if (avf->flags & AVFMT_FLAG_BITEXACT)
creation_program = "Lavf";
else
creation_program = LIBAVFORMAT_IDENT;
} else if (strchr(creation_program, '\n')) {
av_log(avf, AV_LOG_FATAL, "creation_program must not contain multiple lines of text\n");
return AVERROR(EINVAL);
}
if (avf->flags & AVFMT_FLAG_BITEXACT && !av_strcasecmp(mcc->creation_time, "now"))
av_log(avf, AV_LOG_ERROR, "creation_time must be overridden for bit-exact output\n");
int64_t timeval = 0;
int ret = av_parse_time(&timeval, mcc->creation_time, 0);
if (ret < 0) {
av_log(avf, AV_LOG_FATAL, "can't parse creation_time\n");
return ret;
}
struct tm tm;
if (!localtime_r((time_t[1]){ timeval / 1000000 }, &tm))
return AVERROR(EINVAL);
// we can't rely on having the C locale, so convert the date/time to a string ourselves:
static const char months[12][10] = {
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
};
// assert that values are sane so we don't index out of bounds
av_assert0(tm.tm_mon >= 0 && tm.tm_mon < FF_ARRAY_ELEMS(months));
const char *month = months[tm.tm_mon];
static const char weekdays[7][10] = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};
// assert that values are sane so we don't index out of bounds
av_assert0(tm.tm_wday >= 0 && tm.tm_wday < FF_ARRAY_ELEMS(weekdays));
const char *weekday = weekdays[tm.tm_wday];
avio_printf(avf->pb,
MCC_HEADER "\n"
"UUID=%s\n"
"Creation Program=%s\n"
"Creation Date=%s, %s %d, %d\n"
"Creation Time=%02d:%02d:%02d\n"
"Time Code Rate=%u%s\n\n",
MCC_HEADER_PRINTF_ARGS(mcc->mcc_version),
mcc_ffmpeg_uuid,
creation_program,
weekday,
month,
tm.tm_mday,
tm.tm_year + 1900,
tm.tm_hour,
tm.tm_min,
tm.tm_sec,
mcc->timecode.fps,
mcc->timecode.flags & AV_TIMECODE_FLAG_DROPFRAME ? "DF" : "");
return 0;
}
/// convert the input bytes to hexadecimal with mcc's aliases
static void mcc_bytes_to_hex(char *dest, const uint8_t *bytes, size_t bytes_size, int use_u_alias)
{
while (bytes_size != 0) {
switch (bytes[0]) {
case 0xFA:
*dest = '\0';
for (unsigned char code = 'G'; code <= (unsigned char)'O'; code++) {
if (bytes_size < 3)
break;
if (bytes[0] != 0xFA || bytes[1] != 0 || bytes[2] != 0)
break;
*dest = code;
bytes += 3;
bytes_size -= 3;
}
if (*dest) {
dest++;
continue;
}
break;
case 0xFB:
case 0xFC:
case 0xFD:
if (bytes_size >= 3 && bytes[1] == 0x80 && bytes[2] == 0x80) {
*dest++ = bytes[0] - 0xFB + 'P';
bytes += 3;
bytes_size -= 3;
continue;
}
break;
case 0x96:
if (bytes_size >= 2 && bytes[1] == 0x69) {
*dest++ = 'S';
bytes += 2;
bytes_size -= 2;
continue;
}
break;
case 0x61:
if (bytes_size >= 2 && bytes[1] == 0x01) {
*dest++ = 'T';
bytes += 2;
bytes_size -= 2;
continue;
}
break;
case 0xE1:
if (use_u_alias && bytes_size >= 4 && bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0) {
*dest++ = 'U';
bytes += 4;
bytes_size -= 4;
continue;
}
break;
case 0:
*dest++ = 'Z';
bytes++;
bytes_size--;
continue;
default:
// any other bytes falls through to writing hex
break;
}
for (int shift = 4; shift >= 0; shift -= 4) {
int v = (bytes[0] >> shift) & 0xF;
if (v < 0xA)
*dest++ = v + '0';
else
*dest++ = v - 0xA + 'A';
}
bytes++;
bytes_size--;
}
*dest = '\0';
}
static int mcc_write_packet(AVFormatContext *avf, AVPacket *pkt)
{
MCCContext *mcc = avf->priv_data;
int64_t pts = pkt->pts;
int ret;
if (pts == AV_NOPTS_VALUE) {
av_log(avf, AV_LOG_WARNING, "Insufficient timestamps.\n");
return 0;
}
char timecode_str[AV_TIMECODE_STR_SIZE];
// wrap pts values at 24hr ourselves since they can be bigger than fits in an int
av_timecode_make_string(&mcc->timecode, timecode_str, pts % mcc->twenty_four_hr);
for (char *p = timecode_str; *p; p++) {
// .mcc doesn't use ; for drop-frame time codes
if (*p == ';')
*p = ':';
}
AVSmpte436mAncIterator iter;
ret = av_smpte_436m_anc_iter_init(&iter, pkt->data, pkt->size);
if (ret < 0)
return ret;
AVSmpte436mCodedAnc coded_anc;
while ((ret = av_smpte_436m_anc_iter_next(&iter, &coded_anc)) >= 0) {
AVSmpte291mAnc8bit anc;
ret = av_smpte_291m_anc_8bit_decode(
&anc, coded_anc.payload_sample_coding, coded_anc.payload_sample_count, coded_anc.payload, avf);
if (ret < 0)
return ret;
// 4 for did, sdid_or_dbn, data_count, and checksum fields.
uint8_t mcc_anc[4 + AV_SMPTE_291M_ANC_PAYLOAD_CAPACITY];
size_t mcc_anc_len = 0;
mcc_anc[mcc_anc_len++] = anc.did;
mcc_anc[mcc_anc_len++] = anc.sdid_or_dbn;
mcc_anc[mcc_anc_len++] = anc.data_count;
memcpy(mcc_anc + mcc_anc_len, anc.payload, anc.data_count);
mcc_anc_len += anc.data_count;
mcc_anc[mcc_anc_len++] = anc.checksum;
unsigned field_number;
switch (coded_anc.wrapping_type) {
case AV_SMPTE_436M_WRAPPING_TYPE_VANC_FRAME:
case AV_SMPTE_436M_WRAPPING_TYPE_VANC_FIELD_1:
case AV_SMPTE_436M_WRAPPING_TYPE_VANC_PROGRESSIVE_FRAME:
field_number = 0;
break;
case AV_SMPTE_436M_WRAPPING_TYPE_VANC_FIELD_2:
field_number = 1;
break;
default:
av_log(avf,
AV_LOG_WARNING,
"Unsupported SMPTE 436M ANC Wrapping Type %#x -- discarding ANC packet\n",
(unsigned)coded_anc.wrapping_type);
continue;
}
char field_and_line[32] = "";
if (coded_anc.line_number != 9) {
snprintf(field_and_line, sizeof(field_and_line), ".%u,%u", field_number, (unsigned)coded_anc.line_number);
} else if (field_number != 0) {
snprintf(field_and_line, sizeof(field_and_line), ".%u", field_number);
}
switch ((MCCVersion)mcc->mcc_version) {
case MCC_VERSION_1:
if (field_and_line[0] != '\0') {
av_log(avf,
AV_LOG_WARNING,
"MCC Version 1.0 doesn't support ANC packets where the field number (got %u) isn't 0 and "
"line number (got %u) isn't 9: discarding ANC packet\n",
field_number,
(unsigned)coded_anc.line_number);
continue;
}
break;
case MCC_VERSION_2:
break;
}
// 1 for terminating nul. 2 since there's 2 hex digits per byte.
char hex[1 + 2 * sizeof(mcc_anc)];
mcc_bytes_to_hex(hex, mcc_anc, mcc_anc_len, mcc->use_u_alias);
avio_printf(avf->pb, "%s%s\t%s\n", timecode_str, field_and_line, hex);
}
if (ret != AVERROR_EOF)
return ret;
return 0;
}
static int mcc_init(AVFormatContext *avf)
{
MCCContext *mcc = avf->priv_data;
int ret;
if (avf->nb_streams != 1) {
av_log(avf, AV_LOG_ERROR, "mcc muxer supports at most one stream\n");
return AVERROR(EINVAL);
}
AVStream *st = avf->streams[0];
AVRational time_code_rate = st->avg_frame_rate;
int timecode_flags = 0;
AVTimecode twenty_four_hr;
if (mcc->override_time_code_rate && (ret = av_parse_video_rate(&time_code_rate, mcc->override_time_code_rate)) < 0)
return ret;
ret = AVERROR(EINVAL);
for (size_t i = 0; i < FF_ARRAY_ELEMS(valid_time_code_rates); i++) {
if (time_code_rate.num == valid_time_code_rates[i].num && time_code_rate.den == valid_time_code_rates[i].den) {
ret = 0;
break;
}
}
if (ret != 0) {
if (!mcc->override_time_code_rate && (time_code_rate.num <= 0 || time_code_rate.den <= 0)) {
av_log(avf, AV_LOG_FATAL, "time code rate not set, you need to use -override_time_code_rate to set it\n");
} else {
av_log(avf,
AV_LOG_FATAL,
"time code rate not supported by mcc: %d/%d\n",
time_code_rate.num,
time_code_rate.den);
}
return AVERROR(EINVAL);
}
avpriv_set_pts_info(st, 64, time_code_rate.den, time_code_rate.num);
if (time_code_rate.den == 1001 && time_code_rate.num % 30000 == 0) {
timecode_flags |= AV_TIMECODE_FLAG_DROPFRAME;
}
ret = av_timecode_init(&mcc->timecode, time_code_rate, timecode_flags, 0, avf);
if (ret < 0)
return ret;
if (mcc->mcc_version == MCC_VERSION_1) {
if (mcc->timecode.fps == 60 && mcc->timecode.flags & AV_TIMECODE_FLAG_DROPFRAME) {
av_log(avf, AV_LOG_FATAL, "MCC Version 1.0 doesn't support 60DF (59.94 fps drop-frame)\n");
return AVERROR(EINVAL);
}
}
// get av_timecode to calculate how many frames are in 24hr
ret = av_timecode_init_from_components(&twenty_four_hr, time_code_rate, timecode_flags, 24, 0, 0, 0, avf);
if (ret < 0)
return ret;
mcc->twenty_four_hr = twenty_four_hr.start;
if (st->codecpar->codec_id == AV_CODEC_ID_EIA_608) {
char args[64];
snprintf(args, sizeof(args), "cdp_frame_rate=%d/%d", time_code_rate.num, time_code_rate.den);
ret = ff_stream_add_bitstream_filter(st, "eia608_to_smpte436m", args);
if (ret < 0)
return ret;
} else if (st->codecpar->codec_id != AV_CODEC_ID_SMPTE_436M_ANC) {
av_log(avf,
AV_LOG_ERROR,
"mcc muxer supports only codec smpte_436m_anc or codec eia_608\n");
return AVERROR(EINVAL);
}
return 0;
}
static int mcc_query_codec(enum AVCodecID codec_id, int std_compliance)
{
(void)std_compliance;
if (codec_id == AV_CODEC_ID_EIA_608 || codec_id == AV_CODEC_ID_SMPTE_436M_ANC)
return 1;
return 0;
}
#define OFFSET(x) offsetof(MCCContext, x)
#define ENC AV_OPT_FLAG_ENCODING_PARAM
// clang-format off
static const AVOption options[] = {
{ "override_time_code_rate", "override the `Time Code Rate` value in the output", OFFSET(override_time_code_rate), AV_OPT_TYPE_STRING, { .str = NULL }, 0, INT_MAX, ENC },
{ "use_u_alias", "use the U alias for E1h 00h 00h 00h, disabled by default because some .mcc files disagree on whether it has 2 or 3 zero bytes", OFFSET(use_u_alias), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, ENC },
{ "mcc_version", "the mcc file format version", OFFSET(mcc_version), AV_OPT_TYPE_UINT, { .i64 = MCC_VERSION_2 }, MCC_VERSION_MIN, MCC_VERSION_MAX, ENC },
{ "creation_program", "the creation program", OFFSET(creation_program), AV_OPT_TYPE_STRING, { .str = NULL }, 0, INT_MAX, ENC },
{ "creation_time", "the creation time", OFFSET(creation_time), AV_OPT_TYPE_STRING, { .str = "now" }, 0, INT_MAX, ENC },
{ NULL },
};
// clang-format on
static const AVClass mcc_muxer_class = {
.class_name = "mcc muxer",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
};
const FFOutputFormat ff_mcc_muxer = {
.p.name = "mcc",
.p.long_name = NULL_IF_CONFIG_SMALL("MacCaption"),
.p.extensions = "mcc",
.p.flags = AVFMT_GLOBALHEADER,
.p.video_codec = AV_CODEC_ID_NONE,
.p.audio_codec = AV_CODEC_ID_NONE,
.p.subtitle_codec = AV_CODEC_ID_EIA_608,
.p.priv_class = &mcc_muxer_class,
.priv_data_size = sizeof(MCCContext),
.init = mcc_init,
.query_codec = mcc_query_codec,
.write_header = mcc_write_header,
.write_packet = mcc_write_packet,
};