Nuplayer 音視頻同步學習筆記

目錄

 

1. 處理解碼之後的數據

(1) handleAnOutputBuffer

(2). queueBuffer 

2. AudioBuffer的處理

(1) postDrainAudioQueue_l

(2) onDrainAudioQueue

3. Video Buffer的處理

(1) postDrainVideoQueue

(2) onDrainVideoQueue

4. AVsync Audio更新錨點時間

(1) AVsync原理

(2) Audi如何更新錨點

5. AVsync Video 獲取顯示的時間(系統時間)

(1) nowMediaUs 當前播放媒體時間

(2) outRealUs Buffer的顯示時間


1. 處理解碼之後的數據

(1) handleAnOutputBuffer

    NuPlayerDecoder調用handleAnOutputBuffer處理解碼之後的音視頻數據.  函數最終會調用NuPlayerDecoderRenderer::queueBuffer處理這個Buffer

bool NuPlayer::Decoder::handleAnOutputBuffer(size_t index, size_t offset, size_t size, 
                                             int64_t timeUs, int32_t flags) {
    sp<MediaCodecBuffer> buffer;
    //根據Index從MediaCodec獲取Buffer
    mCodec->getOutputBuffer(index, &buffer);
    mOutputBuffers.editItemAt(index) = buffer;
    // 把offset size timeUs信息添加到buffer中
    // 其中timeUs是buffer的媒體時間(Buffer在媒體文件的位置)
    buffer->setRange(offset, size);
    buffer->meta()->clear();
    buffer->meta()->setInt64("timeUs", timeUs);
    // 創建一個消息kWhatRenderBuffer, 消息會被傳入到NuplayerRenderer,
    // 當Buffer被Renderer處理完成後,就會發送這個消息消息
    sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);
    reply->setSize("buffer-ix", index);
    reply->setInt32("generation", mBufferGeneration);

    // 調用NuplayerRenderer 的 queueBuffer
    mRenderer->queueBuffer(mIsAudio, buffer, reply);

(2). queueBuffer 

    把音視頻的buffer添加到Render的隊列 mAudioQueue(audio) mVideoQueue(!audio)
    <1> queueBuffer發送消息kWhatQueueBuffer調用Renderer::onQueueBuffer

void NuPlayer::Renderer::queueBuffer(bool audio, const sp<MediaCodecBuffer> &buffer, const sp<AMessage> &notifyConsumed) {
    int64_t mediaTimeUs = -1;
    buffer->meta()->findInt64("timeUs", &mediaTimeUs);
    // 發送消息kWhatQueueBuffer, 調用onQueueBuffer
    // 把Decoder傳入的消息notifyConsumed放入到新創建的msg
    sp<AMessage> msg = new AMessage(kWhatQueueBuffer, this);
    msg->setInt32("queueGeneration", getQueueGeneration(audio));
    msg->setInt32("audio", static_cast<int32_t>(audio));
    msg->setObject("buffer", buffer);
    msg->setMessage("notifyConsumed", notifyConsumed);
    msg->post();

<2> 把音視頻Buffer添加到隊列mAudioQueue, mVideoQueue。調用postDrainAudioQueue_l, postDrainVideoQueue處理各自隊列中的Buffer

void NuPlayer::Renderer::onQueueBuffer(const sp<AMessage> &msg)
    // 判斷是Audio數據還是Video的數據
    if (audio)  mHasAudio = true;
    else        mHasVideo = true;
    //創建並初始化一個mVideoScheduler
    if (mHasVideo)
        if (mVideoScheduler == NULL)
            mVideoScheduler = new VideoFrameScheduler();
            mVideoScheduler->init();
    //創建一個隊列的節點用來保存buffer的信息。
    CHECK(msg->findMessage("notifyConsumed", &notifyConsumed));
    QueueEntry entry;
    entry.mBuffer = buffer;
    // Decoder傳入的消息kWhatRenderBuffer被放入到節點
    entry.mNotifyConsumed = notifyConsumed;
    entry.mOffset = 0;
    entry.mFinalResult = OK;
    entry.mBufferOrdinal = ++mTotalBuffersQueued;

    // 把創建的節點push到音頻數據的隊列和視頻數據的隊列 mAudioQueue  mVideoQueue
    // 調用postDrainAudioQueue_l 或 postDrainVideoQueue,清空音視頻數據的隊列
    if (audio) {
        mAudioQueue.push_back(entry);
        postDrainAudioQueue_l();
    } else {
        mVideoQueue.push_back(entry);
        postDrainVideoQueue();
    }

2. AudioBuffer的處理

(1) postDrainAudioQueue_l

調用onDrainAudioQueue來處理Auido隊列的Buffer

void NuPlayer::Renderer::postDrainAudioQueue_l(int64_t delayUs)
    //發送消息 kWhatDrainAudioQueue, 調用 
    sp<AMessage> msg = new AMessage(kWhatDrainAudioQueue, this);
    msg->setInt32("drainGeneration", mAudioDrainGeneration);
    msg->post(delayUs);
case kWhatDrainAudioQueue
    // 接下來主要的工作在onDrainAudioQueue中完成
    if (onDrainAudioQueue()) {
        // 函數onDrainAudioQueue,當AudioQueue的數據沒有處理完的情況下,會返回true
        // 返回true的情況下,需要延時delayUs再次調用onDrainAudioQueue處理AudioQueue
        // 1) 調用AudioTrack的getPosition獲得Audio已經播放的數據幀的個數.
        uint32_t numFramesPlayed;
        mAudioSink->getPosition(&numFramesPlayed)
        // 2) 已經寫入到Audio但是還沒有播放的數據(主要是在Audio側Buffer中的數據)
        uint32_t numFramesPendingPlayout =mNumFramesWritten - numFramesPlayed;
        // 3) delayUs: 需要延時delayUs之後,最新寫入Audio的數據纔會開始播放
        int64_t delayUs = mAudioSink->msecsPerFrame()
                          * numFramesPendingPlayout * 1000ll;
        // 4) 根據播放的速率 調整delayUs
        if (mPlaybackRate > 1.0f) delayUs /= mPlaybackRate;
        // 5) 調整delayUs,防止Audio Buffer的數據被清空
        delayUs /= 2;
        // 6) 調用postDrainAudioQueue_l處理AudioQueue的數據
        // 傳入 delayUs
        postDrainAudioQueue_l(delayUs);

(2) onDrainAudioQueue

更新錨點時間, 把AudioBuffer傳給AudioSink播放
     1) 循環處理mAudioQueue中的節點, 直到mAudioQueue中的buffer被清空 
     2) 嘗試更新錨點時間 onNewAudioMediaTime()
     3) 把數據寫入到AudioSink
     4) Buffer被處理完, 通知Decoder
     5) 更新MediaClock中的maxTimeMedia
     6) 判斷mAudioQueue是否還有數據

