从学龄前开始解读FFMPEG代码 之 avformat_find_stream_info函数 一

开始学习前想说的话

avformat_find_stream_info是一个很长很复杂的函数,在开始学习前也查了很多相关资料来帮助阅读学习这个函数的源代码,在整个函数体中中,包含多次的循环操作,用于对读入的视频数据进行检查并获取到视频文件中的stream数据。总函数长度有五百多行,所以可能会分成多个文章部分来学习

函数声明

avformat_find_stream_info函数定义在libavformat/avformat.h文件中,如下所示:

/**
 * Read packets of a media file to get stream information. This
 * is useful for file formats with no headers such as MPEG. This
 * function also computes the real framerate in case of MPEG-2 repeat
 * frame mode.
 * The logical file position is not changed by this function;
 * examined packets may be buffered for later processing.
 *
 * @param ic media file handle
 * @param options  If non-NULL, an ic.nb_streams long array of pointers to
 *                 dictionaries, where i-th member contains options for
 *                 codec corresponding to i-th stream.
 *                 On return each dictionary will be filled with options that were not found.
 * @return >=0 if OK, AVERROR_xxx on error
 *
 * @note this function isn't guaranteed to open all the codecs, so
 *       options being non-empty at return is a perfectly normal behavior.
 *
 * @todo Let the user decide somehow what information is needed so that
 *       we do not waste time getting stuff the user does not need.
 */
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

函数的注释中对功能进行了大体上的描述,这个函数会从媒体文件中读取视频packet(packet也就是未解码之前的视频数据)来获取到stream的信息,对于没有文件头的视频格式非常好用,比如MPEG文件(因为视频文件经常使用文件头header来标注这个视频文件的各项信息,比如muxing格式等等,在上一篇的read_head讲解中我们也看得出来),在一些特殊情况这个函数还会用于计算帧率,比如MPEG-2重复帧模式下。
函数参数有两个,一个是AVFormatContext的上下文句柄,即这个函数实际的操作对象,还有一个是AVDictionary数组,如果第二个参数传入时不为空的话,那么它应该是一个长度等于ic中包含的nb_stream的AVDictionary数组,第i个数组对应视频文件中第i个stream,函数返回时,每一个dictionary将填充没有找到的选项。
返回值为大于等于0的数字,返回>=0代表没有其他情况,其他数字代表不同的错误
在该函数中不能保证stream中的编解码器会被打开,所以在返回之后如果options不为空也是正常的。

从注释中就能看出,这个函数功能复杂而且还非常重要,所以也难怪它有五六百行了。在五六百行中包含对视频文件中总共nb_streams个视频stream的多次循环,所以下面也按照每次循环所做的事情来学习好了,由于函数实现整体代码太长,也按照一小部分一小部分来贴

函数实现0-第一次循环前做的准备

