前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FFmpeg avformat_find_stream_info() 函数源码解析

FFmpeg avformat_find_stream_info() 函数源码解析

作者头像
JoeyBlue
发布2021-09-07 14:51:33
2.3K0
发布2021-09-07 14:51:33
举报
文章被收录于专栏:代码手工艺人代码手工艺人

avformat_find_stream_info() 函数的作用

先来看一下 avformat_find_stream_info() 的头文件里的注释对该函数的介绍,本文我们基于 FFmpeg n4.2 版本的源码分析。

代码语言:javascript
复制
/**
 * 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.
 * ...
 */
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

注释里说这个方法通过读取媒体文件中若干个 packet 来获取流信息,对于 MPEG 这种没有 header 的文件格式比较有用,也可以计算像 MPEG-2 这种支持 repeat mode 的真实帧率。(MPEG-2 支持对于大量静止的画面设置 repeat mode,重复的帧不用编码和存储,可以减少体积)

另外提到这个函数不会修改逻辑文件位置,为了探测流信息所读取到的 packet 不会丢掉,会缓存下来为后面使用。

上面提到的流信息包括音频流的采样率、通道数等,视频包括视频的宽高、pixel format、码率、帧率等信息。

avformat_find_stream_info() 函数体有 600 行左右的代码,我们拆开来看,一些不太重要的部分,这里就直接跳过了。

avformat_find_stream_info() 函数源码解析

我们从这两个循环开始:

代码语言:javascript
复制
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;
    }
    //省略代码
}

for (i = 0; i < ic->nb_streams; i++) {
#if FF_API_R_FRAME_RATE
    ic->streams[i]->info->last_dts = AV_NOPTS_VALUE;
#endif
    ic->streams[i]->info->fps_first_dts = AV_NOPTS_VALUE;
    ic->streams[i]->info->fps_last_dts  = AV_NOPTS_VALUE;
}

这两个循环我们可以先跳过,原因是如果在 avformat_open_input() 之后第一次调用 avformat_find_stream_info(),此时还没有 stream 的信息,所以 ic->nb_streams 为 0(nb_streams 是 stream 的个数),进不去循环体,所以我们可以直接跳过,不影响理解。

接下来这个看着像’死循环’的 for-loop,就是我们重点的分析对象了,为了代码的简洁,这里省略掉一些不影响我们理解整体逻辑的代码。既然是个‘死循环’,如果想跳出来就只有 break 和 goto 语句,我们看的时候多留意一下这两种 case. 我也会在代码的注释里加上 break 的标记,同时也会把一些需要注意的地方加上了我自己的理解(中文部分)。

