Custom I/O Protocol in libavformat Using AVIOContext

This article provides a practical guide on implementing a custom I/O layer in FFmpeg’s libavformat using the AVIOContext structure. You will learn how to define custom callback functions for reading, writing, and seeking, allocate the internal memory buffer, initialize the AVIOContext, and bind it to an AVFormatContext to stream media from non-standard sources such as in-memory buffers, encrypted storage, or proprietary network protocols.


1. Define the Custom Callback Functions

AVIOContext relies on function pointers to perform low-level byte operations. Depending on whether you are reading, writing, or both, implement the required callbacks:

  • Read Callback: Reads up to buf_size bytes into buf. Returns the number of bytes read, or an AVERROR code (such as AVERROR_EOF) on failure or completion.
  • Write Callback: Writes up to buf_size bytes from buf. Returns the number of bytes written, or an AVERROR code on failure.
  • Seek Callback: Repositions the stream offset. Returns the resulting offset in bytes from the beginning of the resource, or an AVERROR code.
#include <libavformat/avformat.h>
#include <libavformat/avio.h>

// User data passed via the opaque pointer
struct CustomIOData {
    uint8_t *data;
    size_t size;
    size_t offset;
};

// Custom read function
static int custom_read(void *opaque, uint8_t *buf, int buf_size) {
    struct CustomIOData *io = (struct CustomIOData *)opaque;
    
    if (io->offset >= io->size) {
        return AVERROR_EOF;
    }

    int bytes_to_read = FFMIN(buf_size, io->size - io->offset);
    memcpy(buf, io->data + io->offset, bytes_to_read);
    io->offset += bytes_to_read;

    return bytes_to_read;
}

// Custom seek function
static int64_t custom_seek(void *opaque, int64_t offset, int whence) {
    struct CustomIOData *io = (struct CustomIOData *)opaque;
    int64_t new_offset = -1;

    switch (whence) {
        case SEEK_SET:
            new_offset = offset;
            break;
        case SEEK_CUR:
            new_offset = (int64_t)io->offset + offset;
            break;
        case SEEK_END:
            new_offset = (int64_t)io->size + offset;
            break;
        case AVSEEK_SIZE:
            return io->size; // Return total size without seeking
        default:
            return AVERROR(EINVAL);
    }

    if (new_offset < 0 || (size_t)new_offset > io->size) {
        return AVERROR(EINVAL);
    }

    io->offset = (size_t)new_offset;
    return io->offset;
}

2. Allocate and Initialize the AVIOContext

FFmpeg requires an internal buffer allocated via av_malloc(). Allocate this buffer along with the AVIOContext using avio_alloc_context().

size_t buffer_size = 4096; // Standard buffer size (typically 4 KB to 32 KB)
unsigned char *avio_buffer = (unsigned char *)av_malloc(buffer_size);

if (!avio_buffer) {
    // Handle allocation error
}

struct CustomIOData custom_data = {
    .data = my_source_pointer,
    .size = my_source_size,
    .offset = 0
};

AVIOContext *avio_ctx = avio_alloc_context(
    avio_buffer,            // Internal buffer
    buffer_size,            // Buffer size
    0,                      // Write flag: 0 for read-only, 1 for write
    &custom_data,           // Opaque user data
    &custom_read,           // Read callback
    NULL,                   // Write callback (NULL if read-only)
    &custom_seek            // Seek callback (NULL if non-seekable)
);

if (!avio_ctx) {
    av_freep(&avio_buffer);
    // Handle initialization error
}

3. Attach AVIOContext to AVFormatContext

Allocate an AVFormatContext and assign your custom AVIOContext to its pb field before opening the stream.

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

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

// Open the stream (pass NULL for filename since custom I/O is used)
int ret = avformat_open_input(&fmt_ctx, NULL, NULL, NULL);
if (ret < 0) {
    // Handle open failure
}

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

4. Proper Cleanup and Destruction

When closing the context, handle buffer and context deallocations in the correct sequence. If the AVIOContext was allocated with an internal buffer from av_malloc(), avio_context_free() can release both the context and the internal buffer safely.

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

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