函数的具体实现在libavformat/utils.c文件中,在进入第一次循环前,先要做好一些准备。

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
{
    int i, count = 0, ret = 0, j;
    int64_t read_size;
    AVStream *st;
    AVCodecContext *avctx;
    AVPacket pkt1, *pkt;
    int64_t old_offset  = avio_tell(ic->pb);
    // new streams might appear, no options for those
    int orig_nb_streams = ic->nb_streams;
    int flush_codecs;
    int64_t max_analyze_duration = ic->max_analyze_duration;
    int64_t max_stream_analyze_duration;
    int64_t max_subtitle_analyze_duration;
    int64_t probesize = ic->probesize;
    int eof_reached = 0;
    int *missing_streams = av_opt_ptr(ic->iformat->priv_class, ic->priv_data, "missing_streams");

    flush_codecs = probesize > 0;

    av_opt_set(ic, "skip_clear", "1", AV_OPT_SEARCH_CHILDREN);

    max_stream_analyze_duration = max_analyze_duration;
    max_subtitle_analyze_duration = max_analyze_duration;
    if (!max_analyze_duration) {
        max_stream_analyze_duration =
        max_analyze_duration        = 5*AV_TIME_BASE;
        max_subtitle_analyze_duration = 30*AV_TIME_BASE;
        if (!strcmp(ic->iformat->name, "flv"))
            max_stream_analyze_duration = 90*AV_TIME_BASE;
        if (!strcmp(ic->iformat->name, "mpeg") || !strcmp(ic->iformat->name, "mpegts"))
            max_stream_analyze_duration = 7*AV_TIME_BASE;
    }

    if (ic->pb)
        av_log(ic, AV_LOG_DEBUG, "Before avformat_find_stream_info() pos: %"PRId64" bytes read:%"PRId64" seeks:%d nb_streams:%d\n",
               avio_tell(ic->pb), ic->pb->bytes_read, ic->pb->seek_count, ic->nb_streams);

首先是变量声明定义部分(在这里额外插入一点,C文件不像C++,函数中的变量一般在变量第一次用到的时候再进行定义,FFmpeg中大部分的代码中,变量都是在函数体刚开始时就声明和定义好的),定义了一些在后面的操作中可能用到和发生变化的变量,例如为读入视频packet数据做准备的pkt,记录原始stream数量的orig_nb_streams(因为可能会发现新的avstream,所以需要先记录好原始stream数量),old_offset,使用了avio_tell函数获取到了原始的初始offset,还有老朋友probe_size,探测大小,这个变量使用在avformat_open_input中经常见到。而读入视频packet来探测stream信息可能会遇到读到EOF文件末尾的情况,所以需要eof_reached标记位…等等变量,在下面的代码实现中再去看看这些变量有哪些具体用处。

在函数中的17行,首先使用av_opt_ptr来尝试获取ic->priv_data中是否存在miss_stream,之后,如果探测数据probesize>0,那么代表编解码器codec需要进行后续包含的flush操作,因为在函数中会读取数据进行probe。

之后,函数的21行,在句柄ic中设置好option“skip_clear”,再设置好两个stream和subtitile的持续时间,在stream持续时间为0时,则需要函数主动设置好持续时间,这个过程中还包含两个特殊情况,文件格式为flv以及mpeg时,函数主动设置的持续时间会有变化。
也就是说,到这里为止,在AVformatContext中设置好一个option后,根据具体的文件格式,设置好max_stream_analyze_duration相关数据,根据宏定义AV_TIME_BASE 1000000为基数来调整max_stream_analyze_duration.

在这一小段的最后,检查句柄中pb的存在,并在DEBUG级别的日志输出中打印在真正开始find_stream操作前,部分重要参数的情况。之后,会开始这个函数中第一次的大循环。

函数实现1-第一次循环的内容

第一次循环的内容,先来看一下代码:

    for (i = 0; i < ic->nb_streams; i++) {
        const AVCodec *codec;
        AVDictionary *thread_opt = NULL;
        st = ic->streams[i];
        avctx = st->internal->avctx;

        if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
            st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
/*            if (!st->time_base.num)
                st->time_base = */
            if (!avctx->time_base.num)
                avctx->time_base = st->time_base;
        }

        /* check if the caller has overridden the codec id */
#if FF_API_LAVF_AVCTX
FF_DISABLE_DEPRECATION_WARNINGS
        if (st->codec->codec_id != st->internal->orig_codec_id) {
            st->codecpar->codec_id   = st->codec->codec_id;
            st->codecpar->codec_type = st->codec->codec_type;
            st->internal->orig_codec_id = st->codec->codec_id;
        }
FF_ENABLE_DEPRECATION_WARNINGS
#endif
        // only for the split stuff
        if (!st->parser && !(ic->flags & AVFMT_FLAG_NOPARSE) && st->request_probe <= 0) {
            st->parser = av_parser_init(st->codecpar->codec_id);
            if (st->parser) {
                if (st->need_parsing == AVSTREAM_PARSE_HEADERS) {
                    st->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
                } else if (st->need_parsing == AVSTREAM_PARSE_FULL_RAW) {
                    st->parser->flags |= PARSER_FLAG_USE_CODEC_TS;
                }
            } else if (st->need_parsing) {
                av_log(ic, AV_LOG_VERBOSE, "parser not found for codec "
                       "%s, packets or times may be invalid.\n",
                       avcodec_get_name(st->codecpar->codec_id));
            }
        }

        if (st->codecpar->codec_id != st->internal->orig_codec_id)
            st->internal->orig_codec_id = st->codecpar->codec_id;

        ret = avcodec_parameters_to_context(avctx, st->codecpar);
        if (ret < 0)
            goto find_stream_info_err;
        if (st->request_probe <= 0)
            st->internal->avctx_inited = 1;

        codec = find_probe_decoder(ic, st, st->codecpar->codec_id);

        /* Force thread count to 1 since the H.264 decoder will not extract
         * SPS and PPS to extradata during multi-threaded decoding. */
        av_dict_set(options ? &options[i] : &thread_opt, "threads", "1", 0);

        if (ic->codec_whitelist)
            av_dict_set(options ? &options[i] : &thread_opt, "codec_whitelist", ic->codec_whitelist, 0);

        /* Ensure that subtitle_header is properly set. */
        if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE
            && codec && !avctx->codec) {
            if (avcodec_open2(avctx, codec, options ? &options[i] : &thread_opt) < 0)
                av_log(ic, AV_LOG_WARNING,
                       "Failed to open codec in %s\n",__FUNCTION__);
        }

        // Try to just open decoders, in case this is enough to get parameters.
        if (!has_codec_parameters(st, NULL) && st->request_probe <= 0) {
            if (codec && !avctx->codec)
                if (avcodec_open2(avctx, codec, options ? &options[i] : &thread_opt) < 0)
                    av_log(ic, AV_LOG_WARNING,
                           "Failed to open codec in %s\n",__FUNCTION__);
        }
        if (!options)
            av_dict_free(&thread_opt);
    }