代码语言:javascript
复制
for (;;) {
    int analyzed_all_streams;
    //break1: 检查是否被打断(或者说取消了继续探测),如果是,直接 break 退出
    if (ff_check_interrupt(&ic->interrupt_callback)) {
        ret = AVERROR_EXIT;
        av_log(ic, AV_LOG_DEBUG, "interrupted\n");
        break;
    }

    /* check if one codec still needs to be handled */
    //这个 for-loop 里做了一些对流信息的检测,如果循环能正常结束,
    //说明流信息的探测基本完成,这时 i == ic->nb_streams;
    //如果中间被 break 了,也就是说某个流的信息还没有完全得到,
    //此时 i < ic->nb_streams 的。
    //(第一次执行的时候,因为还没有流,所以会直接跳过)
    for (i = 0; i < ic->nb_streams; i++) {
        int fps_analyze_framecount = 20;
        int count;

        st = ic->streams[i];
        //codec信息是否完整
        if (!has_codec_parameters(st, NULL))
            break;
        /* If the timebase is coarse (like the usual millisecond precision
         * of mkv), we need to analyze more frames to reliably arrive at
         * the correct fps. */
        if (av_q2d(st->time_base) > 0.0005)
            fps_analyze_framecount *= 2;
        if (!tb_unreliable(st->internal->avctx))
            fps_analyze_framecount = 0;
        if (ic->fps_probe_size >= 0)
            fps_analyze_framecount = ic->fps_probe_size;
        if (st->disposition & AV_DISPOSITION_ATTACHED_PIC)
            fps_analyze_framecount = 0;
        /* variable fps and no guess at the real fps */
        count = (ic->iformat->flags & AVFMT_NOTIMESTAMPS) ?
                   st->info->codec_info_duration_fields/2 :
                   st->info->duration_count;
        if (!(st->r_frame_rate.num && st->avg_frame_rate.num) &&
            st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            if (count < fps_analyze_framecount)
                break;
        }
        // Look at the first 3 frames if there is evidence of frame delay
        // but the decoder delay is not set.
        if (st->info->frame_delay_evidence && count < 2 && st->internal->avctx->has_b_frames == 0)
            break;
        if (!st->internal->avctx->extradata &&
            (!st->internal->extract_extradata.inited ||
             st->internal->extract_extradata.bsf) &&
            extract_extradata_check(st))
            break;
        if (st->first_dts == AV_NOPTS_VALUE &&
            !(ic->iformat->flags & AVFMT_NOTIMESTAMPS) &&
            st->codec_info_nb_frames < ((st->disposition & AV_DISPOSITION_ATTACHED_PIC) ? 1 : ic->max_ts_probe) &&
            (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO ||
             st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO))
            break;
    }
    analyzed_all_streams = 0;
    //上面提到的 missing_streams 起到作用了,判断是否所有流都找到了
    if (!missing_streams || !*missing_streams)
    //上面也提到了,i == ic->nb_streams 时,说明所有的流,以及流信息都没有问题了
    if (i == ic->nb_streams) {
        analyzed_all_streams = 1;
        /* NOTE: If the format has no header, then we need to read some
         * packets to get most of the streams, so we cannot stop here. */
        //如果是有头的封装格式,直接break退出了,如果是像 MPEG 这种没有头的封装格式,需要解析更多的 packets 来探测。
        if (!(ic->ctx_flags & AVFMTCTX_NOHEADER)) {
            /* If we found the info for all the codecs, we can stop. */
            ret = count;
            av_log(ic, AV_LOG_DEBUG, "All info found\n");
            flush_codecs = 0;
            //break2: 所有的流以及流信息都没有问题,正常退出 for 循环
            break;
        }
    }

    //走到这里,说明还有些流没探测出来,或者有些流信息还没完善。

    /* We did not get all the codec info, but we read too much data. */
    //虽然流信息还没完全探测出来,如果已读取到的大小超过了设定的 probesize,也会退出
    if (read_size >= probesize) {
        ret = count;
        for (i = 0; i < ic->nb_streams; i++)
            if (!ic->streams[i]->r_frame_rate.num &&
                ic->streams[i]->info->duration_count <= 1 &&
                ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
                strcmp(ic->iformat->name, "image2"))
                av_log(ic, AV_LOG_WARNING,
                       "Stream #%d: not enough frames to estimate rate; "
                       "consider increasing probesize\n", i);
        //break3: 读取到的大小超过了设定的 probesize,退出
        break;
    }

    //接下来就需要从网络/文件中读取 packet,这个函数里面做的事情很多,
    //拿 flv 来举例子🌰,执行完 read_frame_internal() 函数,
    //正常情况下,音视频对应的 AVStream 结构体会被创建,
    //并且 ic->nb_streams,也就是流的个数也会是正常的值,
    //比如如果包含音频和视频,nb_streams 的值会是 2。

    /* NOTE: A new stream can be added there if no header in file
     * (AVFMTCTX_NOHEADER). */
    ret = read_frame_internal(ic, &pkt1);

    //...

    pkt = &pkt1;

    st = ic->streams[pkt->stream_index];
    //读完packet,增加 read_size,下一轮循环会跟 probesize 做对比
    if (!(st->disposition & AV_DISPOSITION_ATTACHED_PIC))
        read_size += pkt->size;

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

    //处理和更新 dts: st->info->fps_first_dts 和 st->info->fps_last_dts
    if (pkt->dts != AV_NOPTS_VALUE && st->codec_info_nb_frames > 1) {
        //...
        /* update stored dts values */
        if (st->info->fps_first_dts == AV_NOPTS_VALUE) {
            st->info->fps_first_dts     = pkt->dts;
            st->info->fps_first_dts_idx = st->codec_info_nb_frames;
        }
        st->info->fps_last_dts     = pkt->dts;
        st->info->fps_last_dts_idx = st->codec_info_nb_frames;
    }
    if (st->codec_info_nb_frames>1) {
        int64_t t = 0;
        int64_t limit;

        //计算已经读取到的时间长度
        //codec_info_duration:已经取到的packet的总时长
        if (st->time_base.den > 0)
            t = av_rescale_q(st->info->codec_info_duration, st->time_base, AV_TIME_BASE_Q);
        //根据已经读取到的帧数/帧率来计算
        if (st->avg_frame_rate.num > 0)
            t = FFMAX(t, av_rescale_q(st->codec_info_nb_frames, av_inv_q(st->avg_frame_rate), AV_TIME_BASE_Q));
        //根据 fps_last_dts - fps_first_dts 来计算
        if (t == 0
            && st->codec_info_nb_frames>30
            && st->info->fps_first_dts != AV_NOPTS_VALUE
            && st->info->fps_last_dts  != AV_NOPTS_VALUE)
            t = FFMAX(t, av_rescale_q(st->info->fps_last_dts - st->info->fps_first_dts, st->time_base, AV_TIME_BASE_Q));


        //如果流信息都探测完(analyzed_all_streams = 1),limit = max_analyze_duration
        if (analyzed_all_streams)
            limit = max_analyze_duration;
        else if (avctx->codec_type == AVMEDIA_TYPE_SUBTITLE)
            limit = max_subtitle_analyze_duration;
        else limit = max_stream_analyze_duration;

        //如果当前已经读取到packet的总时长 >= 上面的 max_analyze_duration,退出
        if (t >= limit) {
            av_log(ic, AV_LOG_VERBOSE, "max_analyze_duration %"PRId64" reached at %"PRId64" microseconds st:%d\n",
                   limit,
                   t, pkt->stream_index);
            if (ic->flags & AVFMT_FLAG_NOBUFFER)
                av_packet_unref(pkt);
            //break4: 读取到packet的总时间 >= max_analyze_duration
            break;
        }

        //更新已经读取到的packet的总时长
        if (pkt->duration) {
            //...
            st->info->codec_info_duration += pkt->duration;
            //...
        }
    }

    if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
#if FF_API_R_FRAME_RATE
        ff_rfps_add_frame(ic, st, pkt->dts);
#endif
        if (pkt->dts != pkt->pts && pkt->dts != AV_NOPTS_VALUE && pkt->pts != AV_NOPTS_VALUE)
            st->info->frame_delay_evidence = 1;
    }
    if (!st->internal->avctx->extradata) {
        ret = extract_extradata(st, pkt);
        if (ret < 0)
            goto find_stream_info_err;
    }

    /* If still no information, we try to open the codec and to
     * decompress the frame. We try to avoid that in most cases as
     * it takes longer and uses more memory. For MPEG-4, we need to
     * decompress for QuickTime.
     *
     * If AV_CODEC_CAP_CHANNEL_CONF is set this will force decoding of at
     * least one frame of codec data, this makes sure the codec initializes
     * the channel configuration and does not only trust the values from
     * the container. */

    //到这里,调用 try_decode_frame() 对获取的 packet 进行音视频的解码,
    //正常情况下,会得到当前流的所有的解码期信息,
    //比如视频的宽高、pixel format,音频的 sample format, 采样率、通道数等。
    try_decode_frame(ic, st, pkt,
                     (options && i < orig_nb_streams) ? &options[i] : NULL);

    if (ic->flags & AVFMT_FLAG_NOBUFFER)
        av_packet_unref(pkt);

    //已经探测的帧数+1,count总数+1
    st->codec_info_nb_frames++;
    count++;
}

