最近学习FFmpeg音视频同步,着实很难理解,本文记录学习过程中的领悟知识点,如有不对,还望指正!
正常情况来说,在对视频 编码 时,大致是这个样子的:
编码时,以时间线为轴,依次给编码后的数据打上正确的时间pts,如果这个地方pts时间打错了,播放时无路如何也同步不了的;
解码播放时,正常来说,以上图为例,我们只需要在0.1s时播放解码后第一个视频包vp1和音频包ap1,在0.2s时播vp2和ap2,依次播放即可;
但是实际情况,可能会这样:
音频包和视频包播放时对不上,也就会出现画面和声音对不上,也就是卡了
我认为主要原因来自两类:
网络原因如网络卡顿,导致流媒体中的包丢失、延迟到来等,如0.2s的时候该播放vp1和ap2,但是这个时候vp2这个packet还没有从网络中传递过来,无法播放方面,只能重复上一个画面,这样肯定是同步不了的
播放器中涉及解码、播放的逻辑,大多都是在不同的线程中,会在不同的时间得到解码的包,然后拿去播放线程播放,这样的话,如果不对音视频同步,解码后想要使音视频在正确的时间去播放是很难的
针对上面两个原因,解决视频不卡顿的办法就是进行音视频同步,选择一个参考时钟,另一个时钟参考时钟为基准,不停对比两者之间差异,超前就让自己重复上一个画面,延后就加快播放自己的播放进度,大致原理是这样
以下图为例说说大致原理:
当前时刻为0.35s,理论来说在0.3s时应该播放vp3和ap3,但是由于各种原因导致vp2重复播放,使两者没有同步播放,试想,为了修复两者出现的不同步问题,我们该怎么做?
就是在0.35s出尽可能马上播放vp3,然后在0.4s处播放vp4,修复两者播放的差异;那我们这个修复过程是怎么处理的呢?
摘自ffplay.c源码,根据源码来讲最好:
double MediaSync::calculateDelay(double delay) {
double sync_threshold, diff = 0;
// 如果不是同步到视频流,则需要计算延时时间
if (playerState->syncType != AV_SYNC_VIDEO) {
// 计算差值
diff = videoClock->getClock() - getMasterClock();
// 用差值与同步阈值计算延时
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
ALOGI("diff frame_timer %f %f %f", frameTimer, delay, sync_threshold);
if (!isnan(diff) && fabs(diff) < maxFrameDuration) {
if (diff <= -sync_threshold) {
delay = FFMAX(0, delay + diff);
} else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) {
delay = delay + diff;
} else if (diff >= sync_threshold) {
delay = 2 * delay;
}
}
}
return delay;
}
逻辑分析
此函数为修正上个vp的播放时长函数,
入参delay: 理论vp的播放时长,按照上图例子delay = vp3-vp2 = 0.1s
diff:两个时钟差异,实质就是上一个播放的vp和ap的差异,也就是vp2-ap3 = -0.1s
sync_threshold: 同步阈值,超过这个同步阈值就认为要进行音视频同步,sync_threshold在0.1
函数执行完后,会进入以下逻辑
// 计算上一次显示时长
lastDuration = calculateDuration(lastFrame, currentFrame);
// 根据上一次显示的时长,计算延时 主要是视频
delay = calculateDelay(lastDuration);
// 处理超过延时阈值的情况
if (fabs(delay) > AV_SYNC_THRESHOLD_MAX) {
if (delay > 0) {
delay = AV_SYNC_THRESHOLD_MAX;
} else {
delay = 0;
}
}
// 获取当前时间
time = av_gettime_relative() / 1000000.0;
if (isnan(frameTimer) || time < frameTimer) {
frameTimer = time;
}
// 如果当前时间小于帧计时器的时间 + 延时时间,则表示还没到当前帧,需要返回在休眠一段时间
if (time < frameTimer + delay) {
*remaining_time = FFMIN(frameTimer + delay - time, *remaining_time);
break;
}
// 更新帧计时器
frameTimer += delay;
// 帧计时器落后当前时间超过了阈值,则用当前的时间作为帧计时器时间
if (delay > 0 && time - frameTimer > AV_SYNC_THRESHOLD_MAX) {
frameTimer = time;
}
播放业务逻辑 省略........
time为当前系统时间,对应到我们例子中也就是0.35s,frameTimer记录上一个vp的实际播放时间,在例子中vp2是在0.2s时播放,0.3s时也只是重复而已,所以frameTimer是0.2s,frameTimer + delay也还是0.2s,小于当前时间0.35s,所以不会进入break条件,直接进行后续的播放逻辑,直接播放也就达到我们最初设想的立即播放;当然后面的播放逻辑有丢包策略,这里就不阐述了
这只是一个视频时钟落后的例子,还有视频时钟超前的情况,按照上面分析举一反三即可