从学龄前开始解读FFMPEG代码 之 avformat_open_input函数 三

开始学龄前学习前想说的话

这一部分的内容是回到init_input函数,并在完成init_input函数的学习之后,解决avformat_open_input剩下的内容,其实在第二章中,搞懂了三个主要的函数,剩下的内容就不是很多了(坑了许久,捡起来拍拍灰尘)。

init_input()使用三个函数做了什么

在这一部分的第一篇文章中,简单展示了init_input函数的作用,现在,再回顾一下:

static int init_input(AVFormatContext *s, const char *filename,
                      AVDictionary **options)
{
    int ret;
    AVProbeData pd = { filename, NULL, 0 };
    int score = AVPROBE_SCORE_RETRY;

    if (s->pb) {
        s->flags |= AVFMT_FLAG_CUSTOM_IO;
        if (!s->iformat)
            return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                         s, 0, s->format_probesize);
        else if (s->iformat->flags & AVFMT_NOFILE)
            av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
                                      "will be ignored with AVFMT_NOFILE format.\n");
        return 0;
    }

    if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
        (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
        return score;

    if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
        return ret;

    if (s->iformat)
        return 0;
    return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                 s, 0, s->format_probesize);
}

在第二篇文章中,我们对这里面所调用的三个函数av_probe_input_buffer2(),av_probe_input_format2()以及avio_read,进行了总结,分别用一句话总结功能就是:
av_probe_input_buffer2()函数的主要功能就是从输入的url中推断出文件的format
avio_read()函数所做的事就是从IO上下文的文件数据读入到缓存buffer中
av_probe_input_format2()函数所做的事就是循环查format表,让注册的format调用自己的probe函数来判断文件是否属于该种文件format
init_input函数整体并不算长,主要由四个if以及最后一个return构成:
init_input中进来的第一个if,首先判断输入的AVFormatContext中的pb是否设置好。这个pb实际上是一个AVIOContext,这pb,要么就在执行avformat_open_input之前,由调用函数的用户手动设置好,要么,就由avformat_open_input来设置好。在pb已经设置好的时候,情况就很简单了,这代表着用户(实际上代码里写作caller,也就是调用接口的一方,不过我喜欢称作为用户,我就算是ffmpeg的用户嘛)已经设置好了,这个时候,对s所包含的flag标记位置中加上一个 AVFMT_FLAG_CUSTOM_IO,意味着,这个format是由用户来设置好IO的,不需要你open_input多插手。(当然也有可能open_input_format被重复调用了,设置好了之后这也防止了重复设置,没有意义了)
然后,判断s中的输入format,即iformat有没有设置好,没设置好那么就直接调用av_probe_input_buffer2()来用buffer的方式查出文件的format就好了。如果设置好了,那也不需要去probe了,唯一的异常情况就是输入iformat中设置了AVFMT_NOFILE的标记位,这代表着没有输入文件,这种情况下,给个warning,ffmpeg也就撒手不管了(return 0)。

接下来,来到了第二个if,在第二个if中,有两种可能,第一种是输入文件不存在的情况,还有一种可能是s中没有设置好输入iformat,并且进行一次av_probe_input_format2来猜出了具体的format是什么,这个时候,score已经确定下来了(第一种可能,直接返回25分,第二种,通过probe已经得到了对应的得分),那么直接返回score就好。

然后是第三个if,这里,调用了s的io_open函数,并传入了所需要的参数,来获取到最终的ret,一开始看到io_open函数我还以为漏分析了函数,后来才发现这个io_open是回调函数,也就是说,需要每种AVFormatContext在注册时自己就写好的,不同的AVFormatContext拥有自己独特的io_open函数,这个函数的用法就是创建并设置好一个AVIOContext,在这里的话也就是s的pb了,就像第一个if中说的,这个pb也可能由avformat_open_input来做好设置。通过s的io_open,就可以创建IOContext并打开文件里视频流了,这个回调函数最终也会返回一个int的ret

第四个if里,如果文件已经设置好了iformat,那么不需要做无意义的检查,ffmpeg直接撒手摆烂。(return 0)

最后的return中,还是av_probe_input_buffer2()来终结一切可能存在的不知道文件格式的情况,通过载入数据来probe出文件内容。


从上面来看,init_input确实很实诚,函数如其名,很好地完成了初始化IO并推测出文件格式的任务,最后它还能返回给你一个评分,来表示自己的推测靠谱程度有多少,很人性化了。

