| 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 |