Using custom I/O callbacks with ffmpeg

September 9, 2009
FFmpeg

Image via Wikipedia

Video playback in Conspiracies 2 is based on ffmpeg, which is a very fast and easy to use library. I got better results than using DirectShow with less effort and not to mention my video playback code is going to compile everywhere!

Following this tutorial got me started immediately. After an afternoon of coding I had a decent video player running. This tutorial shows you how to play videos from a file in the filesystem. What I needed was a little bit different though. All the assets of the game (including videos) exist in a “virtual file system” in archives. So I need to provide my own I/O functions for reading them. ffmpeg provides this functionality but it is a little bit tricky to figure out, so I thought I’d write this guide here for future reference. Thanks to the kind people at libav-user mailing list who helped me sort this out!

Implementing the I/O routines.

There are 3 routines you need to impement:

int ReadFunc(void *opaque, uint8_t *buf, int buf_size);

This function must read “buf_size” number of bytes from your file handle (which is the “opaque” parameter) and store the data into “buf”. The return value is the number of bytes actually read from your file handle. If the function fails you must return <0.

int WriteFunc(void *opaque, uint8_t *buf, int buf_size);

This is optional and behaves like the ReadFunc.

int64_t SeekFunc(void *opaque, int64_t offset, int whence) ;

This function is like the fseek() C stdio function. “whence” can be either one of the standard C values (SEEK_SET, SEEK_CUR, SEEK_END) or one more value: AVSEEK_SIZE.

When “whence” has this value, your seek function must not seek but return the size of your file handle in bytes. This is also optional and if you don’t implement it you must return <0.

Otherwise you must return the current position of your stream  in bytes (that is, after the seeking is performed). If the seek has failed you must return <0.

Opening your custom stream.

So when you need to open a custom stream for reading you must do the following:

  • Allocate a buffer for I/O operations with your custom stream. The buffer’s size must be (n + FF_INPUT_BUFFER_PADDING_SIZE), where n is the actual useful buffer size.
  • Allocate a new ByteIOContext object and initialize it by calling init_put_byte():
    int init_put_byte ( ByteIOContext * s,
    unsigned char * buffer,
    int buffer_size,
    int write_flag,
    void * opaque,
    int(*)(void *opaque, uint8_t *buf, int buf_size) read_packet,
    int(*)(void *opaque, uint8_t *buf, int buf_size) write_packet,
    int64_t(*)(void *opaque, int64_t offset, int whence) seek
    )

    s is a pointer to your ByteIOContext object.
    buffer is a pointer to your allocated buffer
    buffer_size is “n” (the useful portion of your allocated buffer)
    write_flag must be zero if your stream does not support writing
    opaque is a pointer to your custom file stream. This is going to be passed to your custom routines.
    read_packet, write_packet and seek are pointers to your custom routines. write_packet is optional (it can be NULL)

  • Use av_open_input_stream() instead of av_open_input_file() to open your stream:
    int av_open_input_stream ( AVFormatContext ** ic_ptr,
    ByteIOContext * pb,
    const char * filename,
    AVInputFormat * fmt,
    AVFormatParameters * ap
    )

    ic_ptr, filename, fmt and ap are the same parameters you use with av_open_input_file().
    pb is your initialized ByteIOContext object.

Closing your custom stream.

  • When you are done with the stream call av_close_input_stream() instead of av_close input_file().
  • Deallocate your ByteIOContext object.
  • Deallocate your buffer.

That’s it! Everything else is the same as with av_open_input_file().

Edit: Actually there is more to it.  av_open_input_file automatically probes the input format. With av_open_input_stream you must do this yourself and pass av_open_input_stream a valid AVInputFormat pointer.  Probing the input format is easy: You just fill a buffer with some bytes from the beginning of your stream and you pass it to av_probe_input_format() which hopefully recognizes the input format and returns the pointer needed. Here is my probing code:

AVProbeData probe_data;
probe_data.filename = filename;
probe_data.buf_size = 4096; // av_open_input_file tries this many times with progressively larger buffers each time, but this must be enough
// allocate memory to read the first bytes
probe_data.buf = (unsigned char *) malloc(probe_data.buf_size);
// read first 4096 bytes into buffer
// …
// probe
AVInputFormat *ret = av_probe_input_format(&probe_data, 1);
// cleanup
free(probe_data.buf);
probe_data.buf = NULL;
return ret;

Follow

Get every new post delivered to your Inbox.