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 &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僅作分享科普用,如有侵權歡迎聯系作者删除。