[TOC]
開始前的BB
有些沒有接觸過的童鞋可能還不知道音視頻同步是什么意思,大家印象中應(yīng)該看到過這樣的視頻,畫面中的人物說話和聲音出來的不在一起,小時(shí)候看有些電視臺(tái)轉(zhuǎn)播的港片的時(shí)候(別想歪 TVB)有時(shí)候就會(huì)遇到 明明聲音已經(jīng)播出來了,但是播的圖像比聲音慢了很多,看的極為不舒服,這個(gè)時(shí)候就發(fā)生了音視頻不同步的情況,而音視頻同步,就是讓聲音與畫面對(duì)應(yīng)上
這里有個(gè)知識(shí)點(diǎn)需要記一下
人對(duì)于圖像和聲音的接受靈敏程度不一樣,人對(duì)音頻比對(duì)視頻敏感;視頻放快一點(diǎn),可能察覺的不是特別明顯,但音頻加快或減慢,人耳聽的很敏感
PTS的由來
音視頻同步依賴的一個(gè)東西就是pts(persentation time stamp )顯示時(shí)間戳 告訴我們?cè)撌裁磿r(shí)間顯示這一幀 ,那么,這個(gè)東西是從哪里來的呢?
刨根問底欄目組將帶你深度挖掘
PTS是在拍攝的時(shí)候打進(jìn)去的時(shí)間戳,假如我們現(xiàn)在拍攝一段小視頻(別想歪啊),什么特效都不加,那么走的就是以下的步驟
我們根據(jù)這個(gè)圖可以知道,PTS是在錄制的時(shí)候就打進(jìn)Frame里的
音視頻同步的方式
在ffplay中 音視頻同步有三種方式
- 以視頻為基準(zhǔn),同步音頻到視頻
- 音頻慢了就加快音頻的播放速度,或者直接丟掉一部分音頻幀
- 音頻快了就放慢音頻的播放速度
- 以音頻為基準(zhǔn),同步視頻到音頻
- 視頻慢了則加快播放或丟掉部分視頻幀
- 視頻快了則延遲播放
- 以外部時(shí)鐘為準(zhǔn),同步音頻和視頻到外部時(shí)鐘
- 根據(jù)外部時(shí)鐘改版音頻和視頻的播放速度
視頻基準(zhǔn)
如果以視頻為基準(zhǔn)進(jìn)行同步,那么我們就要考慮可能出現(xiàn)的情況,比如:
掉幀
此時(shí)的音頻應(yīng)該怎么做呢?通常的方法有
- 音頻也丟掉相應(yīng)的音頻幀(會(huì)有斷音,比如你說了一句,我的天啊,好漂亮的草原啊 很不湊巧丟了幾幀視頻,就成,,,臥槽!)
- 音頻加快速度播放(此處可以直接用Audition加快個(gè)幾倍速的播放一首音樂)
音頻基準(zhǔn)
如果以音頻為基準(zhǔn)進(jìn)行同步,很不幸的碰到了掉幀的情況,那么視頻應(yīng)該怎么做呢?通常也有兩種做法
1.視頻丟幀 (畫面跳幀,丟的多的話,俗稱卡成PPT)
2.加快播放速度(畫面加快播放)
外部時(shí)鐘為基準(zhǔn)
假如以外部時(shí)鐘為基準(zhǔn),如果音視頻出現(xiàn)了丟幀,怎么辦呢?
如果丟幀較多,直接重新初始化外部時(shí)鐘 (pts和時(shí)鐘進(jìn)行對(duì)比,超過一定閾值重設(shè)外部時(shí)鐘,比如1s)
音視頻時(shí)間換算
PTS 時(shí)間換算
之前我們稍微講過pts的時(shí)間換算,pts換算成真正的秒是用以下操作
realTime = pts * av_q2d(stream.time_base)
stream是當(dāng)前的視頻/音頻流
我們這里主要講一下在音頻解碼pts可能會(huì)遇到的情況,有時(shí)候音頻幀的pts會(huì)以1/采樣率為單位,像
pts1 = 0
pts2 = 1024
pts3 = 2048
像我們例子中的這個(gè)視頻,我們?cè)诮獯a一幀音頻之后打印出來他的pts
std::cout<<"audio pts : "<<frame->pts<<std::endl;
我們知道當(dāng)前視頻的音頻采樣率為44100,那么這個(gè)音頻幀pts的單位就是1/44100,那么
pts1 = 0 * 1 / 44100 = 0
pts2 = 1024 * 1 / 44100 = 0.232
pts3 = 2048 * 1 / 44100 = 0.464
音頻流的time_base里面正是記錄了這個(gè)值,我們可以通過debug來看一下
利用
realTime = pts * av_q2d(stream.time_base)我們可以直接算出來當(dāng)前音頻幀的pts
另外需要注意
在ffplay中做音視頻同步,都是以秒為單位
音視頻幀播放時(shí)間換算
音頻幀播放時(shí)間計(jì)算
音頻幀的播放和音頻的屬性有關(guān)系,是采用
采樣點(diǎn)數(shù) * 1 / 采樣率
來計(jì)算,像AAC當(dāng)個(gè)通道采樣是1024個(gè)采樣點(diǎn),那么
- 如果是44.1khz,那么一幀的播放時(shí)長就是 1024 * 1 / 44100 = 23.3毫秒
- 如果是48khz,那么一幀的播放時(shí)長就是 1024 * 1 / 48000 = 21.33毫秒
視頻幀的播放時(shí)間計(jì)算
視頻幀的播放時(shí)間也有兩個(gè)計(jì)算方式
-
利用
1/幀率獲取每個(gè)幀平均播放時(shí)間,這個(gè)方式有一個(gè)很大的缺點(diǎn)就是,不能動(dòng)態(tài)響應(yīng)視頻幀的變化,比如說我們做一些快速慢速的特效,有的廠商或者SDK(我們的SDK不是)是直接改變視頻幀的增加/減少視頻幀之間的pts間距來實(shí)現(xiàn)的,這就導(dǎo)致在一些拿幀率計(jì)算顯示時(shí)間的播放器上發(fā)現(xiàn)是整體(快/慢)了,達(dá)不到想要的效果;還有一種情況就是丟幀之后,時(shí)間顯示仍然是固定的 - 相鄰幀相減 這大程度上避免利用幀率去算的各種弊端,但是缺點(diǎn)是使用起來比較復(fù)雜,尤其是暫停/Seek之類的操作的時(shí)候需要進(jìn)行一些時(shí)間差值的計(jì)算
時(shí)間校正
視頻時(shí)間校正
在看ffplay的時(shí)候我們會(huì)發(fā)現(xiàn),他在里面默認(rèn)情況下是用了
frame->pts = frame->best_effort_timestamp;
其實(shí)大多數(shù)情況下pts和best_effort_timestamp的值是一樣的,這個(gè)值是利用各種探索方法去計(jì)算當(dāng)前幀的視頻戳
音頻時(shí)間校正
音頻的pts獲取比視頻的要復(fù)雜一點(diǎn),在ffplay中對(duì)音頻的pts做了三次修改
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
將其由stream->time_base轉(zhuǎn)為(1/采樣率)(decoder_decode_frame()中)af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
將其由(1/采樣率)轉(zhuǎn)換為秒 (audio_thread()中)is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec
根據(jù)實(shí)際輸入進(jìn)SDL2播放的數(shù)據(jù)長度做調(diào)整 (sdl_audio_callback中)
ffplay 時(shí)鐘框架
ffplay中的時(shí)鐘框架主要依靠Clock結(jié)構(gòu)體和相應(yīng)的方法組成
/** 時(shí)鐘結(jié)構(gòu)體 **/
typedef struct Clock {
double pts; /* clock base 時(shí)間基準(zhǔn)*/
double pts_drift; /* clock base minus time at which we updated the clock 時(shí)間基減去更新時(shí)鐘的時(shí)間 */
double last_updated;
double speed;
int serial; /* clock is based on a packet with this serial */
int paused;
int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
/** 初始化時(shí)鐘 **/
static void init_clock(Clock *c, int *queue_serial);
/** 獲取當(dāng)前時(shí)鐘 **/
static double get_clock(Clock *c);
/** 設(shè)置時(shí)鐘 內(nèi)部調(diào)用set_clock_at()**/
static void set_clock(Clock *c, double pts, int serial);
/** 設(shè)置時(shí)鐘 **/
static void set_clock_at(Clock *c, double pts, int serial, double time);
/** 設(shè)置時(shí)鐘速度 **/
static void set_clock_speed(Clock *c, double speed);
/** 音/視頻設(shè)置時(shí)鐘的時(shí)候都回去跟外部時(shí)鐘進(jìn)行對(duì)比,防止丟幀或者丟包情況下時(shí)間差距比較大而進(jìn)行的糾偏 **/
static void sync_clock_to_slave(Clock *c, Clock *slave);
/** 獲取做為基準(zhǔn)的類型 音頻 外部時(shí)鐘 視頻 **/
static int get_master_sync_type(VideoState *is);
/** 獲取主時(shí)間軸的時(shí)間 **/
static double get_master_clock(VideoState *is);
/** 檢查外部時(shí)鐘的速度 **/
static void check_external_clock_speed(VideoState *is);
這個(gè)時(shí)鐘框架也是比較簡(jiǎn)單,可以直接去看FFplay的源碼,這里就不過多的敘述
音視頻同步時(shí)間軸
在ffplay中,我們不管是以哪個(gè)方式做為基準(zhǔn),都是有一個(gè)時(shí)間軸
就像這樣子,有一個(gè)時(shí)鐘一直在跑,所謂基于音頻、視頻、外部時(shí)間 做為基準(zhǔn),也就是將那個(gè)軸的的時(shí)間做為時(shí)間軸的基準(zhǔn),另一個(gè)在軸參照主時(shí)間軸進(jìn)行同步
假如是以音頻為基準(zhǔn),視頻同步音頻的方式,那么就是音頻在每播放一幀的時(shí)候,就去將當(dāng)前的時(shí)間同步到時(shí)間軸,視頻參考時(shí)間軸做調(diào)整
音頻時(shí)鐘設(shè)置
音頻時(shí)鐘的設(shè)置的話需要考慮注意 硬件緩存數(shù)據(jù) 設(shè)置音頻時(shí)鐘的時(shí)候需要將
pts - 硬件緩沖數(shù)據(jù)的播放時(shí)間
詳情參考 ffplay 中
sdl_audio_callback(void *opaque, Uint8 *stream, int len)
set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
這是就是將音頻的pts - 硬件緩沖區(qū)里剩下的時(shí)間設(shè)置到了音頻的時(shí)鐘里
視頻時(shí)鐘設(shè)置
視頻時(shí)鐘設(shè)置的話就比較簡(jiǎn)單了,直接設(shè)置pts,在ffplay中
queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)內(nèi),我們可以直接看到 vp->pts = pts;
,然后在video_refresh里面update_video_pts(is, vp->pts, vp->pos, vp->serial);去調(diào)用了set_clock
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);
}
音視頻同步操作
音視頻在同步上出的處理我們上面有簡(jiǎn)單講到過,我們這里來詳細(xì)看一下他具體是真么做的
音頻同步操作
音頻的同步操作是在audio_decode_frame()中的wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);,注意synchronize_audio
方法,我們來看他注釋
/* return the wanted number of samples to get better sync if sync_type is video
* or external master clock
*
* 如果同步類型為視頻或外部主時(shí)鐘,則返回所需的采樣數(shù)來更好的同步。
*
* */
static int synchronize_audio(VideoState *is, int nb_samples)
這個(gè)方法里面的操作有點(diǎn)多,我這邊簡(jiǎn)單說一下這個(gè)方法,主要是利用音頻時(shí)鐘與主時(shí)鐘相減得到差值(需要先判斷音頻是不是主時(shí)間軸),然后返回如果要同步需要的采樣數(shù),在audio_decode_frame()中用len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
進(jìn)行重采樣,然后才在sdl_audio_callback()中進(jìn)行播放
視頻同步操作
視頻同步操作的主要步驟是在video_refresh()方法中,我們來看一下關(guān)鍵的地方
/* compute nominal last_duration 根據(jù)當(dāng)前幀和上一幀的pts計(jì)算出來上一幀顯示的持續(xù)時(shí)間 */
last_duration = vp_duration(is, lastvp, vp);
/** 計(jì)算當(dāng)前幀需要顯示的時(shí)間 **/
delay = compute_target_delay(last_duration, is);
/** 獲取當(dāng)前的時(shí)間 **/
time= av_gettime_relative()/1000000.0;
/** 如果當(dāng)前時(shí)間小于顯示時(shí)間 則直接進(jìn)行顯示**/
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
/** 更新視頻的基準(zhǔn)時(shí)間 **/
is->frame_timer += delay;
/** 如果當(dāng)前時(shí)間與基準(zhǔn)時(shí)間偏差大于 AV_SYNC_THRESHOLD_MAX 則把視頻基準(zhǔn)時(shí)間設(shè)置為當(dāng)前時(shí)間 **/
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
/** 更新視頻時(shí)間軸 **/
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
/** 如果隊(duì)列中有未顯示的幀,如果開啟了丟幀處理或者不是以視頻為主時(shí)間軸,則進(jìn)行丟幀處理 **/
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 && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}
到這里,ffplay中主要的音視頻同步就講完了,建議去看一下ffplay的源碼,多體會(huì)體會(huì) 印象才會(huì)比較深刻,說實(shí)話ffplay中同步的操作是比較復(fù)雜的,我們?cè)谄匠i_發(fā)中要根據(jù)自己的實(shí)際業(yè)務(wù)進(jìn)行一些簡(jiǎn)化和改進(jìn)的,下一章我們就來寫一個(gè)以音頻為基準(zhǔn)的視頻播放器
未完持續(xù)...