我们用伪代码来简化一下上面代码的主要逻辑:

代码语言:javascript
复制
for (;;) {
    if 所有stream 满足 has_codec_parameters(st, ..)
       || probe_size > 设置值 {
        break 退出;
    } else {
        //继续读取 packet
        read_frame_internal(ic, &pkt1);
        //尝试对读取到的 packet 解码
        try_decode_frame(ic, st, pkt, ...);
    }
}

下面我们详细地来看一下这三个函数的作用。

has_codec_parameters() 、read_frame_internal()、try_decode_frame() 函数的作用

上面提到的 has_codec_parameters() 函数,是一个很重要的函数,它来检测当前的音视频流信息是否完整。

代码语言:javascript
复制
static int has_codec_parameters(AVStream *st, const char **errmsg_ptr)
{
    AVCodecContext *avctx = st->internal->avctx;
    //...
    if (   avctx->codec_id == AV_CODEC_ID_NONE
        && avctx->codec_type != AVMEDIA_TYPE_DATA)
        FAIL("unknown codec");
    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        if (!avctx->frame_size && determinable_frame_size(avctx))
            FAIL("unspecified frame size");
        if (st->info->found_decoder >= 0 &&
            avctx->sample_fmt == AV_SAMPLE_FMT_NONE)
            FAIL("unspecified sample format");
        if (!avctx->sample_rate)
            FAIL("unspecified sample rate");
        if (!avctx->channels)
            FAIL("unspecified number of channels");
        if (st->info->found_decoder >= 0 && !st->nb_decoded_frames && avctx->codec_id == AV_CODEC_ID_DTS)
            FAIL("no decodable DTS frames");
        break;
    case AVMEDIA_TYPE_VIDEO:
        if (!avctx->width)
            FAIL("unspecified size");
        if (st->info->found_decoder >= 0 && avctx->pix_fmt == AV_PIX_FMT_NONE)
            FAIL("unspecified pixel format");
        if (st->codecpar->codec_id == AV_CODEC_ID_RV30 || st->codecpar->codec_id == AV_CODEC_ID_RV40)
            if (!st->sample_aspect_ratio.num && !st->codecpar->sample_aspect_ratio.num && !st->codec_info_nb_frames)
                FAIL("no frame in rv30/40 and no sar");
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        if (avctx->codec_id == AV_CODEC_ID_HDMV_PGS_SUBTITLE && !avctx->width)
            FAIL("unspecified size");
        break;
    case AVMEDIA_TYPE_DATA:
        if (avctx->codec_id == AV_CODEC_ID_NONE) return 1;
    }

    return 1;
}

