之前的文章,已經(jīng)把播放器的讀線程、音頻解碼線程、視頻解碼線程,視頻渲染線程都講了一遍,現(xiàn)在到了播放器實(shí)現(xiàn)最復(fù)雜的功能之一,就是音視頻同步;
ijkplayer 支持 3種同步方式,如下:
1. 以音頻時(shí)鐘為主時(shí)鐘,默認(rèn)方式1. 以視頻時(shí)鐘為主時(shí)鐘-
3. 以外部時(shí)鐘為主時(shí)鐘
因?yàn)槿说亩鋵?duì)音頻特別敏感,所以以音頻為主時(shí)鐘的最為常用;
在ijkplayer中以視頻時(shí)鐘為主時(shí)鐘的只有在視頻流內(nèi)不存在音頻流的情況下才會(huì)啟用,以外部時(shí)鐘為主時(shí)鐘的情況,目前沒看到該邏輯;
以下文章將會(huì)從2個(gè)部分介紹音視頻同步;
1.音視頻同步基礎(chǔ)
2.音頻為主時(shí)鐘同步邏輯分析
1.音視頻同步基礎(chǔ)
以一個(gè)xxx.mp4文件為例,音頻流的格式是 采樣率48000,采樣深度16bit,planer模式,視頻流的格式是 1920x1080,25FPS;
音頻舉例以AAC格式來說,AVFrame里面有1024個(gè)音頻樣本,那么一幀的播放時(shí)間是 0.0213s;
視頻幀率是25FPS,播放一幀視頻的時(shí)間是0.04s

