前面已經介紹過視頻的解碼與顯示,和音頻的解碼與播放了。但這里會有一個問題,那就是視頻和音頻的同步。
不同步有什么后果?
后果就是要么視頻播放太快了,音頻沒有跟上;或者音頻播放太快了,視頻沒有跟上;嚴重影響整體的觀看體驗。
就好比小姐姐當面問你聯(lián)系方式,小姐姐你看到了,但人家說的啥你還沒聽到,之后人家都走了,你才聽到原來是問電話號碼的,多慘。
那怎么解決呢?
方法一:以音頻的解碼流為主參照,視頻流的解碼向音頻的解碼時間靠攏。
方法二:以視頻的解碼流為主參照,音頻流的解碼視頻的解碼時間靠攏。
方法三:以手機系統(tǒng)時間為主參照,視頻流和音頻流的解碼都向系統(tǒng)時間靠攏。
哪一種方法最好?或者說,每種方法的使用場景是什么?
我的理解是:以誰為主參照,就是看重誰。
如果聲音斷開一下下,我們的耳朵是很容易感覺出來的,相反如果聲音連續(xù),視頻幀偶爾卡一下下,一般都影響不大。當需求是極度要求聲音的連續(xù)性的,那就方法一。
相反,當需求是極度要求視頻的連續(xù)性的,那就方法二。
至于方法三,就是折中的方法,感覺啥時候都可以用,我自己也大多數(shù)用方法三。
如何實現(xiàn)?
在這之前,先介紹幾個屬性。
I幀:關鍵幀,幀內編碼幀 又稱intra picture,I 幀通常是每個 GOP(MPEG 所使用的一種視頻壓縮技術)的第一個幀,經過適度地壓縮,做為隨機訪問的參考點,可以當成圖象。I幀可以看成是一個圖像經過壓縮后的產物??瑟毩⒔獯a。
B幀:雙向預測內插編碼幀 又稱bi-directional interpolated prediction frame,可以大大提高壓縮倍數(shù)。(與I幀相似度95%以上)
P幀:前向預測編碼幀 又稱predictive-frame,P 幀圖像只采用前向時間預測,可以提高壓縮效率和圖像質量。(與I幀相似度70%以上)
DTS:幀數(shù)據(jù)的編碼時間戳,這個時間戳的意義在于告訴播放器該在什么時候解碼這一幀的數(shù)據(jù)。
PTS:幀數(shù)據(jù)的顯示時間戳,這個時間戳用來告訴播放器該在什么時候顯示這一幀的數(shù)據(jù)。
怎么理解這些東西?直接復制網絡上的一張圖比較直觀和方便。

上圖就是一個GOP內的幀數(shù)據(jù),通過PTS和DTS,播放器可以知道在某一個時間,解碼哪一幀,顯示哪一幀。
同樣的,當有2個流(視頻流和音頻流),我們需要他們的進度保持相對的一致,那么只要保證他們的PTS或者DTS都相對一致,就可以了。
具體做法
/**
* 解碼一幀數(shù)據(jù)
* @return 0 if OK, < 0 on error or end of file
*/
int BaseDecoder::DecodeOnePacket() {
if (m_SeekPosition > 0) {//拖動進度條
}
//讀取一幀數(shù)據(jù)到 m_Packet 中
int result = av_read_frame(m_AVFormatContext, m_Packet);
while (result == 0) {
//匹配幀的index
if (m_Packet->stream_index == m_StreamIndex) {
if (avcodec_send_packet(m_AVCodecContext, m_Packet) == AVERROR_EOF) {
//解碼結束
result = -1;
goto __EXIT;
}
int frameCount = 0;
while (avcodec_receive_frame(m_AVCodecContext, m_Frame) == 0) {
//更新時間戳
UpdateTimeStamp();
//同步
AVSync();
//渲染視頻
OnFrameAvailable(m_Frame);
frameCount++;
}
//判斷一個 packet 是否解碼完成
if (frameCount > 0) {
result = 0;
goto __EXIT;
}
}
av_packet_unref(m_Packet);
result = av_read_frame(m_AVFormatContext, m_Packet);
}
__EXIT:
av_packet_unref(m_Packet);
return result;
}
這段代碼的主要功能,就是解碼一幀數(shù)據(jù)(視頻幀、音頻幀都可以),然后交給對應的模塊去顯示和播放聲音。其中的UpdateTimeStamp()和AVSync()就是同步的主要方法了。
void BaseDecoder::UpdateTimeStamp() {
LOGE("DecoderBase::UpdateTimeStamp");
std::unique_lock<std::mutex> lock(m_Mutex);
if(m_Frame->pkt_dts != AV_NOPTS_VALUE) {
m_CurTimeStamp = m_Frame->pkt_dts;
} else if (m_Frame->pts != AV_NOPTS_VALUE) {
m_CurTimeStamp = m_Frame->pts;
} else {
m_CurTimeStamp = 0;
}
m_CurTimeStamp = (int64_t)((m_CurTimeStamp * av_q2d(m_AVFormatContext->streams[m_StreamIndex]->time_base)) * 1000);
if(m_SeekPosition > 0 && m_SeekSuccess)
{
m_StartTimeStamp = GetSysCurrentTime() - m_CurTimeStamp;
m_SeekPosition = 0;
m_SeekSuccess = false;
}
}
long BaseDecoder::AVSync() {
LOGD("BaseDecoder::AVSync");
long curSysTime = GetSysCurrentTime();
//基于系統(tǒng)時鐘計算從開始播放流逝的時間
long elapsedTime = curSysTime - m_StartTimeStamp;
long delay = 0;
//向系統(tǒng)時鐘同步
if(m_CurTimeStamp > elapsedTime) {
//休眠時間
auto sleepTime = static_cast<unsigned int>(m_CurTimeStamp - elapsedTime);//ms
//限制休眠時間不能過長
sleepTime = sleepTime > DELAY_THRESHOLD ? DELAY_THRESHOLD : sleepTime;
av_usleep(sleepTime * 1000);
}
delay = elapsedTime - m_CurTimeStamp;
return delay;
}
每次解碼都更新一下時間,然后跟系統(tǒng)時間做對比,根據(jù)時間差進行判斷,要么睡眠等待,要么繼續(xù)執(zhí)行解碼。
還有需要注意的不?
首先,睡眠等待的時間,需要一個閾值,不然很影響體驗。
然后,如果輸入流是網絡,輸入數(shù)據(jù)是不穩(wěn)定的,有可能會丟幀之類的情況出現(xiàn),這些情況需要根據(jù)具體需求去建立方案解決,這里就不寫了。