直播APP原始碼是如何實現音視訊同步的
1.音視訊同步原理
1)時間戳
直播APP原始碼音視訊同步主要用於在音視訊流的播放過程中,讓同一時刻錄製的聲音和影象在播放的時候儘可能的在同一個時間輸出。
解決直播APP原始碼音視訊同步問題的最佳方案就是時間戳:首先選擇一個參考時鐘(要求參考時鐘上的時間是線性遞增的);生成資料流時依據參考時鐘上的時間給每個資料塊都打上時間戳(一般包括開始時間和結束時間);在播放時,讀取資料塊上的時間戳,同時參考當前參考時鐘上的時間來安排播放(如果資料塊的開始時間大於當前參考時鐘上的時間,則不急於播放該資料塊,直到參考時鐘達到資料塊的開始時間;如果資料塊的開始時間小於當前參考時鐘上的時間,則“儘快”播放這塊資料或者索性將這塊資料“丟棄”,以使播放進度追上參考時鐘)。
直播APP原始碼音視訊同步,主要是以audio的時間軸作為參考時鐘,在沒有audio的情況下,以系統的時間軸作為參考時鐘。這是因為audio丟幀很容易就能聽出來,而video丟幀卻不容易被察覺。
避免直播APP原始碼音視訊不同步現象有兩個關鍵因素 —— 一是在生成資料流時要打上正確的時間戳;二是在播放時基於時間戳對資料流的控制策略,也就是對資料塊早到或晚到採取不同的處理方法。
2)錄製同步
在直播APP原始碼視訊錄製過程中,音視訊流都必須要打上正確的時間戳。假如,視訊流內容是從0s開始的,假設10s時有人開始說話,要求配上音訊流,那麼音訊流的起始時間應該是10s,如果時間戳從0s或其它時間開始打,則這個混合的音視訊流在時間同步上本身就存在問題。
3)播放同步
帶有聲音和影象的視訊,在播放的時候都需要處理音視訊同步的問題。Android平臺,是在render影象之前,進行音視訊同步的。
單獨的音訊或者視訊流,不需要進行音視訊同步處理,音視訊同步只針對既有視訊又有音訊的流。
由於Android是以audio的時間軸作為參考時鐘,音視訊播放同步處理主要有如下幾個關鍵因素:
(1)計算audio時間戳;
(2)計算video時間戳相對於audio時間戳的delay time;
(3)依據delay time判斷video是早到,晚到,採取不同處理策略。
2.直播APP原始碼音視訊播放框架
在Android 2.3版本之前,音視訊播放框架主要採用OpenCORE,OpenCORE的音視訊同步做法是設定一個主
時鐘,音訊流和視訊流分別以主時鐘作為輸出的依據。
從Android 2.0版本開始,Google引入了stagefright框架,到2.3版本,完全替代了OpenCORE。Stagefright框架的音視訊同步做法是以音訊流的時間戳作為參考時鐘,視訊流在render前進行同步處理。
從Android 4.0版本開始,Google引入了nuplayer框架,nuplayer主要負責rtsp、hls等流媒體的播放;而stagefright負責本地媒體以及http媒體的播放。nuplayer框架的音視訊同步做法任然是以音訊流的時間戳作為參考時鐘。
在Android 4.1版本上,添加了一個系統屬性media.stagefright.use-nuplayer,表明google用nuplayer替代stagefight的意圖。
直到Android 6.0版本,nuplayer才完全替代了stagefight。StagefrightPlayer從系統中去掉。
3.Nuplayer音視訊同步
1)Nuplayer音視同步簡介
關於Nuplayer的音視訊同步,基於Android M版本進行分析。
NuplayerRender在onQueueBuffer中收到解碼後的buffer,判斷是音訊流還是視訊流,將bufferPush到對應的buffer queue,然後分別呼叫postDrainAudioQueue_l和postDrainVideoQueue進行播放處理。
同步處理分散在postDrainVideoQueue、onDrainVideoQueue以及onRenderBuffer中,音訊流的媒體時間戳在onDrainAudioQueue中獲得。
2)計算音訊流時間戳
A:在onDrainAudioQueue()中獲取並更新音訊時間戳
bool NuPlayer::Renderer::onDrainAudioQueue() { uint32_t numFramesPlayed; while (!mAudioQueue.empty()) { QueueEntry *entry = &*mAudioQueue.begin(); if (entry->mOffset == 0 && entry->mBuffer->size() > 0) { int64_t mediaTimeUs; //獲取並更新音訊流的媒體時間戳 CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); onNewAudioMediaTime(mediaTimeUs); } size_t copy = entry->mBuffer->size() - entry->mOffset; ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset, copy, false /* blocking */); size_t copiedFrames = written / mAudioSink->frameSize(); mNumFramesWritten += copiedFrames; } int64_t maxTimeMedia; { Mutex::Autolock autoLock(mLock); //計算並更新maxTimeMedia maxTimeMedia = mAnchorTimeMediaUs + (int64_t)(max((long long)mNumFramesWritten - mAnchorNumFramesWritten, 0LL) * 1000LL * mAudioSink->msecsPerFrame()); } mMediaClock->updateMaxTimeMedia(maxTimeMedia); bool reschedule = !mAudioQueue.empty() && (!mPaused || prevFramesWritten != mNumFramesWritten); return reschedule; }
B:onNewAudioMediaTime()將時間戳更新到MediaClock
在onNewAudioMediaTime()中,將音訊流的媒體時間戳、當前播放時間戳及系統時間更新到MediaClock用來計算視訊流的顯示時間戳。
void NuPlayer::Renderer::onNewAudioMediaTime(int64_t mediaTimeUs) { Mutex::Autolock autoLock(mLock); if (mediaTimeUs == mAnchorTimeMediaUs) { return; } setAudioFirstAnchorTimeIfNeeded_l(mediaTimeUs); int64_t nowUs = ALooper::GetNowUs(); //將當前播放音訊流時間戳、系統時間、音訊流當前媒體時間戳更新到mMediaClock int64_t nowMediaUs = mediaTimeUs - getPendingAudioPlayoutDurationUs(nowUs); mMediaClock->updateAnchor(nowMediaUs, nowUs, mediaTimeUs); //用於計算maxTimeMedia mAnchorNumFramesWritten = mNumFramesWritten; mAnchorTimeMediaUs = mediaTimeUs; }
MediaClock::updateAnchor()
void MediaClock::updateAnchor( int64_t anchorTimeMediaUs, int64_t anchorTimeRealUs, int64_t maxTimeMediaUs) { if (anchorTimeMediaUs < 0 || anchorTimeRealUs < 0) { return; } Mutex::Autolock autoLock(mLock); int64_t nowUs = ALooper::GetNowUs(); //重新計算當前播放的音訊流的時間戳 int64_t nowMediaUs = anchorTimeMediaUs + (nowUs - anchorTimeRealUs) * (double)mPlaybackRate; if (nowMediaUs < 0) { return; } //系統時間更新到mAnchorTimeRealUs mAnchorTimeRealUs = nowUs; //音訊播放時間戳更新到mAnchorTimeMediaUs mAnchorTimeMediaUs = nowMediaUs; //音訊媒體時間戳更新到mMaxTimeMediaUs mMaxTimeMediaUs = maxTimeMediaUs; }
3)視訊流同步策略
1)postDrainVideoQueue()
postDrainVideoQueue()中進行了大部分同步處理
1)呼叫getRealTimeUs(),根據視訊流的媒體時間戳獲取顯示時間戳;
2)通過VideoFrameScheduler來判斷什麼時候執行onDrainVideoQueue()
void NuPlayer::Renderer::postDrainVideoQueue() { QueueEntry &entry = *mVideoQueue.begin(); sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this); int64_t delayUs; int64_t nowUs = ALooper::GetNowUs(); int64_t realTimeUs; //獲取當前視訊流的媒體時間戳 int64_t mediaTimeUs; CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); { Mutex::Autolock autoLock(mLock); if (mAnchorTimeMediaUs < 0) { //音訊流處理時,會更新該時間戳。如果沒有音訊流,視訊流以系統時間為參考順序播放 mMediaClock->updateAnchor(mediaTimeUs, nowUs, mediaTimeUs); mAnchorTimeMediaUs = mediaTimeUs; realTimeUs = nowUs; } else { //根據視訊流的媒體時間戳和系統時間,獲取顯示時間戳 realTimeUs = getRealTimeUs(mediaTimeUs, nowUs); } } if (!mHasAudio) { //沒有音訊流的情況下,以當前視訊流的媒體時間戳+100ms作為maxTimeMedia // smooth out videos >= 10fps mMediaClock->updateMaxTimeMedia(mediaTimeUs + 100000); } delayUs = realTimeUs - nowUs; //視訊早了500ms,延遲進行下次處理 if (delayUs > 500000) { if (mHasAudio && (mLastAudioBufferDrained - entry.mBufferOrdinal) <= 0) { postDelayUs = 10000; } msg->setWhat(kWhatPostDrainVideoQueue); msg->post(postDelayUs); mVideoScheduler->restart(); mDrainVideoQueuePending = true; return; } //依據Vsync調整顯示時間戳,預留2個Vsync間隔的時間進行render處理 realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000; int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000); delayUs = realTimeUs - nowUs; msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0); mDrainVideoQueuePending = true; }
A:NuPlayer::Renderer::getRealTimeUs()
根據視訊流的媒體時間戳、系統時間,從mMediaClock獲取視訊流的顯示時間戳
int64_t NuPlayer::Renderer::getRealTimeUs(int64_t mediaTimeUs, int64_t nowUs) { int64_t realUs; if (mMediaClock->getRealTimeFor(mediaTimeUs, &realUs) != OK) { // If failed to get current position, e.g. due to audio clock is // not ready, then just play out video immediately without delay. return nowUs; } return realUs; }
B:MediaClock::getRealTimeFor()
計算視訊流的顯示時間戳 = (視訊流的媒體時間戳 - 音訊流的顯示時間戳)/ 除以播放速率 + 當前系統時間
status_t MediaClock::getRealTimeFor( int64_t targetMediaUs, int64_t *outRealUs) const { ...... int64_t nowUs = ALooper::GetNowUs(); int64_t nowMediaUs; //獲取當前系統時間對應音訊流的顯示時間戳即當前音訊流播放位置 status_t status = getMediaTime_l(nowUs, &nowMediaUs, true /* allowPastMaxTime */); if (status != OK) { return status; } //視訊流的媒體時間戳與音訊流的顯示時間戳的差值除以播放速率,再加上當前系統時間,作為視訊流的顯示時間戳 *outRealUs = (targetMediaUs - nowMediaUs) / (double)mPlaybackRate + nowUs; return OK; }
2)onDrainVideoQueue()
A:onDrainVideoQueue()
在onDrainVideoQueue()中,更新了視訊流的顯示時間戳,並判斷視訊延遲是否超過40ms。然後將這些資訊通知NuPlayerDecoder在onRenderBuffer()中呼叫渲染函式渲染視訊流。
void NuPlayer::Renderer::onDrainVideoQueue() { QueueEntry *entry = &*mVideoQueue.begin(); int64_t mediaTimeUs; CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); nowUs = ALooper::GetNowUs(); //重新計算視訊流的顯示時間戳 realTimeUs = getRealTimeUs(mediaTimeUs, nowUs); if (!mPaused) { if (nowUs == -1) { nowUs = ALooper::GetNowUs(); } setVideoLateByUs(nowUs - realTimeUs); 當前視訊流延遲小於40ms就顯示 tooLate = (mVideoLateByUs > 40000); } entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll); entry->mNotifyConsumed->setInt32("render", !tooLate); //通知NuPlayerDecoder entry->mNotifyConsumed->post(); mVideoQueue.erase(mVideoQueue.begin()); entry = NULL; }
B:Decoder::onRenderBuffer()
void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) { //由render去顯示 並釋放video buffer if (msg->findInt32("render", &render) && render) { int64_t timestampNs; CHECK(msg->findInt64("timestampNs", ×tampNs)); err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs); } else { mNumOutputFramesDropped += !mIsAudio; //該幀video太遲,直接丟棄 err = mCodec->releaseOutputBuffer(bufferIx); } }
以上就是直播APP原始碼實現音視訊同步的流程。
本文轉自https://www.cnblogs.com/dyufei/p/8018440.html僅作分享科普用,如有侵權歡迎聯絡作者刪除。