ffmpeg帧率计算

目录

Abstract

一、基本概念

二、整体流程分析

三、详细计算过程

2.1 r_frame_rate计算过程

2.2 avg_frame_rate计算过程

2.3 framerate计算过程

四、帧率查看

1、本文基于ffmpeg5.0代码分析,不同版本代码可能在细微逻辑处有差异,但是整体思路是相同的

Abstract

1、ffmpeg在转码过程中,输出文件的帧率如何确定。

2、深入ffmpeg源码,剖析帧率的数据来源,涉及到的有 基本帧率(tbr/r_frame_rate)、平均帧率(fps/avg_frame_rate)以及编码器帧率(framerate)的数值是如何获取和计算的,以及它们之间的关系。

3、上述数据如何通过ffmpeg/ffprobe/ffplay获取。

一、基本概念

了解清楚主要变量的概念有助于后续文章的理解。

1、r_frame_rate:基本帧率,AVStream中的变量。

(1)这是能够准确表示所有时间戳的最低帧率(它是流中所有帧率的最小公倍数)。注意,这个值只是一个猜测值。

(2)在ffmpeg中是根据帧信息计算出来的。

2、avg_frame_rate:平均帧率,AVStream中的变量。

(1)解封装时,在创建流或者在avformat_find_stream_info()中由libavformat设置。封装时,在avformat_write_header()之前由调用者设置。

(2)在ffmpeg中是根据封装信息计算出来的。

3、framerate:编码器中的帧率信息,AVCodecContext中的变量。

(1)解码时,是通过sps中的信息计算出来的,未知时为{0,1}。编码时,可能用于向编码器传递CFR内容的帧率信息。

(2)在ffmpeg中是根据编码器(sps)信息计算出来的。

二、整体流程分析

1、ffmpeg转码的过程中,如果用户指定了转码帧率则按照指定的帧率输出,如果没有指定帧率则输出的帧率是ffmpeg根据帧信息、封装信息、编码器信息(sps)计算出来的帧率。

(1)主要代码如下:原函数:static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter,AVFilterInOut *in)

其中:

        ist->framerate是用户指定的转码帧率

        ist->framerate_guessed是ffmpeg根据帧信息、封装信息、编码器信息(sps)计算出来的帧率

2、framerate_guessed的默认值是r_frame_rate,如果r_frame_rate、avg_frame_rate、framerate的值相差的比较多,可能认为r_frame_rate有异常,则framerate_guessed会被赋值给avg_frame_rate或framerate。也就是说在没有指定帧率的情况下,ffmpeg的输出帧率是r_frame_rate、avg_frame_rate、framerate中的其中一个。

(1)完整代码如下:

原函数:AVRational av_guess_frame_rate(AVFormatContext *format, AVStream *st, AVFrame *frame)

其中:

        r_frame_rate含义详见“一、基本概念”,是根据帧信息计算出来的,后面会详细介绍。

        avg_frame_rate含义详见“一、基本概念”,是从文件的封装数据中获取的,不同格式的获取方式有所不同,后面会详细介绍。

        framerate含义详见“一、基本概念”,是从编码器信息(sps)中计算得出的,后面会详细介绍。

3、上述函数av_guess_frame_rate在函数avformat_find_stream_info中的函数ist_add中被调用

(1)主要代码如下:原函数:static int ist_add(const OptionsContext *o, Demuxer *d, AVStream *st)

其中:

        ist->framerate是用户指定的帧率。例如 ffmpeg -r 30 -i input output 

        ist->framerate_guessed是上一步计算出来的帧率,也就是 基本帧率(r_frame_rate)、平均帧率(avg_frame_rate) 、 编码器帧率(framerate) 中的一个。

(2)完整调用过程可以参考函数:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)

static int ist_add(const OptionsContext *o, Demuxer *d, AVStream *st)

三、详细计算过程

