12关于FFmpeg的四种时基和时基相关函数的分析及其场景用法
前言:
AVRational类型:是一个分数。例如{1,25}表示1除以25即1/25。
时基:时间的单位,在ffmpeg下被描述成时基。
时间戳:某个时刻的时间。
1 FFmpeg的四种时基
抛开解码器上下文的时基即stream->codec->time_base(因为编码时基本不用,所以我这里不讲它,它也叫tbc,称之为流中的时基)。我们FFmpeg实际上编码经常使用的时基有四种:
- 1)输入流封装上下文的时基,也叫容器的时基,即AVFormatCtx->stream->time_base。
- 2)输出流封装上下文的时基,也叫容器的时基,即AVFormatCtx->stream->time_base。(注意:编码时输入输出各有一个封装上下文,当然也有可能没有输入流的封装上下文,但是这里为了分析四种时基特意不考虑)
- 3)我们人类实际的时基,也就是以秒为单位。
- 4)FFmpeg的内部时基AV_TIME_BASE,百万,故单位为微妙。
解释:
1)输入流封装上下文的时基
例如flv为1000,ts的输入流为90k。
2)输出流封装上下文的时基
这个是我们想要输出时的容器格式的时基,同上面输入一样,如果你想输出flv格式的,那么时基为1000,想输出ts格式的那么时基为90k。
3)人类实际的时基
没什么好讲的,就是转来给人类看的。
4)内部时基AV_TIME_BASE
定义如下:
#define AV_TIME_BASE 1000000 //百万,故单位为微妙
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE} //上面倒数
我这里为什么将FFmpeg的时基列为四种呢?
主要是我们在编码的时候使用的时基只有下面四种。
何为转时基:转时基就是将输入容器中以输入容器时基为单位的时间戳,转换成输出容器中的以输出容器时基为单位的时间戳。
例如输入容器的时间戳为80,时基为1000,想要输出成ts格式的流,那么由于输入输出帧率是要一样的,所以1000/80=90000/x得出x=7200(实际上我下一篇关于推流时pts的运算步骤也讲到)。
有人会想不懂为什么时基/时间戳=帧率,例如1s下帧率为25那么每帧时间戳为0.04,那么时基1除以时间戳0.04=帧率25。
所以这里我们编码时已经用到了两种时基。
那么还有两种为什么要包含进来呢?
因为另外两种时基在转时基时非常常用,例如人类的实际时间经常依靠帧率运算出每帧显示的时长,而FFmpeg的内部时基被用于转换人类实际的时长,当然你也可以不转,但是转了能更精确表示,但需要舍弃小数,难度相对大点。
例如ffmpeg内部时基与人类标准的时间转换方法:
timestamp(ffmpeg内部的时间戳) = AV_TIME_BASE * time(秒)//乘以一百万即获取到微秒单位的内部时间戳,人为认为它是微秒单位
time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部的时间戳)//上面公式转换得出。即对应除以一百万即可得出原来的标准秒数。
2 与时基转换相关的函数
1)av_q2d函数。
/*
* 作用:下面看到,该函数非常简单,只是将传入的AVRational类型转为double类型返回。
*/
static inline double av_q2d(AVRational a){
return a.num / (double) a.den;
}
但是该函数的用于主要是将某个时基转成doule(可以有小数保留,更加精确),以作计算。
例如将标准时间戳转为内部时间的做法是标准时间乘以对应时基,那么下面利用该函数同样也可以做到。
packet播放时刻值:timestamp(单位秒) = packet.pts × av_q2d(stream.time_base);//除法通常是变大单位,例如stream.time_base={1,1000}那么pts*(1/1000)即pts/25变成大单位time_base。注意:stream.time_base是容器的时基tbn,而stream->codec->time_base是编解码器的时基即tbc
packet播放时长值:duration(单位秒) = packet.duration × av_q2d(stream.time_base);
//实际上上面内部时间戳与标准时间戳的转换也可以写成(并且通常这样做):
timestamp(ffmpeg内部的时间戳) = av_q2d({AV_TIME_BASE,1}) * time(秒) //乘以一百万
time(秒) = av_q2d(AV_TIME_BASE_Q) * timestamp(ffmpeg内部的时间戳) //除以一百万
该函数可以使平常的秒按照自己的算法转换成一定的时间戳表示。
注意下面,不能简单的认为是秒转成time_base然后再转成微秒AV_TIME_BASE,毕竟若是秒转成time_base按照上面内部时基的转法,需要在此之前乘以time_base,这里是直接转了,所以我们这里可以认为是一种自定义运算方法。实际上这里也是雷神博主的做法:
int64_t pts = sec / av_q2d(ifmt_ctx->streams[videoindex]->time_base)*AV_TIME_BASE;
2)av_rescale_q函数
作用是将时间戳从一个时基转到另一个时基。常用于将输入容器的时基转成输出容器的时基。例如:
注意:in_stream->time_base,out_stream->time_base在我这里指的是容器的时基(但是很多人也叫视频流的时基,所以很容易歧义),stream->codec->time_base指的是编解码器上下文的时基。
pkt.pts = av_rescale_q(pkt.pts, in_stream->time_base, out_stream->time_base);
pkt.dts = av_rescale_q(pkt.dts, in_stream->time_base, out_stream->time_base);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
3)av_rescale_q_rnd函数
这个函数实际上和上面的av_rescale_q函数一样,但是多了一个参数4可以四舍五入小数,对于FFmpeg的pts需要比较精确,这个函数确实是不错的选择,但是要求比较高,该参数设置需要按照不同视频设置去设置,否则容易报错。
例如:
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
好了,关于时基和其相关函数的讲述差不多了,下一篇我们将讲到使用FFmpeg编码推流时,pts的计算步骤,和运用这些时基及其相关函数理解,因为pts的计算实际上并无固定的算法,你的视频流畅你就是牛逼。
pts的计算有两种可能,一是没有输入流的封装上下文的计算(即没有相关属性,需要自己初始化一些内容),只有输出流的封装上下文;二是有输入流和输出流的封装上下文的计算。但两种的计算步骤是差不多的,只不过内部处理方式有点区别。