这是对文件中包含的所有流stream进行的第一次循环,在进入循环时,先获取到当前的AVStream以及avctx变量。
之后,判断stream类型是否为音频流或者字幕流,如果是音频/字幕流,那么设置好avctx的time_base
在完成time_base设置后,检查函数调用者(APP层,调用ffmpeg api之前,对ffmpeg来说就是码代码用接口的我啦)是否覆盖了codec id,如果是的话,根据覆盖重写后的codec_id进行配置,设置好st的codec_id等codec选项;之后,通过一个if来综合判断stream st中是否包含解析器parser,如果没有解析器parser的话,就根据codec_id来创建一个解析器,并根据need_parsing(该标记会指明stream中哪些部分需要进行分析parser,比如只分析header的AVSTREAM_PARSER_HEADERS,或者全部parser,AVSTREAM_PARSER_FULL,标记位的各个标识含义在libavformat/avformat.h的AVStreamParserType中进行了定义)设置好parser的flags标记位。
之后,将st中internal的原有orig_codec_id设置为现在codecpar的codec_id(当然是在二者不同的情况下),然后,使用avcodec_parameters_to_context函数,来将解析器st->codecpar中的参数对应的拷贝到avctx(AvcodecContext)当中,这个拷贝函数先挖个小坑(因为确实不复杂),之后,就是根据codec_id来进行推测,对应地找到编解码器codec,使用的函数是find_probe_decoder(ic, st, st->codecpar->codec_id);
在完成codec设置之后,对options做好设置,将进程设置为1,根据白名单情况设置好白名单选项,再使用avcodec_open2来对字幕编解码器类型的编解码器进行open,因为光在设置好编解码器avcodec后,这个codec并不是处于open的状态,所以需要进行open,avcodec_open2也会是后面的一个文章小坑(小坑++),而音视频类型的codec则需要在这个函数外边再打开了,这里只是对字幕类型单独拿出来处理。
在处理了字幕类型并打开codec之后,释放掉option,完成第一轮的遍历。

结束时说的话

看上去几千字实际上只是这个函数的刚刚开始,后续还要更新更多关于find_stream函数的内容,下一次攒够了字数咱们继续吹牛比(不是

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值