一、音视频为什么会不同步?
在音视频处理中,经常会出现不同步的现象,常见的原因如下
-
录制设备差异:音频和视频可能使用不同的设备进行录制,这可能会导致录制的音频和视频之间存在时间差。
-
编码和传输延迟:音频和视频在编码、传输和解码过程中可能会引入延迟,导致音频和视频不同步。
-
系统时钟漂移:音频和视频设备可能使用不同的系统时钟,导致时钟漂移,从而影响音视频同步。
-
解码和渲染延迟:音频和视频在解码和渲染过程中可能会产生不同的延迟,导致不同步。在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源码分析(一)主函数
未完待续…