由前文可知,视频帧的pts是借助AVFrame结构体的best_effort_timestamp成员变量获取该帧在视频数据流(AVStream)时间基下的时间戳,然后借助 av_q2d() 函数将时间戳转换为正常时间基。同理音频帧时间戳也是使用同样的方法去获取。
/*pFrame为解码后的帧数据AVFrame*/
pFrame->pts = pFrame->best_effort_timestamp;
pts = pFrame->pts * av_q2d(stream->time_base);
视频时间戳的分辨率约为 1/帧率 (s),音频时间戳的分辨率为 每帧采样数/采样率 (s);从以上可知,两者的精度均不高,因此需要借助av_gettime获取微秒级的外部时钟来提升视频时钟和音频时钟的精度。
以视频时钟举例,当解码了一帧视频帧后,即以转换了时间基的pts作为当前视频时钟的基准,同时保存当前的外部时钟时间。当获取视频时钟时,获取的不是视频帧pts,而是以pts为基准,加上更新视频时钟基准到现在过去的时间,作为视频时钟。这样可以将视频时钟的分辨率提升至微秒级别,即可称为基于视频帧pts的时钟。
class Clock {
public:
Clock();
~Clock();
void set_clock(double _pts); /*更新clock*/
double get_clock(); /*获取clock*/
private:
double pts;
double update_time;
};
/*更新clock, 传入参数为转换了时间基的pts*/
void Clock::set_clock(double _pts)
{
pts = _pts;
// 保存更新pts时的外部时钟
update_time = av_gettime() / MICROSECOND_TO_SECOND;
}
/*获取clock*/
double Clock::get_clock()
{
// 以pts为基准, 加上更新pts到当前获取clock经过的时间
return (pts + av_gettime() / MICROSECOND_TO_SECOND - update_time);
}
关于ffplay中的音视频同步分析在这里,简单来说就是根据音频时钟和视频时钟的差值去动态校准视频帧的持续时长(若视频时钟落后太多则进行丢帧处理),以达到音视频同步的效果。
此处做一个简化版的音视频同步,实际效果上能将音视频时钟差收敛至视频帧一帧持续时长内(测试视频的最高帧率为60 fps)。
aclk = avc->audio_clk.get_clock();
vclk = avc->video_clk.get_clock();
delay = avc->get_delay(aclk, vclk); //获取下一帧延时时长
av_usleep(delay);
/*计算延时时间*/
uint32_t AVCtrl::get_delay(double aclk, double vclk)
{
int64_t delay;
double _duration, diff;
_duration = vc.get_duration(); //获取正常的帧持续时间
if (_isnan(aclk) || _isnan(vclk))
return _duration;
// 计算时钟差值
diff = (aclk - vclk) * MICROSECOND_TO_SECOND;
if (diff < 0) { /*视频时钟超前于音频时钟*/
if (-diff > 2 * _duration) delay = (-diff - _duration);
else delay = _duration;
} else { /*视频时钟落后于音频时钟*/
delay = _duration - diff;
delay = delay < 0 ? 0 : delay;
}
return delay;
}
之前在这篇文章中提到了PID控制器的方法,这里也试着使用这种方法,实际效果上能将音视频时钟差收敛至视频帧一帧持续时长内(测试视频的最高帧率为60 fps)。
uint32_t AVCtrl::get_delay_by_pid(double aclk, double vclk)
{
static int64_t delay = 0;
static int64_t time_diff[3] = { 0,0,0 };
static int64_t time_incre = 0;
static double kp = 0.01, ki = 0.85, kd = 0.1;
static uint8_t flag = 1;
double _duration, diff;
_duration = vc.get_duration();
if (_isnan(aclk) || _isnan(vclk))
return _duration;
if (flag) {
delay = _duration;
flag = 0;
}
diff = (aclk - vclk) * MICROSECOND_TO_SECOND;
time_diff[0] = time_diff[1];
time_diff[1] = time_diff[2];
time_diff[2] = diff;
time_incre = kp * (time_diff[2] - time_diff[1]) +
ki * time_diff[2] +
kd * (time_diff[2] - 2 * time_diff[1] + time_diff[0]);
delay -= time_incre;
delay = delay < 0 ? 0 : delay;
return delay;
}