目录
2.3.1.3 retry_transfer_wrapper()
1 AVIOContext结构体
1.1 功能描述
FFMPEG中有众多的结构体,这些结构体具有一定的层级,可以类比网络分层的概念。本文将要分析的AVIOContext结构体处于FFMPEG音视频处理的IO层(也有称为协议层,不过个人认为叫做IO层更合适),如下图所示。
FFMPEG中的I/O层定义在avformat库中,头文件为avio.h,该头文件包含了IO相关的结构体定义以及IO层的Public APIs,大体上分为目录相关结构体以及APIs,文件相关结构体以及APIs。AVIOContext结构体就在这个头文件中被定义,是文件相关的结构体。
AVIOContext结构体通过持有URLContext,URLContext持有URLProtocol,而URLProtocol持有访问特定协议的所有IO操作,从而AVIOContext获取了根据URL识别并访问资源的能力。
AVIOContext同时在内部提供了一个缓冲区,提供了一个对资源的带缓冲区的访问方式;同时,AVIOContext提供了direct字段用于表示是否使用该缓冲区,direct设置为1时,表示不使用AVIOContext缓冲区,而直接调用底层的协议访问函数,从而提供对资源不带缓冲的访问方式,此时,后续将要介绍的IO层 Public APIs的avio_read()读,avio_write()写函数将不使用AVIOContext缓冲,而avio_seek()跳转函数直接调用更底层的seek函数。
总之:FFMPEG IO层的目标就是提供一个统一的,类似于C语言 "标准IO库" 的接口来访问音视频资源。结构体AVIOContext可以类比 标准IO库中的 文件指针对象FILE。而操作AVIOContext的函数avio_open(),avio_read(),avio_write(),avio_seek(),avio_tell(),avio_flush(),avio_close()等在概念上与标准IO库中的fopen(),fread(),fwrite(),fseek(),ftell(),fflush(),fclose()等是一一对应的。
1.2 结构体成员详解
/**
* Bytestream IO Context.
* New fields can be added to the end with minor version bumps.
* Removal, reordering and changes to existing fields require a major
* version bump.
* sizeof(AVIOContext) must not be used outside libav*.
*
* @note None of the function pointers in AVIOContext should be called
* directly, they should only be set by the client application
* when implementing custom I/O. Normally these are set to the
* function pointers specified in avio_alloc_context()
*/
typedef struct AVIOContext {
/**
* A class for private options.
*
* If this AVIOContext is created by avio_open2(), av_class is set and
* passes the options down to protocols.
*
* If this AVIOContext is manually allocated, then av_class may be set by
* the caller.
*
* warning -- this field can be NULL, be sure to not pass this AVIOContext
* to any av_opt_* functions in that case.
*/
const AVClass *av_class;
/*
* The following shows the relationship between buffer, buf_ptr,
* buf_ptr_max, buf_end, buf_size, and pos, when reading and when writing
* (since AVIOContext is used for both):
*
**********************************************************************************
* READING
**********************************************************************************
*
* | buffer_size |
* |---------------------------------------|
* | |
*
* buffer buf_ptr buf_end
* +---------------+-----------------------+
* |/ / / / / / / /|/ / / / / / /| |
* read buffer: |/ / consumed / | to be read /| |
* |/ / / / / / / /|/ / / / / / /| |
* +---------------+-----------------------+
*
* pos
* +-------------------------------------------+-----------------+
* input file: | | |
* +-------------------------------------------+-----------------+
*
*
**********************************************************************************
* WRITING
**********************************************************************************
*
* | buffer_size |
* |--------------------------------------|
* | |
*
* buf_ptr_max
* buffer (buf_ptr) buf_end
* +-----------------------+--------------+
* |/ / / / / / / / / / / /| |
* write buffer: | / / to be flushed / / | |
* |/ / / / / / / / / / / /| |
* +-----------------------+--------------+
* buf_ptr can be in this
* due to a backward seek
*
* pos
* +-------------+----------------------------------------------+
* output file: | | |
* +-------------+----------------------------------------------+
*
*/
unsigned char *buffer; /**< Start of the buffer. */
int buffer_size; /**< Maximum buffer size */
unsigned char *buf_ptr; /**< Current position in the buffer */
unsigned char *buf_end; /**< End of the data, may be less than
buffer+buffer_size if the read function returned
less data than requested, e.g. for streams where
no more data has been received yet. */
void *opaque; /**< A private pointer, passed to the read/write/seek/...
functions. */
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size);
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size);
int64_t (*seek)(void *opaque, int64_t offset, int whence);
int64_t pos; /**< position in the file of the current buffer */
int eof_reached; /**< true if eof reached */
int write_flag; /**< true if open for writing */
int max_packet_size;
unsigned long checksum;
unsigned char *checksum_ptr;
unsigned long (*update_checksum)(unsigned long checksum, const uint8_t *buf, unsigned int size);
int error; /**< contains the error code or 0 if no error happened */
/**
* Pause or resume playback for network streaming protocols - e.g. MMS.
*/
int (*read_pause)(void *opaque, int pause);
/**
* Seek to a given timestamp in stream with the specified stream_index.
* Needed for some network streaming protocols which don't support seeking
* to byte position.
*/
int64_t (*read_seek)(void *opaque, int stream_index,
int64_t timestamp, int flags);
/**
* A combination of AVIO_SEEKABLE_ flags or 0 when the stream is not seekable.
*/
int seekable;
/**
* max filesize, used to limit allocations
* This field is internal to libavformat and access from outside is not allowed.
*/
int64_t maxsize;
/**
* avio_read and avio_write should if possible be satisfied directly
* instead of going through a buffer, and avio_seek will always
* call the underlying seek function directly.
*/
int direct;
/**
* Bytes read statistic
* This field is internal to libavformat and access from outside is not allowed.
*/
int64_t bytes_read;
/**
* seek statistic
* This field is internal to libavformat and access from outside is not allowed.
*/
int seek_count;
/**
* writeout statistic
* This field is internal to libavformat and access from outside is not allowed.
*/
int writeout_count;
/**
* Original buffer size
* used internally after probing and ensure seekback to reset the buffer size
* This field is internal to libavformat and access from outside is not allowed.
*/
int orig_buffer_size;
/**
* Threshold to favor readahead over seek.
* This is current internal only, do not use from outside.
*/
int short_seek_threshold;
/**
* ',' separated list of allowed protocols.
*/
const char *protocol_whitelist;
/**
* ',' separated list of disallowed protocols.
*/
const char *protocol_blacklist;
/**
* A callback that is used instead of write_packet.
*/
int (*write_data_type)(void *opaque, uint8_t *buf, int buf_size,
enum AVIODataMarkerType type, int64_t time);
/**
* If set, don't call write_data_type separately for AVIO_DATA_MARKER_BOUNDARY_POINT,
* but ignore them and treat them as AVIO_DATA_MARKER_UNKNOWN (to avoid needlessly
* small chunks of data returned from the callback).
*/
int ignore_boundary_point;
/**
* Internal, not meant to be used from outside of AVIOContext.
*/
enum AVIODataMarkerType current_type;
int64_t last_time;
/**
* A callback that is used instead of short_seek_threshold.
* This is current internal only, do not use from outside.
*/
int (*short_seek_get)(void *opaque);
int64_t written;
/**
* Maximum reached position before a backward seek in the write buffer,
* used keeping track of already written data for a later flush.
*/
unsigned char *buf_ptr_max;
/**
* Try to buffer at least this amount of data before flushing it
*/
int min_packet_size;
} AVIOContext;
2 IO层 APIs
FFMPEG在libavformat/avio.h头文件中提供了一系列的IO层操作API,主要分为两个大的类别,操作目录以及操作文件。这些API函数均以avio_*开头,本文将列举目录和文件的操作相关的APIs,但只分析文件相关的源码。列举如下:
1 目录相关APIs
- 打开目录:
int avio_open_dir(AVIODirContext **s, const char *url, AVDictionary **options); - 读取目录:
int avio_read_dir(AVIODirContext *s, AVIODirEntry **next); - 关闭目录:
int avio_close_dir(AVIODirContext **s);
2 文件相关APIs
- 打开文件:
int avio_open(AVIOContext **s, const char *url, int flags);
int avio_open2(AVIOContext **s, const char *url, int flags,const AVIOInterruptCB *int_cb, AVDictionary **options); - 读文件:
1)按数据块读:
int avio_read(AVIOContext *s, unsigned char *buf, int size);
int avio_read_partial(AVIOContext *s, unsigned char *buf, int size);
2)按特定数据类型读:
int avio_r8 (AVIOContext *s);
...此处省略十几行...
int avio_get_str16be(AVIOContext *pb, int maxlen, char *buf, int buflen); - 写文件:
1)按数据块写:
void avio_write(AVIOContext *s, const unsigned char *buf, int size);
2)按特定数据类型写:
void avio_w8(AVIOContext *s, int b);
...此处省略十几行...
int avio_put_str16be(AVIOContext *s, const char *str);
void avio_write_marker(AVIOContext *s, int64_t time, enum AVIODataMarkerType type); - 文件尾:
int avio_feof(AVIOContext *s); - 冲刷缓冲区:
void avio_flush(AVIOContext *s); - seek相关:
int64_t avio_seek(AVIOContext *s, int64_t offset, int whence);
int64_t avio_skip(AVIOContext *s, int64_t offset);
static av_always_inline int64_t avio_tell(AVIOContext *s);
int64_t avio_size(AVIOContext *s); - 关闭文件:
int avio_close(AVIOContext *s);
int avio_closep(AVIOContext **s);
上述APIs族提供了一套类似于C语言标准I/O库的函数接口,区别在于标准I/O库操作的表征文件的结构体对象是FILE,而FFMPEG中表征文件的结构体对象是AVIOContext,因此,可以将二者类比理解。
2.1 avio_open()
avio_open()声明:创建和初始化一个AVIOContext对象来访问url所指代的资源,当该资源以r+b的方式被打开时,AVIOContext仅可用来写操作。
/**
* Create and initialize a AVIOContext for accessing the
* resource indicated by url.
* @note When the resource indicated by url has been opened in
* read+write mode, the AVIOContext can be used only for writing.
*
* @param s Used to return the pointer to the created AVIOContext.
* In case of failure the pointed to value is set to NULL.
* @param url resource to access
* @param flags flags which control how the resource indicated by url
* is to be opened
* @return >= 0 in case of success, a negative value corresponding to an
* AVERROR code in case of failure
*/
int avio_open(AVIOContext **s, const char *url, int flags);
注意flags用来控制访问的资源如何被打开,这个flags可取值AVIO_FLAG_*:
1. 前3个很好理解仅读,仅写,读写
2. AVIO_FLAG_NONBLOCK:表示AVIOContext上的io操作是否是阻塞的,会影响到读写操作是否阻塞,但不会一想到打开文件和连接网络的操作,创建协议连接肯定是阻塞的。而一些协议本身就是非阻塞形式的,那么该表示会被忽略
3. AVIO_FLAG_DIRECT:表示是否使用AVIOContext内部缓冲区,如果该标志被置位,表示读写操作都不同该内部缓冲区,seek操作也直接调用更底层的函数。
#define AVIO_FLAG_READ 1 /**< read-only */
#define AVIO_FLAG_WRITE 2 /**< write-only */
#define AVIO_FLAG_READ_WRITE (AVIO_FLAG_READ|AVIO_FLAG_WRITE) /**< read-write pseudo flag */
/**
* Use non-blocking mode.
* If this flag is set, operations on the context will return
* AVERROR(EAGAIN) if they can not be performed immediately.
* If this flag is not set, operations on the context will never return
* AVERROR(EAGAIN).
* Note that this flag does not affect the opening/connecting of the
* context. Connecting a protocol will always block if necessary (e.g. on
* network protocols) but never hang (e.g. on busy devices).
* Warning: non-blocking protocols is work-in-progress; this flag may be
* silently ignored.
*/
#define AVIO_FLAG_NONBLOCK 8
/**
* Use direct mode.
* avio_read and avio_write should if possible be satisfied directly
* instead of going through a buffer, and avio_seek will always
* call the underlying seek function directly.
*/
#define AVIO_FLAG_DIRECT 0x8000
avio_open()源码:直接调用avio_open2()来实现功能。
int avio_open(AVIOContext **s, const char *filename, int flags)
{
return avio_open2(s, filename, flags, NULL, NULL);
}
2.2 avio_open2()
avio_open2()声明:相比avio_open()多了一个协议层的终端回调函数AVIOInterruptCB和协议层私有选项,当使用avio_open()时,这两个参数传入为空。
/**
* Create and initialize a AVIOContext for accessing the
* resource indicated by url.
* @note When the resource indicated by url has been opened in
* read+write mode, the AVIOContext can be used only for writing.
*
* @param s Used to return the pointer to the created AVIOContext.
* In case of failure the pointed to value is set to NULL.
* @param url resource to access
* @param flags flags which control how the resource indicated by url
* is to be opened
* @param int_cb an interrupt callback to be used at the protocols level
* @param options A dictionary filled with protocol-private options. On return
* this parameter will be destroyed and replaced with a dict containing options
* that were not found. May be NULL.
* @return >= 0 in case of success, a negative value corresponding to an
* AVERROR code in case of failure
*/
int avio_open2(AVIOContext **s, const char *url, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options);
avio_open2()源码: 该函数直接调用ffio_opem_whitelist()来实现功能,该函数已经在FFRMPEG4.1源码分析之 io_open_default() && io_close_default()中进行了分析。
int avio_open2(AVIOContext **s, const char *filename, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options)
{
return ffio_open_whitelist(s, filename, flags, int_cb, options, NULL, NULL);
}
2.3 avio_read()
avio_read()声明:
从AVIOContext中读取指定字节数到buf中,返回读取的字节数或者是错误码
/**
* Read size bytes from AVIOContext into buf.
* @return number of bytes read or AVERROR
*/
int avio_read(AVIOContext *s, unsigned char *buf, int size);
avio_read()源码:在进行该函数源码分析之前,需要了解下AVIOContext内部缓冲区各字段是代表什么意思,因此在本函数源码之前再次将该图粘贴出来,对照分析
**********************************************************************************
* READING
**********************************************************************************
*
* | buffer_size |
* |---------------------------------------|
* | |
*
* buffer buf_ptr buf_end
* +---------------+-----------------------+
* |/ / / / / / / /|/ / / / / / /| |
* read buffer: |/ / consumed / | to be read /| |
* |/ / / / / / / /|/ / / / / / /| |
* +---------------+-----------------------+
*
* pos
* +-------------------------------------------+-----------------+
* input file: | | |
* +-------------------------------------------+-----------------+
*
*
**********************************************************************************
int avio_read(AVIOContext *s, unsigned char *buf, int size)
{
int len, size1;
// size1记录要读取的字节数
size1 = size;
// size记录还需要读取的字节数,当还有数据需要读取时,那么就继续该循环
while (size > 0) {
// len记录本次可读取的字节数或者还需要读取的字节数
// 如上图所示s->buf_end - s->buf_ptr表示AVIOContext缓冲中还有多少未被消费的字节数
len = FFMIN(s->buf_end - s->buf_ptr, size);
// AVIOContext缓冲区可读取字节数为0 或者写标志置位(为啥判断写标志置位?)
if (len == 0 || s->write_flag) {
// 如果direct标志置位,或者要读取数据大小比AVIOContext缓冲区大小要大
// 并且不存在更新数据校验和的函数,那么直接将数据读取到目标缓冲区中
// 为啥要判断数据校验和更新函数
if((s->direct || size > s->buffer_size) && !s->update_checksum) {
// 绕过AVIOContext缓冲区,直接将数据读取进入到目标缓冲区中
// bypass the buffer and read data directly into buf
len = read_packet_wrapper(s, buf, size);
// 读取数据长度匹配文件尾,则设置标志位
if (len == AVERROR_EOF) {
/* do not modify buffer if EOF reached so that a seek back can
be done without rereading data */
s->eof_reached = 1;
break;
// 读取数据长度小于0,出错,eof标志位置位
// 并且错误标志置为返回值
} else if (len < 0) {
s->eof_reached = 1;
s->error= len;
break;
// 读取数据正确
} else {
// 重要!重要!虽然没有把数据读取到内部缓冲区,但是从文件中读取数据
// 是实实在在的,因此pos,bytes_read这两个字段是必须要修改的
// AVIOContext.pos表征文件中的偏移 +=len
// AVIOContext.bytes_read已从文件中读取字节数 +=len
s->pos += len;
s->bytes_read += len;
// 还需读取数据 -=len
// 修改目标buf偏移
size -= len;
buf += len;
// 重置内部buffer的指针,有疑问的是如果s->write_flag被设置的情况下
// 重置缓冲区不会有问题?那么已写入缓冲区不会失效嘛?
// reset the buffer
s->buf_ptr = s->buffer;
s->buf_end = s->buffer/* + len*/;
}
// 读取数据填充AVIOContext内部buffer
} else {
fill_buffer(s);
len = s->buf_end - s->buf_ptr;
// 如果没有读取到数据,则退出数据读取循环
// 此处退出循环意味着整个函数读取到的字节数可能会小于期望的字节数
if (len == 0)
break;
}
// AVIOContext缓冲区可读取len字节
} else {
// 从该内部缓冲区读取len个字节到目标缓冲区
memcpy(buf, s->buf_ptr, len);
// 修改内部缓冲区指针以及目标缓冲区指针
buf += len;
s->buf_ptr += len;
// 修改size为剩余需要读取的字节数
size -= len;
}
}
// 如果循环退出,需要读取的字节数与还需要读取的字节数相同,即没有读到数据
// 有3种情况需要考虑,均发生在第一个循环读取的时候内部缓冲区为0的情况
// 1. read_packet_wrapper()返回AVERROR_EOF;
// 2. read_packet_wrapper()返回负值;
// 3. fill_buffer()也未读取到数据到内部缓冲区的情况;
if (size1 == size) {
if (s->error) return s->error;
if (avio_feof(s)) return AVERROR_EOF;
}
// 返回实际读取到的字节数
return size1 - size;
}
函数的实现流程此处就不再赘述,详细分析见源码及其注释即可,如下指出源码中可能有疑问的几个点来帮助更深的理解:
1. 为什么在该读函数中要判断写标志write_flag?其实很好理解,AVIOContext中的缓冲区是读写复用的,但是不能同时使用,AVIOContext.write_flag为1,表示AVIOContext内部缓冲区当前是为写数据所准备的,内部的缓冲区相关字段的含义与读数据时有很大的不同含义,因此无法利用该缓冲区来进行读取,应该绕过该缓冲区进行读的工作,说明ffmpeg中代码还是比较完备的,考虑了各种情况。
2. 在AVIOContext内部缓冲区已无数据可读,并且AVIOContext.direct标志位置或者需要读取的数据比内部缓冲区要大时,此时,将绕开AVIOContext内部缓冲区,直接读取数据到目标缓冲区。但此时多了一个判断条件,那就是AVIOContext.update_checksum这个函数指针必须为空,为什么必须这个指针为空才能直接读取数据到目标缓冲区呢?
2.1)首先,这个函数是用来更新校验和,即AVIOContext.checksum这个参数,校验和是用来验证读取数据是否正确,传输过程中是否被更改,TCP/IP机制中就有CRC64算法进行数据正确性校验,此处作用类似。
2.2)其次,AVIOContext.update_checksum这个函数指针在创建并初始化AVIOContext过程中一般是被置位NULL,ffio_init_checksum()方法用来给该指针赋值,全局搜索ffmpeg源码该函数被引用的地方,截图如下所示:有mp3,nut,ogg,tak,tta这几种格式才需要进行数据校验,并且可以看到校验方法中都带有crc几个字符,都是属于“循环冗余校验”的变种。
2.3)最后,回归到前头的问题,为啥直接读取数据到目标缓冲区要判断这个函数是否为空呢? 正常读取到AVIOContext缓冲区时,该函数可以不断更新校验和,以保证读取到缓冲区中的数据是正确的,如果直接读取到外部目标缓冲区时缺少了数据验证,这样很难保证上述几种格式读到的数据是正确的。因此,如果是对于需要进行数据校验的格式,还是老老实实使用AVIOContext内部缓冲区吧。
3. 本函数内调用了三个函数,一个是直接读取数据到用户提供的目的缓冲区的函数read_packet_wrapper();读取数据填充AVIOContext内部缓冲区的函数fill_buffer();以及IO层判断是否读取到文件尾的Public API函数avio_feof()。后续先介绍前两个函数。
2.3.1 read_packet_wrapper()
read_packet_wrapper()源码:
static int read_packet_wrapper(AVIOContext *s, uint8_t *buf, int size)
{
int ret;
if (!s->read_packet)
return AVERROR(EINVAL);
ret = s->read_packet(s->opaque, buf, size);
#if FF_API_OLD_AVIO_EOF_0
if (!ret && !s->max_packet_size) {
av_log(NULL, AV_LOG_WARNING, "Invalid return value 0 for stream protocol\n");
ret = AVERROR_EOF;
}
#else
av_assert2(ret || s->max_packet_size);
#endif
return ret;
}
该函数是一个静态函数,主要是调用了s->read_packet函数进行数据的读取,由FFRMPEG4.1源码分析之 io_open_default() && io_close_default()一文分析可以,AVIOContext.read_packet实际上是在使用avio_alloc_context()函数调用时传入的io_read_packet()方法,传入的s->opaque是AVIOInternal对象,该对象持有了URLContext对象的指针。io_read_packet()方法具体见下源码。
2.3.1.1 io_read_packet()
io_read_packet()源码:
static int io_read_packet(void *opaque, uint8_t *buf, int buf_size)
{
AVIOInternal *internal = opaque;
return ffurl_read(internal->h, buf, buf_size);
}
该函数内部也比较简单,第一个参数就是AVIOInternal对象,然后调用ffurl_read(),该函数见下源码
2.3.1.2 ffurl_read()
ffurl_read()源码:
int ffurl_read(URLContext *h, unsigned char *buf, int size)
{
if (!(h->flags & AVIO_FLAG_READ))
return AVERROR(EIO);
return retry_transfer_wrapper(h, buf, size, 1, h->prot->url_read);
}
该函数对标志进行了判断是否为读,然后调用retry_transfer_wrapper()函数进行最终的读操作,传入h->prot->url_read指针就是协议相关的读取函数了,即URLProtocol对象的url_read()方法。
2.3.1.3 retry_transfer_wrapper()
retry_transfer_wrapper()源码:
static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,
int size, int size_min,
int (*transfer_func)(URLContext *h,
uint8_t *buf,
int size))
{
int ret, len;
int fast_retries = 5;
int64_t wait_since = 0;
len = 0;
while (len < size_min) {
// 调用回调函数,若回调函数返回非零值,则函数返回
if (ff_check_interrupt(&h->interrupt_callback))
return AVERROR_EXIT;
// 调用传入的url_read函数读取数据
ret = transfer_func(h, buf + len, size - len);
// 返回中断则继续
if (ret == AVERROR(EINTR))
continue;
// 如果是非阻塞的模式,那么直接返回读取的数据即可
if (h->flags & AVIO_FLAG_NONBLOCK)
return ret;
// 如果是阻塞模式,读操作返回值又是AVERROR(EAGAIN)
// 可以进行下一次尝试读操作
if (ret == AVERROR(EAGAIN)) {
ret = 0;
// 快速尝试读5次,即本线程不休眠1000微妙
if (fast_retries) {
fast_retries--;
// 快速尝试均失败,
} else {
// 如果协议定义了io操作的超时时间,那么
// 记录下当前相对时间,下次进入时,该相对时间必须
// 比上次记录的相对时间间隔长rw_timeout以上
// 否则是io错误
if (h->rw_timeout) {
if (!wait_since)
wait_since = av_gettime_relative();
else if (av_gettime_relative() > wait_since + h->rw_timeout)
return AVERROR(EIO);
}
av_usleep(1000);
}
// 如果读到了文件尾
} else if (ret == AVERROR_EOF)
return (len > 0) ? len : AVERROR_EOF;
// 返回的是其他错误,则直接从本函数返回错误值
else if (ret < 0)
return ret;
// 如果已经读到数据,那么快速重试的策略
// 重试次数修订为至少有两次
if (ret) {
fast_retries = FFMAX(fast_retries, 2);
wait_since = 0;
}
len += ret;
}
return len;
}
该函数如其名是一个包装方法,可以处理读操作,也可以处理写操作,完全取决于最后一个参数提供的函数指针,该函数指针可以传入URLProtocol的url_read(),url_write()。当传入的是URLProtocol的url_read()方法,针对具体的协议比如file协议来说,那么读取数据的函数就是file协议相关的读操作file_read()。上文源码注释中以读操作为例进行注释,大致的读取流程如下:
-> 先调用外部设置AVIOInterruptCB回调函数检查是否需要中断阻塞操作;
-> 调用真实的IO操作函数读取数据,后续根据读操作的返回值进行分类处理;
-> 读操作返回AVERROR(EINTR),EINTR是unix的errno.h中定义的错误类型,该类型为4,表示中断系统调用(Interrupted system call),出现该错误时,可以进行下次读操作即可;
-> 读操作是非阻塞模式,那么直接返回读操作的返回值,让外部进行逻辑处理即可;
-> 读操作是阻塞模式,再细分处理;
-> 读操作返回AVERROR(EAGAIN),暗示本次没有读到数据,但是下次可能会读到数据;
-> 进行5次快速读取尝试,快速读取数据成功,增加已读数据长度,看是否还需要下次继续读操作;
-> 若快速读取尝试都已失败告终,那么先休眠读数据操作所在线程1000us,并且判断IO操作是否真实大于其协议所定的rw_timeout,否则认为是IO错误;
-> 读操作返回AVERROR_EOF,表示读取到文件尾,返回已读取数据或者是EOF;
-> 其他错误,则返回读取错误码。
2.3.1.4 url_read()
我们以本地磁盘文件的读取,即file协议的url_read方法来看,该方法是如何实现的,源码如下,源码分析见注释:
static int file_read(URLContext *h, unsigned char *buf, int size)
{
FileContext *c = h->priv_data;
int ret;
// size取size和c->blocksize最小值
// 如下file_options可知c->blocksize默认为整数最大值,因此一般此处
// 最小值还是size
size = FFMIN(size, c->blocksize);
// 调用c库不带缓冲读文件IO
ret = read(c->fd, buf, size);
// 如果数据读取长度为0,并且c->follow不为0,表示
// 该文件可能边写入边读取,因此,下次读
// 还可能读到数据, 所以不返回EOF,而是返回AVERROR(EAGAIN)
if (ret == 0 && c->follow)
return AVERROR(EAGAIN);
// 读取数据读取长度为0,则返回EOF
if (ret == 0)
return AVERROR_EOF;
// 读错误,返回读错误码,ret为-1时,errno存储着错误码
// 否则,ret返回的就是错误码
return (ret == -1) ? AVERROR(errno) : ret;
}
typedef struct FileContext {
const AVClass *class;
int fd;
int trunc;
int blocksize;
int follow;
#if HAVE_DIRENT_H
DIR *dir;
#endif
} FileContext;
static const AVOption file_options[] = {
{ "truncate", "truncate existing files on write", offsetof(FileContext, trunc), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, AV_OPT_FLAG_ENCODING_PARAM },
{ "blocksize", "set I/O operation maximum block size", offsetof(FileContext, blocksize), AV_OPT_TYPE_INT, { .i64 = INT_MAX }, 1, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM },
{ "follow", "Follow a file as it is being written", offsetof(FileContext, follow), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, AV_OPT_FLAG_DECODING_PARAM },
{ NULL }
};
2.3.2 fill_buffer()
fill_buffer()源码:
static void fill_buffer(AVIOContext *s)
{
// 获取最大buffer大小
int max_buffer_size = s->max_packet_size ?
s->max_packet_size : IO_BUFFER_SIZE;
// 要读取数据存入的缓存起始点,为啥不直接就是s->buf_end?
// 取值为s->buf_end的情形:当前缓冲区大小s->buffer_size非常大,大到从s->buf_end开始到往后的区域内比max_buffer_size还大
// 取值为s->buffer的情形:当前缓冲区并非特别大时,直接读取数据到缓冲开始位置。
// 需要理解的一点是调用fill_buffer函数的前提是AVIOContext内部缓冲区的所有数据已经都被消费完
// 否则将dst置为s->buffer这点就不好理解
uint8_t *dst = s->buf_end - s->buffer + max_buffer_size < s->buffer_size ?
s->buf_end : s->buffer;
// 要读取数据的长度
int len = s->buffer_size - (dst - s->buffer);
// 判断是否可以继续读取数据
/* can't fill the buffer without read_packet, just set EOF if appropriate */
if (!s->read_packet && s->buf_ptr >= s->buf_end)
s->eof_reached = 1;
/* no need to do anything if EOF already reached */
if (s->eof_reached)
return;
// 更新校验和
if (s->update_checksum && dst == s->buffer) {
if (s->buf_end > s->checksum_ptr)
s->checksum = s->update_checksum(s->checksum, s->checksum_ptr,
s->buf_end - s->checksum_ptr);
s->checksum_ptr = s->buffer;
}
// 如果因为格式探测扩展了缓冲区,那么此处先将缓冲区缩回到orig_buffer_size
/* make buffer smaller in case it ended up large after probing */
if (s->read_packet && s->orig_buffer_size && s->buffer_size > s->orig_buffer_size) {
if (dst == s->buffer && s->buf_ptr != dst) {
int ret = ffio_set_buf_size(s, s->orig_buffer_size);
if (ret < 0)
av_log(s, AV_LOG_WARNING, "Failed to decrease buffer size\n");
s->checksum_ptr = dst = s->buffer;
}
av_assert0(len >= s->orig_buffer_size);
len = s->orig_buffer_size;
}
// 使用read_packet_wrapper方法读取len长度数据到dst指向的缓冲区
len = read_packet_wrapper(s, dst, len);
// 读到EOF,设置标志位
if (len == AVERROR_EOF) {
/* do not modify buffer if EOF reached so that a seek back can
be done without rereading data */
s->eof_reached = 1;
// 读取出错,设置标志位
} else if (len < 0) {
s->eof_reached = 1;
s->error= len;
// 正常读取数据后,更新内部缓冲区指针
} else {
s->pos += len;
s->buf_ptr = dst;
s->buf_end = dst + len;
s->bytes_read += len;
}
}
该函数作用很明显,就是读取数据来填充AVIOContext的内部缓冲区,在真正读取数据之前做了一定的容错处理,然后调用之前分析过的read_packet_wrapper()函数将数据读取到指定的内部缓冲区中。大致的过程如下描述或者见源码注释:
1. 计算目标缓冲区的起始位置dst以及本次要读取数据的长度len
目标起始位置dst的计算有点迷惑人,正常的理解应该读取数据从AVIOContext的buf_end处开始才对,否则读取的数据岂不是把之前的数据给冲掉了?要理解这点,需要知道调用fill_buffer的时机是内部缓冲区所有的数据都已经被消费掉之后才被调用,因此不论dst设置在缓冲区哪儿理论上都是没有问题的,但是为了保证读取的效率,缓冲区倾向于"大点"更好,但也不是盲目的大,所以有了源码上的判断;
2. 判断是否存在读取数据包函数,是否已到文件尾,做容错处理;
3. 更新校验和
注意,计算新的校验和的时机是在dst == s->buffer时候,想想,如果此时还不更新校验和,那么势必AVIOContext内部数据会被冲掉,后续校验和的更新就有问题了。调用AVIOContext.update_checksum()进行校验和更新,传入的参数有之前的校验和值AVIOContext.checksum,参于校验和计算的数据的起始位置,参与校验和计算的数据的长度。更新完校验和后,需要更新校验和指针,以便指向下次参与校验和更新的缓冲起始位置。
4. 如果因为格式探测扩展了缓冲区,那么此处先将缓冲区缩回到AVIOContext.orig_buffer_size,调用方法ffio_set_buf_size()进行缓冲区大小重置。
5. 调用read_packet_wrapper()进行数据读取,这个函数在2.3.1中详细的进行了源码跟踪,此处就不再赘述。
2.3.2.1 ffio_set_buf_size()
ffio_set_buf_size()源码:
int ffio_set_buf_size(AVIOContext *s, int buf_size)
{
uint8_t *buffer;
buffer = av_malloc(buf_size);
if (!buffer)
return AVERROR(ENOMEM);
av_free(s->buffer);
s->buffer = buffer;
s->orig_buffer_size =
s->buffer_size = buf_size;
s->buf_ptr = s->buf_ptr_max = buffer;
url_resetbuf(s, s->write_flag ? AVIO_FLAG_WRITE : AVIO_FLAG_READ);
return 0;
}
很简单,先创建新的大小的buffer,然后更新AVIOContext内部的各种缓冲相关的指针,注意,AVIOContext.buf_end指针没有在此处处理。
2.3.2.2 url_resetbuf()
url_resetbuf()源码:
static int url_resetbuf(AVIOContext *s, int flags)
{
av_assert1(flags == AVIO_FLAG_WRITE || flags == AVIO_FLAG_READ);
if (flags & AVIO_FLAG_WRITE) {
s->buf_end = s->buffer + s->buffer_size;
s->write_flag = 1;
} else {
s->buf_end = s->buffer;
s->write_flag = 0;
}
return 0;
}
根据缓冲是用来读还是写,来设置AVIOContext.buf_end指针,在读写过程中该指针代表了不同的含义。
2.4 avio_skip()
avio_skip()声明:向前跳过给定的字节数
/**
* Skip given number of bytes forward
* @return new position or AVERROR.
*/
int64_t avio_skip(AVIOContext *s, int64_t offset);
avio_skip()源码:skip函数调用seek函数来实现向前跳跃,注意传入的参数是SEEK_CUR,表示从当前的点向前跳过offset字节。
int64_t avio_skip(AVIOContext *s, int64_t offset)
{
return avio_seek(s, offset, SEEK_CUR);
}
2.5 avio_seek()
avio_seek()声明:相当于文件操作的fseek()
/**
* fseek() equivalent for AVIOContext.
* @return new position or AVERROR.
*/
int64_t avio_seek(AVIOContext *s, int64_t offset, int whence);
avio_seek()源码:
函数目标:1)计算并返回按指定方式seek后,最终文件当前偏移量
2)保证最终的文件偏移量在AVIOContext内部缓冲区的覆盖范围内
3)保证AVIOContext中关于缓冲区的相关指针都正确的赋值,尤其是s->buf_ptr指向“最终文件当前偏移量”在内部缓冲区的对应位置点。
函数的内部逻辑:检查了参数有效性->计算了最终文件当前偏移量->分4类情况进行seek操作,并保证上述目标3)
int64_t avio_seek(AVIOContext *s, int64_t offset, int whence)
{
int64_t offset1;
int64_t pos;
int force = whence & AVSEEK_FORCE;
int buffer_size;
int short_seek;
whence &= ~AVSEEK_FORCE;
// 检查参数有效性
if(!s)
return AVERROR(EINVAL);
// 读,buffer_size = 缓冲中已被消费的数据 + 已读取等待被消费的数据
// 写,buffer_size = 缓冲中已被写入的数据 + 还可以被写入缓冲的数据
buffer_size = s->buf_end - s->buffer;
// 临时变量pos求取的是内部缓冲起始位置s->buffer对应文件中的位置
// pos is the absolute position that the beginning of s->buffer corresponds to in the file
pos = s->pos - (s->write_flag ? 0 : buffer_size);
// 检查参数有效性
if (whence != SEEK_CUR && whence != SEEK_SET)
return AVERROR(EINVAL);
// 如果是从当前位置计算seek,那么需要计算缓冲区当前所处理数据点,相对于文件的位置
// 也即s->buf_ptr指针所对应的文件中位置。
if (whence == SEEK_CUR) {
// 计算offset1为s->buf_ptr指针所对应的文件中位置
offset1 = pos + (s->buf_ptr - s->buffer);
// 若输入偏移为0,那么直接返回offset1即可,内部缓冲的所有指针可不变动
if (offset == 0)
return offset1;
// 若输入偏移大于还可以进行的最大偏移,则输入偏移参数越界,返回错误信息
if (offset > INT64_MAX - offset1)
return AVERROR(EINVAL);
// 计算出以当前偏移点为基础,最终对应的文件偏移位置
offset += offset1;
}
// 若偏移小于0,则输入偏移参数越界,返回错误信息
if (offset < 0)
return AVERROR(EINVAL);
// 计算跳转阈值
// s->short_seek_get在ffio_fdopen函数创建AVIOContext对象时
// 被赋值为io_short_seek函数,而s->short_seek_threshold被赋值
// 为SHORT_SEEK_THRESHOLD常量4096
if (s->short_seek_get) {
short_seek = s->short_seek_get(s->opaque);
/* fallback to default short seek */
if (short_seek <= 0)
short_seek = s->short_seek_threshold;
} else
short_seek = s->short_seek_threshold;
// 计算offset1是目的offset与缓冲区首地址所对应偏移量之差
offset1 = offset - pos; // "offset1" is the relative offset from the beginning of s->buffer
//s->buf_ptr_max取二者较大值,由于有可能往前seek,向后seek导致s->buf_ptr变动
s->buf_ptr_max = FFMAX(s->buf_ptr_max, s->buf_ptr);
// 以下->分类处理seek的各种情况
// 情形一:读写,往前seek,不支持在文件中直接seek,文件偏移位置在buffer缓冲区有效范围以内
//(读->已读取的数据范围内,写->缓冲区大小范围内)
// 只需修改s->buf_ptr指针即可
if ((!s->direct || !s->seek) &&
offset1 >= 0 && offset1 <= (s->write_flag ? s->buf_ptr_max - s->buffer : buffer_size)) {
/* can do the seek inside the buffer */
s->buf_ptr = s->buffer + offset1;
// 情形二:读,往前seek,不支持在文件中直接seek,文件最终偏移不在当前缓冲区有效范围内,
// 那么只能循环fill_buffer()读取数据到缓冲区中,一直到目标偏移量offset在缓冲区中后,
// 停止数据读取。修改s->buf_ptr指针。
} else if ((!(s->seekable & AVIO_SEEKABLE_NORMAL) ||
offset1 <= buffer_size + short_seek) &&
!s->write_flag && offset1 >= 0 &&
(!s->direct || !s->seek) &&
(whence != SEEK_END || force)) {
while(s->pos < offset && !s->eof_reached)
fill_buffer(s);
if (s->eof_reached)
return AVERROR_EOF;
s->buf_ptr = s->buf_end - (s->pos - offset);
// 情形三:读,往后seek,往后seek大小小于buffer_size的一半,支持在文件中直接seek
// 此时将保持缓冲区含有有效数据,修改s->buf_ptr指针指向offset对应的位置
} else if(!s->write_flag && offset1 < 0 && -offset1 < buffer_size>>1 && s->seek && offset > 0) {
int64_t res;
// 计算直接seek后,缓冲起始位置对应的文件中pos
pos -= FFMIN(buffer_size>>1, pos);
// 实际的文件seek
if ((res = s->seek(s->opaque, pos, SEEK_SET)) < 0)
return res;
// 初始化所有的指针,然后使用fill_buffer()来读取数据填充内部缓冲
s->buf_end =
s->buf_ptr = s->buffer;
s->pos = pos;
s->eof_reached = 0;
fill_buffer(s);
// 递归调用avio_seek(),下一步将进入情形一中修改s->buf_ptr指针
return avio_seek(s, offset, SEEK_SET | force);
// 情形四:读写,支持文件中直接seek,此时,缓冲区中最终将不含有效数据
// 修改s->buf_ptr指向缓冲区的起始位置,起始位置即为offset位置
} else {
int64_t res;
// 写,则先将缓冲区的有效数据写入文件
if (s->write_flag) {
flush_buffer(s);
}
// 不存在seek函数,则返回错误
if (!s->seek)
return AVERROR(EPIPE);
// 直接在文件中seek,注意与情形三设置的偏移量不同
if ((res = s->seek(s->opaque, offset, SEEK_SET)) < 0)
return res;
s->seek_count ++;
// 设置缓冲区内部指针
if (!s->write_flag)
s->buf_end = s->buffer;
s->buf_ptr = s->buf_ptr_max = s->buffer;
s->pos = offset;
}
s->eof_reached = 0;
return offset;
}
2.6 avio_tell()
avio_tell()源码:本函数是个静态内联函数,直接调用了avio_seek函数来实现,可以详细跟踪2.5介绍的avio_seek()函数。
/**
* ftell() equivalent for AVIOContext.
* @return position or AVERROR.
*/
static av_always_inline int64_t avio_tell(AVIOContext *s)
{
return avio_seek(s, 0, SEEK_CUR);
}