You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用libav动态更新音频元数据并在MPV中实时显示?

问题:用libav实现网络电台流的动态元数据更新

目标

我的最终目标是代理网络电台流,并在流传输过程中通过编程添加元数据,让播放音频的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 21:34:54