1、通过第二章的分析可知最终的输出帧率是r_frame_rate、avg_frame_rate、framerate中的一个,本章分别分析这3个帧率的数据来源和计算过程。

2.1 r_frame_rate计算过程

1、r_frame_rate是通过帧的dts计算出来的。获取帧的dts的方法有两种,一种是从封装数据中获取,如mp4、mov的moov中有对帧的dts的描述,另一种是从文件中直接读取音视频数据来获取帧的dts,如flv、ts。

2、mp4、mov格式的帧信息都在moov中,moov中都是封装信息,中间没有音视频数据,相当于封装数据和音视频数据是分开存放的。所以这两种格式不用读取音视频数据就可以获取帧的dts;而flv、ts的帧信息都在这一帧之前,比如flv tag header,ts packet pes,都是和音视频数据交错存储的,所以要想获取帧的dts就需要读取音视频数据然后再解析其中内容。比如flv是直接读取一个tag,然后再分离出音视频数据和这一帧的封装信息。

(如果需要详细了解这mp4、flv、ts这三种封装格式请参考下述文章:

mp4格式详解:MP4格式详解_~小生的博客-CSDN博客

flv格式详解:FLV格式详解_flv格式解析_~小生的博客-CSDN博客

ts格式详解:TS格式详解_ts文件_~小生的博客-CSDN博客

3、正因为上述原因,mp4、mov的r_frame_rate的计算过程比其它格式都多两个步骤(通过查看ffmpeg代码发现也只有mp4、mov有这两个步骤),即mp4、mov会先读取moov中的信息来计算r_frame_rate,如果没有计算出来再用所有格式通用的方法计算r_frame_rate,如果实在计算不出来还有兜底逻辑来赋值。

4、r_fram_rate的详细计算过程:

遵循的原则:

(1)ffmpeg内部有多处逻辑来计算r_frame_rate,只有当前一个步骤没有计算出r_frame_rate之后才会在下一步骤继续计算。

(2)除了最后一步之外,其它任何步骤有可能无法计算出r_frame_rate。

步骤一:从moov中获取数据,计算r_frame_rate(只有mp4、mov才有此步骤)

(1)如果所有帧的时长相同(sc->stts_count == 1)或者 只有1帧的时长和其它帧不同此时也可以近似理解为所有帧的时长相同(sc->stts_count == 2 &&  sc->stts_data[1].count == 1)时,可以按照这种方法去计算:st->r_frame_rate = sc->time_scale / sc->stts_data[0],但是有很多时候不满足这个条件也就无法计算r_frame_rate,就要去步骤二继续计算。

主要代码如下:

原函数:static int mov_read_trak(MOVContext *c, AVIOContext *pb, MOVAtom atom)

其中:

        sc->time_scale就是mdhd中的timescale

        sc->stts_count就是stts中的entry_count,描述有多少组stts_data

        sc->stts_data.count就是stts中的sample_count,即相同时长的sample的数量

        sc->stts_data.duration就是stts中的sample_delta,即sample的时长

(stts的详细介绍可以参考mp4格式详细介绍:MP4格式详解_~小生的博客-CSDN博客

(2)stts_count、stts_data数据来源,都是从moov中读取的

主要代码如下:

 原函数static int mov_read_stts(MOVContext *c, AVIOContext *pb, MOVAtom atom)

步骤二:从moov中获取数据,计算r_frame_rate(只有mp4、mov才有此步骤,上一步没有计算出r_frame_rate才会走此步骤)

(1)从stts中获取帧的dts信息,然后尝试计算其与标准帧率之间的误差(duration_error),并记录分析的帧数(duration_count)、帧的dts的最大公约数(duration_gcd)等信息,用于后续计算。注意不是每一个帧的dts都参与计算,只是部分帧参与计算,具体多少帧参与计算和stsc中的数据有关。

完整代码如下:


int ff_rfps_add_frame(AVFormatContext *ic, AVStream *st, int64_t ts)
{
    FFStream *const sti = ffstream(st);
    FFStreamInfo *info = sti->info;
    int64_t last = info->last_dts;

    if (   ts != AV_NOPTS_VALUE && last != AV_NOPTS_VALUE && ts > last
       && ts - (uint64_t)last < INT64_MAX) {
        double dts = (is_relative(ts) ?  ts - RELATIVE_TS_BASE : ts) * av_q2d(st->time_base);
        int64_t duration = ts - last;

        if (!info->duration_error)
            info->duration_error = av_mallocz(sizeof(info->duration_error[0])*2);
        if (!info->duration_error)
            return AVERROR(ENOMEM);

        // if (st->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        // av_log(NULL, AV_LOG_ERROR, "%f\n", dts);
        // 遍历标准时间基准数组,计算误差并累加到相应的位置。
        /* 这段代码的目的是对于满足条件的元素,计算其与标准帧率之间的误差,并将误差值和误差平方值累加到相应的数组位置中。
        这可能是为了评估和优化视频或音频的时间基准和帧率相关的处理。具体的应用场景和目的需要根据上下文来确定。*/
        for (int i = 0; i < MAX_STD_TIMEBASES; i++) {
            if (info->duration_error[0][1][i] < 1e10) {
                int framerate = get_std_framerate(i);//计算标准帧率
                double sdts = dts*framerate/(1001*12);
                for (int j = 0; j < 2; j++) {
                    int64_t ticks = llrint(sdts+j*0.5);
                    double error = sdts - ticks + j*0.5;
                    info->duration_error[j][0][i] += error;
                    info->duration_error[j][1][i] += error*error;
                }
            }
        }
        if (info->rfps_duration_sum <= INT64_MAX - duration) {
            info->duration_count++;
            info->rfps_duration_sum += duration;
        }

        //每10帧更新一次误差信息
        if (info->duration_count % 10 == 0) {
            int n = info->duration_count;
            for (int i = 0; i < MAX_STD_TIMEBASES; i++) {
                if (info->duration_error[0][1][i] < 1e10) {
                    double a0     = info->duration_error[0][0][i] / n;//误差平均值
                    double error0 = info->duration_error[0][1][i] / n - a0*a0;//误差方差
                    double a1     = info->duration_error[1][0][i] / n;
                    double error1 = info->duration_error[1][1][i] / n - a1*a1;
                    if (error0 > 0.04 && error1 > 0.04) {//如果 error0 和 error1 都大于阈值0.04,表示将误差设置为一个较大的值。
                        info->duration_error[0][1][i] = 2e10;
                        info->duration_error[1][1][i] = 2e10;
                    }
                }
            }
        }

        // ignore the first 4 values, they might have some random jitter
        if (info->duration_count > 3 && is_relative(ts) == is_relative(last)) {
            info->duration_gcd = av_gcd(info->duration_gcd, duration);
            av_log(ic, AV_LOG_DEBUG, "my ff_rfps_add_frame info->duration_gcd: %lld duration: %lld\n", info->duration_gcd, duration);
        } 
    }
    if (ts != AV_NOPTS_VALUE)
        info->last_dts = ts;

    return 0;
}

上层调用函数:static void mov_build_index(MOVContext *mov, AVStream *st)

ff_rfps_add_frame执行一次只是用一帧的数据进行计算,每调用一次ff_rfps_add_frame就相当于多分析一帧,分析的帧数(duration_count)就加1。调用次数(分析的帧数)取决于stsc中的数据,不同的mp4、mov文件调用的次数也不同,详细调用过程可参考函数:static void mov_build_index(MOVContext *mov, AVStream *st)

(2)ff_rfps_add_frame计算的数据,会在ff_rfps_calculate中使用到,ff_rfps_calculate才是真正计算r_frame_rate的地方。从代码中可以看到,当r_frame_rate为0(前面的过程没有计算帧率或者没有计算出帧率)的时候,进入到ff_rfps_calculate时才会计算帧率,否则ff_rfps_calculate不会重新计算r_frame_rate。

完整代码如下:

void ff_rfps_calculate(AVFormatContext *ic)
{
    for (unsigned i = 0; i < ic->nb_streams; i++) {
        AVStream *const st  = ic->streams[i];
        FFStream *const sti = ffstream(st);

        if (st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO)
            continue;
        // the check for tb_unreliable() is not completely correct, since this is not about handling
        // an unreliable/inexact time base, but a time base that is finer than necessary, as e.g.
        // ipmovie.c produces.
        if (tb_unreliable(ic, st) && sti->info->duration_count > 15 && sti->info->duration_gcd > FFMAX(1, st->time_base.den/(500LL*st->time_base.num)) && !st->r_frame_rate.num &&
            sti->info->duration_gcd < INT64_MAX / st->time_base.num) {
                av_reduce(&st->r_frame_rate.num, &st->r_frame_rate.den, st->time_base.den, st->time_base.num * sti->info->duration_gcd, INT_MAX);
            }
        if (sti->info->duration_count > 1 && !st->r_frame_rate.num
            && tb_unreliable(ic, st)) {
            int num = 0;
            double best_error = 0.01;
            AVRational ref_rate = st->r_frame_rate.num ? st->r_frame_rate : av_inv_q(st->time_base);

            //根据视频流的信息计算最佳的帧率。
            for (int j = 0; j < MAX_STD_TIMEBASES; j++) {
                if (sti->info->codec_info_duration &&
                    sti->info->codec_info_duration*av_q2d(st->time_base) < (1001*11.5)/get_std_framerate(j))
                    continue;
                if (!sti->info->codec_info_duration && get_std_framerate(j) < 1001*12)
                    continue;

                if (av_q2d(st->time_base) * sti->info->rfps_duration_sum / sti->info->duration_count < (1001*12.0 * 0.8)/get_std_framerate(j))
                    continue;

                for (int k = 0; k < 2; k++) {
                    int n = sti->info->duration_count;
                    double a = sti->info->duration_error[k][0][j] / n;
                    double error = sti->info->duration_error[k][1][j]/n - a*a;

                    if (error < best_error && best_error> 0.000000001) {
                        best_error= error;
                        num = get_std_framerate(j);
                    }
                    if (error < 0.02)
                        av_log(ic, AV_LOG_DEBUG, "rfps: %f %f\n", get_std_framerate(j) / 12.0/1001, error);
                }
            }
            // do not increase frame rate by more than 1 % in order to match a standard rate.
            // 不要为了匹配标准速率而将帧速率增加超过1%。
            if (num && (!ref_rate.num || (double)num/(12*1001) < 1.01 * av_q2d(ref_rate))){
                av_reduce(&st->r_frame_rate.num, &st->r_frame_rate.den, num, 12*1001, INT_MAX);
            }
        }
        if (   !st->avg_frame_rate.num
            && st->r_frame_rate.num && sti->info->rfps_duration_sum
            && sti->info->codec_info_duration <= 0
            && sti->info->duration_count > 2
            && fabs(1.0 / (av_q2d(st->r_frame_rate) * av_q2d(st->time_base)) - sti->info->rfps_duration_sum / (double)sti->info->duration_count) <= 1.0
            ) {
            av_log(ic, AV_LOG_DEBUG, "Setting avg frame rate based on r frame rate\n");
            st->avg_frame_rate = st->r_frame_rate;
        }

        av_freep(&sti->info->duration_error);
        sti->info->last_dts = AV_NOPTS_VALUE;
        sti->info->duration_count = 0;
        sti->info->rfps_duration_sum = 0;
    }
}

上层调用函数:static int mov_read_header(AVFormatContext *s)

(3)从ff_rfps_calculate中可以看出,每一个试图计算r_frame_rate的过程之前都有很多判断条件,都满足之后才会计算r_frame_rate,所以此处也有可能无法计算出来r_frame_rate,就要去步骤三继续计算。

步骤三:读取音视频数据,计算r_frame_rate(所有格式均有此步骤。相当于mp4、mov的步骤三mp4、mov的上一步没有计算出r_frame_rate才会走此步骤,其它格式的步骤一)

(1)步骤三依然会用ff_rfps_add_frameff_rfps_calculate来计算帧率,和步骤二计算帧率调用的函数是相同的。不同点是此时ff_rfps_add_frameff_rfps_calculate是在avformat_find_stream_info中被调用,此时是真正读取了音视频数据(调用read_frame_internal)来获取的帧信息,再送入ff_rfps_add_frameff_rfps_calculate来对r_frame_rate计算。

(2)步骤三和步骤二的第二个不同点是分析的帧数不同也就是调用ff_rfps_add_frame的次数不同,步骤二分析的帧数取决于stsc中的数据。步骤三中分析的帧数默认是20帧,由变量fps_analyze_framecount标记,如果遇到特殊情况也会进行调整,当然也可以通过命令行设置。

主要代码如下:

原函数:int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)

(3)和步骤二无法计算出r_frame_rate的原因一样,步骤三也有可能无法计算出r_frame_rate,就要去步骤四继续计算。

步骤四:兜底逻辑(所有格式均有此步骤,上一步没有计算出r_frame_rate才会走此步骤。相当于mp4、mov的步骤四,其它格式的步骤二)

(1)计算r_frame_rate的最后一步,也相当于兜底逻辑,一定会计算出r_frame_rate。如果前三步都没有计算出r_frame_rate,会在步骤四继续计算。

(2)r_frame_rate的最后的兜底值有可能是framerate的mul倍,也有可能是时间基的倒数。

主要代码如下:

原函数:int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)

