=====================================================
=====================================================
⾳视频同步策略
- 以⾳频为基准,同步视频到⾳频(AV_SYNC_AUDIO_MASTER)
- 视频慢了则丢掉部分视频帧(视觉->画⾯跳帧)
- 视频快了则继续渲染上⼀帧
- 以视频为基准,同步⾳频到视频(AV_SYNC_VIDEO_MASTER)
- ⾳频慢了则加快播放速度(或丢掉部分⾳频帧,丢帧极容易听出来断⾳)
- ⾳频快了则放慢播放速度(或重复上⼀帧 )
- ⾳频改变播放速度时涉及到重采样
- 以外部时钟为基准,同步⾳频和视频到外部时钟(AV_SYNC_EXTERNAL_CLOCK)
- 前两者的综合,根据外部时钟改变播放速度
- 视频和⾳频各⾃输出,即不作同步处理(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内部时间戳)