This is based on a spec at https://aomediacodec.github.io/id3-emsg/, further based on ISO/IEC 23009-1:2019. Within libavformat, timed ID3 metadata (already supported by the mpegts demuxer and muxer) is handled as a separate data AVStream with codec type AV_CODEC_ID_TIMED_ID3. However, it doesn't have a corresponding track in the mov file - instead, these events are written as separate toplevel 'emsg' boxes.
Unified · Split
| @@ -5519,7 +5519,7 @@ static int mov_write_ftyp_tag(AVIOContext *pb, AVFormatContext *s) · 5518 unmodified lines | |||
| 5519 | { | 5519 | { |
| 5520 | MOVMuxContext *mov = s->priv_data; | 5520 | MOVMuxContext *mov = s->priv_data; |
| 5521 | int64_t pos = avio_tell(pb); | 5521 | int64_t pos = avio_tell(pb); |
| 5522 | int has_h264 = 0, has_av1 = 0, has_video = 0, has_dolby = 0; | 5522 | int has_h264 = 0, has_av1 = 0, has_video = 0, has_dolby = 0, has_id3 = 0; |
| 5523 | int has_iamf = 0; | 5523 | int has_iamf = 0; |
| 5524 | 5524 | ||
| 5525 | #if CONFIG_IAMFENC | 5525 | #if CONFIG_IAMFENC |
| @@ -5550,6 +5550,8 @@ static int mov_write_ftyp_tag(AVIOContext *pb, AVFormatContext *s) · 24 unmodified lines | |||
| 5550 | st->codecpar->nb_coded_side_data, | 5550 | st->codecpar->nb_coded_side_data, |
| 5551 | AV_PKT_DATA_DOVI_CONF)) | 5551 | AV_PKT_DATA_DOVI_CONF)) |
| 5552 | has_dolby = 1; | 5552 | has_dolby = 1; |
| 5553 | if (st->codecpar->codec_id == AV_CODEC_ID_TIMED_ID3) | ||
| 5554 | has_id3 = 1; | ||
| 5553 | } | 5555 | } |
| 5554 | 5556 | ||
| 5555 | avio_wb32(pb, 0); /* size */ | 5557 | avio_wb32(pb, 0); /* size */ |
| @@ -5629,6 +5631,9 @@ static int mov_write_ftyp_tag(AVIOContext *pb, AVFormatContext *s) · 73 unmodified lines | |||
| 5629 | if (mov->flags & FF_MOV_FLAG_DASH && mov->flags & FF_MOV_FLAG_GLOBAL_SIDX) | 5631 | if (mov->flags & FF_MOV_FLAG_DASH && mov->flags & FF_MOV_FLAG_GLOBAL_SIDX) |
| 5630 | ffio_wfourcc(pb, "dash"); | 5632 | ffio_wfourcc(pb, "dash"); |
| 5631 | 5633 | ||
| 5634 | if (has_id3) | ||
| 5635 | ffio_wfourcc(pb, "aid3"); | ||
| 5636 | |||
| 5632 | return update_size(pb, pos); | 5637 | return update_size(pb, pos); |
| 5633 | } | 5638 | } |
| 5634 | 5639 | ||
| @@ -6712,6 +6717,34 @@ static int mov_build_iamf_packet(AVFormatContext *s, MOVTrack *trk, AVPacket *pk · 1077 unmodified lines | |||
| 6712 | } | 6717 | } |
| 6713 | #endif | 6718 | #endif |
| 6714 | 6719 | ||
| 6720 | static int mov_write_emsg_tag(AVIOContext *pb, AVStream *st, AVPacket *pkt) | ||
| 6721 | { | ||
| 6722 | int64_t pos = avio_tell(pb); | ||
| 6723 | const char *scheme_id_uri = "https://aomedia.org/emsg/ID3"; | ||
| 6724 | const char *value = ""; | ||
| 6725 | |||
| 6726 | av_assert0(st->time_base.num == 1); | ||
| 6727 | |||
| 6728 | avio_write_marker(pb, | ||
| 6729 | av_rescale_q(pkt->pts, st->time_base, AV_TIME_BASE_Q), | ||
| 6730 | AVIO_DATA_MARKER_BOUNDARY_POINT); | ||
| 6731 | |||
| 6732 | avio_wb32(pb, 0); /* size */ | ||
| 6733 | ffio_wfourcc(pb, "emsg"); | ||
| 6734 | avio_w8(pb, 1); /* version */ | ||
| 6735 | avio_wb24(pb, 0); | ||
| 6736 | avio_wb32(pb, st->time_base.den); /* timescale */ | ||
| 6737 | avio_wb64(pb, pkt->pts); /* presentation_time */ | ||
| 6738 | avio_wb32(pb, 0xFFFFFFFFU); /* event_duration */ | ||
| 6739 | avio_wb32(pb, 0); /* id */ | ||
| 6740 | /* null terminated UTF8 strings */ | ||
| 6741 | avio_write(pb, scheme_id_uri, strlen(scheme_id_uri) + 1); | ||
| 6742 | avio_write(pb, value, strlen(value) + 1); | ||
| 6743 | avio_write(pb, pkt->data, pkt->size); | ||
| 6744 | |||
| 6745 | return update_size(pb, pos); | ||
| 6746 | } | ||
| 6747 | |||
| 6715 | static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) | 6748 | static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) |
| 6716 | { | 6749 | { |
| 6717 | MOVMuxContext *mov = s->priv_data; | 6750 | MOVMuxContext *mov = s->priv_data; |
| @@ -6722,6 +6755,11 @@ static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) · 4 unmodified lines | |||
| 6722 | return 1; | 6755 | return 1; |
| 6723 | } | 6756 | } |
| 6724 | 6757 | ||
| 6758 | if (s->streams[pkt->stream_index]->codecpar->codec_id == AV_CODEC_ID_TIMED_ID3) { | ||
| 6759 | mov_write_emsg_tag(s->pb, s->streams[pkt->stream_index], pkt); | ||
| 6760 | return 0; | ||
| 6761 | } | ||
| 6762 | |||
| 6725 | trk = s->streams[pkt->stream_index]->priv_data; | 6763 | trk = s->streams[pkt->stream_index]->priv_data; |
| 6726 | 6764 | ||
| 6727 | #if CONFIG_IAMFENC | 6765 | #if CONFIG_IAMFENC |
| @@ -7381,6 +7419,12 @@ static int mov_init(AVFormatContext *s) · 653 unmodified lines | |||
| 7381 | AVStream *st = s->streams[i]; | 7419 | AVStream *st = s->streams[i]; |
| 7382 | if (st->priv_data) | 7420 | if (st->priv_data) |
| 7383 | continue; | 7421 | continue; |
| 7422 | // Don't produce a track in the output file for timed ID3 streams. | ||
| 7423 | if (st->codecpar->codec_id == AV_CODEC_ID_TIMED_ID3) { | ||
| 7424 | // Leave priv_data set to NULL for these AVStreams that don't | ||
| 7425 | // have a corresponding track. | ||
| 7426 | continue; | ||
| 7427 | } | ||
| 7384 | st->priv_data = st; | 7428 | st->priv_data = st; |
| 7385 | mov->nb_tracks++; | 7429 | mov->nb_tracks++; |
| 7386 | } | 7430 | } |
| @@ -7480,6 +7524,9 @@ static int mov_init(AVFormatContext *s) · 93 unmodified lines | |||
| 7480 | MOVTrack *track = st->priv_data; | 7524 | MOVTrack *track = st->priv_data; |
| 7481 | AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL,0); | 7525 | AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL,0); |
| 7482 | 7526 | ||
| 7527 | if (!track) | ||
| 7528 | continue; | ||
| 7529 | |||
| 7483 | if (!track->st) { | 7530 | if (!track->st) { |
| 7484 | track->st = st; | 7531 | track->st = st; |
| 7485 | track->par = st->codecpar; | 7532 | track->par = st->codecpar; |
| @@ -58,7 +58,7 @@ struct AVMD5* md5; · 57 unmodified lines | |||
| 58 | uint8_t hash[HASH_SIZE]; | 58 | uint8_t hash[HASH_SIZE]; |
| 59 | 59 | ||
| 60 | AVPacket *pkt; | 60 | AVPacket *pkt; |
| 61 | AVStream *video_st, *audio_st; | 61 | AVStream *video_st, *audio_st, *id3_st; |
| 62 | int64_t audio_dts, video_dts; | 62 | int64_t audio_dts, video_dts; |
| 63 | 63 | ||
| 64 | int bframes; | 64 | int bframes; |
| @@ -177,7 +177,7 @@ static void check_func(int value, int line, const char *msg, ...) · 112 unmodified lines | |||
| 177 | } | 177 | } |
| 178 | #define check(value, ...) check_func(value, __LINE__, __VA_ARGS__) | 178 | #define check(value, ...) check_func(value, __LINE__, __VA_ARGS__) |
| 179 | 179 | ||
| 180 | static void init_fps(int bf, int audio_preroll, int fps) | 180 | static void init_fps(int bf, int audio_preroll, int fps, int id3) |
| 181 | { | 181 | { |
| 182 | AVStream *st; | 182 | AVStream *st; |
| 183 | int iobuf_size = force_iobuf_size ? force_iobuf_size : sizeof(iobuf); | 183 | int iobuf_size = force_iobuf_size ? force_iobuf_size : sizeof(iobuf); |
| @@ -225,6 +225,17 @@ static void init_fps(int bf, int audio_preroll, int fps) · 41 unmodified lines | |||
| 225 | memcpy(st->codecpar->extradata, aac_extradata, sizeof(aac_extradata)); | 225 | memcpy(st->codecpar->extradata, aac_extradata, sizeof(aac_extradata)); |
| 226 | audio_st = st; | 226 | audio_st = st; |
| 227 | 227 | ||
| 228 | if (id3) { | ||
| 229 | st = avformat_new_stream(ctx, NULL); | ||
| 230 | if (!st) | ||
| 231 | exit(1); | ||
| 232 | st->codecpar->codec_type = AVMEDIA_TYPE_DATA; | ||
| 233 | st->codecpar->codec_id = AV_CODEC_ID_TIMED_ID3; | ||
| 234 | st->time_base.num = 1; | ||
| 235 | st->time_base.den = 1000; | ||
| 236 | id3_st = st; | ||
| 237 | } | ||
| 238 | |||
| 228 | if (avformat_write_header(ctx, &opts) < 0) | 239 | if (avformat_write_header(ctx, &opts) < 0) |
| 229 | exit(1); | 240 | exit(1); |
| 230 | av_dict_free(&opts); | 241 | av_dict_free(&opts); |
| @@ -243,7 +254,7 @@ static void init_fps(int bf, int audio_preroll, int fps) · 12 unmodified lines | |||
| 243 | 254 | ||
| 244 | static void init(int bf, int audio_preroll) | 255 | static void init(int bf, int audio_preroll) |
| 245 | { | 256 | { |
| 246 | init_fps(bf, audio_preroll, 30); | 257 | init_fps(bf, audio_preroll, 30, 0); |
| 247 | } | 258 | } |
| 248 | 259 | ||
| 249 | static void mux_frames(int n, int c) | 260 | static void mux_frames(int n, int c) |
| @@ -314,6 +325,23 @@ static void mux_frames(int n, int c) · 64 unmodified lines | |||
| 314 | } | 325 | } |
| 315 | } | 326 | } |
| 316 | 327 | ||
| 328 | static void mux_id3(void) | ||
| 329 | { | ||
| 330 | uint8_t pktdata[8] = { 0 }; | ||
| 331 | av_packet_unref(pkt); | ||
| 332 | |||
| 333 | pkt->dts = pkt->pts = av_rescale_q(video_dts + (bframes ? duration : 0), | ||
| 334 | video_st->time_base, id3_st->time_base); | ||
| 335 | pkt->stream_index = id3_st->index; | ||
| 336 | pkt->duration = 0; | ||
| 337 | |||
| 338 | AV_WB32(pktdata + 4, pkt->pts); | ||
| 339 | pkt->data = pktdata; | ||
| 340 | pkt->size = 8; | ||
| 341 | |||
| 342 | av_write_frame(ctx, pkt); | ||
| 343 | } | ||
| 344 | |||
| 317 | static void mux_gops(int n) | 345 | static void mux_gops(int n) |
| 318 | { | 346 | { |
| 319 | mux_frames(gop_size * n, 0); | 347 | mux_frames(gop_size * n, 0); |
| @@ -702,7 +730,7 @@ int main(int argc, char **argv) · 382 unmodified lines | |||
| 702 | // by the edit list. | 730 | // by the edit list. |
| 703 | init_out("vfr"); | 731 | init_out("vfr"); |
| 704 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov+dash", 0); | 732 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov+dash", 0); |
| 705 | init_fps(1, 1, 3); | 733 | init_fps(1, 1, 3, 0); |
| 706 | mux_frames(gop_size/2, 0); | 734 | mux_frames(gop_size/2, 0); |
| 707 | duration /= 10; | 735 | duration /= 10; |
| 708 | mux_frames(gop_size/2, 0); | 736 | mux_frames(gop_size/2, 0); |
| @@ -721,7 +749,7 @@ int main(int argc, char **argv) · 12 unmodified lines | |||
| 721 | clear_duration = 1; | 749 | clear_duration = 1; |
| 722 | init_out("vfr-noduration"); | 750 | init_out("vfr-noduration"); |
| 723 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov+dash", 0); | 751 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov+dash", 0); |
| 724 | init_fps(1, 1, 3); | 752 | init_fps(1, 1, 3, 0); |
| 725 | mux_frames(gop_size/2, 0); | 753 | mux_frames(gop_size/2, 0); |
| 726 | duration /= 10; | 754 | duration /= 10; |
| 727 | mux_frames(gop_size/2, 0); | 755 | mux_frames(gop_size/2, 0); |
| @@ -737,7 +765,7 @@ int main(int argc, char **argv) · 9 unmodified lines | |||
| 737 | force_iobuf_size = 1500; | 765 | force_iobuf_size = 1500; |
| 738 | init_out("large_frag"); | 766 | init_out("large_frag"); |
| 739 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov", 0); | 767 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov", 0); |
| 740 | init_fps(1, 1, 3); | 768 | init_fps(1, 1, 3, 0); |
| 741 | mux_gops(2); | 769 | mux_gops(2); |
| 742 | finish(); | 770 | finish(); |
| 743 | close_out(); | 771 | close_out(); |
| @@ -751,7 +779,7 @@ int main(int argc, char **argv) · 7 unmodified lines | |||
| 751 | init_out("vfr-noduration-interleave"); | 779 | init_out("vfr-noduration-interleave"); |
| 752 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov", 0); | 780 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov", 0); |
| 753 | av_dict_set(&opts, "frag_duration", "650000", 0); | 781 | av_dict_set(&opts, "frag_duration", "650000", 0); |
| 754 | init_fps(1, 1, 30); | 782 | init_fps(1, 1, 30, 0); |
| 755 | mux_frames(gop_size/2, 0); | 783 | mux_frames(gop_size/2, 0); |
| 756 | // Pretend that the packet duration is the normal, even if | 784 | // Pretend that the packet duration is the normal, even if |
| 757 | // we actually skip a bunch of frames. (I.e., simulate that | 785 | // we actually skip a bunch of frames. (I.e., simulate that |
| @@ -788,6 +816,19 @@ int main(int argc, char **argv) · 30 unmodified lines | |||
| 788 | finish(); | 816 | finish(); |
| 789 | close_out(); | 817 | close_out(); |
| 790 | 818 | ||
| 819 | // Write a manually fragmented file, with timed ID3 packets at the head | ||
| 820 | // of each fragment. | ||
| 821 | init_out("emsg"); | ||
| 822 | av_dict_set(&opts, "movflags", "+frag_custom+cmaf", 0); | ||
| 823 | init_fps(1, 0, 30, 1); | ||
| 824 | mux_id3(); | ||
| 825 | mux_gops(2); | ||
| 826 | av_write_frame(ctx, NULL); // Flush fragment. | ||
| 827 | mux_id3(); | ||
| 828 | mux_gops(2); | ||
| 829 | finish(); | ||
| 830 | close_out(); | ||
| 831 | |||
| 791 | av_free(md5); | 832 | av_free(md5); |
| 792 | av_packet_free(&pkt); | 833 | av_packet_free(&pkt); |
| 793 | 834 | ||
| @@ -151,3 +151,11 @@ write_data len 900, time 0, type sync atom moof · 150 unmodified lines | |||
| 151 | write_data len 908, time 1000000, type sync atom moof | 151 | write_data len 908, time 1000000, type sync atom moof |
| 152 | write_data len 148, time nopts, type trailer atom - | 152 | write_data len 148, time nopts, type trailer atom - |
| 153 | 3be575022e446855bca1e45b7942cc0c 3115 empty-moov-neg-cts | 153 | 3be575022e446855bca1e45b7942cc0c 3115 empty-moov-neg-cts |
| 154 | write_data len 28, time nopts, type header atom ftyp | ||
| 155 | write_data len 1123, time nopts, type header atom - | ||
| 156 | write_data len 70, time 0, type boundary atom emsg | ||
| 157 | write_data len 1832, time 0, type sync atom moof | ||
| 158 | write_data len 70, time 2000000, type boundary atom emsg | ||
| 159 | write_data len 1840, time 2000000, type sync atom moof | ||
| 160 | write_data len 148, time nopts, type trailer atom - | ||
| 161 | b72c56c795693820b156f452354a51ff 5111 emsg | ||