前言

对于音视频同步是有三种方案的,一种是以外部时钟为基准,音频时钟和视频时钟在播放时都以外部时钟为参考系,谁快了就等待,慢了就丢帧;第二种是以视频时钟为基准,

音频时钟在播放的过程中参考视频时钟;第三种是以音频时钟为基准,视频时钟在播放的过程中参考音频时钟。

由于人体器官对视觉的敏感读没有听觉的灵敏度高,因此为了更好的体验,在音视频同步时一般都是以音频时钟为基准的方案。那是不是说其他两种方案没有用处呢?也不是的,比如说

一个只有视频没有音频的的视频文件,在播放的时候就需要以视频为基准了。

今天介绍的音视频同步方案也是最普遍的视频同步音频的方案。

Clock时钟

结构体

// 时钟/同步时钟
typedef struct Clock {
    double pts;       // 当前正在播放的帧的pts    /* clock base */
    double pts_drift;   // 当前的pts与系统时间的差值  保持设置pts时候的差值,后面就可以利用这个差值推算下一个pts播放的时间点
    double last_updated; // 最后一次更新时钟的时间,应该是一个系统时间吧?
    double speed;  // 播放速度控制
    int serial;     // 播放序列      /* clock is based on a packet with this serial */
    int paused;  // 是否暂停
    int *queue_serial;   // 队列的播放序列 PacketQueue中的 serial /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

函数

// 主要由set_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)
{
    // 如果时钟的播放序列与待解码包队列的序列不一直了,返回NAN,肯定就是不同步或者需要丢帧了
    if (*c->queue_serial != c->serial)
        return NAN;
    if (c->paused) {
        // 暂停状态则返回原来的pts
        return c->pts;
    } else {
        double time = av_gettime_relative() / 1000000.0;
        // speed可以先忽略播放速度控制
        // 如果是1倍播放速度,c->pts_drift + time
        //当speed大于1.0的时候,音视频会加速播放,小于1.0的时候会减少播放
        return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
    }
}

/* get the current master clock value */
static double get_master_clock(VideoState *is)
{
    double val;

    switch (get_master_sync_type(is)) {
        case AV_SYNC_VIDEO_MASTER:
            val = get_clock(&is->vidclk);
            break;
        case AV_SYNC_AUDIO_MASTER:
            val = get_clock(&is->audclk);
            break;
        default:
            val = get_clock(&is->extclk);
            break;
    }
    return val;
}

音频和视频每次在播放新的一帧数据时都会调用函数set_clock更新音频时钟或视频时钟。通过函数set_clock_at我们发现,就是更新了 Clock 结构体的四个变量。

其中pts_drift是当前帧的pts与系统时间的差值,有了这个差值在未来的某一刻就能够很方便地算出当前帧对于的时钟点。

大致原理如图

ffplay音视频同步之视频同步音频_音视频同步

视频同步音频

视频同步到音频的基本方法是:如果视频超前音频,继续显示上一帧,以等待音频;如果视频落后音频,则显示下一帧,以追赶音频。

对于视频同步处理在ffplay有两处地方,一是在函数get_video_frame做了简单的丢帧处理,二是在函数video_refresh显示控制是做的同步处理。

对于函数get_video_frame丢帧处理的主要逻辑如下:

// 同步时钟不以视频为基准时
// framedrop 表示选择丢几帧
//AV_NOPTS_VALUE 无效
//nb_packets  队列内一共有多少元素
/*
可以通过限制size、duration、nb_packets的最大值,来控制PacketQueue队列的大小
*/
// frame_last_filter_delay
/* 
变量存储的是滤镜容器处理上一帧所花的时间,这是一个预估值,假设滤镜容器处理上一帧花了 0.01s,那处理现在这一帧估计也需要0.01s,所以解码出来的 AVFrame,并不是立即就能丢进去 FrameQueue 给播放线程用。而是需要经过滤镜处理的,滤镜处理也需要时间。

所以如果 diff 等于 0.008 ,视频比音频快了 0.008s,但是因为视频要经过滤镜处理,所以需要减去 0.01 ,实际上是 视频比音频播放慢了 0.002s。
*/
        if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            if (frame->pts != AV_NOPTS_VALUE) {
                // 理论上如果需要连续接上播放的话  dpts + diff = get_master_clock(is)
                // 所以可以算出diff  注意绝对值
                double diff = dpts - get_master_clock(is);
                if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
                    diff - is->frame_last_filter_delay < 0 &&
                    is->viddec.pkt_serial == is->vidclk.serial &&
                    is->videoq.nb_packets) {
                    is->frame_drops_early++;
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }

丢帧策略

1 frame_drops_early
get_video_frame 这个函数阶段丢帧
2 frame_drops_late
video_refresh 这个函数阶段丢帧

©著作权归作者所有:来自51CTO博客作者思想觉悟的原创作品,请联系作者获取转载授权,否则将追究法律责任
ffplay音视频同步
https://blog.51cto.com/u_13861442/5260843