本文主要是介绍ijkplayer音视频同步策略分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
音视频同步是播放器的一道必选题,也是面试官常问的面试题。大家应该都知道音视频同步时钟有三种,默认使用音频时钟作为主时钟。但是面试官会有其他变种问法:如果直播流的音频落后或者中断怎么办?如果没有音频流,以什么时钟作为主时钟?如果有两个音频流(原声和伴奏的播放场景)怎么办?如果视频时间戳落后或者超前怎么办,不同落后程度怎么处理?如果设置倍速播放有没影响?
总结一下音视频同步问题:
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;elsereturn AV_SYNC_AUDIO_MASTER;} else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {if (is->audio_st)return AV_SYNC_AUDIO_MASTER;elsereturn 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 speedif (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 clocksync_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 byduplicating or deleting a frame */diff = get_clock(&is->vidclk) - get_master_clock(is);/* skip or repeat frame. We take into account thedelay to compute the threshold. I still don't knowif 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;// 更新视频ptsSDL_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;
}
如果有双轨音频播放,需要选择其中一个音频时钟作为主时钟。比如原声与伴奏同时播放的场景,选择原声的音频时钟作为主时钟,伴奏同步于原声。
这篇关于ijkplayer音视频同步策略分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!