ffmpeg中的io之所以复杂,是因为ffmpeg基本支持所有的读写协议,最底层的有tcp,udp,文件和串口。上层又支持很多协议,譬如rtmp,http,rtsp等等。今天主要分析的就是ffmpeg对于io操作,特别是open和读操作。
从文件开始,看一个io操作的例子。这是截取的一段代码。分五部分:
- av_register_all进行注册
- avformat_open_input打开url(也叫filename),并关联了URLPROTOAL和FORMAT ,譬如URLPROTOAL为file和format为flv。
- av_read_frame读取一个帧,这是一个完整的帧数据。
- 对pkt进行处理,中间过程我们忽略了的。
- close结尾
int main(){
const char *url = argv[1];
AVFormatContext *fmt;
//注册
av_register_all();
//打开文件,初始化 format context
avformat_open_input(&(fmt), url, NULL, NULL);
// 开始读取一个包
AVPacket pkt;
av_init_packet(&pkt); //初始化AVPacket对象
pkt.data = NULL;
pkt.size = 0;
/* read frames from the file */
while (av_read_frame(fmt, &pkt) >= 0) //从输入程序中读取一个包的数据,av_read_frame返回的是一个完整的帧数据
{
// 对一个包进行处理
do
{
。。。。 //解码这个包 Decode pkt
pkt.data += ret;
pkt.size -= ret;
} while (pkt.size > 0);
}
av_close_input_file(fmt);
}
1 注册
有四个宏关于封装,解封装和传输协议。av_register_output_format,av_register_input_format,ffurl_register_protocol分别会添加一个对象到全局链表中。通过查找链表来找对应的对象。有三个结构体:
封装:ff_##x##_muxer
解封装:ff_##x##_demuxer
传输协议:ff_##x##_protocol
#define REGISTER_MUXER(X,x) { \
extern AVOutputFormat ff_##x##_muxer; \
if(CONFIG_##X##_MUXER) av_register_output_format(&ff_##x##_muxer); }
#define REGISTER_DEMUXER(X,x) { \
extern AVInputFormat ff_##x##_demuxer; \
if(CONFIG_##X##_DEMUXER) av_register_input_format(&ff_##x##_demuxer); }
#define REGISTER_MUXDEMUX(X,x) REGISTER_MUXER(X,x); REGISTER_DEMUXER(X,x)
#define REGISTER_PROTOCOL(X,x) { \
extern URLProtocol ff_##x##_protocol; \
if(CONFIG_##X##_PROTOCOL) ffurl_register_protocol(&ff_##x##_protocol, sizeof(ff_##x##_protocol)); }
熟悉的有FLV,RTMP,TCP等,我主要分析的也是这几个协议。但是奇怪的是怎么没有 REGISTER_MUXDEMUX (RTMP, rtmp);,因为rtmp对应的封装也是FLV的。
void av_register_all(void)
{
static int initialized;
if (initialized)
return;
initialized = 1;
/* (de)muxers */
REGISTER_MUXDEMUX (FLV, flv);
/* protocols */
REGISTER_PROTOCOL (FILE, file);
...
REGISTER_PROTOCOL (RTMP, rtmp);
REGISTER_PROTOCOL (TCP, tcp);
}
2 open
avformat_open_input 初始化AVFormatContext,并用AVFormatContext.iformat打开filename。后进行读header操作。AVFormatContext是IO操作的上下文,所有操作都和它相关。
第一部分是初始化 AVFormatContext ,iformat的初始化。并且操作AVOption。调用这个函数一般情况下AVInputFormat *fmt和AVFormatContext **ps都为NULL。
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options)
{
AVFormatContext *s = *ps;
int ret = 0;
AVFormatParameters ap = { 0 };
AVDictionary *tmp = NULL;
if (!s && !(s = avformat_alloc_context())) //s 需要重新创建
return AVERROR(ENOMEM);
if (fmt) //一般情况下fmt为空
s->iformat = fmt;
if (options)
av_dict_copy(&tmp, *options, 0);
if ((ret = av_opt_set_dict(s, &tmp)) < 0)
goto fail;
AVFormatContext和filename的关联。这个函数最重要了, 在这里确定了 URLProtocol 和 input format 确定了下来。
if ((ret = init_input(s, filename)) < 0)
goto fail;
初始化FORMAT的priv_data。
s->duration = s->start_time = AV_NOPTS_VALUE;
av_strlcpy(s->filename, filename ? filename : "", sizeof(s->filename));
/* allocate private data */
if (s->iformat->priv_data_size > 0) {
if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (s->iformat->priv_class) {
*(const AVClass**)s->priv_data = s->iformat->priv_class;
av_opt_set_defaults(s->priv_data);
if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
goto fail;
}
}
调用s->iformat->read_header读header,确定下来offset。
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
if ((ret = s->iformat->read_header(s, &ap)) < 0)
goto fail;
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->pb && !s->data_offset)
s->data_offset = avio_tell(s->pb);
s->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;
if (options) {
av_dict_free(options);
*options = tmp;
}
*ps = s;
return 0;
收尾工作。
fail:
av_dict_free(&tmp);
if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
avio_close(s->pb);
avformat_free_context(s);
*ps = NULL;
return ret;
}
这里面重点在于 init_input ,这函数可是有点长啊。
avformat_alloc_context来初始化AVFormatContext,alloc后有通过AVOpetion进行初始化,这些初始化可以不用管。
2.1 init_input函数
这个函数都是static函数,不能外部调用。
init_input中一般情况下s->pb和s->iformat都是NULL,那就删除这两个不为空的操作代码后就非常简单了。先open文件,初始化AVProbeData,这个结构体很简单,它的作用就是抓取一点点数据进行分析。probe的数据是来进行判断属于哪一种FORMAT。
static int init_input(AVFormatContext *s, const char *filename)
{
int ret;
AVProbeData pd = {filename, NULL, 0};
if ((ret = avio_open(&s->pb, filename, AVIO_FLAG_READ)) < 0)
return ret;
return av_probe_input_buffer(s->pb, &s->iformat, filename, s, 0, 0);
}
2.2 avio_open
绕到这里才看到这个open函数。由两个函数ffurl_open和ffio_fdopen组成,这几个函数都比较小。ffurl_open主要是URLProtocol的操作,并初始化了URLContext,URLContext.prot为 URLProtocol。ffio_fdopen操作AVIOContext。AVIOContext,URLProtocol,URLContext互相关联着。
int avio_open(AVIOContext **s, const char *filename, int flags)
{
URLContext *h;
int err;
err = ffurl_open(&h, filename, flags);
err = ffio_fdopen(s, h);
return 0;
}
2.2.1 ffurl_open
先看ffurl_open,这个函数主要针对于URLProtocol的,alloc URLContext,因为到现在为止还不知道该怎么来操作这个URL。
int ffurl_open(URLContext **puc, const char *filename, int flags)
{
int ret = ffurl_alloc(puc, filename, flags);
ret = ffurl_connect(*puc);
}
#define URL_SCHEME_CHARS \
"abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"0123456789+-."
int ffurl_alloc(URLContext **puc, const char *filename, int flags)
{
URLProtocol *up;
char proto_str[128], proto_nested[128], *ptr;
// URL_SCHEME_CHARS的定义看上面,这个函数返回不在URL_SCHEME_CHARS其中的第一个字符的索引
size_t proto_len = strspn(filename, URL_SCHEME_CHARS);
if (filename[proto_len] != ':' || is_dos_path(filename))
strcpy(proto_str, "file");
else
av_strlcpy(proto_str, filename, FFMIN(proto_len+1, sizeof(proto_str)));
av_strlcpy(proto_nested, proto_str, sizeof(proto_nested));
if ((ptr = strchr(proto_nested, '+')))
*ptr = '\0';
up = first_protocol;
while (up != NULL) {
if (!strcmp(proto_str, up->name))
return url_alloc_for_protocol (puc, up, filename, flags);
if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&
!strcmp(proto_nested, up->name))
return url_alloc_for_protocol (puc, up, filename, flags);
up = up->next;
}
*puc = NULL;
return AVERROR(ENOENT);
}
这里面有一个重要逻辑是通过filename,也叫url,来区分URLProtocol名字。第一步先找到第一个字符串,一个特殊的字符串,不在URL_SCHEME_CHARS中的就是特殊的字符串。前面代表协议的字符串就出现了,举几个例子:
rtmp://…
http://…
/data1/logs/nginxrtmp
三种协议,通过前面的几个字符。
找到了url协议名字,之后在url协议链中查找。找到后调用 url_alloc_for_protocol。malloc URLContext,后进行部分初始化,记住priv_data在这里已经初始化了并且uc->prot = up。
static int url_alloc_for_protocol (URLContext **puc, struct URLProtocol *up,
const char *filename, int flags)
{
URLContext *uc;
int err;
uc = av_mallocz(sizeof(URLContext) + strlen(filename) + 1);
if (!uc) {
err = AVERROR(ENOMEM);
goto fail;
}
uc->av_class = &urlcontext_class;
uc->filename = (char *) &uc[1];
strcpy(uc->filename, filename);
uc->prot = up; //prot 这是啥名字呢?
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 (up->priv_data_class) {
*(const AVClass**)uc->priv_data = up->priv_data_class;
av_opt_set_defaults(uc->priv_data);
}
}
*puc = uc;
return 0;
fail:
*puc = NULL;
return err;
}
URLProtocol,URLContext在ffurl_alloc已经初始化了,并且已经关联了filename,但现在还没打开后。下一步分析:ffurl_connect,该打开url了。
uc->prot就是URLProtocol,奇怪的恨,prot是啥意思。真正调用了 uc->prot->url_open。
int ffurl_connect(URLContext* uc)
{
int err = uc->prot->url_open(uc, uc->filename, uc->flags);
if (err)
return err;
uc->is_connected = 1;
//We must be careful here as ffurl_seek() could be slow, for example for http
if( (uc->flags & AVIO_FLAG_WRITE)
|| !strcmp(uc->prot->name, "file"))
if(!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
uc->is_streamed= 1;
return 0;
}
url操作完成后,在读之前还需要对AVIOContext进行初始化,AVFormatContext.pb为AVIOContext。这些名字命名好奇怪啊。
2.2.2 ffio_fdopen
ffio_fdopen 主要是针对于AVIOContext进行操作。
先定义了初始化buffer,buffer_size大小默认是32768。
初始化AVIOContext
ffio_init_context对AVIOContext进行初始化,特别两个回调函数read_pause和read_seek。
int ffio_fdopen(AVIOContext **s, URLContext *h)
{
uint8_t *buffer;
int buffer_size, max_packet_size;
max_packet_size = h->max_packet_size;
if (max_packet_size) {
buffer_size = max_packet_size; /* no need to bufferize more than one packet */
} else {
buffer_size = IO_BUFFER_SIZE;
}
buffer = av_malloc(buffer_size);
*s = av_mallocz(sizeof(AVIOContext));
if (ffio_init_context(*s, buffer, buffer_size,
h->flags & AVIO_FLAG_WRITE, h,
(void*)ffurl_read, (void*)ffurl_write, (void*)ffurl_seek) < 0) {
av_free(buffer);
av_freep(s);
return AVERROR(EIO);
}
#if FF_API_OLD_AVIO
(*s)->is_streamed = h->is_streamed;
#endif
(*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL;
(*s)->max_packet_size = max_packet_size;
if(h->prot) {
(*s)->read_pause = (int (*)(void *, int))h->prot->url_read_pause;
(*s)->read_seek = (int64_t (*)(void *, int, int64_t, int))h->prot->url_read_seek;
}
return 0;
}
ffio_init_context重要的有三个变量:
s->write_packet = write_packet;
s->read_packet = read_packet;
s->seek = seek;
(void*)ffurl_read, (void*)ffurl_write, (void*)ffurl_seek这是三个全局函数。
int ffio_init_context(AVIOContext *s,
unsigned char *buffer,
int buffer_size,
int write_flag,
void *opaque,
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))
{
s->buffer = buffer;
s->buffer_size = buffer_size;
s->buf_ptr = buffer;
s->opaque = opaque;
url_resetbuf(s, write_flag ? AVIO_FLAG_WRITE : AVIO_FLAG_READ);
s->write_packet = write_packet;
s->read_packet = read_packet;
s->seek = seek;
s->pos = 0;
s->must_flush = 0;
s->eof_reached = 0;
s->error = 0;
#if FF_API_OLD_AVIO
s->is_streamed = 0;
#endif
s->seekable = AVIO_SEEKABLE_NORMAL;
s->max_packet_size = 0;
s->update_checksum= NULL;
if(!read_packet && !write_flag){
s->pos = buffer_size;
s->buf_end = s->buffer + buffer_size;
}
s->read_pause = NULL;
s->read_seek = NULL;
return 0;
}
2.3 av_probe_input_buffer
avio_open分析完成,回到init_open,最后调用了 av_probe_input_buffer 。这个函数读取了一部分文件后来判断数据内容的协议,以文件举例,来区分出是flv,mp4格式等等。
/**
* Probe a bytestream to determine the input format. Each time a probe returns
* with a score that is too low, the probe buffer size is increased and another
* attempt is made. When the maximum probe size is reached, the input format
* with the highest score is returned.
*
* @param pb the bytestream to probe
* @param fmt the input format is put here
* @param filename the filename of the stream
* @param logctx the log context
* @param offset the offset within the bytestream to probe from
* @param max_probe_size the maximum probe buffer size (zero for default)
* @return 0 in case of success, a negative value corresponding to an
* AVERROR code otherwise
*/
```
定义化了max_probe_size,初始化为2048,也就是读取的最大数据。
```c
/** size of probe buffer, for guessing file type from file contents */
#define PROBE_BUF_MIN 2048
#define PROBE_BUF_MAX (1<<20)
int av_probe_input_buffer(AVIOContext *pb, AVInputFormat **fmt,
const char *filename, void *logctx,
unsigned int offset, unsigned int max_probe_size)
{
AVProbeData pd = { filename ? filename : "", NULL, -offset };
unsigned char *buf = NULL;
int ret = 0, probe_size;
if (!max_probe_size) {
max_probe_size = PROBE_BUF_MAX;
} else if (max_probe_size > PROBE_BUF_MAX) {
max_probe_size = PROBE_BUF_MAX;
} else if (max_probe_size < PROBE_BUF_MIN) {
return AVERROR(EINVAL);
}
if (offset >= max_probe_size) {
return AVERROR(EINVAL);
}
通过avio_read读取数据到 buftmp 中,后拷贝到pd中,AVProbeData是存放连续读取数据的。
for(probe_size= PROBE_BUF_MIN;
probe_size<=max_probe_size && !*fmt && ret >= 0;
probe_size = FFMIN(probe_size<<1, FFMAX(max_probe_size, probe_size+1))) {
int ret, score = probe_size < max_probe_size ? AVPROBE_SCORE_MAX/4 : 0;
int buf_offset = (probe_size == PROBE_BUF_MIN) ? 0 : probe_size>>1;
void *buftmp;
if (probe_size < offset) {
continue;
}
/* read probe data */
buftmp = av_realloc(buf, probe_size + AVPROBE_PADDING_SIZE);
buf=buftmp;
if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0) {
score = 0;
ret = 0; /* error was end of file, nothing read */
}
pd.buf_size += ret;
pd.buf = &buf[offset];
memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
开始猜测format。
/* guess file format */
*fmt = av_probe_input_format2(&pd, 1, &score);
}
把probe data放入到AVIOContext的buffer中,要注意在之前读的数据和现在数据的结合。
if (!*fmt) {
av_free(buf);
return AVERROR_INVALIDDATA;
}
/* rewind. reuse probe buffer to avoid seeking */
if ((ret = ffio_rewind_with_probe_data(pb, buf, pd.buf_size)) < 0)
av_free(buf);
return ret;
}
av_probe_input_buffer 有两个地方比较重要,avio_read 和 av_probe_input_format2。读操作后猜测format。
av_probe_input_format 有几个类似函数,主要是av_probe_input_format3。通过av_iformat_next遍历first_iformat,把多个AVInputFormat串起来的。后执行read_probe,或者根据extensions来进行比对。
AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened, int *score_ret)
{
AVProbeData lpd = *pd;
AVInputFormat *fmt1 = NULL, *fmt;
int score, score_max=0;
if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
int id3len = ff_id3v2_tag_len(lpd.buf);
if (lpd.buf_size > id3len + 16) {
lpd.buf += id3len;
lpd.buf_size -= id3len;
}
}
//遍历所有AVInputFormat
fmt = NULL;
while ((fmt1 = av_iformat_next(fmt1))) {
if (!is_opened == !(fmt1->flags & AVFMT_NOFILE))
continue;
score = 0;
if (fmt1->read_probe) {
score = fmt1->read_probe(&lpd); //执行read_probe得到一个score
if(!score && fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions))
score = 1;
} else if (fmt1->extensions) { //判断extensions
if (av_match_ext(lpd.filename, fmt1->extensions)) {
score = 50;
}
}
if (score > score_max) { //得到最高分
score_max = score;
fmt = fmt1;
}else if (score == score_max)
fmt = NULL;
}
*score_ret= score_max;
return fmt;
}
AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max)
{
int score_ret;
AVInputFormat *fmt= av_probe_input_format3(pd, is_opened, &score_ret);
if(score_ret > *score_max){
*score_max= score_ret;
return fmt;
}else
return NULL;
}
AVInputFormat *av_probe_input_format(AVProbeData *pd, int is_opened){
int score=0;
return av_probe_input_format2(pd, is_opened, &score);
}
举个FLV的例子
AVInputFormat ff_flv_demuxer = {
"flv",
NULL_IF_CONFIG_SMALL("FLV format"),
sizeof(FLVContext),
flv_probe,
flv_read_header,
flv_read_packet,
.read_seek = flv_read_seek,
.extensions = "flv",
.value = CODEC_ID_FLV1,
};
既有flv_probe又有extensions。想到一个问题,RTMP是如何来鉴别input format的呢?
2.4 avio_read
读操作,从AVIOContext读取size长的数据到buf中。s->read_packet是一个回调函数,在ffio_init_context函数中进行了赋值为ffurl_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) {
if(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;
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;
}
ffurl_read 是一个全局函数,retry_transfer_wrapper是对h->prot->url_read的封装,是URLProtocol的成员变量。
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);
}
static inline int retry_transfer_wrapper(URLContext *h, unsigned char *buf, int size, int size_min,
int (*transfer_func)(URLContext *h, unsigned char *buf, int size))
{
int ret, len;
int fast_retries = 5;
len = 0;
while (len < size_min) {
ret = transfer_func(h, buf+len, size-len);
if (ret == AVERROR(EINTR))
continue;
if (h->flags & AVIO_FLAG_NONBLOCK)
return ret;
if (ret == AVERROR(EAGAIN)) {
ret = 0;
if (fast_retries)
fast_retries--;
else
usleep(1000);
} else if (ret < 1)
return ret < 0 ? ret : len;
if (ret)
fast_retries = FFMAX(fast_retries, 2);
len += ret;
if (len < size && url_interrupt_cb())
return AVERROR_EXIT;
}
return len;
}
retry_transfer_wrapper这名字取的忒屌啦!
3 av_read_frame
av_read_frame 读取一个包,通过av_read_frame_internal读取数据,后放入到列表AVPacketList中,最后返回列表中的包。
如果列表s->packet_buffer中有包,则通过一些时间戳的逻辑来返回pkt。这个逻辑太细了,先放过。
int av_read_frame(AVFormatContext *s, AVPacket *pkt)
{
AVPacketList *pktl;
int eof=0;
const int genpts= s->flags & AVFMT_FLAG_GENPTS;
for(;;){
pktl = s->packet_buffer;
if (pktl) {
AVPacket *next_pkt= &pktl->pkt;
if(genpts && next_pkt->dts != AV_NOPTS_VALUE){
int wrap_bits = s->streams[next_pkt->stream_index]->pts_wrap_bits;
while(pktl && next_pkt->pts == AV_NOPTS_VALUE){
if( pktl->pkt.stream_index == next_pkt->stream_index
&& (0 > av_compare_mod(next_pkt->dts, pktl->pkt.dts, 2LL << (wrap_bits - 1)))
&& av_compare_mod(pktl->pkt.pts, pktl->pkt.dts, 2LL << (wrap_bits - 1))) { //not b frame
next_pkt->pts= pktl->pkt.dts;
}
pktl= pktl->next;
}
pktl = s->packet_buffer;
}
if( next_pkt->pts != AV_NOPTS_VALUE
|| next_pkt->dts == AV_NOPTS_VALUE
|| !genpts || eof){
/* read packet from packet buffer, if there is data */
*pkt = *next_pkt;
s->packet_buffer = pktl->next;
av_free(pktl);
return 0;
}
}
av_read_frame_internal 读取一个包,并放入到s->packet_buffer中。
if(genpts){
int ret= av_read_frame_internal(s, pkt);
if(ret<0){
if(pktl && ret != AVERROR(EAGAIN)){
eof=1;
continue;
}else
return ret;
}
if(av_dup_packet(add_to_pktbuf(&s->packet_buffer, pkt,
&s->packet_buffer_end)) < 0)
return AVERROR(ENOMEM);
}else{
assert(!s->packet_buffer);
return av_read_frame_internal(s, pkt);
}
}
}
重点在于 av_read_frame_internal 了,读取一个完整的包。如果不完整,是不会返回的。av_read_frame_internal调用了av_read_packet,而av_read_packet调用了s->iformat->read_packet。由于av_read_frame_internal太复杂,不深究了。用FLV来举例,FLV解封装的结构如下:
AVInputFormat ff_flv_demuxer = {
"flv",
NULL_IF_CONFIG_SMALL("FLV format"),
sizeof(FLVContext),
flv_probe,
flv_read_header,
flv_read_packet,
.read_seek = flv_read_seek,
.extensions = "flv",
.value = CODEC_ID_FLV1,
};
read frame时调用的就是flv_read_packet的函数。
4 av_close_input_file
最后收尾函数 av_close_input_file。分两步关闭,第一步关闭AVFormatContext,第二步关闭AVIOContext。
void av_close_input_file(AVFormatContext *s)
{
AVIOContext *pb = (s->iformat->flags & AVFMT_NOFILE) || (s->flags & AVFMT_FLAG_CUSTOM_IO) ?
NULL : s->pb;
av_close_input_stream(s);
if (pb)
avio_close(pb);
}
io部分基本浏览一下,之后我要找个实例,rtmp的。