原文链接:https://blog.csdn.net/weixin_39399492/article/details/132782704
概述
1、ffmpeg在转码过程中,输出文件的帧率如何确定。
2、深入ffmpeg源码,剖析帧率的数据来源,涉及到的有 基本帧率(tbr/r_frame_rate)、平均帧率(fps/avg_frame_rate)以及编码器帧率(framerate)的数值是如何获取和计算的,以及它们之间的关系。
3、上述数据如何通过ffmpeg/ffprobe/ffplay获取。
一、基本概念
了解清楚主要变量的概念有助于后续文章的理解。
1、r_frame_rate:基本帧率,AVStream中的变量。
(1)这是能够准确表示所有时间戳的最低帧率(它是流中所有帧率的最小公倍数)。注意,这个值只是一个猜测值。
如上的解释中,1800 是 3600和1800的最小公倍数,因此使用 90000/1800 = 50/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)
三、详细计算过程
通过第二章的分析可知最终的输出帧率是r_frame_rate、avg_frame_rate、framerate中的一个,本章分别分析这3个帧率的数据来源和计算过程。
3.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_frame、ff_rfps_calculate来计算帧率,和步骤二计算帧率调用的函数是相同的。不同点是此时ff_rfps_add_frame、ff_rfps_calculate是在avformat_find_stream_info中被调用,此时是真正读取了音视频数据(调用read_frame_internal)来获取的帧信息,再送入ff_rfps_add_frame、ff_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计算过程”
3.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)
3.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)