Ffmpeg · 2025年5月13日

Ffmpeg源码开发之从内存中读取音频数据

开发环境

C/C++ 导入Ffmpeg(Version 6.1.1),在程序中调用相关API,非命令行形式调用

需求

当音频数据来源于内存中时,例如源音频是存储在加密文件中,需要读取到内存中解密,然后再用Ffmpeg处理,这样就需要将内存中的数据作为Ffmpeg的数据源。


typedef struct _BufContext {
    uint8_t* buf;
    uint32_t size;
    uint32_t pos;

    _BufContext() {
        buf = NULL;
        size = 0;
        pos = 0;
    }
} BufContext_TypeDef;

static int read_packet(void *arg, uint8_t *buf, int buf_size)
{
    AudioEditorBufContext_TypeDef* fp = (AudioEditorBufContext_TypeDef*) arg;
    int64_t remaining = fp->size - fp->pos;
    if (remaining <= 0)
    {
        return AVERROR_EOF;  // 数据已读完
    }

    // 本次读取的字节数(不超过请求的 buf_size)
    int read_size = (buf_size < remaining) ? buf_size : (int)remaining;
    memcpy(buf, fp->buf + fp->pos, read_size);
    fp->pos += read_size;  // 更新位置

    return read_size;
}

static int64_t seek_packet(void *arg, int64_t offset, int whence) {
    int64_t new_pos;
    AudioEditorBufContext_TypeDef* fp = (AudioEditorBufContext_TypeDef*) arg;

    switch (whence)
    {
    case SEEK_SET:  // 从开头定位
        new_pos = offset;
        break;
    case SEEK_CUR:  // 从当前位置定位
        new_pos = fp->pos + offset;
        break;
    case SEEK_END:  // 从末尾定位
        new_pos = fp->size + offset;
        break;
    case AVSEEK_SIZE:  // 获取总大小
        return fp->size;
    default:
        return -1;  // 无效的 whence
    }

    // 检查新位置是否合法
    if (new_pos < 0 || new_pos > fp->size)
    {
        return -1;
    }

    fp->pos = new_pos;
    return new_pos;
}

int8_t audio_load_from_encode()
{
    uint8_t * buf = NULL;
    /**
     * 因为要从内存中读取数据, avio_context 为手动创建, 故 ffmpeg 不会自动申请缓存区, 所以需要手动管理内存
     * buf 作为 ffmpeg 的缓存区, 缓存区长度要足够大(建议64KB起)
     * 否则会出现 关键头部信息被截断、频繁触发 read_packet 回调 等问题
     */
    uint32_t buf_size = 32768;

    AVIOContext* avio_context = NULL;
    AVFormatContext* format_context = NULL;
    BufContext_TypeDef buf_ctx;

    int ret = 0;
    FILE* fin = NULL;

    /**
     * 将文件加载到内存中
     */
    fopen_s(&fin, "file_path", "rb");
    fseek(fin, 0, SEEK_END);
    buf_ctx.size = ftell(fin);
    fseek(fin, 0, SEEK_SET);
    buf_ctx.buf = (uint8_t*)malloc(buf_ctx.size);
    fread(buf_ctx.buf, sizeof(uint8_t), buf_ctx.size, fin);
    fclose(fin);
    buf_ctx.pos = 0;

    // 为 buf 缓存区申请内存, 这里切记要使用 av_malloc 而非标准函数 malloc, 两者内存对齐方式不一样
    buf = (uint8_t *)av_malloc(buf_size);
    if (buf == NULL)
    {
        ret = -1;
        goto err;
    }

    /**
     * 创建一个 AVIOContext, 使其从内存缓冲区读取数据
     * buf 为 ffmpeg 的缓存区, buf_size 是设置的缓存区大小, 在上文中已经讲过
     * 0 表示 缓存区只读
     * &buf_ctx 为传入的自定义数据, 在 Read 和 Seek 回调函数中作为第一个参数传入
     * read_packet 为手动实现的函数, 主要实现将内存中的数据搬移至 ffmpeg 缓存区
     * seek_packet 为手动实现的函数, 主要是实现 ffmpeg 可以访问内存中任意地址的数据
     * 这里一定要实现 read 回调 和 seek 回调
     * ffmpeg 需要具有随机改变地址偏移量, 访问内存的能力, 因为各种信息会存储在不同的位置
     * 如果不实现 seek 回调, 那么 ffmpeg 就只能顺序读取数据, 这就造成从数据中解析音频信息缺失的情况
     */
    avio_context = avio_alloc_context((unsigned char *)buf, buf_size, 0, &buf_ctx, read_packet, NULL, seek_packet);
    if (!avio_context)
    {
        av_log(NULL, AV_LOG_ERROR, "avio_alloc_context failed\n");
        ret = -1;
        goto err;
    }

    /**
     * 创建 AVFormatContext 并关联 AVIOContext
     * 这里将 format_context.pb 设置为手动创建的 avio_context 后, 在 avformat_open_input 中, 就不会从文件中读取
     * 即 传入的第二个参数, 路径将失效, 而是将 avio_context 作为数据源.
     */
    format_context = avformat_alloc_context();
    if (!format_context)
    {
        av_log(NULL, AV_LOG_ERROR, "avformat_alloc_context failed\n");
        av_free(avio_context);
        return 1;
    }
    format_context->pb = avio_context;
    ret = avformat_open_input(&format_context, NULL, NULL, NULL);
    if (ret < 0)
    {
        avformat_free_context(format_context);
        return 1;
    }

    // 获取流信息
    ret = avformat_find_stream_info(format_context, NULL);
    if (ret < 0)
    {
        avformat_close_input(&format_context);
        return 1;
    }
    goto end;
err:

end:
    if (format_context) avformat_close_input(&format_context);
    if (format_context) avformat_free_context(format_context);
    if (avio_context) avio_context_free(&avio_context);
    if (buf_ctx.buf) free(buf_ctx.buf);

    /**
     * 释放 avio_context 还有另一种方法, 但是风险会大,
     * 仅用于在某些版本的 ffmpeg 中会没有 avio_context_free 的情况
     * avio_context 地址可能会发生变化,包括其关联的 buffer 地址也会变化
     * 因此,avformat_close_input 在释放时,可能无法释放这部分内存
     * 所以最好还是手动释放内存确保内存不会泄露
     */
//    if (avio_context)
//    {
//        av_freep(&avio_context->buffer);
//        av_free(avio_context);
//    }
    return 0;
}