一、为什么需要音视频同步
1、由于计算机系统大部分是分时系统,所以当负载过高或者设备性能差的时候,音频播放线程或者视频播放线程会出现调度不及时,导致视频画面已经更新了,但是声音还没放出来。
2、视频和音频的解码速度也不一样,解码出来的数据也不一定马上就可以显示出来。
3、视频和音频是两个独立的任务在运行,一帧的播放时间,难以精准控制。音视频解码及渲染的耗时不同,可能造成每一帧输出有细微差距,长久累计,不同步便越来越明显。
二、如何音视频同步?
为了解决音视频同步问题,引入了时间戳:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);编码时依据参考时钟上的给每个音视频数据块都打上时间戳;播放时,根据音视频时间戳及参考时钟,来调整播放。所以,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以参考时钟为标准,放快了就减慢播放速度;播放快了就加快播放的速度。
一般来说有以下三种同步策略:
将视频同步到音频上:就是以音频的播放速度为基准来同步视频,ffplay默认的同步机制。
将音频同步到视频上:就是以视频的播放速度为基准来同步音频,一般不常用。
将视频和音频同步外部的时钟上:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。
当播放源比参考时钟慢,则加快其播放速度,或者丢弃;快了,则延迟播放。
这三种是最基本的策略,考虑到人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验,且音频的播放时钟为线性增长,所以一般会以音频时钟为参考时钟,视频同步到音频上。在实际使用基于这三种策略做一些优化调整,例如:调整策略可以尽量采用渐进的方式,因为音视频同步是一个动态调节的过程,一次调整让音视频PTS完全一致,没有必要,且可能导致播放异常较为明显。
三、时钟的概念
1、Clock结构体
typedef struct Clock {
double pts; // 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
// 当前pts与当前系统时钟的差值, audio、video对于该值是独立的
double pts_drift; // clock base minus time at which we updated the clock
// 当前时钟(如视频时钟)最后一次更新时间,也可称当前时钟时间
double last_updated; // 最后一次更新的系统时钟
double speed; // 时钟速度控制,用于控制播放速度
// 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
int serial; // clock is based on a packet with this serial
int paused; // = 1 说明是暂停状态
// 指向packet_serial
int* queue_serial; //指向对应PacketQueue的序列号 用于过时时钟检测
} Clock;
pts:帧的显示时间戳,单位是秒,已经根据时间基换成秒了
pts_drift:c->pts - c->last_updated,是个很大的负数。理解为pts相对更新时间(系统时间)的偏移值。这个值的作用是下一次读取时间时,计算系统时间经过了多少秒,再根据上次更新的pts预估当前时刻的pts。比如在系统时间t1更新了pts1,即所谓进行了”对时“操作,那么t2时刻后,时钟计算方式为:t2-t1+pts1=t2+(pts1-t1)=t2+pts_drift。下面是ffplay读取时钟时的计算逻辑,c->speed默认为1。
static double get_clock(Clock *c)
{
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) {
return c->pts;
} else {
double time = av_gettime_relative() / 1000000.0;
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}
}
在读取时钟get_clock函数中,读取的时间为
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
c->pts_drift为c->pts - last_updated(最近一次更新时钟的系统时间戳),c->speed初始为1.0,所以上面的代码为c->pts_drift + time= c->pts - last_updated+time(读取时钟的系统时间戳)=c-pts+(time-last_updated),计算经过了多长时间来预估当前的时钟pts。
last_updated:最近一次更新时钟的系统时间戳
2、音频更新时钟
/* Let's assume the audio driver that is used by SDL has two periods. */
if (!isnan(is->audio_clock)) {
set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
sync_clock_to_slave(&is->extclk, &is->audclk);
}
音频更新时钟是在SDL的回调函数sdl_audio_callback中更新,每次回调获取音频数据时,计算当前音频时钟的pts。
3、视频更新时钟
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
{
//更新当前时钟为vp的pts
update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新video时钟
}
SDL_UnlockMutex(is->pictq.mutex);
4、时钟一直在增长
音频时钟可看作线性增长,视频时钟每次更新pts后可看作线性增长,从长时间看,围绕音频时钟波动。
四、ffplay同步函数vp_duration
// 计算上一帧需要持续的duration,这里有校正算法
static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
if (vp->serial == nextvp->serial) { // 同一播放序列,序列连续的情况下
double duration = nextvp->pts - vp->pts;
if (isnan(duration) // duration 数值异常
|| duration <= 0 // pts值没有递增时
|| duration > is->max_frame_duration // 超过了最大帧范围
)
return vp->duration; /* 异常时以帧时间为基准(1秒/帧率) */
else
return duration; //使用两帧pts差值计算duration,一般情况下也是走的这个分支
} else { // 不同播放序列, 序列不连续则返回0
return 0.0;
}
}
vp_duration()是计算相邻两帧的pts差值,这个值不是固定的,因为在video_thread解码线程中,有丢帧逻辑。所以返回值有时是一帧的时长,有时是两帧的时长。
五、同步函数compute_target_delay
static double compute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave, we try to correct big delays by
duplicating or deleting a frame */
diff = get_clock(&is->vidclk) - get_master_clock(is);
/* skip or repeat frame. We take into account the
delay to compute the threshold. I still don't know
if it is the best guess */
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
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;
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
return delay;
}
音视频同步的计算函数是compute_target_delay,重点是计算上一帧lastvp显示的时长。
1、diff,delay,sync_threshold
delay:相邻两帧的pts差,如果不丢帧,delay一般为一帧的时长,如果丢帧,delay为两帧或三帧的时长。
diff:代表视频和音频的时钟差,可能为正,可能为负。
sync_threshold:代表同步阈值,音频和视频不可能做到完全同步,只要差值diff不超过一定值可认为音视频同步,这个值就是sync_threshold。sync_threshold的值由AV_SYNC_THRESHOLD_MIN(0.04),AV_SYNC_THRESHOLD_MAX(0.1),delay三个值决定。delay和sync_threshold的关系用如下图所示:
1):当delay<0.04时,在A区间,sync_threshold = FFMAX(0.04, FFMIN(0.1, delay))=0.04,黄线处于蓝线下面,此时delay<sync_threshold。
2):当0.04<=delay<=0.1时,处在B区间,sync_threshold = FFMAX(0.04, FFMIN(0.1, delay))=delay,黄线和蓝线重回,sync_threshold=delay
3):当delay>0.1时,处在C区间,sync_threshold = FFMAX(0.04, FFMIN(0.1, delay))=0.1,黄线在蓝线上面,sync_threshold<delay
总结:
即sync_threshold是一个[0.04,0.1]区间的值。
在AB区间,sync_threshold>=delay,在C区间,sync_threshold<delay。
sync_threshold的值依赖于delay,如果丢帧的话,delay的值会变,所以sync_threshold的值也会变,不管怎么变,始终在区间[0.04,0.1]。
2、delay改变的3种情况
下图是音视频时钟的状态图,以音频时钟为中点,分别向左右延长sync_threshold的长度
delay改变的3种情况如下:
1)diff <= -sync_threshold ,此时视频时钟为最左边的小球pts1_v,时钟差为diff1
2)diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD,此时视频时钟为最右边的小球pts3_v,时钟差为diff3。
3)diff >= sync_threshold(隐含delay<=0.1),同2。
当-sync_threshold < diff < sync_threshold时,delay不变,此时视频时钟为pts2_v,时钟差为diff2。下面分别针对3种情况讨论:
1) diff <= -sync_threshold时
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
即视频时钟-音频时钟<=-sync_threshold,视频比音频慢了至少一个同步阈值的时长。视频相对音频向左偏移超过sync_threshold。
此时delay修正为FFMAX(0, delay + diff)。delay+diff的值分3种情况。
delay在A区间:delay<sync_threshold,if语句的条件diff <= -sync_threshold,两个条件相加,delay+diff<sync_threshold+(-sync_threshold),推算delay+diff<0。
delay在B区间:delay=sync_threshold,if语句的条件diff <= -sync_threshold,两个条件相加,推算delay+diff<=0。
总结:delay在A,B区间时,FFMAX(0, delay + diff)=0。
delay在C区间:delay>0.1,sync_threshold固定为0.1,if语句的条件diff <= -sync_threshold,所以diff<=-0.1,delay+diff有可能大于0,有可能小于0。当delay> | diff| 时,delay+diff>0,delay<|diff|时,delay+diff<0。
假设系统在t1时刻更新视频时钟pts1_v后,出现diff <= -sync_threshold的情况,如图所示:
对于delay在AB区间时,经过delay+diff时长后(时长为0),视频时钟更新为pts2_v,追赶了delay的长度,音频时钟不变,如下图所示
对于C区间, delay<| diff | 时,FFMAX(0, delay + diff)=0,和上图一样。
delay>| diff |时,delay+diff>0。delay+diff等同于delay-|diff|,为了直观表达时长,我们用delay-|diff|表示。假设在T1时刻,音频时钟为白色球pts1_a,视频时钟为白色小球pts1_v,经过delay-|diff|的时长后,音频时钟相求移动delay-|diff|的长度,视频时钟也向前移动delay-|diff|,如下图所示:
此时到了显示下一帧的时间,下一帧pts2_v和pts1_v的时差为delay,所以视频时钟相对于白色小球向前移动delay的长度,如下图所示:
音频时钟向前移动了delay-|diff|的长度,视频时钟向前移动了delay的长度,pts2_v相对于音频的pts1_a多了delay-|diff|的时长,所以音视频时钟同步了。
数学表示如下:
V-A=diff ----------V和A代表T1时刻的视频时钟和音频时钟
V1=V+delay -------------------V1代表t2时刻更新pts后的视频时钟
A1=A+delay-|diff| ----------------A1代表t2时刻的音频时钟
V1-A1=V-A+|diff|=diff+|diff|。diff为负数,所以V1-A1=0。 ---------V1-A1代表t2时刻的时钟差。
2)diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
AV_SYNC_FRAMEDUP_THRESHOLD为固定值0.1,即delay > 0.1,此时delay处在C区间,delay>sync_threshold。假设在系统时刻t1时更新视频的时钟为pts1_v后,出现diff>=sync_threshold的情况,如下图所示:
经过delay+diff的时长后系统时钟为t2时刻,音视频同时向前移动delay+diff的长度,音视频时钟如下:
注意此时视频时钟也一直在走着,所以视频时钟比音频时钟还是快diff时长。到了t2时刻时要更新下一帧的pts(pts2_v),pts2_v和pts1_v的时长差为delay,更新后如图所示:
相当于视频时钟回撤了diff的时长,此时音视频实现同步。
3)diff >= sync_threshold(隐含delay<=0.1)
else if (diff >= sync_threshold)
delay = 2 * delay;
视频时钟-音频时钟>=sync_threshold时,此时delay<=0.1,该帧显示2*delay长度。
当delay在AB区间时,sync_threshold>=delay,if语句的条件diff>=sync_threshold,三者关系为diff>=sync_threshold>=delay。
假设系统在t1时刻更新视频时钟pts1_v后,出现diff>=sync_threshold的情况,如下图所示:
经过2*delay后,音视频时钟如下图所示:
diff>2*delay时,经过2*delay后,音视频时钟都向前移动了2*delay长度,
此时到了显示下一帧的时间,更新下一帧的pts2_v后,时钟如下图所示
即视频比音频快至少一个delay长度。
若diff<2*delay,到了t2时刻更新视频的pts2_v后,时钟如下图所示:
视频比音频快的时差不多于一个delay长度。
既然delay+diff能一次性让音视频完全同步,为什么用2*delay而不用delay+diff?
以diff为1秒,即音视频时钟相差1秒,帧率25帧,duration=0.04为例分析,
如果按照delay+diff=0.04+1=1.04,那么lastvp会显示1.04秒,虽然音视频时钟会同步,但是视觉上会给人感觉视频卡了。
如果按照2*delay=0.08,lastvp会显示0.08秒,音频只赶上0.04秒,虽然不能一次性让音视频完全同步,但是保证了视频画面的流畅度,视觉上基本感觉不到卡顿。音频赶超多次0.04秒也会实现音视频时钟同步。
总结:一次调整让音视频PTS完全一致,没有必要,且可能导致播放异常较为明显,比如视频明显卡顿了,大多数情况都是多次微调。