Custom libavformat I/O Protocol Using AVIOContext

This article explains how to implement a custom input/output (I/O) protocol in FFmpeg’s libavformat using the AVIOContext structure. You will learn how to define custom read, write, and seek callback functions, allocate the necessary buffers, and associate your custom I/O context with an AVFormatContext to read media data directly from custom sources like memory, network streams, or proprietary storage systems.

Understanding AVIOContext

By default, FFmpeg’s libavformat handles file and network I/O internally using standard protocols (like file:, http:, or rtmp:). However, when your media data resides in memory, a custom database, or an encrypted stream, you must bypass FFmpeg’s default file-handling layer.

AVIOContext acts as a bridge between FFmpeg’s demuxers/muxers and your custom data source. By providing your own read, write, and seek callback functions, you instruct FFmpeg to request data from your application code instead of the filesystem.

Step-by-Step Implementation

To implement a custom I/O protocol, you must define a state structure, implement the required callback functions, allocate an internal buffer, and instantiate the AVIOContext.

1. Define Your Custom Source State

FFmpeg callbacks pass an opaque void pointer containing your application’s context. Define a structure to keep track of your data source’s state, such as its memory address, size, and current read/write position.

typedef struct {
    uint8_t *ptr;     // Pointer to the start of the memory buffer
    size_t size;      // Total size of the buffer
    size_t pos;       // Current reading position
} CustomIOState;

2. Implement the Callback Functions

FFmpeg requires specific signatures for the read, write, and seek operations.

Read Callback

The read callback copies data from your custom source into the buffer provided by FFmpeg. It must return the number of bytes read, or AVERROR_EOF if the end of the stream is reached.

int read_packet(void *opaque, uint8_t *buf, int buf_size) {
    CustomIOState *state = (CustomIOState *)opaque;
    
    // Calculate remaining bytes
    int remaining = state->size - state->pos;
    if (remaining <= 0) {
        return AVERROR_EOF;
    }

    // Determine how many bytes to read
    int read_len = (buf_size < remaining) ? buf_size : remaining;
    
    // Copy data to FFmpeg's internal buffer
    memcpy(buf, state->ptr + state->pos, read_len);
    state->pos += read_len;

    return read_len;
}

Seek Callback

The seek callback allows FFmpeg to jump to different parts of the stream. It must handle standard standard seek modes (SEEK_SET, SEEK_CUR, SEEK_END) and the special FFmpeg flag AVSEEK_SIZE.

int64_t seek_packet(void *opaque, int64_t offset, int whence) {
    CustomIOState *state = (CustomIOState *)opaque;

    // Handle size query
    if (whence == AVSEEK_SIZE) {
        return state->size;
    }

    int64_t new_pos = state->pos;
    switch (whence) {
        case SEEK_SET:
            new_pos = offset;
            break;
        case SEEK_CUR:
            new_pos += offset;
            break;
        case SEEK_END:
            new_pos = state->size + offset;
            break;
        default:
            return -1;
    }

    if (new_pos < 0 || new_pos > (int64_t)state->size) {
        return -1;
    }

    state->pos = (size_t)new_pos;
    return new_pos;
}

3. Allocate and Initialize AVIOContext

FFmpeg requires an internal buffer to perform buffered I/O. Use av_malloc to allocate this buffer, then call avio_alloc_context to create the AVIOContext.

// Size of the buffer used by AVIOContext (typically 4096 or 32768 bytes)
const size_t io_buffer_size = 4096;
uint8_t *avio_ctx_buffer = av_malloc(io_buffer_size);
if (!avio_ctx_buffer) {
    // Handle allocation error
}

// Instantiate CustomIOState
CustomIOState *state = av_malloc(sizeof(CustomIOState));
state->ptr = my_media_data_pointer;
state->size = my_media_data_size;
state->pos = 0;

// Allocate the AVIOContext
AVIOContext *avio_ctx = avio_alloc_context(
    avio_ctx_buffer,    // Internal buffer
    io_buffer_size,     // Buffer size
    0,                  // Write flag (0 = read-only, 1 = read-write)
    state,              // User-defined opaque pointer passed to callbacks
    &read_packet,       // Read callback
    NULL,               // Write callback (NULL if read-only)
    &seek_packet        // Seek callback
);

if (!avio_ctx) {
    // Handle error
}

4. Bind to AVFormatContext

Once the AVIOContext is initialized, assign it to the pb member of your AVFormatContext before calling avformat_open_input.

AVFormatContext *fmt_ctx = avformat_alloc_context();
if (!fmt_ctx) {
    // Handle error
}

// Bind the custom I/O context
fmt_ctx->pb = avio_ctx;

// Open the input stream (pass NULL for the URL)
if (avformat_open_input(&fmt_ctx, NULL, NULL, NULL) < 0) {
    // Handle error
}

// Retrieve stream information
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
    // Handle error
}

Resource Cleanup

When you are finished decoding or encoding, you must properly deallocate the structures in the correct order to avoid memory leaks.

// Close the input format context
avformat_close_input(&fmt_ctx);

// Free the AVIOContext and its internal buffer
if (avio_ctx) {
    av_freep(&avio_ctx->buffer);
    avio_context_free(&avio_ctx);
}

// Free your custom state structure
av_free(state);