最终回到了avformat_open_input中

回到这个函数中,剩下的内容看起来很多,但操作都不是那么复杂了,完整版的代码就不贴了,在同文章第一篇中有贴过,直接从init_input开始往后贴然后一起看一下。

    if ((ret = init_input(s, filename, &tmp)) < 0)
        goto fail;
    s->probe_score = ret;

    if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {
        s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);
        if (!s->protocol_whitelist) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    }

    if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {
        s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);
        if (!s->protocol_blacklist) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    }

    if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {
        av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist);
        ret = AVERROR(EINVAL);
        goto fail;
    }

    avio_skip(s->pb, s->skip_initial_bytes);

    /* Check filename in case an image number is expected. */
    if (s->iformat->flags & AVFMT_NEEDNUMBER) {
        if (!av_filename_number_test(filename)) {
            ret = AVERROR(EINVAL);
            goto fail;
        }
    }

    s->duration = s->start_time = AV_NOPTS_VALUE;

    /* 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;
        }
    }

    /* e.g. AVFMT_NOFILE formats will not have a AVIOContext */
    if (s->pb)
        ff_id3v2_read_dict(s->pb, &s->internal->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);


    if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
        if ((ret = s->iformat->read_header(s)) < 0)
            goto fail;

    if (!s->metadata) {
        s->metadata = s->internal->id3v2_meta;
        s->internal->id3v2_meta = NULL;
    } else if (s->internal->id3v2_meta) {
        int level = AV_LOG_WARNING;
        if (s->error_recognition & AV_EF_COMPLIANT)
            level = AV_LOG_ERROR;
        av_log(s, level, "Discarding ID3 tags because more suitable tags were found.\n");
        av_dict_free(&s->internal->id3v2_meta);
        if (s->error_recognition & AV_EF_EXPLODE) {
            ret = AVERROR_INVALIDDATA;
            goto close;
        }
    }

    if (id3v2_extra_meta) {
        if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac") ||
            !strcmp(s->iformat->name, "tta") || !strcmp(s->iformat->name, "wav")) {
            if ((ret = ff_id3v2_parse_apic(s, &id3v2_extra_meta)) < 0)
                goto close;
            if ((ret = ff_id3v2_parse_chapters(s, &id3v2_extra_meta)) < 0)
                goto close;
            if ((ret = ff_id3v2_parse_priv(s, &id3v2_extra_meta)) < 0)
                goto close;
        } else
            av_log(s, AV_LOG_DEBUG, "demuxer does not support additional id3 data, skipping\n");
    }
    ff_id3v2_free_extra_meta(&id3v2_extra_meta);

    if ((ret = avformat_queue_attached_pictures(s)) < 0)
        goto close;

    if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->pb && !s->internal->data_offset)
        s->internal->data_offset = avio_tell(s->pb);

    s->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;

    update_stream_avctx(s);

    for (i = 0; i < s->nb_streams; i++)
        s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id;

    if (options) {
        av_dict_free(options);
        *options = tmp;
    }
    *ps = s;
    return 0;

close:
    if (s->iformat->read_close)
        s->iformat->read_close(s);
fail:
    ff_id3v2_free_extra_meta(&id3v2_extra_meta);
    av_dict_free(&tmp);
    if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
        avio_closep(&s->pb);
    avformat_free_context(s);
    *ps = NULL;
    return ret;
}

从第一行开始,用s的成员probe_score记录init_open推测格式的最终得分,在这个时间段的时候,作为IOContext的pb已经被打开了,之后对s的pb中存在的协议黑白名单拷贝到s的黑白名单,黑名单中是禁止的协议protocol,白名单是允许的protocol,这些protocal都是用char*的方式来存储的,多个protocal中间用逗号隔开就行。

之后,如果s的IOContext pb,在设置了黑白名单的情况下,就把黑白名单复制一下,复制到s的黑白名单中。
完成了黑白名单复制之后,如果s设置了format_whitelist,也就是说只接受某些特定格式文件,那么还需要检查输入文件格式是否在白名单中,不在白名单中一律枪毙。

27行中。s的pb指针还需要做一个前移,移动到打开视频stream的位置。

之后检查flags中是否需要数字表示,比如在做一系列的解码任务是,会存在类似于test%03d.jpg 这种表示法,代表着(test001.jpg,test002.jpg …)一序列的图片,进行设置检查,然后再设置好s的duration和start_time(设置为0);之后,如果AVFormatContext还有外接的内存(可能在不同开发者自己注册的AVFormatContext中,会需要额外的数据结构来维护一些功能,所以需要额外进行划分)为s->priv_data设置划分好数据空间。

