FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/movenc_ttml.c
Date: 2025-11-04 10:33:36
Exec Total Coverage
Lines: 119 150 79.3%
Functions: 4 4 100.0%
Branches: 49 74 66.2%

Line Branch Exec Source
1 /*
2 * MP4, ISMV Muxer TTML helpers
3 * Copyright (c) 2021 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 #include "libavutil/mem.h"
23 #include "avformat.h"
24 #include "avio_internal.h"
25 #include "isom.h"
26 #include "movenc.h"
27 #include "movenc_ttml.h"
28 #include "libavcodec/packet_internal.h"
29
30 static const unsigned char empty_ttml_document[] =
31 "<tt xml:lang=\"\" xmlns=\"http://www.w3.org/ns/ttml\" />";
32
33 28 static int mov_init_ttml_writer(MOVTrack *track, AVFormatContext **out_ctx)
34 {
35 28 AVStream *movenc_stream = track->st, *ttml_stream = NULL;
36 28 int ret = AVERROR_BUG;
37
38
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = avformat_alloc_output_context2(out_ctx, NULL,
39 "ttml", NULL)) < 0)
40 return ret;
41
42
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = avio_open_dyn_buf(&(*out_ctx)->pb)) < 0)
43 return ret;
44
45
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if (!(ttml_stream = avformat_new_stream(*out_ctx, NULL))) {
46 return AVERROR(ENOMEM);
47 }
48
49
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 if ((ret = avcodec_parameters_copy(ttml_stream->codecpar,
50 28 movenc_stream->codecpar)) < 0)
51 return ret;
52
53 28 ttml_stream->time_base = movenc_stream->time_base;
54
55 28 return 0;
56 }
57
58 26 static void mov_calculate_start_and_end_of_other_tracks(
59 AVFormatContext *s, MOVTrack *track, int64_t *start_pts, int64_t *end_pts)
60 {
61 26 MOVMuxContext *mov = s->priv_data;
62
63 // Initialize at the end of the previous document/fragment, which is NOPTS
64 // until the first fragment is created.
65 26 int64_t max_track_end_dts = *start_pts = track->end_pts;
66
67
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 26 times.
78 for (unsigned int i = 0; i < s->nb_streams; i++) {
68 52 MOVTrack *other_track = &mov->tracks[i];
69
70 // Skip our own track, any other track that needs squashing,
71 // or any track which still has its start_dts at NOPTS or
72 // any track that did not yet get any packets.
73
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 26 times.
52 if (track == other_track ||
74
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 other_track->squash_fragment_samples_to_one ||
75
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 other_track->start_dts == AV_NOPTS_VALUE ||
76
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 !other_track->entry) {
77 26 continue;
78 }
79
80 26 int64_t picked_start = av_rescale_q_rnd(other_track->cluster[0].dts + other_track->cluster[0].cts,
81 26 other_track->st->time_base,
82 26 track->st->time_base,
83 AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
84 26 int64_t picked_end = av_rescale_q_rnd(other_track->end_pts,
85 26 other_track->st->time_base,
86 26 track->st->time_base,
87 AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
88
89
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24 times.
26 if (*start_pts == AV_NOPTS_VALUE)
90 2 *start_pts = picked_start;
91
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 else if (picked_start >= track->end_pts)
92 24 *start_pts = FFMIN(*start_pts, picked_start);
93
94 26 max_track_end_dts = FFMAX(max_track_end_dts, picked_end);
95 }
96
97 26 *end_pts = max_track_end_dts;
98 26 }
99
100 28 static int mov_write_ttml_document_from_queue(AVFormatContext *s,
101 AVFormatContext *ttml_ctx,
102 MOVTrack *track,
103 AVPacket *pkt,
104 int64_t *out_start_ts,
105 int64_t *out_duration)
106 {
107 28 int ret = AVERROR_BUG;
108 56 int64_t start_ts = track->start_dts == AV_NOPTS_VALUE ?
109
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 4 times.
28 0 : (track->start_dts + track->track_duration);
110 28 int64_t end_ts = start_ts;
111 28 unsigned int time_limited = 0;
112 28 PacketList back_to_queue_list = { 0 };
113
114
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 2 times.
28 if (*out_start_ts != AV_NOPTS_VALUE) {
115 // we have non-nopts values here, thus we have been given a time range
116 26 time_limited = 1;
117 26 start_ts = *out_start_ts;
118 26 end_ts = *out_start_ts + *out_duration;
119 }
120
121
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = avformat_write_header(ttml_ctx, NULL)) < 0) {
122 return ret;
123 }
124
125
2/2
✓ Branch 1 taken 136 times.
✓ Branch 2 taken 26 times.
162 while (!avpriv_packet_list_get(&track->squashed_packet_queue, pkt)) {
126 136 int64_t pts_before = pkt->pts;
127 136 int64_t duration_before = pkt->duration;
128
129
2/2
✓ Branch 0 taken 62 times.
✓ Branch 1 taken 74 times.
136 if (time_limited) {
130 // special cases first:
131
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 62 times.
62 if (pkt->pts + pkt->duration < start_ts) {
132 // too late for our fragment, unfortunately
133 // unref and proceed to next packet in queue.
134 av_log(s, AV_LOG_WARNING,
135 "Very late TTML packet in queue, dropping packet with "
136 "pts: %"PRId64", duration: %"PRId64"\n",
137 pkt->pts, pkt->duration);
138 av_packet_unref(pkt);
139 continue;
140
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 60 times.
62 } else if (pkt->pts >= end_ts) {
141 // starts after this fragment, put back to original queue
142 2 ret = avpriv_packet_list_put(&track->squashed_packet_queue,
143 pkt, NULL,
144 FF_PACKETLIST_FLAG_PREPEND);
145
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (ret < 0)
146 goto cleanup;
147
148 2 break;
149 }
150
151 // limit packet pts to start_ts
152
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 60 times.
60 if (pkt->pts < start_ts) {
153 pkt->duration -= start_ts - pkt->pts;
154 pkt->pts = start_ts;
155 }
156
157
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 26 times.
60 if (pkt->pts + pkt->duration > end_ts) {
158 // goes over our current fragment, create duplicate and
159 // put it back to list after iteration has finished in
160 // order to handle multiple subtitles at the same time.
161 34 int64_t offset = end_ts - pkt->pts;
162
163 34 ret = avpriv_packet_list_put(&back_to_queue_list,
164 pkt, av_packet_ref,
165 FF_PACKETLIST_FLAG_PREPEND);
166
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (ret < 0)
167 goto cleanup;
168
169 34 back_to_queue_list.head->pkt.pts =
170 34 back_to_queue_list.head->pkt.dts =
171 34 back_to_queue_list.head->pkt.pts + offset;
172 34 back_to_queue_list.head->pkt.duration -= offset;
173
174 // and for our normal packet we just set duration to offset
175 34 pkt->duration = offset;
176 }
177 } else {
178 74 end_ts = FFMAX(end_ts, pkt->pts + pkt->duration);
179 }
180
181 134 av_log(s, AV_LOG_TRACE,
182 "TTML packet writeout: pts: %"PRId64" (%"PRId64"), "
183 "duration: %"PRId64"\n",
184 134 pkt->pts, pkt->pts - start_ts, pkt->duration);
185
3/4
✓ Branch 0 taken 134 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 100 times.
134 if (pkt->pts != pts_before || pkt->duration != duration_before) {
186 34 av_log(s, AV_LOG_TRACE,
187 "Adjustments: pts: %"PRId64", duration: %"PRId64"\n",
188 34 pkt->pts - pts_before, pkt->duration - duration_before);
189 }
190
191 // in case of the 'dfxp' muxing mode, each written document is offset
192 // to its containing sample's beginning.
193
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 67 times.
134 if (track->par->codec_tag == MOV_ISMV_TTML_TAG) {
194 67 pkt->dts = pkt->pts = (pkt->pts - start_ts);
195 }
196
197 134 pkt->stream_index = 0;
198
199 134 av_packet_rescale_ts(pkt, track->st->time_base,
200 134 ttml_ctx->streams[pkt->stream_index]->time_base);
201
202
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 134 times.
134 if ((ret = av_write_frame(ttml_ctx, pkt)) < 0) {
203 goto cleanup;
204 }
205
206 134 av_packet_unref(pkt);
207 }
208
209
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = av_write_trailer(ttml_ctx)) < 0)
210 goto cleanup;
211
212 28 *out_start_ts = start_ts;
213 28 *out_duration = end_ts - start_ts;
214
215 28 ret = 0;
216
217 28 cleanup:
218 28 av_packet_unref(pkt);
219
2/2
✓ Branch 1 taken 34 times.
✓ Branch 2 taken 28 times.
62 while (!avpriv_packet_list_get(&back_to_queue_list, pkt)) {
220 34 ret = avpriv_packet_list_put(&track->squashed_packet_queue,
221 pkt, av_packet_ref,
222 FF_PACKETLIST_FLAG_PREPEND);
223
224 // unrelated to whether we succeed or not, we unref the packet
225 // received from the temporary list.
226 34 av_packet_unref(pkt);
227
228
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (ret < 0) {
229 avpriv_packet_list_free(&back_to_queue_list);
230 break;
231 }
232 }
233 28 return ret;
234 }
235
236 28 int ff_mov_generate_squashed_ttml_packet(AVFormatContext *s,
237 MOVTrack *track, AVPacket *pkt)
238 {
239 28 MOVMuxContext *mov = s->priv_data;
240 28 AVFormatContext *ttml_ctx = NULL;
241 // values for the generated AVPacket
242 28 int64_t start_ts = AV_NOPTS_VALUE;
243 28 int64_t duration = 0;
244
245 28 int ret = AVERROR_BUG;
246
247
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = mov_init_ttml_writer(track, &ttml_ctx)) < 0) {
248 av_log(s, AV_LOG_ERROR, "Failed to initialize the TTML writer: %s\n",
249 av_err2str(ret));
250 goto cleanup;
251 }
252
253
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 2 times.
28 if (mov->flags & FF_MOV_FLAG_FRAGMENT) {
254 26 int64_t calculated_start = AV_NOPTS_VALUE;
255 26 int64_t calculated_end = AV_NOPTS_VALUE;
256
257 26 mov_calculate_start_and_end_of_other_tracks(s, track, &calculated_start, &calculated_end);
258
259
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 if (calculated_start != AV_NOPTS_VALUE) {
260 26 start_ts = calculated_start;
261 26 duration = calculated_end - calculated_start;
262 26 av_log(s, AV_LOG_VERBOSE,
263 "Calculated subtitle fragment start: %"PRId64", "
264 "duration: %"PRId64"\n",
265 start_ts, duration);
266 }
267 }
268
269
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 if (!track->squashed_packet_queue.head) {
270 // empty queue, write minimal empty document with zero duration
271 avio_write(ttml_ctx->pb, empty_ttml_document,
272 sizeof(empty_ttml_document) - 1);
273 if (start_ts == AV_NOPTS_VALUE) {
274 start_ts = 0;
275 duration = 0;
276 }
277 goto generate_packet;
278 }
279
280
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = mov_write_ttml_document_from_queue(s, ttml_ctx, track, pkt,
281 &start_ts,
282 &duration)) < 0) {
283 av_log(s, AV_LOG_ERROR,
284 "Failed to generate a squashed TTML packet from the packet "
285 "queue: %s\n",
286 av_err2str(ret));
287 goto cleanup;
288 }
289
290 28 generate_packet:
291 {
292 // Generate an AVPacket from the data written into the dynamic buffer.
293 28 uint8_t *buf = NULL;
294 28 int buf_len = avio_close_dyn_buf(ttml_ctx->pb, &buf);
295 28 ttml_ctx->pb = NULL;
296
297
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
28 if ((ret = av_packet_from_data(pkt, buf, buf_len)) < 0) {
298 av_log(s, AV_LOG_ERROR,
299 "Failed to create a TTML AVPacket from AVIO data: %s\n",
300 av_err2str(ret));
301 av_freep(&buf);
302 goto cleanup;
303 }
304
305 28 pkt->pts = pkt->dts = start_ts;
306 28 pkt->duration = duration;
307 28 pkt->flags |= AV_PKT_FLAG_KEY;
308 }
309
310 28 ret = 0;
311
312 28 cleanup:
313
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 if (ttml_ctx)
314 28 ffio_free_dyn_buf(&ttml_ctx->pb);
315
316 28 avformat_free_context(ttml_ctx);
317 28 return ret;
318 }
319