Analyse du code source NuPlayer (3) Analyse du traitement des images vidéo et analyse du mécanisme de synchronisation partielle

1. Introduction :
Dans le blog précédent, le traitement du tampon de la partie audio a été analysé. Nuplayer partage beaucoup de code dans le traitement des tampons audio et vidéo. Ce blog analysera directement les différences. Dans l'ensemble, le mécanisme de synchronisation de nuplayer est similaire à celui d'exoplayer. Il est basé sur les pts dans le flux de code et le système Estimation et combinaison avec le point temporel du signal de synchronisation verticale pour déterminer le temps d'affichage final. La différence est que l'étalonnage du temps d'affichage par nuplayer est trop compliqué, et beaucoup d'entre eux sont incompréhensibles, mais si vous ne vous concentrez pas sur le contenu de l'étalonnage, d'autres parties sont toujours faciles à comprendre.

2. Déterminez le temps d'affichage de l'image vidéo :
de nombreuses fonctions de traitement de tampon audio et vidéo sont partagées, et nous localisons directement NuPlayerRenderer.cppla postDrainVideoQueue_lfonction :

void NuPlayer::Renderer::postDrainVideoQueue_l() {
    
    
    if (mDrainVideoQueuePending
            || mSyncQueues
            || (mPaused && mVideoSampleReceived)) {
    
    
        return;
    }

    if (mVideoQueue.empty()) {
    
    
        return;
    }

    QueueEntry &entry = *mVideoQueue.begin();

	/* 1.创建kWhatDrainVideoQueue消息用于后续投递处理 */
    sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, id());
    msg->setInt32("generation", mVideoQueueGeneration);

    if (entry.mBuffer == NULL) {
    
    
        // EOS doesn't carry a timestamp.
        msg->post();
        mDrainVideoQueuePending = true;
        return;
    }

    int64_t delayUs;
    int64_t nowUs = ALooper::GetNowUs();
    int64_t realTimeUs;
    if (mFlags & FLAG_REAL_TIME) {
    
    
        int64_t mediaTimeUs;
        CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
        realTimeUs = mediaTimeUs;
    } else {
    
    
        int64_t mediaTimeUs;
        CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));

        if (mAnchorTimeMediaUs < 0) {
    
    
            setAnchorTime(mediaTimeUs, nowUs);
            mPausePositionMediaTimeUs = mediaTimeUs;
            mAnchorMaxMediaUs = mediaTimeUs;
            realTimeUs = nowUs;
        } else {
    
    
            /* 2.获取当前帧送显时间 */
            realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
        }
        if (!mHasAudio) {
    
    
            mAnchorMaxMediaUs = mediaTimeUs + 100000; // smooth out videos >= 10fps
        }

        // Heuristics to handle situation when media time changed without a
        // discontinuity. If we have not drained an audio buffer that was
        // received after this buffer, repost in 10 msec. Otherwise repost
        // in 500 msec.
        /* 当前帧渲染时间差 = 当前帧时间戳 - 当前帧送显时间 */
        /* 当前帧渲染时间差 = 当前帧送显时间 - 系统时间 */        
        delayUs = realTimeUs - nowUs;
        if (delayUs > 500000) {
    
    
            int64_t postDelayUs = 500000;
            if (mHasAudio && (mLastAudioBufferDrained - entry.mBufferOrdinal) <= 0) {
    
    
                postDelayUs = 10000;
            }
            msg->setWhat(kWhatPostDrainVideoQueue);
            msg->post(postDelayUs);
            mVideoScheduler->restart();
            ALOGI("possible video time jump of %dms, retrying in %dms",
                    (int)(delayUs / 1000), (int)(postDelayUs / 1000));
            mDrainVideoQueuePending = true;
            return;
        }
    }

    /* 3.校准送显时间 */
    realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;

    /* 4.计算出两个垂直同步信号用时时长 */
    int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);

    /* 再次计算下一帧视频渲染时间差 = 校准后的送显时间 - 系统时间 */
    delayUs = realTimeUs - nowUs;

    /* 5.送显:两个垂直同步信号点 */
    ALOGW_IF(delayUs > 500000, "unusually high delayUs: %" PRId64, delayUs);
    // post 2 display refreshes before rendering is due
    msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);

    mDrainVideoQueuePending = true;
}