bool NuPlayer::Renderer::onDrainAudioQueue()
    uint32_t prevFramesWritten = mNumFramesWritten;
    // 1) 循環處理mAudioQueue中的節點, 直到mAudioQueue中的buffer被清空
    while (!mAudioQueue.empty()) {
        // 處理當前的頭節點
        QueueEntry *entry = &*mAudioQueue.begin();
        // Buffer爲空的情況
        if (entry->mBuffer == NULL) {
            // EOS
        }
        // 2) 嘗試更新錨點時間,之後會對錨點時間以及如何更新詳細介紹
        // 第一次處理這個節點, 如果需要, 嘗試通過媒體時間更新錨點時間
        // mOffset Buffer中已經有mOffset大小的數據被處理
        if (entry->mOffset == 0 && entry->mBuffer->size() > 0) {
            int64_t mediaTimeUs;
            // 從Buffer的數據中, 獲得媒體的時間(當前Buffer在媒體文件中的位置)
            CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
            // 嘗試更新錨點時間
            onNewAudioMediaTime(mediaTimeUs);
        }

        // 3) 把數據寫入到AudioSink
        // 需要寫入copy大小的的數據
        size_t copy = entry->mBuffer->size() - entry->mOffset;
        // 調用AudioSink的Write把數據寫到AudioTrack
        ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset,
                                            copy, false /* blocking */);
        // 根據寫入的數據, 更新Offset
        entry->mOffset += written;
        // Buffer中還有remainder的數據需要處理
        size_t remainder = entry->mBuffer->size() - entry->mOffset;

        // 4) Buffer被處理完, 通知Decoder
        // 剩餘的數據小於一幀(一個Audio的採樣點), 當前Buffer已經被處理完
        if ((ssize_t)remainder < mAudioSink->frameSize()) {
            // 通知Decoder當前buffer已經被處理完
            // mNotifyConsumed: Decoder傳入的消息kWhatRenderBuffer
            entry->mNotifyConsumed->post();
            mAudioQueue.erase(mAudioQueue.begin());
            entry = NULL;
        // 更新copiedFrames:已經寫入到Audio的數據
        size_t copiedFrames = written / mAudioSink->frameSize();
        mNumFramesWritten += copiedFrames;
// 5) 更新MediaClock中的maxTimeMedia
    // (上面寫入的Buffer的最後一幀的媒體時間)
    // 媒體時間, Buffer在媒體文件的位置, 可以理解爲進度條的時間
    // 錨點媒體時間戳加上新寫入幀數對應的時長,即爲媒體時間戳最大值
    // mAnchorTimeMediaUs +   
    // 錨點媒體時間, 最新的錨點對應的buffer的MediaTime
    // (mNumFramesWritten - mAnchorNumFramesWritten)  
    // 上次錨點之後,新寫入的幀數
    //  * mAudioSink->msecsPerFrame() * 1000LL        
    // 每一幀播放的時間(delay)
    int64_t maxTimeMedia;
    maxTimeMedia = mAnchorTimeMediaUs +
      (int64_t)(max((long long)mNumFramesWritten - mAnchorNumFramesWritten, 0LL)
           * 1000LL * mAudioSink->msecsPerFrame());
        mMediaClock->updateMaxTimeMedia(maxTimeMedia);

// 6) 如果mAudioQueue還有數據沒有處理返回true, 需要重新調用onDrainAudioQueue處理
    bool reschedule = !mAudioQueue.empty()
            && (!mPaused || prevFramesWritten != mNumFramesWritten); 
   return reschedule;

3. Video Buffer的處理

(1) postDrainVideoQueue

計算數據應該在什麼時間顯示, 根據這個時間延時發送kWhatDrainVideoQueue消息, 調用onDrainVideoQueue
  1) 根據Buffer的媒體時間,獲得Buffer顯示的系統時間(數據應該在這個時間顯示)
  2) 計算出合適的發送kWhatDrainVideoQueue消息的延時時間
  3) 發送消息kWhatDrainVideoQueue