其中:

        desc是包含编码器的各种属性

        desc->props是编码器的AV_Codec_PROP_*标志的组合

        AV_CODEC_PROP_FIELDS是指视频编解码器支持对隔行帧中的字段进行单独编码

        avctx->framerate是编码器帧率,是从sps的信息中计算而来,计算过程可以参考“2.3 framerate计算过程”

2.2 avg_frame_rate计算过程

1、不同格式计算avg_frame_rate的逻辑不同,但是含义是相同的,都是 总帧数/总时长,并且都是从封装信息中直接读取或间接计算出来的。如果封装格式中缺少相关信息,也有兜底逻辑给avg_frame_rate赋值。

2、avg_fram_rate的详细计算过程:

步骤一:从封装信息中直接读取或间接计算出来

(1)mp4、mov格式计算如下:

原函数:static int mov_read_header(AVFormatContext *s)

其中:

        sc->time_scale是mdhd中的timescale

        sc->nb_frames_for_fps是总帧数,就是将stts中的sample_count累加

        sc->duration_for_fps每一帧时长的和

原函数:static int mov_read_stts(MOVContext *c, AVIOContext *pb, MOVAtom atom)

(2)flv格式计算如下:

原函数:static int amf_parse_object(AVFormatContext *s, AVStream *astream,AVStream *vstream, const char *key,int64_t max_pos, int depth)

