ffplay源码分析__音视频同步compute_target_delay

一、为什么需要音视频同步

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完全一致,没有必要,且可能导致播放异常较为明显,比如视频明显卡顿了,大多数情况都是多次微调。

六、参考链接

原文链接:音视频同步原理与实践:解决不同步问题的策略-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值