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);