| 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 | 342 | static void ttml_write_time(AVIOContext *pb, const char tag[], | |
| 68 | int64_t millisec) | ||
| 69 | { | ||
| 70 | int64_t sec, min, hour; | ||
| 71 | 342 | sec = millisec / 1000; | |
| 72 | 342 | millisec -= 1000 * sec; | |
| 73 | 342 | min = sec / 60; | |
| 74 | 342 | sec -= 60 * min; | |
| 75 | 342 | hour = min / 60; | |
| 76 | 342 | min -= 60 * hour; | |
| 77 | |||
| 78 | 342 | avio_printf(pb, "%s=\"%02"PRId64":%02"PRId64":%02"PRId64".%03"PRId64"\"", | |
| 79 | tag, hour, min, sec, millisec); | ||
| 80 | 342 | } | |
| 81 | |||
| 82 | 29 | static int ttml_set_header_values_from_extradata( | |
| 83 | AVCodecParameters *par, struct TTMLHeaderParameters *header_params) | ||
| 84 | { | ||
| 85 | 29 | size_t additional_data_size = | |
| 86 | 29 | par->extradata_size - TTMLENC_EXTRADATA_SIGNATURE_SIZE; | |
| 87 | 29 | char *value = | |
| 88 | 29 | (char *)par->extradata + TTMLENC_EXTRADATA_SIGNATURE_SIZE; | |
| 89 | 29 | size_t value_size = av_strnlen(value, additional_data_size); | |
| 90 | 29 | struct TTMLHeaderParameters local_params = { 0 }; | |
| 91 | |||
| 92 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
|
29 | 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 29 times.
✗ Branch 1 not taken.
|
29 | if (value_size == additional_data_size || |
| 102 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
|
29 | value[value_size] != '\0') |
| 103 | ✗ | return AVERROR_INVALIDDATA; | |
| 104 | |||
| 105 | 29 | local_params.tt_element_params = value; | |
| 106 | |||
| 107 | 29 | additional_data_size -= value_size + 1; | |
| 108 | 29 | value += value_size + 1; | |
| 109 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
|
29 | if (!additional_data_size) |
| 110 | ✗ | return AVERROR_INVALIDDATA; | |
| 111 | |||
| 112 | 29 | value_size = av_strnlen(value, additional_data_size); | |
| 113 |
1/2✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
|
29 | if (value_size == additional_data_size || |
| 114 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
|
29 | value[value_size] != '\0') |
| 115 | ✗ | return AVERROR_INVALIDDATA; | |
| 116 | |||
| 117 | 29 | local_params.pre_body_elements = value; | |
| 118 | |||
| 119 | 29 | *header_params = local_params; | |
| 120 | |||
| 121 | 29 | return 0; | |
| 122 | } | ||
| 123 | |||
| 124 | 29 | static int ttml_write_header(AVFormatContext *ctx) | |
| 125 | { | ||
| 126 | 29 | TTMLMuxContext *ttml_ctx = ctx->priv_data; | |
| 127 | 29 | AVStream *st = ctx->streams[0]; | |
| 128 | 29 | AVIOContext *pb = ctx->pb; | |
| 129 | |||
| 130 | 29 | const AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL, | |
| 131 | 0); | ||
| 132 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
29 | const char *printed_lang = (lang && lang->value) ? lang->value : ""; |
| 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 | 29 | int ret = ttml_set_header_values_from_extradata( | |
| 144 | st->codecpar, &header_params); | ||
| 145 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
|
29 | 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 | 29 | avio_printf(pb, ttml_header_text, | |
| 153 | header_params.tt_element_params, | ||
| 154 | printed_lang, | ||
| 155 | header_params.pre_body_elements); | ||
| 156 | } | ||
| 157 | |||
| 158 | 29 | return 0; | |
| 159 | } | ||
| 160 | |||
| 161 | 171 | static int ttml_write_packet(AVFormatContext *ctx, AVPacket *pkt) | |
| 162 | { | ||
| 163 | 171 | TTMLMuxContext *ttml_ctx = ctx->priv_data; | |
| 164 | 171 | AVIOContext *pb = ctx->pb; | |
| 165 | |||
| 166 |
1/3✓ Branch 0 taken 171 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
|
171 | switch (ttml_ctx->input_type) { |
| 167 | 171 | case PACKET_TYPE_PARAGRAPH: | |
| 168 | // write out a paragraph element with the given contents. | ||
| 169 | 171 | avio_printf(pb, " <p\n"); | |
| 170 | 171 | ttml_write_time(pb, " begin", pkt->pts); | |
| 171 | 171 | avio_w8(pb, '\n'); | |
| 172 | 171 | ttml_write_time(pb, " end", pkt->pts + pkt->duration); | |
| 173 | 171 | avio_printf(pb, ">"); | |
| 174 | 171 | avio_write(pb, pkt->data, pkt->size); | |
| 175 | 171 | avio_printf(pb, "</p>\n"); | |
| 176 | 171 | 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 | 171 | return 0; | |
| 197 | } | ||
| 198 | |||
| 199 | 29 | static int ttml_write_trailer(AVFormatContext *ctx) | |
| 200 | { | ||
| 201 | 29 | TTMLMuxContext *ttml_ctx = ctx->priv_data; | |
| 202 | 29 | AVIOContext *pb = ctx->pb; | |
| 203 | |||
| 204 |
1/2✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
|
29 | if (ttml_ctx->input_type == PACKET_TYPE_PARAGRAPH) |
| 205 | 29 | avio_printf(pb, ttml_footer_text); | |
| 206 | |||
| 207 | 29 | 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 |