可以看到音频要检测是否拿到 frame size,sample format, sample rate, channels 等重要参数,视频则会检测视频的 width, pixel format 等等。

代码中,我们可以看到,为了获取音视频流信息,涉及到了两个重要的函数调用:

  1. read_frame_internal() 函数
  2. try_decode_frame() 函数

在讨论他们的作用之前,我们首先以 FLV 封装格式为例(以音频编码为 AAC,视频编码为 H.264 为例),来解释一下为什么需要这两个函数。

FLV 封装格式的大概示意图如下(为了简洁,省略了一些信息,详细细节参考 FLV specification),

-w510
-w510

我们可以这么来理解 FLV 封装格式,除了 header 之外,它里面包含了一系列的 Tag,可能是 Video Tag,也可能是 Audio Tag, 这个是 FLV 文档来定义的。其中每个 Tag 里面包含了一些描述性信息,以及对应的编码后的 AAC 或者 H.264 数据。如果我们分为两层来看的话,一层是 FLV Tag,一层是编码后的音视频数据。

了解这个之后,我们再来试着理解上面提到的 read_frame_internal() 函数和 try_decode_frame() 在这里的作用。

read_frame_internal() 函数本质上就是从网络中读取音视频的 packet,对应到 FLV 格式的话,其实就是读取第一层,也就是 也就是 FLV Tag 信息,从 tag 里可以读取 tag 的一些描述性信息, 这些描述性信息包括(参考自 FLV Specification):

音频:

  1. 编码格式,是 AAC 还是 MP3,还是 Linear PCM?
  2. Sample rate, 采样率,比如 48000
  3. 通道数,单声道还是双声道?(FLV最多支持两个声道)
  4. Bit depth

视频:

  1. 帧类型,关键帧还是中间帧?
  2. 编码格式,H.264 还是 H.263,还是 VP6 等其他格式?

read_frame_internal() 函数能拿到的就是上面的这些信息,这个信息是否够全呢?跟上面提到的 has_codec_parameters() 检测函数里的要求相比,确实还差了一些信息。比如音频的 sample format(比如 AV_SAMPLE_FMT_FLTP),它需要打开音频解码器之后才能确定,sample rate 等信息也是解码后得到的更加准确。

视频的宽高、pixel format 信息是存放在 H.264 流信息里,所以也需要解码之后才能获取到,所以,这里才需要 try_decode_frame() 函数去做解码的工作才能拿到完整的信息。

