FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/ttmlenc.c
Date: 2026-10-31 18:41:55
Exec Total Coverage
Lines: 66 87 75.9%
Functions: 5 5 100.0%
Branches: 12 27 44.4%

Line Branch Exec Source
1 /*
2 * TTML subtitle muxer
3 * Copyright (c) 2020 24i
4 *
5 * This file is part of FFmpeg.
6 *
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.
11 *
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.
16 *
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
20 */
21
22 /**
23 * @file
24 * TTML subtitle muxer
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
28 */
29
30 #include "libavutil/avstring.h"
31 #include "avformat.h"
32 #include "internal.h"
33 #include "mux.h"
34 #include "ttmlenc.h"
35 #include "libavcodec/ttmlenc.h"
36 #include "libavutil/internal.h"
37 #include "libavutil/mem.h"
38
39 enum TTMLPacketType {
40 PACKET_TYPE_PARAGRAPH,
41 PACKET_TYPE_DOCUMENT,
42 };
43
44 struct TTMLHeaderParameters {
45 const char *tt_element_params;
46 const char *pre_body_elements;
47 };
48
49 typedef struct TTMLMuxContext {
50 enum TTMLPacketType input_type;
51 unsigned int document_written;
52 } TTMLMuxContext;
53
54 static const char ttml_header_text[] =
55 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
56 "<tt\n"
57 "%s"
58 " xml:lang=\"%s\">\n"
59 "%s"
60 " <body>\n"
61 " <div>\n";
62
63 static const char ttml_footer_text[] =
64 " </div>\n"
65 " </body>\n"
66 "</tt>\n";
67
68 342 static void ttml_write_time(AVIOContext *pb, const char tag[],
69 int64_t millisec)
70 {
71 int64_t sec, min, hour;
72 342 sec = millisec / 1000;
73 342 millisec -= 1000 * sec;
74 342 min = sec / 60;
75 342 sec -= 60 * min;
76 342 hour = min / 60;
77 342 min -= 60 * hour;
78
79 342 avio_printf(pb, "%s=\"%02"PRId64":%02"PRId64":%02"PRId64".%03"PRId64"\"",
80 tag, hour, min, sec, millisec);
81 342 }
82
83 29 static int ttml_set_header_values_from_extradata(
84 AVCodecParameters *par, struct TTMLHeaderParameters *header_params)
85 {
86 29 size_t additional_data_size =
87 29 par->extradata_size - TTMLENC_EXTRADATA_SIGNATURE_SIZE;
88 29 char *value =
89 29 (char *)par->extradata + TTMLENC_EXTRADATA_SIGNATURE_SIZE;
90 29 size_t value_size = av_strnlen(value, additional_data_size);
91 29 struct TTMLHeaderParameters local_params = { 0 };
92
93
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (!additional_data_size) {
94 // simple case, we don't have to go through local_params and just
95 // set default fall-back values (for old extradata format).
96 ✗ header_params->tt_element_params = TTML_DEFAULT_NAMESPACING;
97 ✗ header_params->pre_body_elements = "";
98
99 ✗ return 0;
100 }
101
102
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (value_size == additional_data_size ||
103
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 value[value_size] != '\0')
104 ✗ return AVERROR_INVALIDDATA;
105
106 29 local_params.tt_element_params = value;
107
108 29 additional_data_size -= value_size + 1;
109 29 value += value_size + 1;
110
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (!additional_data_size)
111 ✗ return AVERROR_INVALIDDATA;
112
113 29 value_size = av_strnlen(value, additional_data_size);
114
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (value_size == additional_data_size ||
115
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 value[value_size] != '\0')
116 ✗ return AVERROR_INVALIDDATA;
117
118 29 local_params.pre_body_elements = value;
119
120 29 *header_params = local_params;
121
122 29 return 0;
123 }
124
125 29 static int ttml_write_header(AVFormatContext *ctx)
126 {
127 29 TTMLMuxContext *ttml_ctx = ctx->priv_data;
128 29 AVStream *st = ctx->streams[0];
129 29 AVIOContext *pb = ctx->pb;
130
131 29 const AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL,
132 0);
133
134 29 ttml_ctx->document_written = 0;
135 29 ttml_ctx->input_type = ff_is_ttml_stream_paragraph_based(st->codecpar) ?
136 29 PACKET_TYPE_PARAGRAPH :
137 PACKET_TYPE_DOCUMENT;
138
139 29 avpriv_set_pts_info(st, 64, 1, 1000);
140
141
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (ttml_ctx->input_type == PACKET_TYPE_PARAGRAPH) {
142 struct TTMLHeaderParameters header_params;
143 char *printed_lang;
144 29 int ret = ttml_set_header_values_from_extradata(
145 st->codecpar, &header_params);
146
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (ret < 0) {
147 ✗ av_log(ctx, AV_LOG_ERROR,
148 "Failed to parse TTML header values from extradata: "
149 ✗ "%s!\n", av_err2str(ret));
150 ✗ return ret;
151 }
152
153
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 ret = av_escape(&printed_lang, lang ? lang->value : "", NULL,
154 AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
155
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (ret < 0)
156 ✗ return ret;
157
158 29 avio_printf(pb, ttml_header_text,
159 header_params.tt_element_params,
160 printed_lang,
161 header_params.pre_body_elements);
162 29 av_free(printed_lang);
163 }
164
165 29 return 0;
166 }
167
168 171 static int ttml_write_packet(AVFormatContext *ctx, AVPacket *pkt)
169 {
170 171 TTMLMuxContext *ttml_ctx = ctx->priv_data;
171 171 AVIOContext *pb = ctx->pb;
172
173
1/3
✓ Branch 0 taken 171 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
171 switch (ttml_ctx->input_type) {
174 171 case PACKET_TYPE_PARAGRAPH:
175 // write out a paragraph element with the given contents.
176 171 avio_printf(pb, " <p\n");
177 171 ttml_write_time(pb, " begin", pkt->pts);
178 171 avio_w8(pb, '\n');
179 171 ttml_write_time(pb, " end", pkt->pts + pkt->duration);
180 171 avio_printf(pb, ">");
181 171 avio_write(pb, pkt->data, pkt->size);
182 171 avio_printf(pb, "</p>\n");
183 171 break;
184 ✗ case PACKET_TYPE_DOCUMENT:
185 // dump the given document out as-is.
186 ✗ if (ttml_ctx->document_written) {
187 ✗ av_log(ctx, AV_LOG_ERROR,
188 "Attempting to write multiple TTML documents into a "
189 "single document! The XML specification forbids this "
190 "as there has to be a single root tag.\n");
191 ✗ return AVERROR(EINVAL);
192 }
193 ✗ avio_write(pb, pkt->data, pkt->size);
194 ✗ ttml_ctx->document_written = 1;
195 ✗ break;
196 ✗ default:
197 ✗ av_log(ctx, AV_LOG_ERROR,
198 "Internal error: invalid TTML input packet type: %d!\n",
199 ✗ ttml_ctx->input_type);
200 ✗ return AVERROR_BUG;
201 }
202
203 171 return 0;
204 }
205
206 29 static int ttml_write_trailer(AVFormatContext *ctx)
207 {
208 29 TTMLMuxContext *ttml_ctx = ctx->priv_data;
209 29 AVIOContext *pb = ctx->pb;
210
211
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (ttml_ctx->input_type == PACKET_TYPE_PARAGRAPH)
212 29 avio_printf(pb, ttml_footer_text);
213
214 29 return 0;
215 }
216
217 const FFOutputFormat ff_ttml_muxer = {
218 .p.name = "ttml",
219 .p.long_name = NULL_IF_CONFIG_SMALL("TTML subtitle"),
220 .p.extensions = "ttml",
221 .p.mime_type = "text/ttml",
222 .priv_data_size = sizeof(TTMLMuxContext),
223 .p.flags = AVFMT_GLOBALHEADER | AVFMT_VARIABLE_FPS |
224 AVFMT_TS_NONSTRICT,
225 .p.video_codec = AV_CODEC_ID_NONE,
226 .p.audio_codec = AV_CODEC_ID_NONE,
227 .p.subtitle_codec = AV_CODEC_ID_TTML,
228 .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH |
229 FF_OFMT_FLAG_ONLY_DEFAULT_CODECS,
230 .write_header = ttml_write_header,
231 .write_packet = ttml_write_packet,
232 .write_trailer = ttml_write_trailer,
233 };
234