其中:

        num_val是从metadata中的"framerate"字段读出来的数据,num_val/1000才是avg_frame_rate

步骤二:兜底逻辑,如果文件的封装信息异常可能无法计算出avg_frame_rate,这时就需要其它数据来计算avg_frame_rate。有多处兜底逻辑,按照调用顺序一一介绍。

(1)兜底逻辑一:当avg_frame_rate=0并且r_frame_rate!=0时,可能将r_frame_rate赋值给avg_frame_rate

原函数:void ff_rfps_calculate(AVFormatContext *ic)

(2)兜底逻辑二:根据编码器信息(codec_info_duration_fields)和帧的时长总和(codec_info_duration)计算avg_frame_rate,然后再和标准帧率(std_fps)进行比较,将最接近avg_frame_rate的标准帧率赋值给avg_frame_rate

原函数:int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)

其中:

        sti->info->codec_info_duration_fields是编码器信息,数据基本来源于AVCodecParserContext中的信息

        sti->info->codec_info_duration是读取的帧的时长和

        std_fps是标准帧率

原函数:int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)

2.3 framerate计算过程

1、编码器帧率,通过sps中的vui_parameters的信息计算出来的,和格式无关。

                        acvtx->framerate = time_scale / (num_nuits_in_tick*2)

主要代码如下:

