Android 音視頻同步(A/V Sync)

1.  音視頻同步原理

1)時間戳

音視頻同步主要用於在音視頻流的播放過程當中,讓同一時刻錄製的聲音和圖像在播放的時候儘量的在同一個時間輸出。框架

解決音視頻同步問題的最佳方案就是時間戳:首先選擇一個參考時鐘(要求參考時鐘上的時間是線性遞增的);生成數據流時依據參考時鐘上的時間給每一個數據塊都打上時間戳(通常包括開始時間和結束時間);在播放時,讀取數據塊上的時間戳,同時參考當前參考時鐘上的時間來安排播放(若是數據塊的開始時間大於當前參考時鐘上的時間,則不急於播放該數據塊,直到參考時鐘達到數據塊的開始時間;若是數據塊的開始時間小於當前參考時鐘上的時間,則「儘快」播放這塊數據或者索性將這塊數據「丟棄」,以使播放進度追上參考時鐘)。ide

Android音視頻同步,主要是以audio的時間軸做爲參考時鐘,在沒有audio的狀況下,以系統的時間軸做爲參考時鐘。這是由於audio丟幀很容易就能聽出來,而video丟幀卻不容易被察覺。函數

避免音視頻不一樣步現象有兩個關鍵因素 —— 一是在生成數據流時要打上正確的時間戳;二是在播放時基於時間戳對數據流的控制策略,也就是對數據塊早到或晚到採起不一樣的處理方法。oop

2) 錄製同步

在視頻錄製過程當中,音視頻流都必需要打上正確的時間戳。假如,視頻流內容是從0s開始的,假設10s時有人開始說話,要求配上音頻流,那麼音頻流的起始時間應該是10s,若是時間戳從0s或其它時間開始打,則這個混合的音視頻流在時間同步上自己就存在問題。post

3)  播放同步

帶有聲音和圖像的視頻,在播放的時候都須要處理音視頻同步的問題。Android平臺,是在render圖像以前,進行音視頻同步的。ui

單獨的音頻或者視頻流,不須要進行音視頻同步處理,音視頻同步只針對既有視頻又有音頻的流。this

因爲Android是以audio的時間軸做爲參考時鐘,音視頻播放同步處理主要有以下幾個關鍵因素:google

1)計算audio時間戳;spa

         (2)計算video時間戳相對於audio時間戳的delay time;rest

         (3)依據delay time判斷video是早到,晚到,採起不一樣處理策略。

2. Android音視頻播放框架

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。而後將這些信息通知NuPlayerDecoderonRenderBuffer()中調用渲染函數渲染視頻流。

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", &timestampNs));
        err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
    } else {
        mNumOutputFramesDropped += !mIsAudio;
        //該幀video太遲,直接丟棄
        err = mCodec->releaseOutputBuffer(bufferIx);
    }
}
相關文章
相關標籤/搜索