对于一个电影,帧是这样来显示的:I B B P。现在我们需要在显示B帧之前知道P帧中的信息。因此,帧可能会按照这样的方式来存储:IPBB。这就是为什么我们会有一个解码时间戳和一个显示时间戳的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的流可以是这样的:
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
通常PTS和DTS只有在流中有B帧的时候会不同。
DTS和PTS
音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,如果我们只是简单的通过数帧和乘以帧率的方式来同步视频,那么就很有可能会失去同步。于是作为一种补充,在流中的包有种叫做DTS(解码时间戳)和PTS(显示时间戳)的机制。为了这两个参数,你需要了解电影存放的方式。像MPEG等格式,使用被叫做B帧(B表示双向bidrectional)的方式。另外两种帧被叫做I帧和P帧(I表示关键帧,P表示预测帧)。I帧包含了某个特定的完整图像。P帧依赖于前面的I帧和P帧并且使用比较或者差分的方式来编码。B帧与P帧有点类似,但是它是依赖于前面和后面的帧的信息的。这也就解释了为什么我们可能在调用avcodec_decode_video
以后会得不到一帧图像。
ffmpeg中的时间单位 AV_TIME_BASE
ffmpeg中的内部计时单位(时间基),ffmepg中的所有时间都是以它为一个单位,比如AVStream中的duration即以这个流的长度为duration个AV_TIME_BASE。AV_TIME_BASE 定义为:
#define AV_TIME_BASE 1000000
AV_TIME_BASE_Q
ffmpeg内部时间基的分数表示,实际上它是AV_TIME_BASE的倒数。从它的定义能很清楚的看到这点:
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
AVRatioal 的定义如下:
typedef struct AVRational{
int num; //numerator
int den; //denominator
} AVRational;
ffmpeg提供了一个把 AVRatioal 结构转换成 double 的函数:
复制代码
static inline double av_q2d(AVRational a){
/**
* Convert rational to double.
* @param a rational to convert
**/
return a.num / (double) a.den;
}
复制代码
现在可以根据pts来计算一桢在整个视频中的时间位置:
timestamp(秒) = pts * av_q2d(st->time_base)
计算视频长度的方法:
time(秒) = st->duration * av_q2d(st->time_base)
这里的st是一个AVStream对象指针。
时间基转换公式
timestamp(ffmpeg内部时间戳) = AV_TIME_BASE * time(秒)
time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部时间戳)
所以当需要把视频跳转到N秒的时候可以使用下面的方法:
int64_t timestamp = N * AV_TIME_BASE;
av_seek_frame(fmtctx,index_of_video,timestamp,AVSEEK_FLAG_BACKWARD);
ffmpeg同样为我们提供了不同时间基之间的转换函数:
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)
这个函数的作用是计算a * bq / cq,来把时间戳从一个时基调整到另外一个时基。在进行时基转换的时候,我们应该首选这个函数,因为它可以避免溢出的情况发生。
后记:
因为工作原因写了一个文件解析模块,音视频同步经过很多次修改,考虑过n多种情况终于找到了一个比较合适的方式:
1)视频向音频同步,因为音频必须连续,否则很容易听出问题,视频少一帧没多少问题
2)每解码一个音频帧记录下音频帧时间跨度,这样累加。起来作为一个标准的时间尺。
3)视频时间戳不应当简单的用视频帧间隔乘以帧数,因为对于视频有时候是不连续的。
4)视频时间戳应当取值 AVFrame.pkt_pts
,注意第一帧视频的这个值不一定为0,所以要记录下第一帧的数值,以后每帧视频要减去这个值,才是真正的视频时间戳。
t3 = m_TimeBase * m_pVideoFrame->pkt_pts * 1000;
m_Videots = (int64_t)t3;
if(m_VideoFirstTM == 0){
m_VideoFirstTM = m_Videots;
}
m_Videots -= m_VideoFirstTM;
参考文章:
FFMPEG中的时间问题