理论
一定要有基础知识再来看。
深入理解音视频pts,dts,time_base以及时间数学公式-优快云博客
FFmpeg中的时间单位
ffplay是基于ffmpeg api开发的播放器,而ffmpeg是以秒做为时间基
ffmpeg中的内部计时单位(时间基)
#define AV_TIME_BASE 1 000 000
ffmpeg内部时间基的分数表示,实际上它是AV_TIME_BASE的倒数
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
基本公式 在头部标题 基础内容有提到
ffplay定义时钟概念
⾳频、视频、外部时钟
typedef struct Clock {
double pts; // 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
double pts_drift; //两时间差值,可以理解为持续时间
double last_updated; // 最后一次更新的系统时钟
double speed; // 时钟速度控制,用于控制播放速度
int serial; // clock is based on a packet with this serial
int paused; // = 1 说明是暂停状态
int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
api和设计原理
在做同步的时候,我们需要⼀个"时钟"的概念,⾳频、视频、外部时钟都有⾃⼰独⽴的时钟,各⾃ set各⾃的时钟,以谁为基准(master), 其他的则只能get该时钟进⾏同步。比如
aclk起始:10ms set_clock(&is->audclk, 10ms, is->audclk.serial);
vclk起始:20ms set_clock(&is->vidclk, 20ms, is->vidclk.serial);
eclk起始:30ms ...
过了10ms
is->av_sync_type = AV_SYNC_VIDEO_MASTER;
printf("AUDIO get master_clock = %0.3lf\n", get_master_clock(is)); //20-30ms内 系统调用时间
is->av_sync_type = AV_SYNC_VIDEO_MASTER;
printf("VIDEO get master_clock = %0.3lf\n", get_master_clock(is));//30-40ms内 与楼上相差10ms
总之每个时钟有自己的物理时间,那这种是如何设计的?
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 void set_clock_at(Clock *c, double pts, int serial, double time)
{
c->pts = pts; // 当前帧的pts
c->last_updated = time; // 最后更新的时间,实际上是当前的一个系统时间
c->pts_drift = c->pts - time;// 当前帧pts和系统时间的差值,正常播放情况下两者的差值应该是比较固定的,因为两者都是以时间为基准进行线性增长
static int s_print_interval = 0;
if(s_print_interval++ > 50) // 用来观察pts_drift是怎么变化的,一般暂停/快进/快退其值才会大变动,如果一直是长速播放则相对值是稳定的
{
s_print_interval = 0;
//printf("pts_drift = %0.3lf\n", c->pts_drift );
}
c->serial = serial;
}
static double get_clock(Clock *c)
{
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) { //如果当前是暂停状态,则返回最新的pts即可,因为暂停时时间没走
return c->pts;
} else { // 如果当前正处在播放状态,则返回的时间为最新的pts + 更新pts之后流逝的时间
double time = av_gettime_relative() / 1000000.0;
// 正常速度播放时,speed=1.0
/* 如果是正常速度播放,speed =1.0,则为c->pts_drift + time,即是c->pts - time1 + time2,即是可以
* 计算出来当前时钟的位置因为time都是一直在走的
*/
/* 并不支持真正的倍速播放,只是在使用外部时钟时的一个微调
*
*/
// printf("base %0.3lf, \n", (time - c->last_updated) * (1.0 - c->speed));
// (time - c->last_updated) * (1.0 - c->speed) 外部时钟变速时有影响 调节播放速度
//getclock pst = c->pts_drift + time (time - c->last_updated)从上次更新到现在的时间差
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}
}
举例
ffplay 音视频同步规则
已经熟悉了api,现在我们来看看ffplay是如何实现音视频同步的!ffplay提供三种同步策略
⾳视频的同步策略,⼀般有如下⼏种:
以⾳频为基准,同步视频到⾳频(AV_SYNC_AUDIO_MASTER)
视频慢了则丢掉部分视频帧(视觉->画⾯跳帧)
视频快了则继续渲染上⼀帧。
以视频为基准,同步⾳频到视频(AV_SYNC_VIDEO_MASTER)
⾳频慢了则加快播放速度(或丢掉部分⾳频帧,丢帧极容易听出来断⾳)
⾳频快了则放慢播放速度(或重复上⼀帧 ) ⾳频改变播放速度时涉及到重采样
以外部时钟为基准,同步⾳频和视频到外部时钟(AV_SYNC_EXTERNAL_CLOCK) 前两者的综合,根据外部时钟改变播放速度
视频和⾳频各⾃输出,即不作同步处理(FREE RUN)
技术层如何实现?
在这里先进行总体概述,具体细节看《实现细节》
以⾳频为基准(最常用) 主体关注视频的帧,通过修改frame_time显示时间,来控制同步。
比如:当前画面落后音频,那就需要减少frame_time,也有drop帧机制对落后的帧进行“减少播放时长”,实现视频提速。
当前画面超过音频,那就需要增加frame_time,对超前的帧长播放(不是一帧画面一直播放,是增加每帧的一点时间有个阈值区间,达到落后音频的时间)。
以视频为基准,主体关注音频的采样点,通过修改wanted_nb_samples控制音频帧播放,来控制同步。
⾳频慢了则加快播放速度,只需减少nb_samples的值。
⾳频快了则放慢播放速度,只需增大nb_samples的值。
以外部时钟为基准
依赖于已同步的audio或video的Clock video,不断通过校正⽅法extclk计算diff值。(音和视频这两者都需要extclk拿值“对时间”)
小总结:
音频为基准 则关注frame_time
视频为基准,则关注wanted_nb_samples
外部时钟,不断set get计算diff实现两者同步
实现细节
以音频为基准
先来回顾视频刷新一帧逻辑
1 upload_texture ffplay.c 1059 0x408f96 根据frame中的像素格式与SDL⽀持的像素格式的匹配,将图像数据输出到 tex
2 video_image_display ffplay.c 1195 0x409797 取出当前需要播放帧,使用sdl渲染显示
3 video_display ffplay.c 1549 0x40a8a2 显示一帧图像 设置纹理 渲染器 窗口
4 video_refresh ffplay.c 1948 0x40b8bc 显示一帧图像,重点是音视频同步规则。lastvp vp nextvp 检查这三帧的合法性
5 refresh_loop_wait_event ffplay.c 3787 0x411217 注册输入设备监听事件,不断取出事件队列处理。也就是不断更新视频的帧
6 event_loop ffplay.c 3834 0x41143c
7 main ffplay.c 4339 0x412b09
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
...
//如果以audio或外部时钟同步,则需要比对主时钟调整待显示帧vp要等待的时间。
//如果以video同步,则delay直接等于last_duration。
delay = compute_target_delay(last_duration, is);
compute_target_delay:
//A. 只要主时钟不是video,就需要作同步校正 计算frame播放时长
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)
diff = get_clock(&is->vidclk) - get_master_clock(is);
...
if (time < is->frame_timer + delay) { //判断是否继续显示上一帧
// 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。
// 计算出最小等待时间
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
...
//B. 更新vidclk,同时更新extclk
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
}
以视频为基准
⾳频的时钟设置在sdl_audio_callback:
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len){
audio_callback_time = av_gettime_relative();
...
static int audio_decode_frame(VideoState *is){
wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
synchronize_audio:
//C. 只要主时钟不是audio,就需要作同步校正
if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER)
diff = get_clock(&is->audclk) - get_master_clock(is);//diff是个正数或负数
}
...
if (!isnan(is->audio_clock)) {
//pts = 最终播放clock - 播放buffer+stream buffer +剩余可用 / 每秒字节数 (最终time-duration数据字节长度/每秒字节数)
//time = 刚刚计算的累计时间
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);//更新音频时钟
//D. 更新audclk,同时更新extclk
sync_clock_to_slave(&is->extclk, &is->audclk);
}
}
以外部时钟为基准
外部时钟为主的同步策略是这样的:video输出和audio输出时都需要作校正,校正的⽅法是参考 extclk计算diff值。因此abcd都会触发
在⾳视频同步基础概念中我们分析过Clock是需要⼀直对时 以保持pts_drift估算出来的pts不会偏差太远,并且get_clock的返回值实际是这⼀Clock对应的流的pts。问题是外部时钟(extclk)是如何对时的?
答案就在B和D步骤中。他们底层都会调用sync_clock_to_slave
/// @brief 从时钟的pts和serial对主时钟对时。
/// @param c
/// @param slave
static void sync_clock_to_slave(Clock *c, Clock *slave)
{
double clock = get_clock(c);
double slave_clock = get_clock(slave);
if (!isnan(slave_clock) && (isnan(clock) || fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD))//范围超过10 重新设置
set_clock(c, slave_clock, slave->serial);
}
既然要把video和audio同步到extclk,我们⽤的extclk校正video和audio,得到更新后的audclk和vidclk,却⼜反过来⽤audclk和vidclk去对时extclk。 分明就是蛋要鸡来⽣,鸡要蛋来敷嘛。
ffplay在计算估算值时,有一个 !isnan(diff)条件。这在第⼀帧的⾳频或视频显示前是不成⽴的,也就⽆需做 同步校正。在第⼀帧视频或⾳频显示后,此时extclk得到对时,接下来就可以进⼊正常的同步“循环”了。
小总结:
以音频为基准
A. 只要主时钟不是video,就需要作同步校正 (改变frame_time)
B. 更新vidclk,同时更新extclk
以视频为基准
C. 只要主时钟不是audio,就需要作同步校正 (改变nb_sample)
D. 更新audclk,同时更新extclk
以外部时钟为基准
ABCD
总结
ffplay音视频同步选项
ffplay -sync
audio:视频同步到⾳频。上⼀节中的A被触发,video输出需要作同步,同步的参考 (get_master_clock)是audclk.
video:⾳频同步到视频。上⼀节中的C被触发,audio输出需要作同步,同步的参考是vidclk。
ext:视频和⾳频都同步到外部时钟,上⼀节中的A和C都被触发,同步的参考是extclk
不论选择的是哪⼀个选项,B和D始终都有执⾏。
使用推荐
推荐使用 ffplay -sync audio 也是默认同步模式
媒体流⾥⾯只有视频成分,这个时候才会⽤以视频为基准。
ext在seek的时候体验⾮常差,没有必要选择这种同步⽅式。
学习资料分享