ffplay源码分析(六)音视频同步

一、音视频为什么会不同步?

在音视频处理中,经常会出现不同步的现象,常见的原因如下

  1. 录制设备差异:音频和视频可能使用不同的设备进行录制,这可能会导致录制的音频和视频之间存在时间差。

  2. 编码和传输延迟:音频和视频在编码、传输和解码过程中可能会引入延迟,导致音频和视频不同步。

  3. 系统时钟漂移:音频和视频设备可能使用不同的系统时钟,导致时钟漂移,从而影响音视频同步。

  4. 解码和渲染延迟:音频和视频在解码和渲染过程中可能会产生不同的延迟,导致不同步。在ffplay中,音视频的输出有自己的线程。音频的输出线程是sdl的回调线程,视频的输出是程序的主线程。这两个线程调度过程中不一定那么准确。

二、音视频同步策略

小时后很多同学都有电子手表。当发现自己的手表和别人的手表的时间不一样的时候,我们会去对表。
音视频同步就是一个对表的过程。
但是大家的时钟都走的好好的,以谁的为准呢?
无所谓,只要设定一个基准,大家一致就行了。
在ffplay中,根据基准的不同,三种同步方式,分别以不同的时间为基准。

  • 以音频为基准,同步视频到音频(AV_SYNC_AUDIO_MASTER)

    • 视频慢了,丢帧或快放
    • 视频快了,继续渲染上一帧
  • 以视频为基准,同步音频到视频(AV_SYNC_VIDEO_MASTER)

    • 音频慢了,加快播放速度(或丢弃部分帧,但极容易听出断音)
    • 音频快了,降低播放速度(或重复上一帧)
  • 以外部时钟为基准,同步音频和视频到外部时钟(AV_SYNC_EXTERNAL_CLOCK)

    • 选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

这三种是最基本的策略,考虑到人对声音的敏感度要强于视频,一般情况下我们会采取视频去同步音频的策略。

但实际还是要根据情况调整的。比如一个没有声音的视频,或者刚播放的只有音频没有视频,视频比较晚到,那前面的音频也可以先丢弃。

三、ffplay中的音视频同步

由于大部分的场景都是视频去同步音频,其它的几乎不怎么用。
所以这里只看视频同步音频的方法。
ffplay中的音视频同步主要是在video_refresh中去同步的。

video_refresh 是刷新视频帧的结构体,分析这个函数的时候,先不用考虑音视频同步,只要考虑一帧需要显示多久

//事件循环
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
    //收集事件
    SDL_PumpEvents();
    //检查是否有事件
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
       //隐藏鼠标的操作
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
        if (remaining_time > 0.0)
            av_usleep((int64_t)(remaining_time * 1000000.0));
        //
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(is, &remaining_time);
        SDL_PumpEvents();
    }
}


/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
    VideoState *is = opaque;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
        check_external_clock_speed(is);
    //仅音频时,显示音频波形图
    if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
            video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
    }

    if (is->video_st) {
retry:
        
        if (frame_queue_nb_remaining(&is->pictq) == 0) {//所有帧都显示完了
            //什么都不用做,因为队列中没有图片
        } else {//有未显示的帧
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* 上一帧:已显示 */
            lastvp = frame_queue_peek_last(&is->pictq);
            /* 当前帧:待显示 */
            vp = frame_queue_peek(&is->pictq);

            /* vp不是当前serial下的包,取下一帧(发生在跳转时,否则用户点了跳转还在播放之前的视频,那体验就不太好) */
            if (vp->serial != is->videoq.serial) {
                frame_queue_next(&is->pictq);
                goto retry;
            }
            // lastvp和vp不是同一播放序列(发生seek时会开始一个新的播放序列)
            //,将frame_timer更新为当前时间
            if (lastvp->serial != vp->serial)
                is->frame_timer = av_gettime_relative() / 1000000.0;

            // 暂停时,不停的播放上一帧图像
            if (is->paused)
                goto display;

            /* 计算上一帧的持续时长 vp->pts - lastvp->pts */
            last_duration = vp_duration(is, lastvp, vp);
          
            //根据视频时钟和基准时钟的差值,计算delay,这是音视频同步的关键
            delay = compute_target_delay(last_duration, is);
           
            time= av_gettime_relative()/1000000.0;
            //上一帧播放结束的时间(is->frame_timer + delay) 大于当前时间,表示播放时刻未到
            if (time < is->frame_timer + delay) {
            //remaining_time 表示剩余的播放时间,先延时一段时间再显示
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                //重新播放上一帧
                goto display;
            }
            //上一帧播放结束的时间(is->frame_timer + delay) 小于当前时间,表明当前帧的播放时间到了
            //更新frame_timer值
            is->frame_timer += delay;
          
            //校对frame_timer,如果frame_timer值落后于当前时间太久(超过最大同步阈值),则更新为当前系统时间
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                is->frame_timer = time;

            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                //更新视频时钟
                update_video_pts(is, vp->pts, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            //缓存中积累了太多帧了,可能会触发丢帧策略(这个和音视频同步无关,可以暂时不考虑)
            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                //下一帧:待显示的帧
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                //当前帧的显示时长 = nextvp->pts - vp->pts
                duration = vp_duration(is, vp, nextvp);
                //1、非步进模式 2、丢帧策略生效 3、当前帧未能及时播放(当前帧pts + duration 还是落后于当前时间)
                if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
                    is->frame_drops_late++;
                    //往下走一帧
                    frame_queue_next(&is->pictq);
                    goto retry;
                }
            }
            //字幕播放
            ...

            //下一帧
            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            if (is->step && !is->paused)
                stream_toggle_pause(is);
        }