void NuPlayer::Renderer::postDrainVideoQueue()
    QueueEntry &entry = *mVideoQueue.begin();
    // 準備發送消息kWhatDrainVideoQueue,
    sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this);
    msg->setInt32("drainGeneration", getDrainGeneration(false /* audio */));
    // 1) 根據Buffer的媒體時間,獲得Buffer顯示的系統時間(數據應該在這個時間顯示)
    // 獲得當前系統的時間
    int64_t nowUs = ALooper::GetNowUs();
    // 獲取當前Buffer的媒體時間(當前Buffer在媒體文件的位置)
    entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs);
    // 獲取當前Buffer應該在什麼時間顯示(數據顯示的系統的時間)
    // 主要根據mediaTimeUs來計算, 需要依賴Audio側更新的錨點時間
    // 如果獲得realTimeUs和delayUs有問題
    // 通常需要檢查Audio側更新的錨點時間和Audio getTimestamp或getPosition的返回值
    realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);

    // 2) 計算出合適的發送kWhatDrainVideoQueue消息的延時時間, 
    // delayUs, 當前Buffer在delayUs之後顯示
    delayUs = realTimeUs - nowUs;
    // Video的Buffer來的太早, 或錨點時間有問題,延時重新調用postDrainVideoQueue
    if (delayUs > 500000) {
        postDelayUs = 500000;
    if (postDelayUs >= 0) {
        msg->setWhat(kWhatPostDrainVideoQueue);
        msg->post(postDelayUs);
        mVideoScheduler->restart();

    // 利用VideoScheduler更新realTimeUs和delayUs
    realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;
    int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);
    delayUs = realTimeUs - nowUs;
    // 3) 發送消息kWhatDrainVideoQueue調用onDrainVideoQueue
    //如果 delayUs大於2倍的Vsync, 延時delayUs減去2倍的Vsync的時間發送kWhatDrainVideoQueue
    //否則立即發送kWhatDrainVideoQueue, 立即發送kWhatDrainVideoQueue,處理buffer
    msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);

(2) onDrainVideoQueue

重新計算buffer顯示的系統時間realTimeUs, 通知Decoder Buffer已經處理完
發送realTimeUs 和 tooLate的信息

void NuPlayer::Renderer::onDrainVideoQueue() {
    // 取出第一個Buffer
    QueueEntry *entry = &*mVideoQueue.begin();
    // 當前Real系統的時間
    int64_t nowUs = ALooper::GetNowUs();
    // 獲取媒體時間
    entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs);
    // 顯示的Real系統時間
    realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);

    bool tooLate = false;
    if (!mPaused) {
        // 視頻的數據來晚了nowUs - realTimeUs的時間
            // mVideoLateByUs = nowUs - realTimeUs
        setVideoLateByUs(nowUs - realTimeUs);
        // 視頻晚了40ms
        tooLate = (mVideoLateByUs > 40000);

    // 通知Decoder當前buffer已經被處理完, 發送realTimeUs和tooLate
    entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll);
    entry->mNotifyConsumed->setInt32("render", !tooLate);
    entry->mNotifyConsumed->post();
    mVideoQueue.erase(mVideoQueue.begin());
    entry = NULL;

