协议操作对象结构:
协议(文件)操作的顶层结构是AVIOContext,这个对象实现了带缓冲的读写操作;FFMPEG的输入对象AVFormat的pb字段指向一个AVIOContext。
AVIOContext的opaque实际指向一个URLContext对象,这个对象封装了协议对象及协议操作对象,其中prot指向具体的协议操作对象,priv_data指向具体的协议对象。
URLProtocol为协议操作对象,针对每种协议,会有一个这样的对象,每个协议操作对象和一个协议对象关联,比如,文件操作对象为ff_file_protocol,它关联的结构体是FileContext。
代码分析:
初始化AVIOFormat函数调用关系:
我们采用从底至上的方法分析源码。
URLProtocol是FFMPEG操作文件的结构(包括文件,网络数据流等等),包括open、close、read、write、seek等操作。
在在av_register_all()函数中,通过调用REGISTER_PROTOCOL()宏,所有的URLProtocol都保存在以first_protocol为链表头的链表中。
URLProtocol结构体的定义为(简化版,未完全列出所有成员):
typedef struct URLProtocol { const char *name; int (*url_open)( URLContext *h, const char *url, int flags); int (*url_read)( URLContext *h, unsigned char *buf, int size); int (*url_write)(URLContext *h, const unsigned char *buf, int size); int64_t (*url_seek)( URLContext *h, int64_t pos, int whence); int (*url_close)(URLContext *h); int (*url_get_file_handle)(URLContext *h); struct URLProtocol *next; // 指向下一个URLProtocol对象(所有URLProtocol以链表链接在一起) int priv_data_size; // 和该URLProtocol对象关联的对象的大小 const AVClass *priv_data_class; } URLProtocol;
以文件协议为例,ff_file_protocol变量定义为:
URLProtocol ff_file_protocol = { .name = "file", .url_open = file_open, .url_read = file_read, .url_write = file_write, .url_seek = file_seek, .url_close = file_close, .url_get_file_handle = file_get_handle, .url_check = file_check, .priv_data_size = sizeof(FileContext), .priv_data_class = &file_class, };
从中可以看出,.priv_data_size的值为sizeof(FileContext),即ff_file_protocol和FileContext想关联。
FileContext对象的定义为:
typedef struct FileContext { const AVClass *class; int fd; <span style="white-space:pre"> </span>// 文件描述符 int trunc; // 截断属性 int blocksize; // 块大小,每次读写文件最大字节数 } FileContext;
返回去看ff_file_protocol里的函数指针,以url_read成员为例,它指向file_read()函数,该函数的定义为:
static int file_read(URLContext *h, unsigned char *buf, int size) { FileContext *c = h->priv_data; int r; size = FFMIN(size, c->blocksize); r = read(c->fd, buf, size); return (-1 == r)?AVERROR(errno):r; }
1. 调用此函数时,URLContext的priv_data指向一个FileContext对象;
2. 该函数每次最大只读取FileContext.blocksize大小的数据。
从上面的代码中,还可发现一个重要的对象:URLContext,根据代码推测,这个对象的priv_data指向一个FileContext,下面来看看这个对象的定义及初始化:
定义:
typedef struct URLContext { const AVClass *av_class; /**< information for av_log(). Set by url_open(). */ struct URLProtocol *prot; void *priv_data; char *filename; /**< specified URL */ int flags; int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */ int is_streamed; /**< true if streamed (no seek possible), default = false */ int is_connected; AVIOInterruptCB interrupt_callback; int64_t rw_timeout; /**< maximum time to wait for (network) read/write operation completion, in mcs */ } URLContext;
注释已经很明了,接下来看看这个结构体的初始化,在url.h(URLContext定义的地方)文件中很容易发现有一个ffurl_open()函数,猜测这应该是个初始化函数,从
该函数的内容可知,它首先调用了ffurl_alloc()申请空间,然后调用了ffurl_connect()建立连接。因此我们的目标转向这两个函数
先看ffurl_alloc()函数,该函数先调用url_find_protocol()查找和输入文件名称对应的协议对象,然后调用url_alloc_for_protocol()申请空间。
从url_find_protocol()易知:filename和协议匹配的规则是finename的前缀名(本地文件被统一处理为file前缀)和协议的name字段所指向的字符串相等。
再看url_alloc_for_protocol()函数:
static int url_alloc_for_protocol(URLContext **puc, struct URLProtocol *up, const char *filename, int flags, const AVIOInterruptCB *int_cb) { URLContext *uc; int err; // ... uc = av_mallocz(sizeof(URLContext) + strlen(filename) + 1); // 注意申请的空间大小 if (!uc) { err = AVERROR(ENOMEM); goto fail; } uc->av_class = &ffurl_context_class; uc->filename = (char *)&uc[1]; // 指向uc+sizeof(URLContext)的低昂 strcpy(uc->filename, filename); uc->prot = up; uc->flags = flags; uc->is_streamed = 0; /* default = not streamed */ uc->max_packet_size = 0; /* default: stream file */ if (up->priv_data_size) { uc->priv_data = av_mallocz(up->priv_data_size); if (!uc->priv_data) { err = AVERROR(ENOMEM); goto fail; } if (up->priv_data_class) { int proto_len= strlen(up->name); char *start = strchr(uc->filename, ','); *(const AVClass **)uc->priv_data = up->priv_data_class; av_opt_set_defaults(uc->priv_data); // ... } }
由上代码可以看出,URLContext的filename保存了输入文件名,prot字段保存了查找到的协议操作对象指针,flags由入参指定,is_streamed及max_packet_size默认值为0,
priv_data指向了一个由协议操作对象的priv_data_size指定大小的空间(考虑ff_file_protocol,即指向了一个sizeof(FileContext)大小的空间),且该空间的被初始化为0。
至此,URLContext的大部分内容已初始化,ffurl_alloc()工作已经完成,接着看ffurl_connect(),从代码易知,它调用了prot->url_open()函数打开协议,并设置了URLContext的
is_connected和is_streamed为1。
注意,av_opt_set_defaults(uc->priv_data)为为priv_data所指向的结构也进行了默认赋值操作,针对ff_file_protocol,赋值字段及值可参考file_options[]的定义:
static const AVOption file_options[] = { { "truncate", "truncate existing files on write", offsetof(FileContext, trunc), AV_OPT_TYPE_INT, { .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 }, { NULL } };即,truncate字段赋值为1,blocksize字段赋值为INT_MAX。
最后看看file_open()函数,从代码容易看出,该函数打开了文件,并将文件句柄保存在FileContext的fd字段。并且,该函数还根据文件属性设置了FileContext的is_streamed的值。
URLContext再往上一层是AVIOContext,该结构体的定义为:
typedef struct AVIOContext { const AVClass *av_class; unsigned char *buffer; /**< 读写缓冲buffer起始地址 */ int buffer_size; /**< buffer大小 */ unsigned char *buf_ptr; /**< Current position in the buffer */ unsigned char *buf_end; /**< End of the data */ void *opaque; /**< 指向URLContext */ int (*read_packet)(void *opaque, uint8_t *buf, int buf_size); /* 指向ffurl_read() */ int (*write_packet)(void *opaque, uint8_t *buf, int buf_size); /* 指向ffurl_write() */ int64_t (*seek)(void *opaque, int64_t offset, int whence); /* 指向ffurl_seek() */ int64_t pos; /**< 当前buffer对应的文件内容中的位置 */ int must_flush; /**< true if the next seek should flush */ 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 */ int (*read_pause)(void *opaque, int pause);/ int64_t (*read_seek)(void *opaque, int stream_index, int64_t timestamp, int flags); int seekable; // 是否可seek,0表示不可搜索 int64_t maxsize; int direct; int64_t bytes_read; int seek_count; int writeout_count; int orig_buffer_size; }AVIOContext;
avio_open2()负责初始化AVIOContext,从代码易知,该函数首先调用ffurl_open()申请了一个URLContext对象并打开了文件。然后调用ffio_fdopen()申请一个AVIOContext对象并赋初值。
ffio_fdopen()先申请了一个读写缓冲buffer(结合文件协议,buffer大小为IO_BUFFER_SIZE),然后调用avio_alloc_context()赋值,注意ffurl_read,ffurl_write,ffurl_seek的地址作为入参被传入。
最终调用的是ffio_init_context赋值,结合入参可知:
AVIOContext.buffer指向申请到的buffer, AVIOContext.orig_buffer_size和AVIOContext.buffer_size值为buffer_size(I_BUFFER_SIZE),buf_ptr字段初始化为buffer的开始地址,opaque指向URLContext对象。
再关注下avio_read()函数:
int avio_read(AVIOContext *s, unsigned char *buf, int size) { int len, size1; size1 = size; while (size > 0) { len = s->buf_end - s->buf_ptr; if (len > size) len = size; if (len == 0 || s->write_flag) { if((s->direct || size > s->buffer_size) && !s->update_checksum){ if(s->read_packet) len = s->read_packet(s->opaque, buf, size); if (len <= 0) { /* do not modify buffer if EOF reached so that a seek back can be done without rereading data */ s->eof_reached = 1; if(len<0) s->error= len; break; } else { s->pos += len; s->bytes_read += len; size -= len; buf += len; s->buf_ptr = s->buffer; s->buf_end = s->buffer/* + len*/; } } else { fill_buffer(s); len = s->buf_end - s->buf_ptr; if (len == 0) break; } } else { memcpy(buf, s->buf_ptr, len); buf += len; s->buf_ptr += len; size -= len; } } if (size1 == size) { if (s->error) return s->error; if (url_feof(s)) return AVERROR_EOF; } return size1 - size; }从代码易知,该函数实现了缓冲读写功能,即调用者传递的size比buffer_size大,则buffer_size的倍数部分会直接读取到buf,余数部分会首先读取buffer_size大小的数据到buffer(fill_buffer()函数),然后再拷贝到入参buf指向的地址中。
AVIOContext再往上一层是AVFormatContext对象,也是ffmpeg的顶层对象,AVFormatContext的pb字段指向一个AVIOContext对象。