ijkplayer 源码解析5(音视频同步)
之前的文章,已经把播放器的读线程
、音频解码线程
、视频解码线程
,视频渲染线程
都讲了一遍,现在到了播放器实现最复杂的功能之一,就是音视频同步
;
ijkplayer
支持 3种同步方式,如下:
1. 以音频时钟为主时钟,默认方式
1. 以视频时钟为主时钟
-
3. 以外部时钟为主时钟
因为人的耳朵对音频特别敏感,所以以音频为主时钟的最为常用;
在ijkplayer
中以视频时钟
为主时钟的只有在视频流内不存在音频流的情况
下才会启用,以外部时钟为主时钟的情况,目前没看到该逻辑;
以下文章将会从2个部分介绍音视频同步;
1.音视频同步基础
2.音频为主时钟同步逻辑分析
1.音视频同步基础
以一个xxx.mp4
文件为例,音频流的格式是 采样率48000
,采样深度16bit
,planer模式
,视频流的格式是 1920x1080
,25FPS
;
音频举例以AAC
格式来说,AVFrame
里面有1024
个音频样本,那么一帧的播放时间是 0.0213s
;
视频帧率是25FPS
,播放一帧视频的时间是0.04s
以上的流程图是,当12:00:00
时播放 文件,此时音频和视频都是播放第一帧,按照预定的时间,12:00:120
的时刻应该播放 视频第4帧
,音频的第7帧
;
1.1 何为音视频不同步
假如手机的打开了某个软件,导致视频播放线程被卡住了,导致线程调度不及时,从而导致视频第4帧
在 12:00:140
才开始播放,音频播放线程一切正常的话,是不是音视频就不同步了✅;
那如果在上述情况下,音频播放线程也被卡住了, 导致12:00:140
正好播放的是音频第7帧
和 视频第4帧
,是不是就音视频能正常同步✅;
1.2 视频落后于音频
假设一个场景,系统卡顿,导致 视频第4帧
在12:00:150
的时刻才播放,但是音频播放卡顿没那么严重,音频帧第7帧
在 12:00:153
的时刻才可以播放,那是不是意味着 视频比音频慢了 0.03 s
?,这样的表述是错误的❌;
因为音频帧 应该在14:00:126
的时刻播放第7帧,但是 实际上是12:00:153
的时刻才可以播放,音频帧也慢了0.027s
;
预定的时间可以消除,所以正确的计算如下:
视频pts - 预定时间 = 0.03
音频pts - 预定时间 = 0.027
视频pts - 音频pts = 0.003
这就是以音频为主时钟的逻辑,拉长或者缩短视频帧的播放时长,或者丢弃视频帧。
以视频为主时钟,就是拉长或者缩短音频帧的播放时长,但是不会丢弃音频帧。音频帧连续性太长,丢帧很容易被耳朵发现。
以上的举例,只是为了说明,参考不同的时钟,应该如何操作音/视频-帧
,从而实现同步操作;
2.音频为主时钟同步逻辑分析
ijkplayer
默认以音频为主时钟,视频播放过程中,当视频帧晚于预定播放时间则丢弃,当视频帧遭遇预定播放时间,则重复播放上一帧 或 增加待显示帧的播放时间;
默认以音频主时钟
ffp->av_sync_type = AV_SYNC_AUDIO_MASTER;
当视频流不存在音频,且存在视频流的情况下,以视频为主时钟
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
} else {
/// 音频不存在的情况下,以视频时钟为主时钟
ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
is->av_sync_type = ffp->av_sync_type;
}
代码中,音视频的同步,主要在video_refresh
函数中实现;而在上一篇文章ijkplayer 源码解析4(视频解码+渲染) 中,已经对video_refresh()
的代码做了分析,且有源码注释,这里还需要再对代码调用到的相关函数进行更深层的分析;
下面来分析一下 compute_target_delay()
函数,如下:
当以音频时钟为主时钟的情况,就会进入到if 条件内,变量 diff 代表视频时钟和主时钟的时间差,
当diff >0
的情况,代表视频时钟比音频时钟快
;
当diff <0
的情况,代表视频始终比音频时钟慢
;
同步阈值sync_threshold
也是一个很重要的参数, 用来根据不同的FPS
调整阀值大小
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
1,AV_SYNC_THRESHOLD_MIN
,最小的同步阈值,值为0.04
,单位是 秒
2,AV_SYNC_THRESHOLD_MAX
,最大的同步阈值,值为0.1
,单位是 秒
上面的代码,就是从0.04 ~ 0.1
之间选出一个值作为 同步阈值。
对于1/12
帧的视频,delay
是 0.082,所以sync_threshold
等于 0.082
,等于一帧的播放时长。
对于 1/24
帧的视频,delay
是 0.041,所以sync_threshold
等于 0.041
,等于一帧的播放时长。
对于 1/48
帧的视频,delay
是 0.0205,所以sync_threshold
等于 0.04
,约等于两帧的播放时长。
compute_target_delay()
函数代码注释如下:
static double compute_target_delay(FFPlayer *ffp, 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);
// 计算同步阀值 (这个会单独介绍)
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)
/// diff 为负值代表 视频比音频慢
delay = FFMAX(0, delay + diff);
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
/// diff为正数,且延迟超过 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;
}
#ifdef FFP_SHOW_AUDIO_DELAY
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
#endif
return delay;
}
当播放的时候,通过compute_target_delay()
函数,对比当前播放进度,决定当前帧是立即丢弃还是,延迟播放,视频刷新代码代码video_refresh()
逻辑和注释如下⬇️;
static void video_refresh(FFPlayer *opaque, double *remaining_time)
{
FFPlayer *ffp = opaque;
VideoState *is = ffp->is;
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 (!ffp->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 + ffp->rdftspeed < time) {
/// 符合立即刷新的情况,且 is->last_vis_time +0.02 仍然小于当前时间
video_display2(ffp);
is->last_vis_time = time;
}
*remaining_time = FFMIN(*remaining_time, is->last_vis_time + ffp->rdftspeed - time);
}
/// 判断视频流存在
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) {
/// 队列中没有视频帧 什么也不做
} else {
double last_duration, duration, delay;
Frame *vp, *lastvp;
/* dequeue the picture */
/// 获取上一帧
lastvp = frame_queue_peek_last(&is->pictq);
/// 获取准备播放的当前帧
vp = frame_queue_peek(&is->pictq);
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
if (lastvp->serial != vp->serial)
/// 当seek或者快进、快退的情况重新赋值 frame_time 时间
is->frame_timer = av_gettime_relative() / 1000000.0;
if (is->paused)
/// 暂停的情况重复显示上一帧
goto display;
/* compute nominal last_duration */
/// last_duration 表示上一帧播放时间
last_duration = vp_duration(is, lastvp, vp);
/// delay 表示当前帧需要播放的时间
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 不准的情况下更新
is->frame_timer = time;
if (time < is->frame_timer + delay) {
/// 即将播放的帧+播放时长 大于 当前时间,则可以播放,跳转到display播放
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
/// 更新 is->frame_timer时间
is->frame_timer += delay;
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->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
if (frame_queue_nb_remaining(&is->pictq) > 1) {
/// 队列视频帧>1 的情况 当前帧展示时间 大于当前时间,则丢掉该帧
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
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;
}
}
if (is->subtitle_st) {
/// 视频字幕流的情况 暂不分析
while (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
if (frame_queue_nb_remaining(&is->subpq) > 1)
sp2 = frame_queue_peek_next(&is->subpq);
else
sp2 = NULL;
if (sp->serial != is->subtitleq.serial
|| (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
|| (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
{
if (sp->uploaded) {
ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, "", 1);
}
frame_queue_next(&is->subpq);
} else {
break;
}
}
}
/// Video Frame queue 索引+1
frame_queue_next(&is->pictq);
/// 设置立即刷新
is->force_refresh = 1;
SDL_LockMutex(ffp->is->play_mutex);
if (is->step) {
is->step = 0;
if (!is->paused)
stream_update_pause_l(ffp);
}
SDL_UnlockMutex(ffp->is->play_mutex);
}
display:
/* display picture */
/// 渲染开启、force_refresh ==1 、show_mode 默认为 SHOW_MODE_VIDEO 的情况渲染
if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display2(ffp);
}
is->force_refresh = 0;
if (ffp->show_status) {
/// show_status 默认为 0,这部分逻辑不分析
省略部分代码...
}
}
2.1 解码视频帧丢帧的情况
在解码视频帧的时候,如果发现解码后的视频帧已经晚于当前播放时间,则丢弃
get_video_frame()
逻辑如下,代码注释;
static int get_video_frame(FFPlayer *ffp, AVFrame *frame)
{
VideoState *is = ffp->is;
int got_picture;
ffp_video_statistic_l(ffp);
/// 解码
if ((got_picture = decoder_decode_frame(ffp, &is->viddec, frame, NULL)) < 0)
return -1;
if (got_picture) {
double dpts = NAN;
if (frame->pts != AV_NOPTS_VALUE)
/// 获取当前展示帧 的PTS
dpts = av_q2d(is->video_st->time_base) * frame->pts;
frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
if (ffp->framedrop>0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
ffp->stat.decode_frame_count++;
if (frame->pts != AV_NOPTS_VALUE) {
/// 获取当前展示帧 于系统时间的 差值diff
double diff = dpts - get_master_clock(is);
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD && /// 差值小于100
diff - is->frame_last_filter_delay < 0 && /// 视频比音频慢的条件
is->viddec.pkt_serial == is->vidclk.serial && /// 序列号一致(同一个播放序列)
is->videoq.nb_packets) { /// 视频队列还有其它视频帧
is->frame_drops_early++;
is->continuous_frame_drops_early++;
if (is->continuous_frame_drops_early > ffp->framedrop) {
is->continuous_frame_drops_early = 0;
} else {
ffp->stat.drop_frame_count++;
ffp->stat.drop_frame_rate = (float)(ffp->stat.drop_frame_count) / (float)(ffp->stat.decode_frame_count);
/// 丢弃该帧
av_frame_unref(frame);
got_picture = 0;
}
}
}
}
}
return got_picture;
}
至此,音视频同步的逻辑已经讲解完毕了,看官老爷们,参照源码,一行不落的阅读,方能修成正果🙏;
`