音视频同步是播放器的一道必选题,也是面试官常问的面试题。大家应该都知道音视频同步时钟有三种,默认使用音频时钟作为主时钟。但是面试官会有其他变种问法:如果直播流的音频落后或者中断怎么办?如果没有音频流,以什么时钟作为主时钟?如果有两个音频流(原声和伴奏的播放场景)怎么办?如果视频时间戳落后或者超前怎么办,不同落后程度怎么处理?如果设置倍速播放有没影响?
总结一下音视频同步问题:
1、视频时间戳落后或超前的处理;
2、倍速播放的处理;
3、双轨音频播放的处理;
4、没有音频流的情况处理;
5、音频流落后或中断的处理;
我们从ijkplayer的ff_ffplay.c进行分析,基本方法有get_clock()、set_clock()、set_clock_at()、set_clock_speed(),具体代码如下:
static double get_clock(Clock *c)
{
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) {
return c->pts;
} else {
double time = av_gettime_relative() / 1000000.0;
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}
}
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 int get_master_sync_type(VideoState *is) {
if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) {
if (is->video_st)
return AV_SYNC_VIDEO_MASTER;
else
return AV_SYNC_AUDIO_MASTER;
} else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {
if (is->audio_st)
return AV_SYNC_AUDIO_MASTER;
else
return AV_SYNC_EXTERNAL_CLOCK;
} else {
return AV_SYNC_EXTERNAL_CLOCK;
}
}
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;
}
接着是设置与检查时钟速度,在倍速播放时需要用到设置时钟速度。代码如下:
static void set_clock_speed(Clock *c, double speed)
{
set_clock(c, get_clock(c), c->serial);
c->speed = speed;
}
static void check_external_clock_speed(VideoState *is) {
if ((is->video_stream >= 0 && is->videoq.nb_packets <= EXTERNAL_CLOCK_MIN_FRAMES) ||
(is->audio_stream >= 0 && is->audioq.nb_packets <= EXTERNAL_CLOCK_MIN_FRAMES)) {
set_clock_speed(&is->extclk, FFMAX(EXTERNAL_CLOCK_SPEED_MIN, is->extclk.speed - EXTERNAL_CLOCK_SPEED_STEP));
} else if ((is->video_stream < 0 || is->videoq.nb_packets > EXTERNAL_CLOCK_MAX_FRAMES) &&
(is->audio_stream < 0 || is->audioq.nb_packets > EXTERNAL_CLOCK_MAX_FRAMES)) {
set_clock_speed(&is->extclk, FFMIN(EXTERNAL_CLOCK_SPEED_MAX, is->extclk.speed + EXTERNAL_CLOCK_SPEED_STEP));
} else {
double speed = is->extclk.speed;
// if isn't normal speed, need to set clock speed
if (speed != 1.0)
set_clock_speed(&is->extclk, speed + EXTERNAL_CLOCK_SPEED_STEP * (1.0 - speed) / fabs(1.0 - speed));
}
}
在音频播放时,如果音频时钟落后或者发生异常,需要把外部时钟同步给音频时钟。具体代码在sdl_audio_callback()方法中:
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
......
is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
/* Let's assume the audio driver that is used by SDL has two periods. */
if (!isnan(is->audio_clock)) {
set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);
// sync external clock to audio clock
sync_clock_to_slave(&is->extclk, &is->audclk);
}
if (!ffp->first_audio_frame_rendered) {
ffp->first_audio_frame_rendered = 1;
ffp_notify_msg1(ffp, FFP_MSG_AUDIO_RENDERING_START);
}
}
在视频播放时,会检查外部时钟速度、计算目标延时时间、更新视频的pts显示时间戳。
计算延时的时候,如果主时钟不是视频时钟,会计算视频时钟与主时钟的差值diff,然后用diff与sync_threshold比较,最终更新delay延时时间:
static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/* update delay to follow master synchronisation source */
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
/* if video is slave, we try to correct big delays by
duplicating or deleting a frame */
diff = get_clock(&is->vidclk) - get_master_clock(is);
/* skip or repeat frame. We take into account the
delay to compute the threshold. I still don't know
if it is the best guess */
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
/* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
else if (diff >= sync_threshold)
delay = 2 * delay;
}
}
if (ffp) {
ffp->stat.avdelay = delay;
ffp->stat.avdiff = diff;
}
return delay;
}
更新视频pts的方法,主要是重新设置时钟、同步从时钟:
static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
/* update current video pts */
set_clock(&is->vidclk, pts, serial);
sync_clock_to_slave(&is->extclk, &is->vidclk);
}
视频播放核心代码在video_refresh()方法。如果视频时间戳落后小于delay,直接去渲染;如果主时钟不是视频时钟,并且视频时间戳落后大于duration,丢弃当前视频帧取下一帧:
static void video_refresh(FFPlayer *opaque, double *remaining_time)
{
// 检查外部时钟
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);
......
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) {
// nothing to do, no picture to display in the queue
} else {
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp);
// 计算延时时间
delay = compute_target_delay(ffp, last_duration, is);
time= av_gettime_relative()/1000000.0;
if (isnan(is->frame_timer) || time < is->frame_timer)
is->frame_timer = time;
// 视频时间戳落后小于delay,直接去渲染
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
is->frame_timer += delay;
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
// 更新视频pts
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
if (frame_queue_nb_remaining(&is->pictq) > 1) {
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
// 如果主时钟不是视频时钟,并且视频时间戳落后大于duration,丢弃当前视频帧取下一帧
if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {
frame_queue_next(&is->pictq);
goto retry;
}
}
frame_queue_next(&is->pictq);
is->force_refresh = 1;
}
display:
/* display picture */
if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display2(ffp);
}
is->force_refresh = 0;
}
如果有双轨音频播放,需要选择其中一个音频时钟作为主时钟。比如原声与伴奏同时播放的场景,选择原声的音频时钟作为主时钟,伴奏同步于原声。