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