3 * Copyright (c) 2020 24i
5 * This file is part of FFmpeg.
7 * FFmpeg is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
12 * FFmpeg is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with FFmpeg; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
25 * @see https://www.w3.org/TR/ttml1/
26 * @see https://www.w3.org/TR/ttml2/
27 * @see https://www.w3.org/TR/ttml-imsc/rec
32 #include "libavcodec/ttmlenc.h"
33 #include "libavutil/internal.h"
36 PACKET_TYPE_PARAGRAPH,
40 struct TTMLHeaderParameters {
41 const char *tt_element_params;
42 const char *pre_body_elements;
45 typedef struct TTMLMuxContext {
46 enum TTMLPacketType input_type;
47 unsigned int document_written;
50 static const char ttml_header_text[] =
51 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
59 static const char ttml_footer_text[] =
64 static void ttml_write_time(AVIOContext *pb, const char tag[],
67 int64_t sec, min, hour;
68 sec = millisec / 1000;
69 millisec -= 1000 * sec;
75 avio_printf(pb, "%s=\"%02"PRId64":%02"PRId64":%02"PRId64".%03"PRId64"\"",
76 tag, hour, min, sec, millisec);
79 static int ttml_set_header_values_from_extradata(
80 AVCodecParameters *par, struct TTMLHeaderParameters *header_params)
82 size_t additional_data_size =
83 par->extradata_size - TTMLENC_EXTRADATA_SIGNATURE_SIZE;
85 (char *)par->extradata + TTMLENC_EXTRADATA_SIGNATURE_SIZE;
86 size_t value_size = av_strnlen(value, additional_data_size);
87 struct TTMLHeaderParameters local_params = { 0 };
89 if (!additional_data_size) {
90 // simple case, we don't have to go through local_params and just
91 // set default fall-back values (for old extradata format).
92 header_params->tt_element_params = ttml_default_namespacing;
93 header_params->pre_body_elements = "";
98 if (value_size == additional_data_size ||
99 value[value_size] != '\0')
100 return AVERROR_INVALIDDATA;
102 local_params.tt_element_params = value;
104 additional_data_size -= value_size + 1;
105 value += value_size + 1;
106 if (!additional_data_size)
107 return AVERROR_INVALIDDATA;
109 value_size = av_strnlen(value, additional_data_size);
110 if (value_size == additional_data_size ||
111 value[value_size] != '\0')
112 return AVERROR_INVALIDDATA;
114 local_params.pre_body_elements = value;
116 *header_params = local_params;
121 static int ttml_write_header(AVFormatContext *ctx)
123 TTMLMuxContext *ttml_ctx = ctx->priv_data;
124 ttml_ctx->document_written = 0;
126 if (ctx->nb_streams != 1 ||
127 ctx->streams[0]->codecpar->codec_id != AV_CODEC_ID_TTML) {
128 av_log(ctx, AV_LOG_ERROR, "Exactly one TTML stream is required!\n");
129 return AVERROR(EINVAL);
133 AVStream *st = ctx->streams[0];
134 AVIOContext *pb = ctx->pb;
136 AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL,
138 const char *printed_lang = (lang && lang->value) ? lang->value : "";
140 // Not perfect, but decide whether the packet is a document or not
141 // by the existence of the lavc ttmlenc extradata.
142 ttml_ctx->input_type = (st->codecpar->extradata &&
143 st->codecpar->extradata_size >= TTMLENC_EXTRADATA_SIGNATURE_SIZE &&
144 !memcmp(st->codecpar->extradata,
145 TTMLENC_EXTRADATA_SIGNATURE,
146 TTMLENC_EXTRADATA_SIGNATURE_SIZE)) ?
147 PACKET_TYPE_PARAGRAPH :
148 PACKET_TYPE_DOCUMENT;
150 avpriv_set_pts_info(st, 64, 1, 1000);
152 if (ttml_ctx->input_type == PACKET_TYPE_PARAGRAPH) {
153 struct TTMLHeaderParameters header_params;
154 int ret = ttml_set_header_values_from_extradata(
155 st->codecpar, &header_params);
157 av_log(ctx, AV_LOG_ERROR,
158 "Failed to parse TTML header values from extradata: "
159 "%s!\n", av_err2str(ret));
163 avio_printf(pb, ttml_header_text,
164 header_params.tt_element_params,
166 header_params.pre_body_elements);
173 static int ttml_write_packet(AVFormatContext *ctx, AVPacket *pkt)
175 TTMLMuxContext *ttml_ctx = ctx->priv_data;
176 AVIOContext *pb = ctx->pb;
178 switch (ttml_ctx->input_type) {
179 case PACKET_TYPE_PARAGRAPH:
180 // write out a paragraph element with the given contents.
181 avio_printf(pb, " <p\n");
182 ttml_write_time(pb, " begin", pkt->pts);
184 ttml_write_time(pb, " end", pkt->pts + pkt->duration);
185 avio_printf(pb, ">");
186 avio_write(pb, pkt->data, pkt->size);
187 avio_printf(pb, "</p>\n");
189 case PACKET_TYPE_DOCUMENT:
190 // dump the given document out as-is.
191 if (ttml_ctx->document_written) {
192 av_log(ctx, AV_LOG_ERROR,
193 "Attempting to write multiple TTML documents into a "
194 "single document! The XML specification forbids this "
195 "as there has to be a single root tag.\n");
196 return AVERROR(EINVAL);
198 avio_write(pb, pkt->data, pkt->size);
199 ttml_ctx->document_written = 1;
202 av_log(ctx, AV_LOG_ERROR,
203 "Internal error: invalid TTML input packet type: %d!\n",
204 ttml_ctx->input_type);
211 static int ttml_write_trailer(AVFormatContext *ctx)
213 TTMLMuxContext *ttml_ctx = ctx->priv_data;
214 AVIOContext *pb = ctx->pb;
216 if (ttml_ctx->input_type == PACKET_TYPE_PARAGRAPH)
217 avio_printf(pb, ttml_footer_text);
222 const AVOutputFormat ff_ttml_muxer = {
224 .long_name = NULL_IF_CONFIG_SMALL("TTML subtitle"),
225 .extensions = "ttml",
226 .mime_type = "text/ttml",
227 .priv_data_size = sizeof(TTMLMuxContext),
228 .flags = AVFMT_GLOBALHEADER | AVFMT_VARIABLE_FPS |
230 .subtitle_codec = AV_CODEC_ID_TTML,
231 .write_header = ttml_write_header,
232 .write_packet = ttml_write_packet,
233 .write_trailer = ttml_write_trailer,