Feed In-Memory Buffers to FFmpeg libavformat

This article demonstrates how to demux and decode multimedia data residing in a RAM buffer using FFmpeg's libavformat library. By setting up a custom AVIOContext with a user-defined read callback, you can feed in-memory data directly into an AVFormatContext without writing temporary files to disk, drastically reducing I/O latency.

1. Define the Buffer State

To track your position within the in-memory buffer, define a custom structure to store the data pointer, the total size, and the current read position.

#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libavutil/avutil.h>
#include <string.h>

typedef struct BufferData {
    const uint8_t *ptr;
    size_t size;
    size_t offset;
} BufferData;

2. Implement the Custom Read Callback

FFmpeg expects a callback matching the signature int (*read_packet)(void *opaque, uint8_t *buf, int buf_size). This function calculates the remaining data, copies the requested number of bytes into FFmpeg's internal buffer, updates the offset, and returns the number of bytes read. If no bytes remain, return AVERROR_EOF.

static int read_packet(void *opaque, uint8_t *buf, int buf_size) {
    BufferData *bd = (BufferData *)opaque;
    size_t remaining = bd->size - bd->offset;

    if (remaining == 0) {
        return AVERROR_EOF;
    }

    int to_copy = FFMIN((size_t)buf_size, remaining);
    memcpy(buf, bd->ptr + bd->offset, to_copy);
    bd->offset += to_copy;

    return to_copy;
}

If your media format requires seeking (such as MP4 with the moov atom at the end), you can optionally implement a seek callback:

static int64_t seek_packet(void *opaque, int64_t offset, int whence) {
    BufferData *bd = (BufferData *)opaque;

    switch (whence) {
        case SEEK_SET:
            bd->offset = offset;
            break;
        case SEEK_CUR:
            bd->offset += offset;
            break;
        case SEEK_END:
            bd->offset = bd->size + offset;
            break;
        case AVSEEK_SIZE:
            return bd->size;
        default:
            return AVERROR(EINVAL);
    }
    return bd->offset;
}

3. Create and Attach the AVIOContext

Allocate an internal buffer for AVIOContext using av_malloc(). Pass this buffer along with your callback functions and state structure to avio_alloc_context().

int open_buffer_context(const uint8_t *memory_data, size_t data_size, AVFormatContext **out_fmt_ctx) {
    BufferData bd = {
        .ptr = memory_data,
        .size = data_size,
        .offset = 0
    };

    size_t avio_buf_size = 4096;
    uint8_t *avio_buf = (uint8_t *)av_malloc(avio_buf_size);
    if (!avio_buf) {
        return AVERROR(ENOMEM);
    }

    AVIOContext *avio_ctx = avio_alloc_context(
        avio_buf,
        avio_buf_size,
        0,              // 0 for read-only, 1 for write
        &bd,            // opaque user data
        &read_packet,   // read callback
        NULL,           // write callback (NULL for read-only)
        &seek_packet    // seek callback (or NULL if non-seekable)
    );

    if (!avio_ctx) {
        av_free(avio_buf);
        return AVERROR(ENOMEM);
    }

    AVFormatContext *fmt_ctx = avformat_alloc_context();
    if (!fmt_ctx) {
        avio_context_free(&avio_ctx);
        return AVERROR(ENOMEM);
    }

    fmt_ctx->pb = avio_ctx;
    fmt_ctx->flags |= AVFMT_FLAG_CUSTOM_IO;

    // Passing NULL as the filename tells FFmpeg to use fmt_ctx->pb
    int ret = avformat_open_input(&fmt_ctx, NULL, NULL, NULL);
    if (ret < 0) {
        avio_context_free(&avio_ctx);
        avformat_free_context(fmt_ctx);
        return ret;
    }

    *out_fmt_ctx = fmt_ctx;
    return 0;
}

4. Proper Resource Cleanup

When you are finished processing the media packets, clean up the format context and the custom I/O context. When using avio_alloc_context(), FFmpeg manages the memory allocated to avio_buf, so you must free the context using avio_context_free().

void cleanup_buffer_context(AVFormatContext *fmt_ctx) {
    if (!fmt_ctx) return;

    AVIOContext *avio_ctx = fmt_ctx->pb;

    avformat_close_input(&fmt_ctx);

    if (avio_ctx) {
        av_freep(&avio_ctx->buffer);
        avio_context_free(&avio_ctx);
    }
}