簡(jiǎn)介
本文主要描述如何以音頻的播放時(shí)長(zhǎng)為基準(zhǔn),將視頻同步到音頻上以實(shí)現(xiàn)音頻的同步播放。音視頻同步指的是視頻和音頻同步,也就是說(shuō)播放的聲音和視頻顯示的畫(huà)面保持一致。
正文
- 視頻幀率(Frame Rate):指的是一秒顯示的幀數(shù)
- 音頻采樣率(Sample Rate):指的是一秒播放的樣本個(gè)數(shù)
我們可以通過(guò)簡(jiǎn)單的計(jì)算得出one Frame 的播放時(shí)間或者是 one Sample的播放時(shí)間。以這樣的速度音頻和視頻各自進(jìn)行播放,在理想情況下,音視頻是會(huì)同步的。但是,實(shí)際情況下,音視頻會(huì)慢慢出現(xiàn)不同步的。不是視頻快了,就是音頻快了,很難準(zhǔn)確的同步。這就需要一種隨著時(shí)間會(huì)線性增長(zhǎng)的量,視頻和音頻的播放速度都以此為準(zhǔn)。播放快了就減慢播放速度,播放快了就加快播放速度。所以,視頻和音頻的同步實(shí)際上是一個(gè)動(dòng)態(tài)同步的過(guò)程,同步是暫時(shí)的,不同步是常態(tài)。以選擇的播放速度量為基準(zhǔn),快的等待慢點(diǎn),慢得則加快速度。
播放速度標(biāo)準(zhǔn)量的選擇一般來(lái)說(shuō)有三種:
- 將視頻同步到音頻上,就是以音頻的播放速度為基準(zhǔn)來(lái)同步視頻。視頻比音頻慢了,加快其播放速度;快了,則減慢其播放速度。
- 將音頻同步到視頻上,就是以視頻的播放速度為基準(zhǔn)來(lái)同步音頻。
- 將視頻和音頻同步外部的時(shí)鐘上,選擇一個(gè)外部時(shí)鐘為基準(zhǔn),視頻和音頻的播放速度都以該時(shí)鐘為標(biāo)準(zhǔn)。
DTS和PTS
在音視頻流中的包都含有DTS和PTS,我們以此作為選擇基準(zhǔn),到底是播放快了還是慢了,或者正以同步的速度播放。
- DTS:Decoding Time Stamp 解碼時(shí)間戳——告訴解碼器packet解碼順序
- PTS:Presenting Time Stamp 顯示時(shí)間戳——指示從packet中解碼出來(lái)的數(shù)據(jù)的顯示順序
計(jì)算視頻Frame的顯示時(shí)間
在計(jì)算某一幀的顯示時(shí)間之前,我們要先了解一下FFMpeg中的時(shí)間單位:Time Base。AVStream的TimeBase用來(lái)顯示的時(shí)間戳。
/**
* This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented.
*
*/
AVRational time_base;
可以看出,AVStream是以秒(s)為單位來(lái)表示Frame的顯示時(shí)間,其類型是AVRational。
PTS為一個(gè)uint64_t的整型,其單位就是time_base。表示視頻長(zhǎng)度的duration也是一個(gè)uint64_t,st為一個(gè)AVStream的指針,av_q2d將一個(gè)AVRational轉(zhuǎn)換為雙精度浮點(diǎn)數(shù)。那么使用如下方法就可以計(jì)算出一個(gè)視頻流的時(shí)間長(zhǎng)度:
time(second) = st->duration * av_q2d(st->time_base)
同樣的方法也可以得到視頻中某幀的顯示時(shí)間:
timestamp(second) = pts * av_q2d(st->time_base)
也就是說(shuō),得到了Frame的PTS后,就可以得到該frame顯示的時(shí)間戳。
得到Frame的PTS
通過(guò)上面的描述知道,如果有了Frame的PTS就計(jì)算出幀的顯示的時(shí)間。下面的代碼展示了在從packet中解碼出frame后,如何得到frame的PTS。
ret = avcodec_receive_frame(video->video_ctx, frame);
if (ret < 0 && ret != AVERROR_EOF)
continue;
if ((pts = av_frame_get_best_effort_timestamp(frame)) == AV_NOPTS_VALUE)
pts = 0;
pts *= av_q2d(video->stream->time_base);
pts = video->synchronize(frame, pts);
frame->opaque = &pts;
注意,這里的pts是double型,因?yàn)閷⑵涑艘粤藅ime_base,代表了該幀在視頻中的時(shí)間位置(秒為單位)。有可能存在調(diào)用av_frame_get_best_effort_timestamp得不到一個(gè)正確的PTS,這樣的情況放在函數(shù)synchronize中處理。
double VideoState::synchronize(AVFrame *srcFrame, double pts)
{
double frame_delay;
if (pts != 0)
video_clock = pts; // Get pts,then set video clock to it
else
pts = video_clock; // Don't get pts,set it to video clock
frame_delay = av_q2d(stream->codec->time_base);
frame_delay += srcFrame->repeat_pict * (frame_delay * 0.5);
video_clock += frame_delay;
return pts;
}
獲取Audio Clock
Audio Clock,也就是Audio的播放時(shí)長(zhǎng),可以在Audio時(shí)更新Audio Clock。在函數(shù)audio_decode_frame中解碼新的packet,這是可以設(shè)置Auddio clock為該packet的PTS
if (pkt.pts != AV_NOPTS_VALUE)
{
audio_state->audio_clock = av_q2d(audio_state->stream->time_base) * pkt.pts;
}
由于一個(gè)packet中可以包含多個(gè)幀,packet中的PTS比真正的播放的PTS可能會(huì)早很多,可以根據(jù)Sample Rate 和 Sample Format來(lái)計(jì)算出該packet中的數(shù)據(jù)可以播放的時(shí)長(zhǎng),再次更新Audio clock 。
// 每秒鐘音頻播放的字節(jié)數(shù) sample_rate * channels * sample_format(一個(gè)sample占用的字節(jié)數(shù))
audio_state->audio_clock += static_cast<double>(data_size) / (2 * audio_state->stream->codec->channels *
audio_state->stream->codec->sample_rate);
上面乘以2是因?yàn)閟ample format是16位的無(wú)符號(hào)整型,占用2個(gè)字節(jié)。
有了Audio clock后,在外面獲取該值的時(shí)候卻不能直接返回該值,因?yàn)閍udio緩沖區(qū)的可能還有未播放的數(shù)據(jù),需要減去這部分的時(shí)間
double AudioState::get_audio_clock()
{
int hw_buf_size = audio_buff_size - audio_buff_index;
int bytes_per_sec = stream->codec->sample_rate * audio_ctx->channels * 2;
double pts = audio_clock - static_cast<double>(hw_buf_size) / bytes_per_sec;
return pts;
}
用audio緩沖區(qū)中剩余的數(shù)據(jù)除以每秒播放的音頻數(shù)據(jù)得到剩余數(shù)據(jù)的播放時(shí)間,從Audio clock中減去這部分的值就是當(dāng)前的audio的播放時(shí)長(zhǎng)。
同步
現(xiàn)在有了video中Frame的顯示時(shí)間,并且得到了作為基準(zhǔn)時(shí)間的音頻播放時(shí)長(zhǎng)Audio clock ,可以將視頻同步到音頻了。
- 用當(dāng)前幀的PTS - 上一播放幀的PTS得到一個(gè)延遲時(shí)間
- 用當(dāng)前幀的PTS和Audio Clock進(jìn)行比較,來(lái)判斷視頻的播放速度是快了還是慢了
- 根據(jù)上一步額判斷結(jié)果,設(shè)置播放下一幀的延遲時(shí)間。
使用要播放的當(dāng)前幀的PTS和上一幀的PTS差來(lái)估計(jì)播放下一幀的延遲時(shí)間,并根據(jù)video的播放速度來(lái)調(diào)整這個(gè)延遲時(shí)間,以實(shí)現(xiàn)視音頻的同步播放。
具體實(shí)現(xiàn):
// 將視頻同步到音頻上,計(jì)算下一幀的延遲時(shí)間
// 使用要播放的當(dāng)前幀的PTS和上一幀的PTS差來(lái)估計(jì)播放下一幀的延遲時(shí)間,并根據(jù)video的播放速度來(lái)調(diào)整這個(gè)延遲時(shí)間
double current_pts = *(double*)video->frame->opaque;
double delay = current_pts - video->frame_last_pts;
if (delay <= 0 || delay >= 1.0)
delay = video->frame_last_delay;
video->frame_last_delay = delay;
video->frame_last_pts = current_pts;
// 根據(jù)Audio clock來(lái)判斷Video播放的快慢
double ref_clock = media->audio->get_audio_clock();
double diff = current_pts - ref_clock;// diff < 0 => video slow,diff > 0 => video quick
double threshold = (delay > SYNC_THRESHOLD) ? delay : SYNC_THRESHOLD;
// 調(diào)整播放下一幀的延遲時(shí)間,以實(shí)現(xiàn)同步
if (fabs(diff) < NOSYNC_THRESHOLD) // 不同步
{
if (diff <= -threshold) // 慢了,delay設(shè)為0
delay = 0;
else if (diff >= threshold) // 快了,加倍delay
delay *= 2;
}
video->frame_timer += delay;
double actual_delay = video->frame_timer - static_cast<double>(av_gettime()) / 1000000.0;
if (actual_delay <= 0.010)
actual_delay = 0.010;
// 設(shè)置一下幀播放的延遲
schedule_refresh(media, static_cast<int>(actual_delay * 1000 + 0.5));
frame_last_pts和frame_last_delay是上一幀的PTS以及設(shè)置的播放上一幀時(shí)的延遲時(shí)間。
- 首先根據(jù)當(dāng)前播放幀的PTS和上一播放幀的PTS估算出一個(gè)延遲時(shí)間。
- 用當(dāng)前幀的PTS和Audio clock相比較判斷此時(shí)視頻播放的速度是快還是慢了
- 視頻播放過(guò)快則加倍延遲,過(guò)慢則將延遲設(shè)置為0
- frame_timer保存著視頻播放的延遲時(shí)間總和,這個(gè)值和當(dāng)前時(shí)間點(diǎn)的差值就是播放下一幀的真正的延遲時(shí)間
- schedule_refresh 設(shè)置播放下一幀的延遲時(shí)間。
總結(jié)
本文主要描述如何利用audio的播放時(shí)長(zhǎng)作為基準(zhǔn),將視頻同步到音頻上以實(shí)現(xiàn)視音頻的同步播放。