Includes basic support for both the ISMV ('dfxp') and MP4 ('stpp') methods. This initial version also foregoes fragmentation support in case the built-in sample squashing is to be utilized, as this eases the initial review. Additionally, add basic tests for both muxing modes in MP4.
Unified · Split
| @@ -337,7 +337,7 @@ OBJS-$(CONFIG_MOV_DEMUXER) += mov.o mov_chan.o mov_esds.o \ · 336 unmodified lines | ||
| 337 | 337 | qtpalette.o replaygain.o |
| 338 | 338 | OBJS-$(CONFIG_MOV_MUXER) += movenc.o av1.o avc.o hevc.o vpcc.o \ |
| 339 | 339 | movenchint.o mov_chan.o rtp.o \ |
| 340 | − movenccenc.o rawutils.o | |
| 340 | + movenccenc.o movenc_ttml.o rawutils.o | |
| 341 | 341 | OBJS-$(CONFIG_MP2_MUXER) += rawenc.o |
| 342 | 342 | OBJS-$(CONFIG_MP3_DEMUXER) += mp3dec.o replaygain.o |
| 343 | 343 | OBJS-$(CONFIG_MP3_MUXER) += mp3enc.o rawenc.o id3v2enc.o |
| @@ -387,4 +387,7 @@ static inline enum AVCodecID ff_mov_get_lpcm_codec_id(int bps, int flags) · 386 unmodified lines | ||
| 387 | 387 | return ff_get_pcm_codec_id(bps, flags & 1, flags & 2, flags & 4 ? -1 : 0); |
| 388 | 388 | } |
| 389 | 389 | |
| 390 | +#define MOV_ISMV_TTML_TAG MKTAG('d', 'f', 'x', 'p') | |
| 391 | +#define MOV_MP4_TTML_TAG MKTAG('s', 't', 'p', 'p') | |
| 392 | + | |
| 390 | 393 | #endif /* AVFORMAT_ISOM_H */ |
| @@ -57,6 +57,8 @@ · 56 unmodified lines | ||
| 57 | 57 | #include "hevc.h" |
| 58 | 58 | #include "rtpenc.h" |
| 59 | 59 | #include "mov_chan.h" |
| 60 | +#include "movenc_ttml.h" | |
| 61 | +#include "ttmlenc.h" | |
| 60 | 62 | #include "vpcc.h" |
| 61 | 63 | |
| 62 | 64 | static const AVOption options[] = { |
| @@ -120,6 +122,7 @@ static const AVClass mov_isobmff_muxer_class = { · 57 unmodified lines | ||
| 120 | 122 | }; |
| 121 | 123 | |
| 122 | 124 | static int get_moov_size(AVFormatContext *s); |
| 125 | +static int mov_write_single_packet(AVFormatContext *s, AVPacket *pkt); | |
| 123 | 126 | |
| 124 | 127 | static int utf8len(const uint8_t *b) |
| 125 | 128 | { |
| @@ -1788,7 +1791,29 @@ static int mov_write_subtitle_tag(AVIOContext *pb, MOVTrack *track) · 1662 unmodified lines | ||
| 1788 | 1791 | |
| 1789 | 1792 | if (track->par->codec_id == AV_CODEC_ID_DVD_SUBTITLE) |
| 1790 | 1793 | mov_write_esds_tag(pb, track); |
| 1791 | − else if (track->par->extradata_size) | |
| 1794 | + else if (track->par->codec_id == AV_CODEC_ID_TTML) { | |
| 1795 | + switch (track->par->codec_tag) { | |
| 1796 | + case MOV_ISMV_TTML_TAG: | |
| 1797 | + // ISMV dfxp requires no extradata. | |
| 1798 | + break; | |
| 1799 | + case MOV_MP4_TTML_TAG: | |
| 1800 | + // As specified in 14496-30, XMLSubtitleSampleEntry | |
| 1801 | + // Namespace | |
| 1802 | + avio_put_str(pb, "http://www.w3.org/ns/ttml"); | |
| 1803 | + // Empty schema_location | |
| 1804 | + avio_w8(pb, 0); | |
| 1805 | + // Empty auxiliary_mime_types | |
| 1806 | + avio_w8(pb, 0); | |
| 1807 | + break; | |
| 1808 | + default: | |
| 1809 | + av_log(NULL, AV_LOG_ERROR, | |
| 1810 | + "Unknown codec tag '%s' utilized for TTML stream with " | |
| 1811 | + "index %d (track id %d)!\n", | |
| 1812 | + av_fourcc2str(track->par->codec_tag), track->st->index, | |
| 1813 | + track->track_id); | |
| 1814 | + return AVERROR(EINVAL); | |
| 1815 | + } | |
| 1816 | + } else if (track->par->extradata_size) | |
| 1792 | 1817 | avio_write(pb, track->par->extradata, track->par->extradata_size); |
| 1793 | 1818 | |
| 1794 | 1819 | if (track->mode == MODE_MP4 && |
| @@ -2662,6 +2687,14 @@ static int mov_write_nmhd_tag(AVIOContext *pb) · 867 unmodified lines | ||
| 2662 | 2687 | return 12; |
| 2663 | 2688 | } |
| 2664 | 2689 | |
| 2690 | +static int mov_write_sthd_tag(AVIOContext *pb) | |
| 2691 | +{ | |
| 2692 | + avio_wb32(pb, 12); | |
| 2693 | + ffio_wfourcc(pb, "sthd"); | |
| 2694 | + avio_wb32(pb, 0); | |
| 2695 | + return 12; | |
| 2696 | +} | |
| 2697 | + | |
| 2665 | 2698 | static int mov_write_tcmi_tag(AVIOContext *pb, MOVTrack *track) |
| 2666 | 2699 | { |
| 2667 | 2700 | int64_t pos = avio_tell(pb); |
| @@ -2788,6 +2821,8 @@ static int mov_write_hdlr_tag(AVFormatContext *s, AVIOContext *pb, MOVTrack *tra · 120 unmodified lines | ||
| 2788 | 2821 | hdlr_type = "sbtl"; |
| 2789 | 2822 | } else if (track->tag == MKTAG('m','p','4','s')) { |
| 2790 | 2823 | hdlr_type = "subp"; |
| 2824 | + } else if (track->tag == MOV_MP4_TTML_TAG) { | |
| 2825 | + hdlr_type = "subt"; | |
| 2791 | 2826 | } else { |
| 2792 | 2827 | hdlr_type = "text"; |
| 2793 | 2828 | } |
| @@ -2866,6 +2901,8 @@ static int mov_write_minf_tag(AVFormatContext *s, AVIOContext *pb, MOVMuxContext · 72 unmodified lines | ||
| 2866 | 2901 | else if (track->par->codec_type == AVMEDIA_TYPE_SUBTITLE) { |
| 2867 | 2902 | if (track->tag == MKTAG('t','e','x','t') || is_clcp_track(track)) { |
| 2868 | 2903 | mov_write_gmhd_tag(pb, track); |
| 2904 | + } else if (track->tag == MOV_MP4_TTML_TAG) { | |
| 2905 | + mov_write_sthd_tag(pb); | |
| 2869 | 2906 | } else { |
| 2870 | 2907 | mov_write_nmhd_tag(pb); |
| 2871 | 2908 | } |
| @@ -5251,6 +5288,68 @@ static int mov_flush_fragment_interleaving(AVFormatContext *s, MOVTrack *track) · 2379 unmodified lines | ||
| 5251 | 5288 | return 0; |
| 5252 | 5289 | } |
| 5253 | 5290 | |
| 5291 | +static int mov_write_squashed_packet(AVFormatContext *s, MOVTrack *track) | |
| 5292 | +{ | |
| 5293 | + MOVMuxContext *mov = s->priv_data; | |
| 5294 | + AVPacket *squashed_packet = mov->pkt; | |
| 5295 | + int ret = AVERROR_BUG; | |
| 5296 | + | |
| 5297 | + switch (track->st->codecpar->codec_id) { | |
| 5298 | + case AV_CODEC_ID_TTML: { | |
| 5299 | + int had_packets = !!track->squashed_packet_queue; | |
| 5300 | + | |
| 5301 | + if ((ret = ff_mov_generate_squashed_ttml_packet(s, track, squashed_packet)) < 0) { | |
| 5302 | + goto finish_squash; | |
| 5303 | + } | |
| 5304 | + | |
| 5305 | + // We have generated a padding packet (no actual input packets in | |
| 5306 | + // queue) and its duration is zero. Skipping writing it. | |
| 5307 | + if (!had_packets && squashed_packet->duration == 0) { | |
| 5308 | + goto finish_squash; | |
| 5309 | + } | |
| 5310 | + | |
| 5311 | + track->end_reliable = 1; | |
| 5312 | + break; | |
| 5313 | + } | |
| 5314 | + default: | |
| 5315 | + ret = AVERROR(EINVAL); | |
| 5316 | + goto finish_squash; | |
| 5317 | + } | |
| 5318 | + | |
| 5319 | + squashed_packet->stream_index = track->st->index; | |
| 5320 | + | |
| 5321 | + ret = mov_write_single_packet(s, squashed_packet); | |
| 5322 | + | |
| 5323 | +finish_squash: | |
| 5324 | + av_packet_unref(squashed_packet); | |
| 5325 | + | |
| 5326 | + return ret; | |
| 5327 | +} | |
| 5328 | + | |
| 5329 | +static int mov_write_squashed_packets(AVFormatContext *s) | |
| 5330 | +{ | |
| 5331 | + MOVMuxContext *mov = s->priv_data; | |
| 5332 | + | |
| 5333 | + for (int i = 0; i < s->nb_streams; i++) { | |
| 5334 | + MOVTrack *track = &mov->tracks[i]; | |
| 5335 | + int ret = AVERROR_BUG; | |
| 5336 | + | |
| 5337 | + if (track->squash_fragment_samples_to_one && !track->entry) { | |
| 5338 | + if ((ret = mov_write_squashed_packet(s, track)) < 0) { | |
| 5339 | + av_log(s, AV_LOG_ERROR, | |
| 5340 | + "Failed to write squashed packet for %s stream with " | |
| 5341 | + "index %d and track id %d. Error: %s\n", | |
| 5342 | + avcodec_get_name(track->st->codecpar->codec_id), | |
| 5343 | + track->st->index, track->track_id, | |
| 5344 | + av_err2str(ret)); | |
| 5345 | + return ret; | |
| 5346 | + } | |
| 5347 | + } | |
| 5348 | + } | |
| 5349 | + | |
| 5350 | + return 0; | |
| 5351 | +} | |
| 5352 | + | |
| 5254 | 5353 | static int mov_flush_fragment(AVFormatContext *s, int force) |
| 5255 | 5354 | { |
| 5256 | 5355 | MOVMuxContext *mov = s->priv_data; |
| @@ -5262,6 +5361,11 @@ static int mov_flush_fragment(AVFormatContext *s, int force) · 5 unmodified lines | ||
| 5262 | 5361 | if (!(mov->flags & FF_MOV_FLAG_FRAGMENT)) |
| 5263 | 5362 | return 0; |
| 5264 | 5363 | |
| 5364 | + // Check if we have any tracks that require squashing. | |
| 5365 | + // In that case, we'll have to write the packet here. | |
| 5366 | + if ((ret = mov_write_squashed_packets(s)) < 0) | |
| 5367 | + return ret; | |
| 5368 | + | |
| 5265 | 5369 | // Try to fill in the duration of the last packet in each stream |
| 5266 | 5370 | // from queued packets in the interleave queues. If the flushing |
| 5267 | 5371 | // of fragments was triggered automatically by an AVPacket, we |
| @@ -5727,7 +5831,8 @@ int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt) · 459 unmodified lines | ||
| 5727 | 5831 | trk->cluster[trk->entry].entries = samples_in_chunk; |
| 5728 | 5832 | trk->cluster[trk->entry].dts = pkt->dts; |
| 5729 | 5833 | trk->cluster[trk->entry].pts = pkt->pts; |
| 5730 | − if (!trk->entry && trk->start_dts != AV_NOPTS_VALUE) { | |
| 5834 | + if (!trk->squash_fragment_samples_to_one && | |
| 5835 | + !trk->entry && trk->start_dts != AV_NOPTS_VALUE) { | |
| 5731 | 5836 | if (!trk->frag_discont) { |
| 5732 | 5837 | /* First packet of a new fragment. We already wrote the duration |
| 5733 | 5838 | * of the last packet of the previous fragment based on track_duration, |
| @@ -6022,6 +6127,33 @@ static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) · 288 unmodified lines | ||
| 6022 | 6127 | } |
| 6023 | 6128 | } |
| 6024 | 6129 | |
| 6130 | + if (trk->squash_fragment_samples_to_one) { | |
| 6131 | + /* | |
| 6132 | + * If the track has to have its samples squashed into one sample, | |
| 6133 | + * we just take it into the track's queue. | |
| 6134 | + * This will then be utilized as the samples get written in either | |
| 6135 | + * mov_flush_fragment or when the mux is finalized in | |
| 6136 | + * mov_write_trailer. | |
| 6137 | + */ | |
| 6138 | + int ret = AVERROR_BUG; | |
| 6139 | + | |
| 6140 | + if (pkt->pts == AV_NOPTS_VALUE) { | |
| 6141 | + av_log(s, AV_LOG_ERROR, | |
| 6142 | + "Packets without a valid presentation timestamp are " | |
| 6143 | + "not supported with packet squashing!\n"); | |
| 6144 | + return AVERROR(EINVAL); | |
| 6145 | + } | |
| 6146 | + | |
| 6147 | + if ((ret = avpriv_packet_list_put(&trk->squashed_packet_queue, | |
| 6148 | + &trk->squashed_packet_queue_end, | |
| 6149 | + pkt, av_packet_ref, 0)) < 0) { | |
| 6150 | + return ret; | |
| 6151 | + } | |
| 6152 | + | |
| 6153 | + return 0; | |
| 6154 | + } | |
| 6155 | + | |
| 6156 | + | |
| 6025 | 6157 | if (trk->mode == MODE_MOV && trk->par->codec_type == AVMEDIA_TYPE_VIDEO) { |
| 6026 | 6158 | AVPacket *opkt = pkt; |
| 6027 | 6159 | int reshuffle_ret, ret; |
| @@ -6300,6 +6432,11 @@ static void mov_free(AVFormatContext *s) · 272 unmodified lines | ||
| 6300 | 6432 | |
| 6301 | 6433 | ff_mov_cenc_free(&mov->tracks[i].cenc); |
| 6302 | 6434 | ffio_free_dyn_buf(&mov->tracks[i].mdat_buf); |
| 6435 | + | |
| 6436 | + if (mov->tracks[i].squashed_packet_queue) { | |
| 6437 | + avpriv_packet_list_free(&(mov->tracks[i].squashed_packet_queue), | |
| 6438 | + &(mov->tracks[i].squashed_packet_queue_end)); | |
| 6439 | + } | |
| 6303 | 6440 | } |
| 6304 | 6441 | |
| 6305 | 6442 | av_freep(&mov->tracks); |
| @@ -6690,6 +6827,36 @@ static int mov_init(AVFormatContext *s) · 384 unmodified lines | ||
| 6690 | 6827 | } |
| 6691 | 6828 | } else if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) { |
| 6692 | 6829 | track->timescale = st->time_base.den; |
| 6830 | + | |
| 6831 | + if (track->par->codec_id == AV_CODEC_ID_TTML) { | |
| 6832 | + /* 14496-30 requires us to use a single sample per fragment | |
| 6833 | + for TTML, for which we define a per-track flag. | |
| 6834 | + | |
| 6835 | + We set the flag in case we are receiving TTML paragraphs | |
| 6836 | + from the input, in other words in case we are not doing | |
| 6837 | + stream copy. */ | |
| 6838 | + track->squash_fragment_samples_to_one = | |
| 6839 | + ff_is_ttml_stream_paragraph_based(track->par); | |
| 6840 | + | |
| 6841 | + if (mov->flags & FF_MOV_FLAG_FRAGMENT && | |
| 6842 | + track->squash_fragment_samples_to_one) { | |
| 6843 | + av_log(s, AV_LOG_ERROR, | |
| 6844 | + "Fragmentation is not currently supported for " | |
| 6845 | + "TTML in MP4/ISMV (track synchronization between " | |
| 6846 | + "subtitles and other media is not yet implemented)!\n"); | |
| 6847 | + return AVERROR_PATCHWELCOME; | |
| 6848 | + } | |
| 6849 | + | |
| 6850 | + if (track->mode != MODE_ISM && | |
| 6851 | + track->par->codec_tag == MOV_ISMV_TTML_TAG && | |
| 6852 | + s->strict_std_compliance > FF_COMPLIANCE_UNOFFICIAL) { | |
| 6853 | + av_log(s, AV_LOG_ERROR, | |
| 6854 | + "ISMV style TTML support with the 'dfxp' tag in " | |
| 6855 | + "non-ISMV formats is not officially supported. Add " | |
| 6856 | + "'-strict unofficial' if you want to use it.\n"); | |
| 6857 | + return AVERROR_EXPERIMENTAL; | |
| 6858 | + } | |
| 6859 | + } | |
| 6693 | 6860 | } else if (st->codecpar->codec_type == AVMEDIA_TYPE_DATA) { |
| 6694 | 6861 | track->timescale = st->time_base.den; |
| 6695 | 6862 | } else { |
| @@ -7036,6 +7203,11 @@ static int mov_write_trailer(AVFormatContext *s) · 340 unmodified lines | ||
| 7036 | 7203 | } |
| 7037 | 7204 | } |
| 7038 | 7205 | |
| 7206 | + // Check if we have any tracks that require squashing. | |
| 7207 | + // In that case, we'll have to write the packet here. | |
| 7208 | + if ((res = mov_write_squashed_packets(s)) < 0) | |
| 7209 | + return res; | |
| 7210 | + | |
| 7039 | 7211 | // If there were no chapters when the header was written, but there |
| 7040 | 7212 | // are chapters now, write them in the trailer. This only works |
| 7041 | 7213 | // when we are not doing fragments. |
| @@ -7180,6 +7352,8 @@ static const AVCodecTag codec_mp4_tags[] = { · 138 unmodified lines | ||
| 7180 | 7352 | { AV_CODEC_ID_MOV_TEXT, MKTAG('t', 'x', '3', 'g') }, |
| 7181 | 7353 | { AV_CODEC_ID_BIN_DATA, MKTAG('g', 'p', 'm', 'd') }, |
| 7182 | 7354 | { AV_CODEC_ID_MPEGH_3D_AUDIO, MKTAG('m', 'h', 'm', '1') }, |
| 7355 | + { AV_CODEC_ID_TTML, MOV_MP4_TTML_TAG }, | |
| 7356 | + { AV_CODEC_ID_TTML, MOV_ISMV_TTML_TAG }, | |
| 7183 | 7357 | { AV_CODEC_ID_NONE, 0 }, |
| 7184 | 7358 | }; |
| 7185 | 7359 | #if CONFIG_MP4_MUXER || CONFIG_PSP_MUXER |
| @@ -7188,6 +7362,7 @@ static const AVCodecTag *const mp4_codec_tags_list[] = { codec_mp4_tags, NULL }; · 2 unmodified lines | ||
| 7188 | 7362 | |
| 7189 | 7363 | static const AVCodecTag codec_ism_tags[] = { |
| 7190 | 7364 | { AV_CODEC_ID_WMAPRO , MKTAG('w', 'm', 'a', ' ') }, |
| 7365 | + { AV_CODEC_ID_TTML , MOV_ISMV_TTML_TAG }, | |
| 7191 | 7366 | { AV_CODEC_ID_NONE , 0 }, |
| 7192 | 7367 | }; |
| 7193 | 7368 | |
| @@ -26,6 +26,7 @@ · 25 unmodified lines | ||
| 26 | 26 | |
| 27 | 27 | #include "avformat.h" |
| 28 | 28 | #include "movenccenc.h" |
| 29 | +#include "libavcodec/packet_internal.h" | |
| 29 | 30 | |
| 30 | 31 | #define MOV_FRAG_INFO_ALLOC_INCREMENT 64 |
| 31 | 32 | #define MOV_INDEX_CLUSTER_SIZE 1024 |
| @@ -163,6 +164,10 @@ typedef struct MOVTrack { · 131 unmodified lines | ||
| 163 | 164 | int pal_done; |
| 164 | 165 | |
| 165 | 166 | int is_unaligned_qt_rgb; |
| 167 | + | |
| 168 | + unsigned int squash_fragment_samples_to_one; //< flag to note formats where all samples for a fragment are to be squashed | |
| 169 | + | |
| 170 | + PacketList *squashed_packet_queue, *squashed_packet_queue_end; | |
| 166 | 171 | } MOVTrack; |
| 167 | 172 | |
| 168 | 173 | typedef enum { |
| @@ -0,0 +1,171 @@ | ||
| 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 "avformat.h" | |
| 23 | +#include "avio_internal.h" | |
| 24 | +#include "isom.h" | |
| 25 | +#include "movenc.h" | |
| 26 | +#include "movenc_ttml.h" | |
| 27 | +#include "libavcodec/packet_internal.h" | |
| 28 | + | |
| 29 | +static const unsigned char empty_ttml_document[] = | |
| 30 | + "<tt xml:lang=\"\" xmlns=\"http://www.w3.org/ns/ttml\" />"; | |
| 31 | + | |
| 32 | +static int mov_init_ttml_writer(MOVTrack *track, AVFormatContext **out_ctx) | |
| 33 | +{ | |
| 34 | + AVStream *movenc_stream = track->st, *ttml_stream = NULL; | |
| 35 | + int ret = AVERROR_BUG; | |
| 36 | + | |
| 37 | + if ((ret = avformat_alloc_output_context2(out_ctx, NULL, | |
| 38 | + "ttml", NULL)) < 0) | |
| 39 | + return ret; | |
| 40 | + | |
| 41 | + if ((ret = avio_open_dyn_buf(&(*out_ctx)->pb)) < 0) | |
| 42 | + return ret; | |
| 43 | + | |
| 44 | + if (!(ttml_stream = avformat_new_stream(*out_ctx, NULL))) { | |
| 45 | + return AVERROR(ENOMEM); | |
| 46 | + } | |
| 47 | + | |
| 48 | + if ((ret = avcodec_parameters_copy(ttml_stream->codecpar, | |
| 49 | + movenc_stream->codecpar)) < 0) | |
| 50 | + return ret; | |
| 51 | + | |
| 52 | + ttml_stream->time_base = movenc_stream->time_base; | |
| 53 | + | |
| 54 | + return 0; | |
| 55 | +} | |
| 56 | + | |
| 57 | +static int mov_write_ttml_document_from_queue(AVFormatContext *s, | |
| 58 | + AVFormatContext *ttml_ctx, | |
| 59 | + MOVTrack *track, | |
| 60 | + AVPacket *pkt, | |
| 61 | + int64_t *out_start_ts, | |
| 62 | + int64_t *out_duration) | |
| 63 | +{ | |
| 64 | + int ret = AVERROR_BUG; | |
| 65 | + int64_t start_ts = track->start_dts == AV_NOPTS_VALUE ? | |
| 66 | + 0 : (track->start_dts + track->track_duration); | |
| 67 | + int64_t end_ts = start_ts; | |
| 68 | + | |
| 69 | + if ((ret = avformat_write_header(ttml_ctx, NULL)) < 0) { | |
| 70 | + return ret; | |
| 71 | + } | |
| 72 | + | |
| 73 | + while (!avpriv_packet_list_get(&track->squashed_packet_queue, | |
| 74 | + &track->squashed_packet_queue_end, | |
| 75 | + pkt)) { | |
| 76 | + end_ts = FFMAX(end_ts, pkt->pts + pkt->duration); | |
| 77 | + | |
| 78 | + // in case of the 'dfxp' muxing mode, each written document is offset | |
| 79 | + // to its containing sample's beginning. | |
| 80 | + if (track->par->codec_tag == MOV_ISMV_TTML_TAG) { | |
| 81 | + pkt->dts = pkt->pts = (pkt->pts - start_ts); | |
| 82 | + } | |
| 83 | + | |
| 84 | + pkt->stream_index = 0; | |
| 85 | + | |
| 86 | + av_packet_rescale_ts(pkt, track->st->time_base, | |
| 87 | + ttml_ctx->streams[pkt->stream_index]->time_base); | |
| 88 | + | |
| 89 | + if ((ret = av_write_frame(ttml_ctx, pkt)) < 0) { | |
| 90 | + goto cleanup; | |
| 91 | + } | |
| 92 | + | |
| 93 | + av_packet_unref(pkt); | |
| 94 | + } | |
| 95 | + | |
| 96 | + if ((ret = av_write_trailer(ttml_ctx)) < 0) | |
| 97 | + goto cleanup; | |
| 98 | + | |
| 99 | + *out_start_ts = start_ts; | |
| 100 | + *out_duration = end_ts - start_ts; | |
| 101 | + | |
| 102 | + ret = 0; | |
| 103 | + | |
| 104 | +cleanup: | |
| 105 | + return ret; | |
| 106 | +} | |
| 107 | + | |
| 108 | +int ff_mov_generate_squashed_ttml_packet(AVFormatContext *s, | |
| 109 | + MOVTrack *track, AVPacket *pkt) | |
| 110 | +{ | |
| 111 | + AVFormatContext *ttml_ctx = NULL; | |
| 112 | + // values for the generated AVPacket | |
| 113 | + int64_t start_ts = 0; | |
| 114 | + int64_t duration = 0; | |
| 115 | + | |
| 116 | + int ret = AVERROR_BUG; | |
| 117 | + | |
| 118 | + if ((ret = mov_init_ttml_writer(track, &ttml_ctx)) < 0) { | |
| 119 | + av_log(s, AV_LOG_ERROR, "Failed to initialize the TTML writer: %s\n", | |
| 120 | + av_err2str(ret)); | |
| 121 | + goto cleanup; | |
| 122 | + } | |
| 123 | + | |
| 124 | + if (!track->squashed_packet_queue) { | |
| 125 | + // empty queue, write minimal empty document with zero duration | |
| 126 | + avio_write(ttml_ctx->pb, empty_ttml_document, | |
| 127 | + sizeof(empty_ttml_document) - 1); | |
| 128 | + start_ts = 0; | |
| 129 | + duration = 0; | |
| 130 | + goto generate_packet; | |
| 131 | + } | |
| 132 | + | |
| 133 | + if ((ret = mov_write_ttml_document_from_queue(s, ttml_ctx, track, pkt, | |
| 134 | + &start_ts, | |
| 135 | + &duration)) < 0) { | |
| 136 | + av_log(s, AV_LOG_ERROR, | |
| 137 | + "Failed to generate a squashed TTML packet from the packet " | |
| 138 | + "queue: %s\n", | |
| 139 | + av_err2str(ret)); | |
| 140 | + goto cleanup; | |
| 141 | + } | |
| 142 | + | |
| 143 | +generate_packet: | |
| 144 | + { | |
| 145 | + // Generate an AVPacket from the data written into the dynamic buffer. | |
| 146 | + uint8_t *buf = NULL; | |
| 147 | + int buf_len = avio_close_dyn_buf(ttml_ctx->pb, &buf); | |
| 148 | + ttml_ctx->pb = NULL; | |
| 149 | + | |
| 150 | + if ((ret = av_packet_from_data(pkt, buf, buf_len)) < 0) { | |
| 151 | + av_log(s, AV_LOG_ERROR, | |
| 152 | + "Failed to create a TTML AVPacket from AVIO data: %s\n", | |
| 153 | + av_err2str(ret)); | |
| 154 | + av_freep(&buf); | |
| 155 | + goto cleanup; | |
| 156 | + } | |
| 157 | + | |
| 158 | + pkt->pts = pkt->dts = start_ts; | |
| 159 | + pkt->duration = duration; | |
| 160 | + pkt->flags |= AV_PKT_FLAG_KEY; | |
| 161 | + } | |
| 162 | + | |
| 163 | + ret = 0; | |
| 164 | + | |
| 165 | +cleanup: | |
| 166 | + if (ttml_ctx) | |
| 167 | + ffio_free_dyn_buf(&ttml_ctx->pb); | |
| 168 | + | |
| 169 | + avformat_free_context(ttml_ctx); | |
| 170 | + return ret; | |
| 171 | +} | |
| @@ -0,0 +1,31 @@ | ||
| 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 | +#ifndef AVFORMAT_MOVENC_TTML_H | |
| 23 | +#define AVFORMAT_MOVENC_TTML_H | |
| 24 | + | |
| 25 | +#include "avformat.h" | |
| 26 | +#include "movenc.h" | |
| 27 | + | |
| 28 | +int ff_mov_generate_squashed_ttml_packet(AVFormatContext *s, | |
| 29 | + MOVTrack *track, AVPacket *pkt); | |
| 30 | + | |
| 31 | +#endif /* AVFORMAT_MOVENC_TTML_H */ | |
| @@ -109,6 +109,10 @@ fate-sub-dvb: CMD = framecrc -i $(TARGET_SAMPLES)/sub/dvbsubtest_filter.ts -map · 108 unmodified lines | ||
| 109 | 109 | FATE_SUBTITLES-$(call ALLYES, FILE_PROTOCOL PIPE_PROTOCOL SRT_DEMUXER SUBRIP_DECODER TTML_ENCODER TTML_MUXER) += fate-sub-ttmlenc |
| 110 | 110 | fate-sub-ttmlenc: CMD = fmtstdout ttml -i $(TARGET_SAMPLES)/sub/SubRip_capability_tester.srt |
| 111 | 111 | |
| 112 | +FATE_SUBTITLES-$(call ALLYES, FILE_PROTOCOL SRT_DEMUXER MOV_DEMUXER SUBRIP_DECODER TTML_ENCODER TTML_MUXER MOV_MUXER) += fate-sub-ttml-mp4-stpp fate-sub-ttml-mp4-dfxp | |
| 113 | +fate-sub-ttml-mp4-stpp: CMD = transcode srt $(TARGET_SAMPLES)/sub/SubRip_capability_tester.srt mp4 "-map 0:s -c:s ttml -time_base:s 1:1000" "-map 0 -c copy" "" "-of json -show_entries packet:stream=index,codec_type,codec_tag_string,codec_tag,codec_name,time_base,start_time,duration_ts,duration,nb_frames,nb_read_packets:stream_tags" | |
| 114 | +fate-sub-ttml-mp4-dfxp: CMD = transcode srt $(TARGET_SAMPLES)/sub/SubRip_capability_tester.srt mp4 "-map 0:s -c:s ttml -time_base:s 1:1000 -tag:s dfxp -strict unofficial" "-map 0 -c copy" "" "-of json -show_entries packet:stream=index,codec_type,codec_tag_string,codec_tag,codec_name,time_base,start_time,duration_ts,duration,nb_frames,nb_read_packets:stream_tags" | |
| 115 | + | |
| 112 | 116 | FATE_SUBTITLES-$(call ENCMUX, ASS, ASS) += $(FATE_SUBTITLES_ASS-yes) |
| 113 | 117 | FATE_SUBTITLES += $(FATE_SUBTITLES-yes) |
| 114 | 118 | |
| @@ -0,0 +1,44 @@ | ||
| 1 | +2e7e01c821c111466e7a2844826b7f6d *tests/data/fate/sub-ttml-mp4-dfxp.mp4 | |
| 2 | +8519 tests/data/fate/sub-ttml-mp4-dfxp.mp4 | |
| 3 | +#tb 0: 1/1000 | |
| 4 | +#media_type 0: data | |
| 5 | +#codec_id 0: none | |
| 6 | +0, 0, 0, 68500, 7866, 0x456c36b7 | |
| 7 | +{ | |
| 8 | + "packets": [ | |
| 9 | + { | |
| 10 | + "codec_type": "data", | |
| 11 | + "stream_index": 0, | |
| 12 | + "pts": 0, | |
| 13 | + "pts_time": "0.000000", | |
| 14 | + "dts": 0, | |
| 15 | + "dts_time": "0.000000", | |
| 16 | + "duration": 68500, | |
| 17 | + "duration_time": "68.500000", | |
| 18 | + "size": "7866", | |
| 19 | + "pos": "44", | |
| 20 | + "flags": "K_" | |
| 21 | + } | |
| 22 | + ], | |
| 23 | + "programs": [ | |
| 24 | + | |
| 25 | + ], | |
| 26 | + "streams": [ | |
| 27 | + { | |
| 28 | + "index": 0, | |
| 29 | + "codec_type": "data", | |
| 30 | + "codec_tag_string": "dfxp", | |
| 31 | + "codec_tag": "0x70786664", | |
| 32 | + "time_base": "1/1000", | |
| 33 | + "start_time": "0.000000", | |
| 34 | + "duration_ts": 68500, | |
| 35 | + "duration": "68.500000", | |
| 36 | + "nb_frames": "1", | |
| 37 | + "nb_read_packets": "1", | |
| 38 | + "tags": { | |
| 39 | + "language": "und", | |
| 40 | + "handler_name": "SubtitleHandler" | |
| 41 | + } | |
| 42 | + } | |
| 43 | + ] | |
| 44 | +} | |
| @@ -0,0 +1,44 @@ | ||
| 1 | +cbd2c7ff864a663b0d893deac5a0caec *tests/data/fate/sub-ttml-mp4-stpp.mp4 | |
| 2 | +8547 tests/data/fate/sub-ttml-mp4-stpp.mp4 | |
| 3 | +#tb 0: 1/1000 | |
| 4 | +#media_type 0: data | |
| 5 | +#codec_id 0: none | |
| 6 | +0, 0, 0, 68500, 7866, 0x456c36b7 | |
| 7 | +{ | |
| 8 | + "packets": [ | |
| 9 | + { | |
| 10 | + "codec_type": "data", | |
| 11 | + "stream_index": 0, | |
| 12 | + "pts": 0, | |
| 13 | + "pts_time": "0.000000", | |
| 14 | + "dts": 0, | |
| 15 | + "dts_time": "0.000000", | |
| 16 | + "duration": 68500, | |
| 17 | + "duration_time": "68.500000", | |
| 18 | + "size": "7866", | |
| 19 | + "pos": "44", | |
| 20 | + "flags": "K_" | |
| 21 | + } | |
| 22 | + ], | |
| 23 | + "programs": [ | |
| 24 | + | |
| 25 | + ], | |
| 26 | + "streams": [ | |
| 27 | + { | |
| 28 | + "index": 0, | |
| 29 | + "codec_type": "data", | |
| 30 | + "codec_tag_string": "stpp", | |
| 31 | + "codec_tag": "0x70707473", | |
| 32 | + "time_base": "1/1000", | |
| 33 | + "start_time": "0.000000", | |
| 34 | + "duration_ts": 68500, | |
| 35 | + "duration": "68.500000", | |
| 36 | + "nb_frames": "1", | |
| 37 | + "nb_read_packets": "1", | |
| 38 | + "tags": { | |
| 39 | + "language": "und", | |
| 40 | + "handler_name": "SubtitleHandler" | |
| 41 | + } | |
| 42 | + } | |
| 43 | + ] | |
| 44 | +} | |