Remarque 1 :
Après avoir confirmé l'heure d'envoi, le message sera envoyé et l'opération de rendu sera effectuée pendant le traitement du message.
Remarque 2 :
Ceci permet d'obtenir un temps d'affichage préliminaire, mediaTimeUset nowUsles moyens représentatifs sont les pts dans le flux de code et l'heure système actuelle, suivez getRealTimeUs pour voir comment le temps d'affichage initial est calculé :

int64_t NuPlayer::Renderer::getRealTimeUs(int64_t mediaTimeUs, int64_t nowUs) {
    
    
    int64_t currentPositionUs;
    /* 获取当前播放位置 */
    if (mPaused || getCurrentPositionOnLooper(
            &currentPositionUs, nowUs, true /* allowPastQueuedVideo */) != 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 (mediaTimeUs - currentPositionUs) + nowUs;
}

En regardant le calcul de la valeur de retour, vous devez obtenir la position de lecture actuelle, qui getCurrentPositionOnLooperest obtenue par la fonction :

status_t NuPlayer::Renderer::getCurrentPositionOnLooper(
        int64_t *mediaUs, int64_t nowUs, bool allowPastQueuedVideo) {
    
    
    int64_t currentPositionUs;
    /* if判断条件返回false,除非pause状态才会进入这里 */
    if (getCurrentPositionIfPaused_l(&currentPositionUs)) {
    
    
        *mediaUs = currentPositionUs;
        return OK;
    }

    return getCurrentPositionFromAnchor(mediaUs, nowUs, allowPastQueuedVideo);
}

Suivi jusqu'à getCurrentPositionFromAnchor:

// Called on any threads.
status_t NuPlayer::Renderer::getCurrentPositionFromAnchor(
        int64_t *mediaUs, int64_t nowUs, bool allowPastQueuedVideo) {
    
    
    Mutex::Autolock autoLock(mTimeLock);
    if (!mHasAudio && !mHasVideo) {
    
    
        return NO_INIT;
    }

    if (mAnchorTimeMediaUs < 0) {
    
    
        return NO_INIT;
    }

    /* 计算当前播放时间 = (系统时间 - 已播放时间) + 上一帧音频时间戳 */
    int64_t positionUs = (nowUs - mAnchorTimeRealUs) + mAnchorTimeMediaUs;

    if (mPauseStartedTimeRealUs != -1) {
    
    
        positionUs -= (nowUs - mPauseStartedTimeRealUs);
    }

    // limit position to the last queued media time (for video only stream
    // position will be discrete as we don't know how long each frame lasts)
    if (mAnchorMaxMediaUs >= 0 && !allowPastQueuedVideo) {
    
    
        if (positionUs > mAnchorMaxMediaUs) {
    
    
            positionUs = mAnchorMaxMediaUs;
        }
    }

    if (positionUs < mAudioFirstAnchorTimeMediaUs) {
    
    
        positionUs = mAudioFirstAnchorTimeMediaUs;
    }

    *mediaUs = (positionUs <= 0) ? 0 : positionUs;
    return OK;
}

Nous devons faire attention au fait que la synchronisation de nuplayer utilise toujours la stratégie d'ajustement de l'image vidéo en fonction de l'horodatage audio, nous devons donc ici obtenir la position de l'image audio lue. Le cœur est le commentaire dans le code. un coup d'oeil à mAnchorTimeRealUsceci Où la variable est-elle mise à jour ? mAnchorTimeRealUs enregistre la position de lecture de l'audio, qui est setAnchorTimemise à jour via la fonction :

void NuPlayer::Renderer::setAnchorTime(
        int64_t mediaUs, int64_t realUs, int64_t numFramesWritten, bool resume) {
    
    
    Mutex::Autolock autoLock(mTimeLock);
    /* 更新码流中获得的音频时间戳 */
    mAnchorTimeMediaUs = mediaUs;
    /* 更新AudioTrack实际的播放时间 */
    mAnchorTimeRealUs = realUs;
    /* 更新实际已写入AudioTrack的帧数 */
    mAnchorNumFramesWritten = numFramesWritten;
    if (resume) {
    
    
        mPauseStartedTimeRealUs = -1;
    }
}

setAnchorTime est implémenté en rappelant la fonction fillAudioBuffer d'AudioSink via le lecteur de couche supérieure :

size_t NuPlayer::Renderer::fillAudioBuffer(void *buffer, size_t size) {
    
    
	...
    if (mAudioFirstAnchorTimeMediaUs >= 0) {
    
    
       int64_t nowUs = ALooper::GetNowUs();
       setAnchorTime(mAudioFirstAnchorTimeMediaUs, nowUs - getPlayedOutAudioDurationUs(nowUs));
   }
	...
}

Le point clé est getPlayedOutAudioDurationUsde savoir comment obtenir la lecture réelle dans la fonction :

int64_t NuPlayer::Renderer::getPlayedOutAudioDurationUs(int64_t nowUs) {
    
    
    uint32_t numFramesPlayed;
    int64_t numFramesPlayedAt;
    AudioTimestamp ts;
    static const int64_t kStaleTimestamp100ms = 100000;

	/* 调用getTimestamp来获取精确播放时间 */
    status_t res = mAudioSink->getTimestamp(ts);
    if (res == OK) {
    
                     // case 1: mixing audio tracks and offloaded tracks.
    	/* 获取音频已播放帧数 */
        numFramesPlayed = ts.mPosition;
        /* 获取底层更新该值时的系统时间 */
        numFramesPlayedAt =
            ts.mTime.tv_sec * 1000000LL + ts.mTime.tv_nsec / 1000;
        /* 计算底层更新时与当前系统时间的时差 */
        const int64_t timestampAge = nowUs - numFramesPlayedAt;
        /* 如果差值超过100ms,则系统系统时间变更为当前时间 - 100ms */
        if (timestampAge > kStaleTimestamp100ms) {
    
    
            // This is an audio FIXME.
            // getTimestamp returns a timestamp which may come from audio mixing threads.
            // After pausing, the MixerThread may go idle, thus the mTime estimate may
            // become stale. Assuming that the MixerThread runs 20ms, with FastMixer at 5ms,
            // the max latency should be about 25ms with an average around 12ms (to be verified).
            // For safety we use 100ms.
            ALOGV("getTimestamp: returned stale timestamp nowUs(%lld) numFramesPlayedAt(%lld)",
                    (long long)nowUs, (long long)numFramesPlayedAt);
            numFramesPlayedAt = nowUs - kStaleTimestamp100ms;
        }
        //ALOGD("getTimestamp: OK %d %lld", numFramesPlayed, (long long)numFramesPlayedAt);
    } else if (res == WOULD_BLOCK) {
    
     // case 2: transitory state on start of a new track
        numFramesPlayed = 0;
        numFramesPlayedAt = nowUs;
        //ALOGD("getTimestamp: WOULD_BLOCK %d %lld",
        //        numFramesPlayed, (long long)numFramesPlayedAt);
    } else {
    
                             // case 3: transitory at new track or audio fast tracks.
    	/* 调用getPlaybackHeadPosition获取当前播放帧数 */
        res = mAudioSink->getPosition(&numFramesPlayed);
        CHECK_EQ(res, (status_t)OK);
        numFramesPlayedAt = nowUs;
        numFramesPlayedAt += 1000LL * mAudioSink->latency() / 2; /* XXX */
        //ALOGD("getPosition: %d %lld", numFramesPlayed, numFramesPlayedAt);
    }

    // TODO: remove the (int32_t) casting below as it may overflow at 12.4 hours.
    //CHECK_EQ(numFramesPlayed & (1 << 31), 0);  // can't be negative until 12.4 hrs, test
    /* 实际播放时间(us) = audiotrack已播放帧数 * 1000 * 每帧大小(2ch,16bit即为4)+ 当前时间 - 底层最新值对应的系统时间 */
    int64_t durationUs = (int64_t)((int32_t)numFramesPlayed * 1000LL * mAudioSink->msecsPerFrame())
            + nowUs - numFramesPlayedAt;
    if (durationUs < 0) {
    
    
        // Occurs when numFramesPlayed position is very small and the following:
        // (1) In case 1, the time nowUs is computed before getTimestamp() is called and
        //     numFramesPlayedAt is greater than nowUs by time more than numFramesPlayed.
        // (2) In case 3, using getPosition and adding mAudioSink->latency() to
        //     numFramesPlayedAt, by a time amount greater than numFramesPlayed.
        //
        // Both of these are transitory conditions.
        ALOGV("getPlayedOutAudioDurationUs: negative duration %lld set to zero", (long long)durationUs);
        durationUs = 0;
    }
    ALOGV("getPlayedOutAudioDurationUs(%lld) nowUs(%lld) frames(%u) framesAt(%lld)",
            (long long)durationUs, (long long)nowUs, numFramesPlayed, (long long)numFramesPlayedAt);
    return durationUs;
}

La compréhension de cette fonction doit être basée sur AudioTrack getTimestampet getPlaybackHeadPositiondeux fonctions.Voici les deux interfaces de la couche java :

/* 1 */
public boolean getTimestamp(AudioTimestamp timestamp);
/* 2 */
public int getPlaybackHeadPosition();

La première doit être implémentée en bas de l'appareil. On peut comprendre qu'il s'agit d'une fonction permettant d'obtenir des points avec précision, mais cette fonction a la particularité de ne pas rafraîchir fréquemment la valeur, il n'est donc pas approprié d'appeler fréquemment D'après la référence donnée dans le document officiel, il est recommandé d'utiliser 10s~ Call une fois en 60s, regardez la classe d'entrée :

public long framePosition; /* 写入帧的位置 */
public long nanoTime;      /* 更新帧位置时的系统时间 */

Cette dernière est une interface qui peut être appelée fréquemment. Sa valeur renvoie les données que audiotrack continue d'écrire sur la couche hal depuis le début de la lecture. Après avoir compris ces deux fonctions, nous savons qu'il existe deux façons de les obtenir à partir d'audiotrack Audio pts. Il faut expliquer ici qu'audiosink encapsule AudioTrack, et qu'il existe des différences entre la couche java et la couche native, nous constaterons donc que lors de l'appel de la fonction d'AudioTrack pour obtenir la position de lecture dans le code nuplayer, le nom de la fonction et le retour la valeur aura différent.
Regardons le code, qui est en fait un étalonnage de l'heure de lecture actuelle. Le principe de l'étalonnage consiste à comparer l'heure système lorsque la valeur de l'image inférieure est mise à jour avec l'heure actuelle, à calculer une différence, puis à utiliser cette différence pour faire Étalonnage pour calculer le temps de jeu réellement écoulé.
Revenons à la fonction ci-dessus getCurrentPositionFromAnchor:

    /* 计算当前帧播放时间 = (系统时间 - 已播放时间) + 上一帧音频时间戳 */
    int64_t positionUs = (nowUs - mAnchorTimeRealUs) + mAnchorTimeMediaUs;

Après avoir obtenu le temps de lecture actuel calculé, nous pouvons comprendre la note 2. Le but est de déduire le temps d'affichage de l'image vidéo à l'état synchrone grâce à l'étalonnage et à l'analyse des données audio sous-jacentes.
Note 3 :
Ici, un calibrage sera effectué sur l'heure d'affichage, mais le calibrage de nuplayer est trop compliqué à comprendre.
Remarque 4 :
Dans le document officiel MediaCodec, la suggestion d'envoyer des images vidéo à l'écran est de deux points de signal de synchronisation à l'avance, donc nuplayer calcule la durée de deux points de signal de synchronisation ici.
Remarque 5 :
Assurez-vous que le message est livré pour le rendu dans les deux points de signal de synchronisation, et ce message est le kWhatDrainVideoQueuemessage de la Remarque 1.

3. Rendu vidéo :
voyez kWhatDrainVideoQueuecomment le message gère le rendu :

        case kWhatDrainVideoQueue:
        {
    
    
            int32_t generation;
            CHECK(msg->findInt32("generation", &generation));
            if (generation != mVideoQueueGeneration) {
    
    
                break;
            }

            mDrainVideoQueuePending = false;

            onDrainVideoQueue();

            Mutex::Autolock autoLock(mLock);
            postDrainVideoQueue_l();
            break;
        }

Suivez la fonction onDrainVideoQueue() :

void NuPlayer::Renderer::onDrainVideoQueue() {
    
    
    if (mVideoQueue.empty()) {
    
    
        return;
    }

    QueueEntry *entry = &*mVideoQueue.begin();

    if (entry->mBuffer == NULL) {
    
    
        // EOS

        notifyEOS(false /* audio */, entry->mFinalResult);

        mVideoQueue.erase(mVideoQueue.begin());
        entry = NULL;

        setVideoLateByUs(0);
        return;
    }

    int64_t nowUs = -1;
    int64_t realTimeUs;
    if (mFlags & FLAG_REAL_TIME) {
    
    
        CHECK(entry->mBuffer->meta()->findInt64("timeUs", &realTimeUs));
    } else {
    
    
        int64_t mediaTimeUs;
        CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));

        nowUs = ALooper::GetNowUs();
        /* 重新计算实际送显时间  */
        realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
    }

    bool tooLate = false;

    if (!mPaused) {
    
    
        if (nowUs == -1) {
    
    
            nowUs = ALooper::GetNowUs();
        }
        /* 计算出当前帧播放时差是否大于40ms */
        setVideoLateByUs(nowUs - realTimeUs);
        /* 大于40ms,即视频帧来的太迟了,将丢帧 */
        tooLate = (mVideoLateByUs > 40000);

        if (tooLate) {
    
    
            ALOGV("video late by %lld us (%.2f secs)",
                 mVideoLateByUs, mVideoLateByUs / 1E6);
        } else {
    
    
            ALOGV("rendering video at media time %.2f secs",
                    (mFlags & FLAG_REAL_TIME ? realTimeUs :
                    (realTimeUs + mAnchorTimeMediaUs - mAnchorTimeRealUs)) / 1E6);
        }
    } else {
    
    
        setVideoLateByUs(0);
        if (!mVideoSampleReceived && !mHasAudio) {
    
    
            // This will ensure that the first frame after a flush won't be used as anchor
            // when renderer is in paused state, because resume can happen any time after seek.
            setAnchorTime(-1, -1);
        }
    }

	/* 注意这两个值决定是送显还是丢帧 */
    entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll);
    entry->mNotifyConsumed->setInt32("render", !tooLate);
    entry->mNotifyConsumed->post();
    mVideoQueue.erase(mVideoQueue.begin());
    entry = NULL;

    mVideoSampleReceived = true;

    if (!mPaused) {
    
    
        if (!mVideoRenderingStarted) {
    
    
            mVideoRenderingStarted = true;
            notifyVideoRenderingStart();
        }
        notifyIfMediaRenderingStarted();
    }
}