4. AVsync Audio更新錨點時間

(1) AVsync原理

系統時間和媒體時間應該是線性關係: (mediaTimeUs - anchorTimeMediaUs) = PlaybackRate*(nowUs - anchorTimeMediaUs)。

所以理論上,我們可以根據錨點, 計算出任意一點的媒體時間對應的系統時間(Buffer應該播放的時間). AVsync 的進本機理就是通過Audio每隔一段時間更新錨點, Video的Buffer根據錨點和媒體時間計算出應該播放的時(系統時間)

更新錨點時間 anchorTimeMediaUs anchorTimeRealUs
anchorTimeRealUs  更新錨點時的 系統時間
anchorTimeMediaUs 更新錨點時的 媒體時間(正在播放的媒體的時間)
下面我們主要來看一下Audi如何更新錨點

(2) Audi如何更新錨點

anchorTimeRealUs = nowUs
錨點系統時間被設置爲當前系統時間, 所以 anchorTimeMediaUs 就應該當前正在播放的媒體時間,接下來重點是如何確定anchorTimeMediaUs. 

<1> 如何計算當前正在播放的媒體時間

當前正在播放的媒體時間 = 正在寫入的媒體時間 - 已經寫入但是沒有播放的數據需要播放的時間
pendingAudioPlayoutDurationUs: 已經寫入但是沒有播放的數據需要播放的時間(主要是在AudioBuffer裏面的數據)
anchorTimeMediaUs = mediaTimeUs - pendingAudioPlayoutDurationUs;

<2> 計算已經寫入但是沒有播放的數據需要播放的時間
writtenAudioDurationUs: 已經寫入的數據的持續時間
PlayedOutDurationUs: 當前已經播放的數據的持續時間
兩者相減就是pendingAudioPlayoutDurationUs
pendingAudioPlayoutDurationUs = writtenAudioDurationUs - PlayedOutDurationUs
<3> 已經寫入的數據的持續時間
mNumFramesWritten 已經寫入的數據的幀的個數 * 每一幀多少時間(1000000LL / sampleRate)
writtenAudioDurationUs = mNumFramesWritten * (1000000LL / sampleRate)
 <4> 計算當前已經播放的數據的持續時間
ts.mPosition * 1000000LL/mSampleRateHz 在 ts.mTime時間已經播放的數據的時間
因爲nowUs與ts.mTime不相等, 最後需要根據nowUs進行調整
PlayedOutDurationUs = ts.mPosition * 1000000LL/mSampleRateHz + nowUs - ts.mTime

void NuPlayer::Renderer::onNewAudioMediaTime(int64_t mediaTimeUs)
    // 設置初始錨點媒體時間
    setAudioFirstAnchorTimeIfNeeded_l(mediaTimeUs);

    int64_t nowUs = ALooper::GetNowUs();
    if (mNextAudioClockUpdateTimeUs >= 0)
        //是否需要更新錨點時間, 根據kMinimumAudioClockUpdatePeriodUs的時間
        if (nowUs >= mNextAudioClockUpdateTimeUs) 
            //  (1) nowMediaUs:  當前正在播放的媒體時間
            //  mediaTimeUs: 當前正在寫入到Audio的數據的媒體時間
            //  getPendingAudioPlayoutDurationUs 
            //  已經寫入到Audio但是還沒有播放的數據持續時間
            int64_t nowMediaUs = mediaTimeUs - 
                                 getPendingAudioPlayoutDurationUs(nowUs);
            //  根據nowMediaUs和mediaTimeUs更新錨點時間
            mMediaClock->updateAnchor(nowMediaUs, nowUs, mediaTimeUs);
            mNextAudioClockUpdateTimeUs = nowUs + kMinimumAudioClockUpdatePeriodUs;
 
// (2) 需要分析一下getPendingAudioPlayoutDurationUs
// 計算方法使用 writtenAudioDurationUs - PlayedOutDurationUs
int64_t NuPlayer::Renderer::getPendingAudioPlayoutDurationUs(int64_t nowUs)
    // (3) writtenAudioDurationUs 已經寫入的數據的持續時間
    // mNumFramesWritten * (1000000LL / sampleRate)
    // 寫入的幀的個數 * 每一幀的持續時間(us)
    // (1000000LL / sampleRate): sampleRate一秒的採樣數, 取倒數每一個採樣的持續時間
    int64_t writtenAudioDurationUs = 
             getDurationUsIfPlayedAtSampleRate(mNumFramesWritten);
    // PlayedOutDurationUs 已經播放時間, 需要從Audio側獲取
    return writtenAudioDurationUs - mAudioSink->getPlayedOutDurationUs(nowUs);

