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