Cette fonction n'est pas compliquée et le temps d'envoi et d'affichage est à nouveau calibré pour éliminer le retard dans le traitement de l'envoi et de la réception des messages. Si la trame vidéo arrive trop tard et est supérieure à 40 ms, la variable sera définie sur faux et la trame sera être déposé directement tooLate. Voici le premier endroit auquel il faut faire attention. Nous avons vu que le temps d'affichage était recalculé dans le code, mais il n'a pas été appelé pour calibrer scheduleà nouveau. Je ne sais pas si c'est un bug. scheduleLe principe de la fonction n'est pas compris, mais nous pouvons être sûrs que, Cette fonction doit ajuster le temps d'envoi théorique à un certain point de temps synchrone. mNotifyConsumed->post()Le message délivré est que kWhatRenderBuffercela a été analysé dans la partie audio, et regardons à nouveau le traitement du message, donc je ne le répéterai pas :

void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) {
    
    
    status_t err;
    int32_t render;
    size_t bufferIx;
    CHECK(msg->findSize("buffer-ix", &bufferIx));

    if (!mIsAudio) {
    
    
        int64_t timeUs;
        sp<ABuffer> buffer = mOutputBuffers[bufferIx];
        buffer->meta()->findInt64("timeUs", &timeUs);

        if (mCCDecoder != NULL && mCCDecoder->isSelected()) {
    
    
            mCCDecoder->display(timeUs);
        }
    }

	/* 如果render为false,就丢帧不去渲染 */
    if (msg->findInt32("render", &render) && render) {
    
    
        int64_t timestampNs;
        CHECK(msg->findInt64("timestampNs", &timestampNs));
        err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
    } else {
    
    
        err = mCodec->releaseOutputBuffer(bufferIx);
    }
    if (err != OK) {
    
    
        ALOGE("failed to release output buffer for %s (err=%d)",
                mComponentName.c_str(), err);
        handleError(err);
    }
}

4. Résumé :
Le schéma logique du mécanisme de traitement des trames vidéo et de la synchronisation est le suivant :
insérez la description de l'image ici

Je suppose que tu aimes

Origine blog.csdn.net/achina2011jy/article/details/113929427
conseillé
Classement