以上的流程圖是,當(dāng)12:00:00 時(shí)播放 文件,此時(shí)音頻和視頻都是播放第一幀,按照預(yù)定的時(shí)間,12:00:120的時(shí)刻應(yīng)該播放 視頻第4幀,音頻的第7幀;
1.1 何為音視頻不同步
假如手機(jī)的打開了某個(gè)軟件,導(dǎo)致視頻播放線程被卡住了,導(dǎo)致線程調(diào)度不及時(shí),從而導(dǎo)致視頻第4幀在 12:00:140才開始播放,音頻播放線程一切正常的話,是不是音視頻就不同步了?;
那如果在上述情況下,音頻播放線程也被卡住了, 導(dǎo)致12:00:140正好播放的是音頻第7幀 和 視頻第4幀,是不是就音視頻能正常同步?;
1.2 視頻落后于音頻
假設(shè)一個(gè)場景,系統(tǒng)卡頓,導(dǎo)致 視頻第4幀在12:00:150的時(shí)刻才播放,但是音頻播放卡頓沒那么嚴(yán)重,音頻幀第7幀在 12:00:153 的時(shí)刻才可以播放,那是不是意味著 視頻比音頻慢了 0.03 s?,這樣的表述是錯(cuò)誤的?;
因?yàn)橐纛l幀 應(yīng)該在14:00:126 的時(shí)刻播放第7幀,但是 實(shí)際上是12:00:153的時(shí)刻才可以播放,音頻幀也慢了0.027s;
預(yù)定的時(shí)間可以消除,所以正確的計(jì)算如下:
視頻pts - 預(yù)定時(shí)間 = 0.03
音頻pts - 預(yù)定時(shí)間 = 0.027
視頻pts - 音頻pts = 0.003
這就是以音頻為主時(shí)鐘的邏輯,拉長或者縮短視頻幀的播放時(shí)長,或者丟棄視頻幀。
以視頻為主時(shí)鐘,就是拉長或者縮短音頻幀的播放時(shí)長,但是不會(huì)丟棄音頻幀。音頻幀連續(xù)性太長,丟幀很容易被耳朵發(fā)現(xiàn)。
以上的舉例,只是為了說明,參考不同的時(shí)鐘,應(yīng)該如何操作音/視頻-幀,從而實(shí)現(xiàn)同步操作;
2.音頻為主時(shí)鐘同步邏輯分析
ijkplayer 默認(rèn)以音頻為主時(shí)鐘,視頻播放過程中,當(dāng)視頻幀晚于預(yù)定播放時(shí)間則丟棄,當(dāng)視頻幀遭遇預(yù)定播放時(shí)間,則重復(fù)播放上一幀 或 增加待顯示幀的播放時(shí)間;
默認(rèn)以音頻主時(shí)鐘
ffp->av_sync_type = AV_SYNC_AUDIO_MASTER;
當(dāng)視頻流不存在音頻,且存在視頻流的情況下,以視頻為主時(shí)鐘
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
} else {
/// 音頻不存在的情況下,以視頻時(shí)鐘為主時(shí)鐘
ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
is->av_sync_type = ffp->av_sync_type;
}
代碼中,音視頻的同步,主要在video_refresh 函數(shù)中實(shí)現(xiàn);而在上一篇文章ijkplayer 源碼解析4(視頻解碼+渲染) 中,已經(jīng)對(duì)video_refresh()的代碼做了分析,且有源碼注釋,這里還需要再對(duì)代碼調(diào)用到的相關(guān)函數(shù)進(jìn)行更深層的分析;
下面來分析一下 compute_target_delay()函數(shù),如下:
當(dāng)以音頻時(shí)鐘為主時(shí)鐘的情況,就會(huì)進(jìn)入到if 條件內(nèi),變量 diff 代表視頻時(shí)鐘和主時(shí)鐘的時(shí)間差,
當(dāng)diff >0 的情況,代表視頻時(shí)鐘比音頻時(shí)鐘快;
當(dāng)diff <0的情況,代表視頻始終比音頻時(shí)鐘慢;
同步閾值sync_threshold也是一個(gè)很重要的參數(shù), 用來根據(jù)不同的FPS調(diào)整閥值大小
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 之間選出一個(gè)值作為 同步閾值。
對(duì)于1/12幀的視頻,delay是 0.082,所以sync_threshold 等于 0.082,等于一幀的播放時(shí)長。
對(duì)于 1/24幀的視頻,delay是 0.041,所以sync_threshold 等于 0.041,等于一幀的播放時(shí)長。
對(duì)于 1/48幀的視頻,delay是 0.0205,所以sync_threshold 等于 0.04,約等于兩幀的播放時(shí)長。
compute_target_delay()函數(shù)代碼注釋如下:
static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{
double sync_threshold, diff = 0;
/// 以音頻時(shí)鐘為主時(shí)鐘的情況
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
//獲取當(dāng)前播放的音頻和視頻 進(jìn)度 的差值
diff = get_clock(&is->vidclk) - get_master_clock(is);
// 計(jì)算同步閥值 (這個(gè)會(huì)單獨(dú)介紹)
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 為負(fù)值代表 視頻比音頻慢
delay = FFMAX(0, delay + diff);
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
/// diff為正數(shù),且延遲超過 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;
}
當(dāng)播放的時(shí)候,通過compute_target_delay()函數(shù),對(duì)比當(dāng)前播放進(jìn)度,決定當(dāng)前幀是立即丟棄還是,延遲播放,視頻刷新代碼代碼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) {
///以音頻時(shí)鐘為主時(shí)鐘的情況
time = av_gettime_relative() / 1000000.0;
if (is->force_refresh || is->last_vis_time + ffp->rdftspeed < time) {
/// 符合立即刷新的情況,且 is->last_vis_time +0.02 仍然小于當(dāng)前時(shí)間
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) {
/// 隊(duì)列中沒有視頻幀 什么也不做
} else {
double last_duration, duration, delay;
Frame *vp, *lastvp;
/* dequeue the picture */
/// 獲取上一幀
lastvp = frame_queue_peek_last(&is->pictq);
/// 獲取準(zhǔn)備播放的當(dāng)前幀
vp = frame_queue_peek(&is->pictq);
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
if (lastvp->serial != vp->serial)
/// 當(dāng)seek或者快進(jìn)、快退的情況重新賦值 frame_time 時(shí)間
is->frame_timer = av_gettime_relative() / 1000000.0;
if (is->paused)
/// 暫停的情況重復(fù)顯示上一幀
goto display;
/* compute nominal last_duration */
/// last_duration 表示上一幀播放時(shí)間
last_duration = vp_duration(is, lastvp, vp);
/// delay 表示當(dāng)前幀需要播放的時(shí)間
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 不準(zhǔn)的情況下更新
is->frame_timer = time;
if (time < is->frame_timer + delay) {
/// 即將播放的幀+播放時(shí)長 大于 當(dāng)前時(shí)間,則可以播放,跳轉(zhuǎn)到display播放
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
/// 更新 is->frame_timer時(shí)間
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))
/// 更新視頻時(shí)鐘
update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
if (frame_queue_nb_remaining(&is->pictq) > 1) {
/// 隊(duì)列視頻幀>1 的情況 當(dāng)前幀展示時(shí)間 大于當(dāng)前時(shí)間,則丟掉該幀
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);
/// 設(shè)置立即刷新
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 默認(rèn)為 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 默認(rèn)為 0,這部分邏輯不分析
省略部分代碼...
}
}
2.1 解碼視頻幀丟幀的情況
在解碼視頻幀的時(shí)候,如果發(fā)現(xiàn)解碼后的視頻幀已經(jīng)晚于當(dāng)前播放時(shí)間,則丟棄
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)
/// 獲取當(dāng)前展示幀 的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) {
/// 獲取當(dāng)前展示幀 于系統(tǒng)時(shí)間的 差值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 && /// 序列號(hào)一致(同一個(gè)播放序列)
is->videoq.nb_packets) { /// 視頻隊(duì)列還有其它視頻幀
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;
}
至此,音視頻同步的邏輯已經(jīng)講解完畢了,看官老爺們,參照源碼,一行不落的閱讀,方能修成正果??;
`