也就是说 read_frame_internal() 函数负责从第一层 FLV Tag 里获得流信息,try_decode_frame() 函数负责解码第二层的编码数据,来获取更多的流编码信息,最终汇总为完整的流信息,所以两处函数调用都是必要的。

OK,我们这里不展开 read_frame_internal() 函数 和 try_decode_frame() 函数内部的实现,有兴趣的小伙伴可以自己读读源码。

如何合理设置 probesize 来降低播放首屏时间?

在直播场景下,我们为了提高用户的体验,减少首屏时间,希望直播流是秒开的。这个时候我们会希望 avformat_find_stream_info() 函数在可以完成流信息探测完整的情况下,尽可能的早一些返回。根据前面的分析,我们可以知道,read_frame_internal() 函数的耗时依赖网络的情况,try_decode_frame() 函数负责解码,依赖软件/硬件执行的效率,这两者可能都会比较耗时,所以我们要尽可能的减少这两个方法的调用,从而减少 avformat_find_stream_info() 函数执行的时间。

avformat_find_stream_info() 退出的条件有很多个,probesizemax_analyze_durationfps_analyze_framecount 以及是其中的三个:

  1. 已读取的数据 > probesize
  2. 已读取视频帧播放时间长度 > max_analyze_duration

我们先来看 probesizemax_analyze_duration,这两个判断是强制性的,就是说不管是否获取到了完整的流信息,只要达到了这两个条件,就会退出。下面这段代码摘自 avformat_find_stream_info() 函数的前一部分:

代码语言:javascript
复制
...
int64_t max_analyze_duration = ic->max_analyze_duration;
...
int64_t probesize = ic->probesize;
int *missing_streams = av_opt_ptr(ic->iformat->priv_class, ic->priv_data, "missing_streams");


if (!max_analyze_duration) {
    max_stream_analyze_duration =
    max_analyze_duration        = 5*AV_TIME_BASE;
}

这两个值用来设置函数探测流信息的最大 size,以及最大时长。probesize 默认值是 5,000,000 bytes 也就是 5MB 大小,max_analyze_duration 默认值是 5*AV_TIME_BASE, 也就是 5,000,000 micro-seconds 也就是 5秒。

PS: 另外 missing_streams 是个指向 int 的指针,*missing_streams 只要是 > 0,就说明还有某些流没探测到,这个后面的循环有个关键判断会用到。(假设我们要播放的是flv流,有兴趣的同学可以到 flvdec.c 文件搜索一下这个属性)

默认值对直播来说都蛮大的,不过他们都支持在调用 avformat_find_stream_info() 之前手动地设置。那设置多少比较合适呢?

通过我们刚才的分析,理论上获取到一帧视频和一帧音频,并对他们解码,我们就可以拿到完整的音视频信息。所以理论上我们把 probe size 设置为第一次获取完音频和视频帧时所需读取的长度即可。当然这是理想的情况,实际中有诸多意外因素。比如第一帧视频帧通常是关键帧会比较大,对于不同的码率的流,大小差异会比较大,还有些 CDN 下发的流中,前面放了很多的视频帧之后,才有一个音频帧(这种最好要求 CDN 厂商修改)。

可见 probe size 如果设置的太大会导致首帧时间比较长,设置的太小,可能一些 case 下会获取流信息失败,所以需要根据自己流信息的情况(特别是码率,因为码率会影响音频、视频帧的大小)去设置。我们目前用的一种策略是,设置 probe size 为一个针对我们目前直播稍大于上面所说的长度的一个值,应对大部分 case,对于一小部分 case,比如可能要播放外部流,我们除了支持服务器动态配置之外,还会在失败之后,对 probe sizemax_analyze_duration 乘以一个系数之后再重试。

PS: 对于 FLV 格式,具体到理论上的最小的 probe size 的大小,大概等于 >smallest probe_size = sizeof(FLV header) + sizeof(script tag) + sizeof(audio tag of AAC sequence header) + sizeof(audio tag of AAC raw data) + sizeof(video tag of H.264 sequence header) + sizeof(video tag of H.264 NALU data)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-04-022,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • avformat_find_stream_info() 函数的作用
  • avformat_find_stream_info() 函数源码解析
  • has_codec_parameters() 、read_frame_internal()、try_decode_frame() 函数的作用
  • 如何合理设置 probesize 来降低播放首屏时间?
相关产品与服务
云直播
云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档