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 | 5520 | MOVMuxContext *mov = s->priv_data; |
| 5521 | 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 | 5523 | int has_iamf = 0; |
| 5524 | 5524 | |
| 5525 | 5525 | #if CONFIG_IAMFENC |
| @@ -5550,6 +5550,8 @@ static int mov_write_ftyp_tag(AVIOContext *pb, AVFormatContext *s) · 24 unmodified lines | ||
| 5550 | 5550 | st->codecpar->nb_coded_side_data, |
| 5551 | 5551 | AV_PKT_DATA_DOVI_CONF)) |
| 5552 | 5552 | has_dolby = 1; |
| 5553 | + if (st->codecpar->codec_id == AV_CODEC_ID_TIMED_ID3) | |
| 5554 | + has_id3 = 1; | |
| 5553 | 5555 | } |
| 5554 | 5556 | |
| 5555 | 5557 | avio_wb32(pb, 0); /* size */ |
| @@ -5629,6 +5631,9 @@ static int mov_write_ftyp_tag(AVIOContext *pb, AVFormatContext *s) · 73 unmodified lines | ||
| 5629 | 5631 | if (mov->flags & FF_MOV_FLAG_DASH && mov->flags & FF_MOV_FLAG_GLOBAL_SIDX) |
| 5630 | 5632 | ffio_wfourcc(pb, "dash"); |
| 5631 | 5633 | |
| 5634 | + if (has_id3) | |
| 5635 | + ffio_wfourcc(pb, "aid3"); | |
| 5636 | + | |
| 5632 | 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 | 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 | 6748 | static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) |
| 6716 | 6749 | { |
| 6717 | 6750 | MOVMuxContext *mov = s->priv_data; |
| @@ -6722,6 +6755,11 @@ static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) · 4 unmodified lines | ||
| 6722 | 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 | 6763 | trk = s->streams[pkt->stream_index]->priv_data; |
| 6726 | 6764 | |
| 6727 | 6765 | #if CONFIG_IAMFENC |
| @@ -7381,6 +7419,12 @@ static int mov_init(AVFormatContext *s) · 653 unmodified lines | ||
| 7381 | 7419 | AVStream *st = s->streams[i]; |
| 7382 | 7420 | if (st->priv_data) |
| 7383 | 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 | 7428 | st->priv_data = st; |
| 7385 | 7429 | mov->nb_tracks++; |
| 7386 | 7430 | } |
| @@ -7480,6 +7524,9 @@ static int mov_init(AVFormatContext *s) · 93 unmodified lines | ||
| 7480 | 7524 | MOVTrack *track = st->priv_data; |
| 7481 | 7525 | AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL,0); |
| 7482 | 7526 | |
| 7527 | + if (!track) | |
| 7528 | + continue; | |
| 7529 | + | |
| 7483 | 7530 | if (!track->st) { |
| 7484 | 7531 | track->st = st; |
| 7485 | 7532 | track->par = st->codecpar; |
| @@ -58,7 +58,7 @@ struct AVMD5* md5; · 57 unmodified lines | ||
| 58 | 58 | uint8_t hash[HASH_SIZE]; |
| 59 | 59 | |
| 60 | 60 | AVPacket *pkt; |
| 61 | −AVStream *video_st, *audio_st; | |
| 61 | +AVStream *video_st, *audio_st, *id3_st; | |
| 62 | 62 | int64_t audio_dts, video_dts; |
| 63 | 63 | |
| 64 | 64 | int bframes; |
| @@ -177,7 +177,7 @@ static void check_func(int value, int line, const char *msg, ...) · 112 unmodified lines | ||
| 177 | 177 | } |
| 178 | 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 | 182 | AVStream *st; |
| 183 | 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 | 225 | memcpy(st->codecpar->extradata, aac_extradata, sizeof(aac_extradata)); |
| 226 | 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 | 239 | if (avformat_write_header(ctx, &opts) < 0) |
| 229 | 240 | exit(1); |
| 230 | 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 | 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 | 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 | 345 | static void mux_gops(int n) |
| 318 | 346 | { |
| 319 | 347 | mux_frames(gop_size * n, 0); |
| @@ -702,7 +730,7 @@ int main(int argc, char **argv) · 382 unmodified lines | ||
| 702 | 730 | // by the edit list. |
| 703 | 731 | init_out("vfr"); |
| 704 | 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 | 734 | mux_frames(gop_size/2, 0); |
| 707 | 735 | duration /= 10; |
| 708 | 736 | mux_frames(gop_size/2, 0); |
| @@ -721,7 +749,7 @@ int main(int argc, char **argv) · 12 unmodified lines | ||
| 721 | 749 | clear_duration = 1; |
| 722 | 750 | init_out("vfr-noduration"); |
| 723 | 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 | 753 | mux_frames(gop_size/2, 0); |
| 726 | 754 | duration /= 10; |
| 727 | 755 | mux_frames(gop_size/2, 0); |
| @@ -737,7 +765,7 @@ int main(int argc, char **argv) · 9 unmodified lines | ||
| 737 | 765 | force_iobuf_size = 1500; |
| 738 | 766 | init_out("large_frag"); |
| 739 | 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 | 769 | mux_gops(2); |
| 742 | 770 | finish(); |
| 743 | 771 | close_out(); |
| @@ -751,7 +779,7 @@ int main(int argc, char **argv) · 7 unmodified lines | ||
| 751 | 779 | init_out("vfr-noduration-interleave"); |
| 752 | 780 | av_dict_set(&opts, "movflags", "+frag_keyframe+delay_moov", 0); |
| 753 | 781 | av_dict_set(&opts, "frag_duration", "650000", 0); |
| 754 | − init_fps(1, 1, 30); | |
| 782 | + init_fps(1, 1, 30, 0); | |
| 755 | 783 | mux_frames(gop_size/2, 0); |
| 756 | 784 | // Pretend that the packet duration is the normal, even if |
| 757 | 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 | 816 | finish(); |
| 789 | 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 | 832 | av_free(md5); |
| 792 | 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 | 151 | write_data len 908, time 1000000, type sync atom moof |
| 152 | 152 | write_data len 148, time nopts, type trailer atom - |
| 153 | 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 | |