//(4) 當前已經播放的數據的持續時間
int64_t MediaPlayerService::AudioOutput::getPlayedOutDurationUs(int64_t nowUs)
    // 從Audio側獲取已經獲取當前播放的幀,和對應的系統時間
    // 注意ts.mPosition並不是正在播放的幀的位置, 
    // 應該是ts.mTime這個系統時間點正在播放的幀的位置
    // ts.mTime與當前時間nowUs並不相等,會有ms級的差別
    status_t res = mTrack->getTimestamp(ts);
    if (res == OK)
        numFramesPlayed = ts.mPosition;
        numFramesPlayedAt = ts.mTime.tv_sec * 1000000LL + ts.mTime.tv_nsec / 1000;
    //  numFramesPlayed * 1000000LL / mSampleRateHz: ts.mTime時間點已經播放的時間
    // 最後計算的時候需要考慮ts.mTime與當前時間nowUs之間的差異
    // durationUs  nowUs時間點已經播放的時間(正在播放的媒體時間)
    int64_t durationUs = (int64_t)((int32_t)numFramesPlayed * 1000000LL / 
                         mSampleRateHz) + nowUs - numFramesPlayedAt;


// (5) 調用MediaClock::updateAnchor更新錨點
//    傳入參數:
//    anchorTimeMediaUs     正在播放的媒體時間
//    anchorTimeRealUs      anchorTimeMediaUs對應的系統時間
void MediaClock::updateAnchor(int64_t anchorTimeMediaUs, 
                              int64_t anchorTimeRealUs, int64_t maxTimeMediaUs)
    // 獲得當前的系統時間, 可能與anchorTimeRealUs有差別
    int64_t nowUs = ALooper::GetNowUs();
    // 獲得當前正在播放的媒體時間 nowMediaUs
    int64_t nowMediaUs =
        anchorTimeMediaUs + (nowUs - anchorTimeRealUs) * (double)mPlaybackRate;
    // 更新當前播放的媒體時間爲錨點媒體時間
    // 更新當前系統時間爲錨點系統時間
    mAnchorTimeRealUs = nowUs;
    mAnchorTimeMediaUs = nowMediaUs;

5. AVsync Video 獲取顯示的時間(系統時間)

     AVsync的目的是獲得Buffer顯示的時間(buffer的系統時間).我們可以根據媒體時間和系統時間的線性關係計算出顯示的時間
(mediaTimeUs - anchorTimeMediaUs) = PlaybackRate*(nowUs - anchorTimeMediaUs)

(1) nowMediaUs 當前播放媒體時間

根據當前的系統時間和錨點時間計算出當前播放媒體時間
nowMediaUs = mAnchorTimeMediaUs + (realUs - mAnchorTimeRealUs) * mPlaybackRate

(2) outRealUs Buffer的顯示時間

根據當前播放的媒體時間和系統時間, 計算出Buffer的顯示時間(系統時間)
outRealUs = (targetMediaUs - nowMediaUs) / (double)mPlaybackRate + nowUs

// outRealUs 獲得當前Buffer播放的系統時間(應該在這個時間點播放)
// 傳入參數 targetMediaUs. 當前Buffer的媒體時間
status_t MediaClock::getRealTimeFor(int64_t targetMediaUs, int64_t *outRealUs)
    int64_t nowUs = ALooper::GetNowUs();
    // (1) nowMediaUs video正在播放的媒體時間, nowUs對應的媒體時間
    getMediaTime_l(nowUs, &nowMediaUs, true /* allowPastMaxTime */);
    // (2) 計算出Buffer的顯示時間
    *outRealUs = (targetMediaUs - nowMediaUs) / (double)mPlaybackRate + nowUs;

// outMediaUs video正在播放的媒體時間, nowUs對應的媒體時間
// realUs  當前系統時間
status_t MediaClock::getMediaTime_l(
        int64_t realUs, int64_t *outMediaUs, bool allowPastMaxTime)
    //mediaUs 當前Audio正在播放的媒體時間, 對應video正在播放的媒體時間
    int64_t mediaUs = mAnchorTimeMediaUs
            + (realUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
    *outMediaUs = mediaUs;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章