一般音视频同步有三种方法:视频同步音频、音频同步视频、外部时钟。IjkPlayer 没有实现外部时钟这种方式。所以只介绍视频同步音频和音频同步视频。
音视频同步的核心就是:比较两个的播放时间,然后判断一方是追赶还是等待另一方。所以我们先分析音频和视频更新播放时间的代码:
视频设置时间轴:
其中的vp->pts 为当前帧的显示时间,其他不重要。
set_clock_at:
重点:
- pts:最新视频帧的显示时间;(同步时比较的时间)
- last_updated :更新时间
- pts_drift: 视频帧的显示时间与更新时间差;
音频设置时间轴:
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_clock_to_slave(&is->extclk, &is->audclk);
}
也是调用set_clock_at:
重点:
- pts = is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout) 该帧音频播放完成时间 - 该帧还未播放时长 - 延迟时间(同步时比较的时间)
- last_updated = ffp->audio_callback_time / 1000000.0 获取需要播放音频数据的时间
- pts_drift = pts - last_updated
解释下pts的式子:
is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout)
- is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate; 即该帧音频播放完成时间
- audio_write_buf_size: 表示该帧还未发送给播放器的字节数;
- bytes_per_sec:每秒钟消耗的音频字节数
- (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec:该帧还未播放时长
- SDL_AoutGetLatencySeconds(ffp->aout) :播放延迟时间,作者自己假设的值 = 4 * AudioTrack.getMinBufferSize() / 采样率
- 播放延迟:https://blog.csdn.net/King1425/article/details/104901314
IjkPlayer中,因为每次发送给播放器的音频数据最多为256byte,如果该帧大于256byte,则会被拆分多次发送。这也是为啥有需要减去该帧还未播放时长的原因
由上面可以知道 pts = 该帧音频播放完成时间 - 该帧还未播放时长 - 延迟时间;
OK,知道了上面的知识后,就可以分析音视频同步了。先分析视频同步音频:
在ff_ffplay.c中的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);
// 用当前帧与上一帧的显示间隔时间来确定一个合理的同步阀值,该值一定大于0。合理----不能太小,也不能太大
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
// 同步补偿时间还在能处理的范围
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
// 由于sync_threshold一定大于0 =====》-sync_threshold <0
// -0.12 < -0.1
// diff <= -sync_threshold =====》diff <0 ,说明视频比音频慢,但是同步阀值不足以弥补该误差,缩短两帧显示间隔时间,即提前播放该帧,追赶音频
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
// diff >= sync_threshold>0 =====>视频比音频快。
// delay > AV_SYNC_FRAMEDUP_THRESHOLD======>且两帧间隔时间足够长。
// =======> 增加两帧显示间隔时间,即延迟播放该帧,等待音频
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
// 视频比音频,适量增加两帧显示间隔时间,即延迟播放该帧,等待音频
else if (diff >= sync_threshold)
// 注意:乘2,多余的一个delay为适当的等待时间
delay = 2 * delay;
}
}
if (ffp) {
// delay>0
// 记录本次同步补偿的时间
ffp->stat.avdelay = delay;
// 记录需要同步补偿的总时间
ffp->stat.avdiff = diff;
}
// 返回同步补偿时间
return delay;
}
上面代码就做了4几件事:
- 判断音视频的时间差是否在能处理的范围内???
- 视频比音频慢sync_threshold (同步阀值)以上,则delay = FFMAX(0, delay + diff);(diff < 0) 表示缩短相邻视频帧间的显示间隔时间,即加快播放当前帧。
- 视频比音频快sync_threshold以上,且相邻视频帧间的显示间隔足够长(上一帧会显示很久),则delay = delay + diff; 增加相邻视频帧间的显示间隔时间,即延迟播放当前帧。
- 视频比音频快sync_threshold以上,但相邻视频帧间的显示间隔比较短,则delay = 2 * delay; 适当的增加相邻视频帧间的显示间隔时间,即适当的延迟播放当前帧。多次调控。
但是你会发现还有两种情况并没有处理 :
视频比音频慢,但小于sync_threshold
视频比音频快,但小于sync_threshold
为啥了这两种情况不同处理了???这里我给出自己的猜测:
视频时间轴中 pts = 最新视频帧的显示时间;其下一次pts的更新时间是下一帧的显示时间
音频时间轴中的 pts = 该帧音频播放完成时间 - 该帧还未播放时长 - 延迟时间;每向播放器发送256字节音频数据,更新一次pts
所以 两个时间轴中的 pts 相减的极值 = 相邻视频帧的显示时间间隔。
而 sync_threshold是用相邻帧的显示间隔时间来确定一个合理的同步阀值。
所以小于sync_threshold的情况,可能是时间轴更新不及时引起的,不需要处理。
上面处理了音视频的时间差,在能处理的范围内,如果不再能处理的范围内,是如何处理的那???丢帧,如下:
// 如果还有两帧以上没展示,进入
if (frame_queue_nb_remaining(&is->pictq) > 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;
}
}
OK,视频同步音频就介绍完了,详细的代码请看:Android --- IjkPlayer 阅读native层源码之如何刷新视频的播放界面(七)
音频同步视频:
在ff_ffplay.c中的synchronize_audio函数中:
static int synchronize_audio(VideoState *is, int nb_samples)
{
int wanted_nb_samples = nb_samples;
// 如果不是视频同步音频,进入
if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) {
double diff, avg_diff;
int min_nb_samples, max_nb_samples;
// 音频和视频的时间差
diff = get_clock(&is->audclk) - get_master_clock(is);
// diff!=NAN 且差值大小在可接受范围
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
// 将音视频时间差放入权重池
is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
// 如果参与计算的帧数小于AUDIO_DIFF_AVG_NB=20,样本不足,不计算
if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
is->audio_diff_avg_count++;
} else {
// 计算平均的时间差
avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
// 如果平均时间差大于了该帧两倍采样时间(audio_diff_threshold)
if (fabs(avg_diff) >= is->audio_diff_threshold) {
//计算期望的播放samples数量,公式:时间*频率=采集的sample数
wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
// 计算最小的音频采集速度。能获得的最少的sample数量
min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));
// 计算最大的音频采集速度。能获得的最多的sample数量
max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));
// 保证期望输出的sample数量不要变化太大
wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
}
}
} else {
// 音视频时间差超过了可控范围,清空权重池
is->audio_diff_avg_count = 0;
is->audio_diff_cum = 0;
}
}
return wanted_nb_samples;
}
上面写了详细的注释,但还是多说几句:
- a. 首先降低原来的所有时间差的权重-------audio_diff_cum*audio_diff_avg_coef;
- b. 接着将当前帧的时间差加入a中,产生新的audio_diff_cum;
- c. 再计算出平均的时间差 = audio_diff_cum * (1.0 - audio_diff_avg_coef),
- d. 然后计算出添加或者减少的sample数量并与当前帧相加得到期望输出的sample数;
- e. 最后如果平均的时间差大于播放器的延迟时间(上面提到的SDL_AoutGetLatencySeconds),就会改变该的输出sample数,并保证期望输出的sample数量变化在SAMPLE_CORRECTION_PERCENT_MAX范围内。
计算好了该帧需要播放的样本sample数量(wanted_nb_samples),将其设置给重采样上下文,然后通过重采样将该帧播放的样本sample数改变为wanted_nb_samples,由公式:播放的样本sample数 / 采样率 = 该帧的播放时间。 因为采样率没变,所以该帧的播放时间和播放的样本sample数成正相关。从而达到音频去同步视频的目的。如下图,设置重采样上下文:
OK,音频同步视频也将完了,如果你不懂具体音频流程请看:Android --- IjkPlayer 阅读native层源码之解码成功后的音频数据如何发送回Android播放(九)
虽然写完了这篇博客,但是发现里面有很多变量没有解释,要读懂,你可能还需要联系我前几篇博客的内容阅读。
最后说一点:IjkPlayer 默认使用的是视频同步音频,原因在于人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验。并且音频的播放时钟为线性增长。