| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * SSA/ASS muxer | ||
| 3 | * Copyright (c) 2008 Michael Niedermayer | ||
| 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 | #include "libavutil/avstring.h" | ||
| 23 | #include "libavutil/mem.h" | ||
| 24 | #include "avformat.h" | ||
| 25 | #include "avio_internal.h" | ||
| 26 | #include "internal.h" | ||
| 27 | #include "mux.h" | ||
| 28 | |||
| 29 | #include "libavutil/opt.h" | ||
| 30 | |||
| 31 | typedef struct DialogueLine { | ||
| 32 | int readorder; | ||
| 33 | char *line; | ||
| 34 | struct DialogueLine *prev, *next; | ||
| 35 | } DialogueLine; | ||
| 36 | |||
| 37 | typedef struct ASSContext { | ||
| 38 | const AVClass *class; | ||
| 39 | int expected_readorder; | ||
| 40 | DialogueLine *dialogue_cache; | ||
| 41 | DialogueLine *last_added_dialogue; | ||
| 42 | int cache_size; | ||
| 43 | int ssa_mode; | ||
| 44 | int ignore_readorder; | ||
| 45 | uint8_t *trailer; | ||
| 46 | size_t trailer_size; | ||
| 47 | } ASSContext; | ||
| 48 | |||
| 49 | 26 | static int write_header(AVFormatContext *s) | |
| 50 | { | ||
| 51 | 26 | ASSContext *ass = s->priv_data; | |
| 52 | 26 | AVCodecParameters *par = s->streams[0]->codecpar; | |
| 53 | |||
| 54 | 26 | avpriv_set_pts_info(s->streams[0], 64, 1, 100); | |
| 55 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | if (par->extradata_size > 0) { |
| 56 | 26 | size_t header_size = par->extradata_size; | |
| 57 | 26 | uint8_t *trailer = strstr(par->extradata, "\n[Events]"); | |
| 58 | |||
| 59 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | if (trailer) |
| 60 | 26 | trailer = strstr(trailer, "Format:"); | |
| 61 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | if (trailer) |
| 62 | 26 | trailer = strstr(trailer, "\n"); | |
| 63 | |||
| 64 |
1/2✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
|
26 | if (trailer) { |
| 65 | 26 | header_size = (++trailer - par->extradata); | |
| 66 | 26 | ass->trailer_size = par->extradata_size - header_size; | |
| 67 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24 times.
|
26 | if (ass->trailer_size) |
| 68 | 2 | ass->trailer = trailer; | |
| 69 | } | ||
| 70 | |||
| 71 | 26 | ffio_write_lines(s->pb, par->extradata, header_size, NULL); | |
| 72 | |||
| 73 | 26 | ass->ssa_mode = !strstr(par->extradata, "\n[V4+ Styles]"); | |
| 74 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
|
26 | if (!strstr(par->extradata, "\n[Events]")) |
| 75 | ✗ | avio_printf(s->pb, "[Events]\nFormat: %s, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n", | |
| 76 | ✗ | ass->ssa_mode ? "Marked" : "Layer"); | |
| 77 | } | ||
| 78 | |||
| 79 | 26 | return 0; | |
| 80 | } | ||
| 81 | |||
| 82 | 511 | static void purge_dialogues(AVFormatContext *s, int force) | |
| 83 | { | ||
| 84 | 511 | int n = 0; | |
| 85 | 511 | ASSContext *ass = s->priv_data; | |
| 86 | 511 | DialogueLine *dialogue = ass->dialogue_cache; | |
| 87 | |||
| 88 |
5/6✓ Branch 0 taken 512 times.
✓ Branch 1 taken 484 times.
✓ Branch 2 taken 485 times.
✓ Branch 3 taken 27 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 27 times.
|
996 | while (dialogue && (dialogue->readorder == ass->expected_readorder || force)) { |
| 89 | 485 | DialogueLine *next = dialogue->next; | |
| 90 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 485 times.
|
485 | if (dialogue->readorder != ass->expected_readorder) { |
| 91 | ✗ | av_log(s, AV_LOG_WARNING, "ReadOrder gap found between %d and %d\n", | |
| 92 | ass->expected_readorder, dialogue->readorder); | ||
| 93 | ✗ | ass->expected_readorder = dialogue->readorder; | |
| 94 | } | ||
| 95 | 485 | avio_print(s->pb, "Dialogue: ", dialogue->line, "\n"); | |
| 96 |
1/2✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
|
485 | if (dialogue == ass->last_added_dialogue) |
| 97 | 485 | ass->last_added_dialogue = next; | |
| 98 | 485 | av_freep(&dialogue->line); | |
| 99 | 485 | av_free(dialogue); | |
| 100 |
2/2✓ Branch 0 taken 27 times.
✓ Branch 1 taken 458 times.
|
485 | if (next) |
| 101 | 27 | next->prev = NULL; | |
| 102 | 485 | dialogue = ass->dialogue_cache = next; | |
| 103 | 485 | ass->expected_readorder++; | |
| 104 | 485 | n++; | |
| 105 | } | ||
| 106 | 511 | ass->cache_size -= n; | |
| 107 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 509 times.
|
511 | if (n > 1) |
| 108 | 2 | av_log(s, AV_LOG_DEBUG, "wrote %d ASS lines, cached dialogues: %d, waiting for event id %d\n", | |
| 109 | n, ass->cache_size, ass->expected_readorder); | ||
| 110 | 511 | } | |
| 111 | |||
| 112 | 485 | static void insert_dialogue(ASSContext *ass, DialogueLine *dialogue) | |
| 113 | { | ||
| 114 | 485 | DialogueLine *cur, *next = NULL, *prev = NULL; | |
| 115 | |||
| 116 | /* from the last added to the end of the list */ | ||
| 117 |
2/2✓ Branch 0 taken 27 times.
✓ Branch 1 taken 458 times.
|
485 | if (ass->last_added_dialogue) { |
| 118 |
2/2✓ Branch 0 taken 36 times.
✓ Branch 1 taken 14 times.
|
50 | for (cur = ass->last_added_dialogue; cur; cur = cur->next) { |
| 119 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 23 times.
|
36 | if (cur->readorder > dialogue->readorder) |
| 120 | 13 | break; | |
| 121 | 23 | prev = cur; | |
| 122 | 23 | next = cur->next; | |
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | /* from the beginning to the last one added */ | ||
| 127 |
2/2✓ Branch 0 taken 471 times.
✓ Branch 1 taken 14 times.
|
485 | if (!prev) { |
| 128 | 471 | next = ass->dialogue_cache; | |
| 129 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 467 times.
|
471 | for (cur = next; cur != ass->last_added_dialogue; cur = cur->next) { |
| 130 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | if (cur->readorder > dialogue->readorder) |
| 131 | 4 | break; | |
| 132 | ✗ | prev = cur; | |
| 133 | ✗ | next = cur->next; | |
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 471 times.
|
485 | if (prev) { |
| 138 | 14 | prev->next = dialogue; | |
| 139 | 14 | dialogue->prev = prev; | |
| 140 | } else { | ||
| 141 | 471 | dialogue->prev = ass->dialogue_cache; | |
| 142 | 471 | ass->dialogue_cache = dialogue; | |
| 143 | } | ||
| 144 |
2/2✓ Branch 0 taken 13 times.
✓ Branch 1 taken 472 times.
|
485 | if (next) { |
| 145 | 13 | next->prev = dialogue; | |
| 146 | 13 | dialogue->next = next; | |
| 147 | } | ||
| 148 | 485 | ass->cache_size++; | |
| 149 | 485 | ass->last_added_dialogue = dialogue; | |
| 150 | 485 | } | |
| 151 | |||
| 152 | 485 | static int write_packet(AVFormatContext *s, AVPacket *pkt) | |
| 153 | { | ||
| 154 | 485 | ASSContext *ass = s->priv_data; | |
| 155 | |||
| 156 | long int layer; | ||
| 157 | int text_len; | ||
| 158 | 485 | char *p = pkt->data; | |
| 159 | 485 | int64_t start = pkt->pts; | |
| 160 | 485 | int64_t end = start + pkt->duration; | |
| 161 | int hh1, mm1, ss1, ms1; | ||
| 162 | int hh2, mm2, ss2, ms2; | ||
| 163 | 485 | DialogueLine *dialogue = av_mallocz(sizeof(*dialogue)); | |
| 164 | |||
| 165 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 485 times.
|
485 | if (!dialogue) |
| 166 | ✗ | return AVERROR(ENOMEM); | |
| 167 | |||
| 168 | 485 | dialogue->readorder = strtol(p, &p, 10); | |
| 169 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 485 times.
|
485 | if (dialogue->readorder < ass->expected_readorder) |
| 170 | ✗ | av_log(s, AV_LOG_WARNING, "Unexpected ReadOrder %d\n", | |
| 171 | dialogue->readorder); | ||
| 172 |
1/2✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
|
485 | if (*p == ',') |
| 173 | 485 | p++; | |
| 174 | |||
| 175 |
3/4✓ Branch 0 taken 35 times.
✓ Branch 1 taken 450 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 35 times.
|
485 | if (ass->ssa_mode && !strncmp(p, "Marked=", 7)) |
| 176 | ✗ | p += 7; | |
| 177 | |||
| 178 | 485 | layer = strtol(p, &p, 10); | |
| 179 |
1/2✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
|
485 | if (*p == ',') |
| 180 | 485 | p++; | |
| 181 | 485 | hh1 = (int)(start / 360000); mm1 = (int)(start / 6000) % 60; | |
| 182 | 485 | hh2 = (int)(end / 360000); mm2 = (int)(end / 6000) % 60; | |
| 183 | 485 | ss1 = (int)(start / 100) % 60; ms1 = (int)(start % 100); | |
| 184 | 485 | ss2 = (int)(end / 100) % 60; ms2 = (int)(end % 100); | |
| 185 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 485 times.
|
485 | if (hh1 > 9) hh1 = 9, mm1 = 59, ss1 = 59, ms1 = 99; |
| 186 |
2/2✓ Branch 0 taken 12 times.
✓ Branch 1 taken 473 times.
|
485 | if (hh2 > 9) hh2 = 9, mm2 = 59, ss2 = 59, ms2 = 99; |
| 187 | |||
| 188 | 485 | text_len = strlen(p); | |
| 189 |
3/6✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 485 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 485 times.
|
485 | while (text_len > 0 && p[text_len - 1] == '\r' || p[text_len - 1] == '\n') |
| 190 | ✗ | text_len--; | |
| 191 | |||
| 192 | 485 | dialogue->line = av_asprintf("%s%ld,%d:%02d:%02d.%02d,%d:%02d:%02d.%02d,%.*s", | |
| 193 |
2/2✓ Branch 0 taken 35 times.
✓ Branch 1 taken 450 times.
|
485 | ass->ssa_mode ? "Marked=" : "", |
| 194 | layer, hh1, mm1, ss1, ms1, hh2, mm2, ss2, ms2, text_len, p); | ||
| 195 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 485 times.
|
485 | if (!dialogue->line) { |
| 196 | ✗ | av_free(dialogue); | |
| 197 | ✗ | return AVERROR(ENOMEM); | |
| 198 | } | ||
| 199 | 485 | insert_dialogue(ass, dialogue); | |
| 200 | 485 | purge_dialogues(s, ass->ignore_readorder); | |
| 201 | |||
| 202 | 485 | return 0; | |
| 203 | } | ||
| 204 | |||
| 205 | 26 | static int write_trailer(AVFormatContext *s) | |
| 206 | { | ||
| 207 | 26 | ASSContext *ass = s->priv_data; | |
| 208 | |||
| 209 | 26 | purge_dialogues(s, 1); | |
| 210 | |||
| 211 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24 times.
|
26 | if (ass->trailer) { |
| 212 | 2 | ffio_write_lines(s->pb, ass->trailer, ass->trailer_size, NULL); | |
| 213 | } | ||
| 214 | |||
| 215 | 26 | return 0; | |
| 216 | } | ||
| 217 | |||
| 218 | #define OFFSET(x) offsetof(ASSContext, x) | ||
| 219 | #define E AV_OPT_FLAG_ENCODING_PARAM | ||
| 220 | static const AVOption options[] = { | ||
| 221 | { "ignore_readorder", "write events immediately, even if they're out-of-order", OFFSET(ignore_readorder), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, E }, | ||
| 222 | { NULL }, | ||
| 223 | }; | ||
| 224 | |||
| 225 | static const AVClass ass_class = { | ||
| 226 | .class_name = "ass muxer", | ||
| 227 | .item_name = av_default_item_name, | ||
| 228 | .option = options, | ||
| 229 | .version = LIBAVUTIL_VERSION_INT, | ||
| 230 | }; | ||
| 231 | |||
| 232 | const FFOutputFormat ff_ass_muxer = { | ||
| 233 | .p.name = "ass", | ||
| 234 | .p.long_name = NULL_IF_CONFIG_SMALL("SSA (SubStation Alpha) subtitle"), | ||
| 235 | .p.mime_type = "text/x-ass", | ||
| 236 | .p.extensions = "ass,ssa", | ||
| 237 | .p.audio_codec = AV_CODEC_ID_NONE, | ||
| 238 | .p.video_codec = AV_CODEC_ID_NONE, | ||
| 239 | .p.subtitle_codec = AV_CODEC_ID_ASS, | ||
| 240 | .p.flags = AVFMT_GLOBALHEADER | AVFMT_NOTIMESTAMPS | AVFMT_TS_NONSTRICT, | ||
| 241 | .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH | | ||
| 242 | FF_OFMT_FLAG_ONLY_DEFAULT_CODECS, | ||
| 243 | .p.priv_class = &ass_class, | ||
| 244 | .priv_data_size = sizeof(ASSContext), | ||
| 245 | .write_header = write_header, | ||
| 246 | .write_packet = write_packet, | ||
| 247 | .write_trailer = write_trailer, | ||
| 248 | }; | ||
| 249 |