之后,从54行开始,需要对文件的ID3v2数据做好处理。首先,使用ff_id3v2_read_dict函数,该函数的备注说明该函数功能是将文件中的ID3v2数据置入到AVDictionary中,该函数实际的调用是libavformat/id3v2.c中的函数,

id3v2_read_internal(AVIOContext *pb, AVDictionary **metadata,
AVFormatContext *s, const char *magic,
ID3v2ExtraMeta **extra_meta, int64_t max_search_size)

但是在这里调用的时候,最后传入的max_search_size被设置为了0,这样在实际的操作过程中,直接在该函数处理的第一步就直接被返回了,所以是没有做操作

在58行,使用了另外一个比较重要的函数,在iformat对应的格式设置了read_header回调函数时,需要进行该函数功能,读取文件头并做好初始化工作。基本上对于每个格式的read_header函数,都会创建好AVStream,并等待在后续的流程中,可以取出或写入音视频流信息。

之后,是61行到74行,对id3v2的额外数据的处理,对于相应的音频格式,处理好id3v2数据,而其他格式则不需要。

在90行,则是用函数avformat_queue_attached_pictures来处理好音频流中可能需要附加的照片(这里可以举个例子说明一下:在播放音频时出现的类似专辑封面的照片,就是所谓的音频附加的照片,这种经常可以在各种音乐播放器中看见。)

93行的位置,做好处理完成数据之后的数据指针位移,然后98-101行,将s中已经打开的AVStream进行更新,完成每个音视频流的配置。这里的update_stream_avctx()函数也是一个很重要的函数,下面可以看一看它是干啥的。在update之后,由于一个视频文件中可能包含多个码流stream,所以就遍历所有的码流,并设置好orig_codec_id初始的codec id,也就是说,最后的这一点工作,也涉及到了编解码器的配置任务,在这个任务之后,avformat_open_input收尾工作开始,该设置的设置,该销毁的销毁。

update_stream_avctx()的作用

这个函数只在open_input这里曾经用过,其实算是处理文件中视频码流(AVStream)的开头,相当于太极拳里的起势,一般做解码的话,接下来的工作就是围绕着AVStream来展开的。
update_stream_avctx()的实现在文件libavformat/utils.c中,内容如下:

static int update_stream_avctx(AVFormatContext *s)
{
    int i, ret;
    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];

        if (!st->internal->need_context_update)
            continue;

        /* close parser, because it depends on the codec */
        if (st->parser && st->internal->avctx->codec_id != st->codecpar->codec_id) {
            av_parser_close(st->parser);
            st->parser = NULL;
        }

        /* update internal codec context, for the parser */
        ret = avcodec_parameters_to_context(st->internal->avctx, st->codecpar);
        if (ret < 0)
            return ret;

#if FF_API_LAVF_AVCTX
FF_DISABLE_DEPRECATION_WARNINGS
        /* update deprecated public codec context */
        ret = avcodec_parameters_to_context(st->codec, st->codecpar);
        if (ret < 0)
            return ret;
FF_ENABLE_DEPRECATION_WARNINGS
#endif

        st->internal->need_context_update = 0;
    }
    return 0;
}

首先进入st的一个大的遍历中,不需要update信息的stream则进行跳过,需要update的,那么继续接下来的流程。
接下来是一个判断,如果遍历到的当前的stream拥有分析器parser,且st解码器id与parser的解码器id不一致,那么就关闭parser,正如同注释中所说的那样:close parser, because it depends on the codec,这都是取决于解码器的。
之后,按照注释里写的,update internal codec context, for the parser,也就是说从parser中取出信息,并更新stream internal对应的codec编解码器,同样的招式对codec也是有效的,那么就再使一次,更新一下每个stream所设置的codec;
完成了update之后,这个就不需要update惹,所以将标记变量设置为不需要update,结束整个函数的执行。

总结与最后的一些话

鸽了许久,把open_input可算是更新完了,这几个月事情比较多,更新有所暂停,接下来的目标是一到二周更新一下,下一篇文章,就可以从一些具体的文件类型入手,熟悉一下不同的文件类型到底是怎么分辨自己的文件的,以mp4或者flv为例子,这样会更好去理解read_probe()以及read_head()这两个函数大概是要做什么的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值