13关于FFmpeg的pts转时基时的计算步骤(包括无输入流地址和有输入流地址)
理解前提:
相关计算格式:
//1 s与内部时基ms的转换,实际上上一篇文章也讲了
pts(ms)=AV_TIME_BASE*pts(s);
pts(s) = pts(ms)/AV_TIME_BASE;
//2 两帧时间差的计算,注:1是我们的人类的时基单位,即秒(s)
时间差=时基/帧率。//例如帧率为25时,x=1/25=0.04。可换算成知道时基求帧率公式。
//3
pts=时间差*n。//其中n为自增变量,pts为直接运算的结果,不带任何单位,你可以对它进行除时基,即使除了此时也不算是有任何时间单位,你也可以不除。 注意与第一条公式对比,上面除以时基即可转换成新时基单位的时间戳,而我们这里由于是直接计算得出的,所以除了之后也不算任何单位。例如下面雷神直接求出时间戳后再除以相应时基也不算是转换时基单位。即除之前必须有对应的乘法才算。
1 对于有输入输出封装格式上下文的pts计算做法(实际上对于有输入封装格式上下文的,并不需要计算每帧的pts,我们只需要直接转换时基即可,因为输入封装格式上下文内部保留着输入监控视频已计算好的pts,下面雷神转换可能是想练练这中编码做法):
雷神的链接地址:
最简单的基于FFmpeg的推流器(以推送RTMP为例)
- 注意:第四步是可自行根据上面网页地址查看雷神的文章。
雷神的RTMP推流例子pts计算的做法(也就是有输入流地址的做法):
//雷神的RTMP推流例子pts计算的做法:
//这里给出第一步的代码,方便理解第一步。
pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(ifmt_ctx->streams[videoindex]->time_base)*AV_TIME_BASE);
1)首先通过每次自加的自变量frame_index乘以预先求出的两帧时间差,得出每帧的显示时间戳,然后再除以当前流的时基得出对应的时间戳(并非得出当前流的时基的时间戳,只是雷神自己处理的一种方式,你也可以不除,最后乘以内部时基才是转成微秒。例如时间戳(单位一般是s,但这里雷神除以了流时基作直接运算的时间戳)乘以内部时基变成了微秒,再除以内部时基就变成了转换前的时间戳),最后因为除以当前流的时基使得pts非常小,所以雷神在除以之前又把它乘以FFmepg的内部时基一百万转成了内部时基的时间戳。
2)利用av_gettime()(或者av_gettime()_relative())进行延时处理。
3)最后利用av_rescale_q,av_rescale_q_rnd将时基单位为输入流(in_stream->time_base)的时间戳转换成时基单位为输出流(out_stream->time_base)的时间戳。
4)写包。
注意并解释上面的步骤:
- 1) 雷神的第一点时:由于frame_index是通过自增来计算每帧的pts,所以当frame_index自增到一定程度也就播个几小时左右,再与AV_TIME_BASE一百万/time_base的结果相乘,更别说还没有乘以calc_duration,所以该数非常容易溢出int64_t,例如10000乘以10000除以(1000乘以一百万)(假设会溢出)。所以这是不建议这样做的,因为我试过溢出。
建议是:
方法1:计算出每帧的pts后什么也不做,直接送去做输入输出流的时基单位的转换。即去掉除以时基和乘以内部时基后,pkt.pts=(double)(frame_index*calc_duration)。
方法2:将AV_TIME_BASE改小点,但也不能不乘,因为这样会导致在输入流的时基范围内,计算出的pts非常小。例如输入流时基为flv即ifmt_ctx->streams[videoindex]->time_base = {1,1000},两帧时间差为40,那么0x40=0,1x40=40,2x40=80...,再除以1000的话pts几乎是0,所以送去输出流转时基后结果会导致画面跳得非常快相当于画面没变化。
-
2) 雷神的第二点延时处理:
因为延时时是利用av_gettime()(或者av_gettime()_relative())进行延时处理,它们的返回值都是以微秒为单位的AV_TIME_BASE,那么如果不以相同的单位进行运算,那么求出来的结果就会被系统舍弃掉,造成错误的延时计算。 -
3) 时基的转换是针对输入流和输出流的时基转换,即输入容器的视频流的时基ifmt_ctx->streams[videoindex]->time_base和输出容器的视频流的时基ofmt_ctx->streams[videoindex]->time_base的转换,这两个时基被称为tbn,即容器的时基(实际上很多人叫它为视频流的时基)。
例如:
Ex1:
从封装格式为flv的文件推流到封装同样为flv的文件,他们输入输出流的时基都为1000,
假设输入流某个画面pkt.pts=80,那么经过av_rescale_q转时基函数,
转换成输出流的时基单位的时间戳为:80/1000=x/1000,得出x=80,即输出流的时基单位的时间戳也为80。
注:
av_rescale_q:内部转换时基的算法逻辑是:
输入流的pts/输入流的时基=输出流的pts/输出流的时基
Ex2:
将上面例子的输出封装格式变成ts文件,那么输入流的时基1000,输出流的时基变成了90000。
再由公式得出:80/1000=x/90000,x=7200,即输出流的时基单位的时间戳变为7200。
但是FFmpeg内部会继续进行计算,以保证输入输出流的显示时间都是一样的,
该真正的显示时间为pkt_pts_time。
看图理解(这两张图是引用别人博主的,但现在找不到了,如果介意的话我删了哈哈):
可以发现,对于同一个视频帧,它们时间基(tbn)不同因此时间戳(pkt_pts)也不同,但是计算出来的时刻值(pkt_pts_time)是相同的。
看第一帧的时间戳,计算关系:80×{1,1000} = 7200×{1,90000} = 0.080000。
2 对于只有一个输出封装格式上下文的pts计算做法:
1)直接计算出pkt.pts,计算方法可以是上面的乘法或者响应的加法。
2)利用av_gettime()(或者av_gettime()_relative())进行延时处理。
3)进行相应的时基转换(可以不转,笔者没有转)。
4)写包。
//注:我的无输入流的代码不能拿出来,但是对于接入过海康SDK获取到PS流后解析成H264(这一步就代替了无输入流地址),再输出发布流的人应该知道这个意思。
注意:
-
1) 第一点需要注意的是:由于我们在求两帧的时间差时即calc_duration=w1/fps,需要用到输出文件的帧率,但是我们这里没有输入文件即没有输入封装格式上下文中的输入流的帧率,无法拷贝输入文件的帧率到输出文件中,那么我们只能自定义一个输出文件的帧率(放在m_outputContext->streams[0]->r_frame_rate,在new新流是自定义),一般为flv的帧率=25, 所以这一点注意是需要我们自定义一个输出流的帧率。
-
2) 第二点的注意:和雷神上面的例子一样,在进行延时时,延时运算的单位必须保持一致。
-
3) 第三点需要注意的是:由于没有输入文件,即没有输入文件的帧率,我自定义了一个输出文件的帧率,所以输出的放在格式就为flv。若你想改成其它输出流格式的话,你不仅需要改自定义输出流的帧率,还需要改开辟空间和初始化m_outputContext的函数avformat_alloc_output_context2(&this->m_outputContext, nullptr, “flv”, outUrl.c_str())的参3。
所以对于单个输出流的pts计算,想要改变输出流的封装格式,必须修改以下两点:
1)修改avformat_alloc_output_context2的参3为对应的格式。
2)修改上面参3对应的帧率(需要我们自己知道对应封装格式的帧率)。 -
4)写包直接调用函数即可。
int flag_ = av_interleaved_write_frame(this->m_outputContext, &pkt);//可以帮你将音频和视频组合
//int flag_ = av_write_frame(this->m_outputContext, &pkt);//只能写视频帧,当音频帧存在时会出错。
3 总结
经过上面两个例子可以看到,不管是具有两个封装格式上下文的还是只有单个输出封装格式上下文,pts的计算步骤都是一样的,所以总结pts的计算步骤为以下步骤:
1)计算出pts。
2)利用av_gettime()(或者av_gettime()_relative())进行延时处理。
3)进行相应时基的转换。
4)写包。