原函数:static inline int parse_nal_units(AVCodecParserContext *s,AVCodecContext *avctx,const uint8_t * const buf, int buf_size)

其中:(time_scale & num_units_in_tick - 简书

        sps->timing_info_present_flag是标志位,等于1表示num_units_in_tick,time_scale在比特流中存在,等于0表示num_units_in_tick,time_scale在比特流中不存在。

        sps->time_scale是一秒钟内经过的时间单位数(即将1秒分成多少份)。 例如,一个使用27 MHz时钟测量时间的时间坐标系统具有时间刻度为27,000,000。 time_scale必须大于0。

        sps->num_units_in_tick是以time_scale Hz频率运行的时钟的时间单位数,它对应于时钟计数器的一个增量(称为时钟刻度)。num_units_in_tick应大于0。时钟刻度是编码数据中可以表示的最小时间间隔。例如,当视频信号的时钟频率为30000÷1001 Hz时,time_scale可以为30000,num_units_in_tick可以为1001。

sps信息示例:

四、帧率查看

1、ffmpeg、ffplay看到的帧率都是fps和tbr,通过代码可以发现fps就是avg_frame_rate,tbr就是r_frame_rate。

原函数:static void dump_stream_format(const AVFormatContext *ic, int i,int index, int is_output)

2、通过ffmpeg查看帧率:ffmpeg -i input output

可以看到原文件的fps(avg_frame_rate),tbr(r_frame_rate)和输出文件的fps(avg_frame_rate)。如果要查看frame则需要自行增加日志。

3、ffprobe查看帧率:ffprobe -v quiet -show_streams -of json input 可以查看音视频信息,可以查看到r_frame_rate、avg_frame_rate。如果要查看frame则需要自行增加日志。

完整信息如下:

{
    "streams": [
        {
            "index": 0,
            "codec_name": "mp3",
            "codec_long_name": "MP3 (MPEG audio layer 3)",
            "codec_type": "audio",
            "codec_tag_string": "[0][0][0][0]",
            "codec_tag": "0x0000",
            "sample_fmt": "fltp",
            "sample_rate": "44100",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/1000",
            "start_pts": 0,
            "start_time": "0.000000",
            "bit_rate": "128000",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0,
                "captions": 0,
                "descriptions": 0,
                "metadata": 0,
                "dependent": 0,
                "still_image": 0
            }
        },
        {
            "index": 1,
            "codec_name": "flv1",
            "codec_long_name": "FLV / Sorenson Spark / Sorenson H.263 (Flash Video)",
            "codec_type": "video",
            "codec_tag_string": "[0][0][0][0]",
            "codec_tag": "0x0000",
            "width": 720,
            "height": 1280,
            "coded_width": 720,
            "coded_height": 1280,
            "closed_captions": 0,
            "film_grain": 0,
            "has_b_frames": 0,
            "pix_fmt": "yuv420p",
            "level": -99,
            "refs": 1,
            "r_frame_rate": "25/1",
            "avg_frame_rate": "25/1",
            "time_base": "1/1000",
            "start_pts": 25,
            "start_time": "0.025000",
            "bit_rate": "200000",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0,
                "captions": 0,
                "descriptions": 0,
                "metadata": 0,
                "dependent": 0,
                "still_image": 0
            }
        }
    ]
}

4、ffplay查看帧率:ffplay input 可以查看fps(avg_frame_rate),tbr(r_frame_rate)

猜你喜欢

转载自blog.csdn.net/weixin_39399492/article/details/132782704