ffplay源码分析07 ---- 音视频同步

 =====================================================

ffplay源码分析01 ---- 框架

ffplay源码分析02 ---- 数据读取线程

ffplay源码分析03 ---- 视频解码线程

ffplay源码分析03 ---- 音频解码线程

ffplay源码分析04 ---- 音频输出

ffplay源码分析05 ---- 音频重采样

ffplay源码分析06 ---- 视频输出

ffplay源码分析07 ---- 音视频同步

=====================================================

⾳视频同步策略

  1. 以⾳频为基准,同步视频到⾳频(AV_SYNC_AUDIO_MASTER
    • 视频慢了则丢掉部分视频帧(视觉->画⾯跳帧)
    • 视频快了则继续渲染上⼀帧 
  2. 以视频为基准,同步⾳频到视频(AV_SYNC_VIDEO_MASTER
    • ⾳频慢了则加快播放速度(或丢掉部分⾳频帧,丢帧极容易听出来断⾳)
    • ⾳频快了则放慢播放速度(或重复上⼀帧 )
    • ⾳频改变播放速度时涉及到重采样
  3. 以外部时钟为基准,同步⾳频和视频到外部时钟(AV_SYNC_EXTERNAL_CLOCK
    • 前两者的综合,根据外部时钟改变播放速度
  4. 视频和⾳频各⾃输出,即不作同步处理(FREE RUN

一般是第一种,就是将视频同步到音频。

音视频同步基本概念

  • DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这⼀帧的数据。
  • PTS(Presentation Time Stamp):即显示时间戳,这个时间戳⽤来告诉播放器该在什么时候显示这⼀帧的数据。
  • timebase 时基:pts的值的真正单位
  • ffplay中的pts,ffplay在做⾳视频同步时使⽤秒为单位,使⽤double类型去标识pts,在ffmpeg内部不会⽤浮点数去标记pts。
  • Clock 时钟

当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是⼀致的。但存在B帧的时候两者的顺序就不⼀致了。

1. pts是presentation timestamp的缩写,即显示时间戳,⽤于标记⼀个帧的呈现时刻,它的单位由timebase决定。timebase的类型是结构体AVRational(⽤于表示分数):

typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

如timebase={1, 1000} 表示千分之⼀秒(毫秒),那么pts=1000,即为pts*1/1000 = 1秒,那么这⼀帧就需要在第⼀秒的时候呈现。

将AVRatioal结构转换成double:

static inline double av_q2d(AVRational a){
    return a.num / (double) a.den;
}

计算时间戳:

timestamp(秒) = pts * av_q2d(st->time_base)

计算帧时长:

time(秒) = st->duration * av_q2d(st->time_base)

不同时间基之间的转换:

int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)

在ffplay中,将pts转化为秒,⼀般做法是: pts * av_q2d(timebase)

2. 在做同步的时候,我们需要⼀个"时钟"的概念,⾳频、视频、外部时钟都有⾃⼰独⽴的时钟,各⾃set各⾃的时钟,以谁为基准(master), 其他的则只能get该时钟进⾏同步,ffplay定义的结构体是Clock:

// 这里讲的系统时钟 是通过av_gettime_relative()获取到的时钟,单位为微妙
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;      /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

这个时钟的⼯作原理是这样的:
1. 需要不断“对时”。对时的⽅法set_clock_at(Clock *c, double pts, int serial,double time) ,需要⽤pts、serial、time(系统时间)进⾏对时。

2. 获取的时间是⼀个估算值。估算是通过对时时记录的pts_drift估算的。pts_drift是最精华的设计,⼀定要理解。

可以看这个图来帮助理解:

图中央是⼀个时间轴(time是⼀直在按时间递增),从左往右看。⾸先我们调⽤set_clock 进⾏⼀次对时,假设这时的pts 是落后时间time 的,那么计算pts_drift = pts - time ,计算出pts和time的相对差值。这个可以理解为pts到time的距离,所以pts = time + pts_drift。pts是随着时间增加的,系统时间也在同步增加,这个距离pts_drift是不会变的。

接着,过了⼀会⼉,且在下次对时前,通过get_clock 来查询时间,因为set_clock时的pts 已经过时,不能直接拿set_clock时的pts当做这个时钟的时间。不过我们前⾯计算过pts_drift ,所以我们可以通过当前时刻的时间来估算当前时刻的pts: pts = time + pts_drift 。

FFmpeg中的时间单位

AV_TIME_BASE

  • 定义#define AV_TIME_BASE 1 000 000
  • ffmpeg中的内部计时单位(时间基)

AV_TIME_BASE_Q

  • 定义#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
  • ffmpeg内部时间基的分数表示,实际上它是AV_TIME_BASE的倒数

时间基转换公式

  • timestamp(ffmpeg内部时间戳) = AV_TIME_BASE * time(秒)
  • time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部时间戳)
原文地址:https://www.cnblogs.com/vczf/p/14192668.html