如何用libav动态更新音频元数据并在MPV中实时显示?
目标
我的最终目标是代理网络电台流,并在流传输过程中通过编程添加元数据,让播放音频的MPV播放器能显示并更新这些元数据。
我要添加的元数据主要是歌曲标题,理想情况下还包括艺术家、作曲者和专辑等额外信息。
预期运行方式
我设想的代理程序运行方式有两种:
第一种:
$ curl https://example.com/stream.mp3 | ./proxy_add_metadata | mpv -
第二种:
$ ./proxy_add_metadata & #=> <output> $ mpv <output>
当前尝试与问题
我使用的是FFmpeg 7.1版本。
一开始我尝试在写入帧前不久修改元数据字典,测试了几种不同的容器格式:
av_dict_set(&output_ctx->metadata, "title", "Title 1", 0); /* [...] */ av_interleaved_write_frame(output_ctx, packet);
但发现av_dict_set(&output_ctx->metadata, "title", "Title 1", 0);只有在写入输出头之前操作才有效,后续修改不会生效。
接下来我想尝试在AVPacket的side data中设置元数据,但不清楚哪种容器格式支持这类元数据。
我接受任何输出媒体容器格式或FFmpeg原生的网络流,不确定元数据应该写入媒体容器中的单独流,还是作为输出数据包的side data写入。如果我想要实现的功能无法完成,请说明原因。
当前代码
目前我的代码可以从标准输入读取MP3音频并写入标准输出,元数据更新的无效代码段已被注释:
/* proxy_add_metadata.c */ #include <stdio.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> int main() { int _err; /* MP3 input */ AVFormatContext *input_ctx = avformat_alloc_context(); _err = avformat_open_input(&input_ctx, "pipe:0", NULL, NULL); _err = avformat_find_stream_info(input_ctx, NULL); AVFormatContext *output_ctx; _err = avformat_alloc_output_context2(&output_ctx, NULL, "matroska", "pipe:1"); AVStream *input_stream = input_ctx->streams[0]; AVStream *output_stream = avformat_new_stream(output_ctx, NULL); _err = avcodec_parameters_copy(output_stream->codecpar, input_stream->codecpar); _err = avio_open(&output_ctx->pb, "pipe:1", AVIO_FLAG_WRITE); _err = avformat_write_header(output_ctx, NULL); AVPacket *packet = av_packet_alloc(); /* Set up packet side data. */ /* AVDictionary *packet_side_data_dict; av_dict_set(&packet_side_data_dict, "title", "Title 1", 0); size_t packet_side_data_size = 0; uint8_t *packet_side_data = av_packet_pack_dictionary( packet_side_data_dict, &packet_side_data_size ); av_dict_free(&packet_side_data_dict); */ while (1) { _err = av_read_frame(input_ctx, packet); if (_err < 0) { break; } /* Can metadata updates be made here? */ /* Option 1: Attempt to write metadata to the container. */ /* _err = av_dict_set(&output_ctx->metadata, "title", "Title 1", 0); if (_err < 0) { fprintf(stderr, "error: can't set metadata title in stream: %s\n", av_err2str(_err)); break; } */ /* Option 2: Attempt to write metadata to packet side data. */ /* _err = av_packet_add_side_data( packet, AV_PKT_DATA_METADATA_UPDATE, packet_side_data, packet_side_data_size ); if (_err < 0) { fprintf(stderr, "error: can't add side data to packet: %s\n", av_err2str(_err)); break; } */ AVStream *input_stream = input_ctx->streams[packet->stream_index]; AVStream *output_stream = output_ctx->streams[packet->stream_index]; av_packet_rescale_ts(packet, input_stream->time_base, output_stream->time_base); packet->pos = -1; _err = av_interleaved_write_frame(output_ctx, packet); if (_err < 0) { fprintf(stderr, "error: packet write: %s\n", av_err2str(_err)); break; } } av_write_trailer(output_ctx); av_packet_free_side_data(packet); av_packet_free(&packet); avio_closep(&output_ctx->pb); avformat_free_context(output_ctx); avformat_close_input(&input_ctx); return 0; }
编译命令
cc \ -Wall \ -g \ -I/.../ffmpeg7/include \ -o proxy_add_metadata \ proxy_add_metadata.c \ -L/.../ffmpeg7/lib -lavformat -lavcodec
测试命令
$ < sample.mp3 ./proxy_add_metadata | mpv -
核心思路:选对支持动态元数据的容器
大部分传统容器(比如MP3)的元数据存在头部,写入后无法动态更新。要实现流传输中实时更新元数据,必须选择支持内联元数据更新的容器,推荐:
- Matroska (MKV):支持在流中插入EBML元数据块,FFmpeg和MPV的兼容性最好
- Ogg:支持动态更新Vorbis Comment,适合流媒体场景
- MPEG-TS:FFmpeg原生支持在传输流中插入元数据
实现关键:显式调用avformat_write_metadata
修改output_ctx的metadata后,不能仅靠av_interleaved_write_frame触发更新,必须显式调用avformat_write_metadata将新元数据写入流中。这个操作要放在avformat_write_header之后、av_write_trailer之前执行。
修改后的核心代码片段
在循环中加入元数据更新逻辑(示例为模拟歌曲切换):
while (1) { _err = av_read_frame(input_ctx, packet); if (_err < 0) { break; } // 模拟歌曲切换触发元数据更新(替换为你的实际触发逻辑) static int song_counter = 0; if (song_counter % 100 == 0) { char title_buf[64]; snprintf(title_buf, sizeof(title_buf), "当前歌曲 %d", song_counter / 100 + 1); // 更新容器级元数据 av_dict_set(&output_ctx->metadata, "title", title_buf, 0); av_dict_set(&output_ctx->metadata, "artist", "测试歌手", 0); av_dict_set(&output_ctx->metadata, "album", "测试专辑", 0); // 显式写入元数据到流 _err = avformat_write_metadata(output_ctx, NULL); if (_err < 0) { fprintf(stderr, "写入元数据失败: %s\n", av_err2str(_err)); break; } } song_counter++; // 原有数据包处理逻辑 AVStream *input_stream = input_ctx->streams[packet->stream_index]; AVStream *output_stream = output_ctx->streams[packet->stream_index]; av_packet_rescale_ts(packet, input_stream->time_base, output_stream->time_base); packet->pos = -1; _err = av_interleaved_write_frame(output_ctx, packet); if (_err < 0) { fprintf(stderr, "写入数据包失败: %s\n", av_err2str(_err)); break; } }
关于Packet Side Data的说明
AV_PKT_DATA_METADATA_UPDATE这类side data仅少数容器支持,且MPV对其解析的稳定性不如容器级元数据,因此优先选择容器级动态写入方案。
验证方式
运行代理程序后,MPV会自动实时刷新显示的元数据:
curl https://example.com/stream.mp3 | ./proxy_add_metadata | mpv -
内容的提问来源于stack exchange,提问作者Teddy