display:
        /* display picture */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display(is);
    }
    is->force_refresh = 0;
}

其中比较关键的函数是

compute_target_delay, 他计算这一帧需要delay的时间,

这是决定视频要快放还是慢放的关键

/* 输入的delay是理论值 */
static double compute_target_delay(double delay, VideoState *is)
{
    double sync_threshold, diff = 0;

    /* 仅当不以视频为基准准的时候计算 */
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
        //获取和主时钟的差异
        diff = get_clock(&is->vidclk) - get_master_clock(is);

        /*同步阈值,就是需要进行同步的阈值,设置为delay,但要求范围在AV_SYNC_THRESHOLD_MIN(40)和AV_SYNC_THRESHOLD_MAX(100)之间,
        比如delay为20,那sync_threashold为40,当差值大于1帧时去校准
        比如delay为50,那sync_threashold为50,当差值达到1帧时去校准
         比如delay为120,那sync_threashold为100,当差值接近1帧时去校准
         */
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
        //差值合理,如果不合理就没必要做同步了。
        if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
            /* 视频落后,少等待fabs(diff)的时间,但delay最多也就是0 */
            if (diff <= -sync_threshold)
                delay = FFMAX(0, delay + diff);
            /* delay本身比较长,一次调整到位 */
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                delay = delay + diff;
            /* delay正常,调整为2*delay,为的是平滑处理,避免画面卡太久*/
            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;
}

四、时钟

前面有这样一段代码

//获取和主时钟的差异
        diff = get_clock(&is->vidclk) - get_master_clock(is);

轻松的就把音频和视频的差值给计算出来了。

需要考虑一个问题

使用时钟的时候,时间已经过去了,那怎么办?就像你用你的手表和另外一个人对表,它告诉你现在是3:40分,你过了2秒钟才去对,那肯定是不准的。

ffplay设计这个时钟的代码还是比较巧妙的,

可以先看一下时钟结构体的设计

clock时钟结构体

typedef struct Clock {
    double pts; //当前播放的pts
    double pts_drift; //关键字段,当前pts与当前系统时间的差值,保持设置pts时候的差值,后面就可以根据这个差值推算下一个pts播放的时间点。
    double last_updated; //当前时钟最后一次更新的时间。
    double speed; 
    int serial;//播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
    int paused;
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

设置时钟和获取时钟函数

static void set_clock_at(Clock *c, double pts, int serial, double time)
{
    c->pts = pts; 
    c->last_updated = time; 
    c->pts_drift = c->pts - time; 
    c->serial = serial;
}

static void set_clock(Clock *c, double pts, int serial)
{
    double time = av_gettime_relative() / 1000000.0; //获取当前实际时间,以秒为单位
    set_clock_at(c, pts, serial, time);
}

static double get_clock(Clock *c)
{
    if (*c->queue_serial != c->serial)
        return NAN;
    if (c->paused) {
        return c->pts;
    } else {
      //获取时,此时的当前假设是1001s
        double time = av_gettime_relative() / 1000000.0;
      /* 暂时先不考虑倍速,c->speed = 1,这个等式就变成return c->pts_drift + time */
        return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
    }
}

可以结合下图去理
在这里插入图片描述

首先看视频时钟,
设置的时候,视频的pts假设是1s,t1假设是1000s(实际是一个很大的值),
那 pts_drift = -999s,
然后到了t3时刻,假设是1002s,
那算出来的理论的pts 就是 -999 + 1002 = 3s,
所以刚好系统时间走了2s,视频的pts也会走2s。

音频同理,音频的pts假设是1.5s,设置时时刻t1假设是1001s(实际是一个很大的值),
那pts_drift = -999.5s,
然后到了t3时刻,假设是1002s(和视频同一时刻)
那算出来的理论pts就是 -999.5 + 1002 = 2.5s
所以计算出来的差值就是3s-2.5s = 0.5s,视频比音频快0.5s。
细心的小伙伴会发现。
最开始不是1s - 1.5s = -0.5吗?怎么过了同样的时间结果不一样了?
因为比较音视频同不同步要在同一个时刻进行比较,刚开始设置的时间点本来就是不一样的。
得考虑设置的时间的差值。
视频时1000s的时候设置的,音频是1001s的时候设置的,
那可以算出理论上视频到了1001s的时候,它的时间是2s,
所以最开始的时候的计算方式应该是2s-1.5s,结果是0.5s,和t3时刻计算的结果是一致的。

系列文章目录

ffplay源码分析(一)主函数
未完待续…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值