diff --git a/libavformat/hls.c b/libavformat/hls.c index b03201c690..4c56b954bb 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -174,6 +174,14 @@ struct playlist { int n_init_sections; struct segment **init_sections; int is_subtitle; /* Indicates if it's a subtitle playlist */ + + /* Some HLS streams repeat the current timed-ID3 metadata at every segment + * boundary so late-joining clients get a fresh copy. Track the last seen + * value to avoid raising AVSTREAM_EVENT_FLAG_METADATA_UPDATED spuriously + * on those duplicates. timed_id3_stream_index is the subdemuxer stream + * index of the first timed-ID3 stream seen; -1 if none. */ + AVDictionary *timed_id3_metadata; + int timed_id3_stream_index; }; /* @@ -275,6 +283,7 @@ static void free_playlist_list(HLSContext *c) av_freep(&pls->renditions); av_freep(&pls->id3_buf); av_dict_free(&pls->id3_initial); + av_dict_free(&pls->timed_id3_metadata); ff_id3v2_free_extra_meta(&pls->id3_deferred_extra); av_freep(&pls->init_sec_buf); av_packet_free(&pls->pkt); @@ -335,6 +344,7 @@ static struct playlist *new_playlist(HLSContext *c, const char *url, pls->is_id3_timestamped = -1; pls->id3_mpegts_timestamp = AV_NOPTS_VALUE; + pls->timed_id3_stream_index = -1; dynarray_add(&c->playlists, &c->n_playlists, pls); return pls; @@ -2108,6 +2118,16 @@ static int update_streams_from_subdemuxer(AVFormatContext *s, struct playlist *p err = set_stream_info_from_input_stream(st, pls, ist); if (err < 0) return err; + + if (ist->codecpar->codec_id == AV_CODEC_ID_TIMED_ID3) { + if (pls->timed_id3_stream_index < 0) + pls->timed_id3_stream_index = ist_idx; + else + av_log(s, AV_LOG_WARNING, + "Playlist has multiple timed ID3 streams; " + "only stream %d will be used for metadata propagation\n", + pls->timed_id3_stream_index); + } } return 0; @@ -2553,6 +2573,29 @@ static int compare_ts_with_wrapdetect(int64_t ts_a, struct playlist *pls_a, return av_compare_mod(scaled_ts_a, scaled_ts_b, 1LL << 33); } +/** + * Check whether any entry in @p update differs from the corresponding entry + * in @p current. + * + * HLS segments can repeat timed ID3 metadata in every segment so that + * listeners joining mid-stream immediately receive the current track + * information. Only signal a metadata change when values actually differ + * to avoid spurious updates on every segment boundary. + */ +static int metadata_has_changed(const AVDictionary *current, + const AVDictionary *update) +{ + if (av_dict_count(current) != av_dict_count(update)) + return 1; + const AVDictionaryEntry *e = NULL; + while ((e = av_dict_iterate(update, e))) { + const AVDictionaryEntry *cur = av_dict_get(current, e->key, NULL, 0); + if (!cur || strcmp(cur->value, e->value)) + return 1; + } + return 0; +} + static int hls_read_packet(AVFormatContext *s, AVPacket *pkt) { HLSContext *c = s->priv_data; @@ -2694,6 +2737,24 @@ static int hls_read_packet(AVFormatContext *s, AVPacket *pkt) ist = pls->ctx->streams[pls->pkt->stream_index]; st = pls->main_streams[pls->pkt->stream_index]; + // Propagate timed-ID3 metadata changes to the main stream, suppressing + // duplicate packets that repeat unchanged metadata at segment boundaries. + if (ist->codecpar->codec_id == AV_CODEC_ID_TIMED_ID3 && + pls->pkt->stream_index == pls->timed_id3_stream_index && + ist->event_flags & AVSTREAM_EVENT_FLAG_METADATA_UPDATED) { + ist->event_flags &= ~AVSTREAM_EVENT_FLAG_METADATA_UPDATED; + if (metadata_has_changed(pls->timed_id3_metadata, ist->metadata)) { + int ret = av_dict_copy(&st->metadata, ist->metadata, 0); + if (ret < 0) + return ret; + av_dict_free(&pls->timed_id3_metadata); + ret = av_dict_copy(&pls->timed_id3_metadata, ist->metadata, 0); + if (ret < 0) + return ret; + st->event_flags |= AVSTREAM_EVENT_FLAG_METADATA_UPDATED; + } + } + av_packet_move_ref(pkt, pls->pkt); pkt->stream_index = st->index; diff --git a/tests/ref/fate/ts-timed-id3-hls-demux b/tests/ref/fate/ts-timed-id3-hls-demux index c0da932512..ea238b0738 100644 --- a/tests/ref/fate/ts-timed-id3-hls-demux +++ b/tests/ref/fate/ts-timed-id3-hls-demux @@ -1,4 +1,4 @@ packet|codec_type=data|stream_index=0|pts=126000|pts_time=1.400000|dts=126000|dts_time=1.400000|duration=N/A|duration_time=N/A|size=26|pos=564|flags=K__|data_hash=CRC32:469f474b|side_datum/mpegts_stream_id:side_data_type=MPEGTS Stream ID|side_datum/mpegts_stream_id:id=189 packet|codec_type=data|stream_index=0|pts=577350|pts_time=6.415000|dts=577350|dts_time=6.415000|duration=N/A|duration_time=N/A|size=26|pos=1316|flags=K__|data_hash=CRC32:469f474b|side_datum/mpegts_stream_id:side_data_type=MPEGTS Stream ID|side_datum/mpegts_stream_id:id=189 -stream|index=0|codec_name=timed_id3|profile=unknown|codec_type=data|codec_tag_string=ID3 |codec_tag=0x20334449|id=0x0|r_frame_rate=0/0|avg_frame_rate=0/0|time_base=1/90000|start_pts=126000|start_time=1.400000|duration_ts=N/A|duration=N/A|bit_rate=N/A|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=N/A|nb_read_packets=2|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:variant_bitrate=0 +stream|index=0|codec_name=timed_id3|profile=unknown|codec_type=data|codec_tag_string=ID3 |codec_tag=0x20334449|id=0x0|r_frame_rate=0/0|avg_frame_rate=0/0|time_base=1/90000|start_pts=126000|start_time=1.400000|duration_ts=N/A|duration=N/A|bit_rate=N/A|max_bit_rate=N/A|bits_per_raw_sample=N/A|nb_frames=N/A|nb_read_frames=N/A|nb_read_packets=2|disposition:default=0|disposition:dub=0|disposition:original=0|disposition:comment=0|disposition:lyrics=0|disposition:karaoke=0|disposition:forced=0|disposition:hearing_impaired=0|disposition:visual_impaired=0|disposition:clean_effects=0|disposition:attached_pic=0|disposition:timed_thumbnails=0|disposition:non_diegetic=0|disposition:captions=0|disposition:descriptions=0|disposition:metadata=0|disposition:dependent=0|disposition:still_image=0|disposition:multilayer=0|tag:variant_bitrate=0|tag:title=test format|filename=id3.m3u8|nb_streams=1|nb_programs=1|nb_stream_groups=0|format_name=hls|start_time=1.400000|duration=2.000000|size=65|bit_rate=260|probe_score=100