Android多媒体框架--13:MediaClock分析与音视频同步

"本文转载自:[yanbixing123]的 Android MultiMedia框架完全解析 - MediaClock分析与音视频同步"

1.概述

  音视频同步是一个播放器要处理的基本问题,音视频同步的好坏直接影响到播放效果。解码后的音频片段和视频片段,都分别带有 pts 时间戳信息。回放时需要做的,就是尽量保证 apts(音频时间戳)和 vpts(视频时间戳),之间的差值是最小的。为了达到这个目的,就需要在 audio device 和 video device进行渲染的时候进行控制。控制的方法就是delay。

  由于音频的采样率是固定的,在回放时我们必须保证连续性,就是说两个时间上连续的音频片段是不允许有 delay 的,(一般声卡每次播一个采样点而不是一帧。声音当一个采样点丢失了都可以听出来,视频则不然)如果有了 delay 人的耳朵可以很明显的分辨出来,给人的主观感受就是声音卡顿。反则视频则不如此,虽然表面上说的是30p,不一定每一帧的间隔就必须精确到33.33ms,因为人肉眼是观察不出来的。

1.1 同步方法

  音视频同步的三种方法:

   (1)音频同步到视频;

  (2)视频同步到音频 ;

  (3)音视频都同步到外部时钟。

  音频和视频是各自线程独立播放的,所以需要同步行为来保证声画的时间节点是一致的或者时间偏差值在一定的范围内。从技术上来说,解决音视频同步问题的最佳方案就是时间戳:

   (1)首先选择一个参考时钟(要求参考时钟上的时间是线性递增的,通常选择系统时钟);

   (2)生成数据流时依据参考时钟上的时间给每个数据块都打上时间戳(一般包括开始时间和结束时间);

  (3)在播放时,读取数据块上的时间戳,同时参考当前参考时钟上的时间来安排播放(如果数据块的开始时间大于当前参考时钟上的时间,则不急于播放该数据块,直到参考时钟达到数据块的开始时间;如果数据块的开始时间小于当前参考时钟上的时间,则“尽快”播放这块数据或者索性将这块数据“丢弃”,以使播放进度追上参考时钟)。

01.png

可见,避免音视频不同步现象有两个关键:

   (1)在生成数据流时要打上正确的时间戳。如果数据块上打的时间戳本身就有问题,那么播放时再怎么调整也于事无补。

如图,视频流内容是从0s开始的,假设10s时有人开始说话,要求配上音频流,那么音频流的起始时间应该是10s,如果时间戳从0s或其它时间开始打,则这个混合的音视频流在时间同步上本身就出了问题。打时间戳时,视频流和音频流都是参考参考时钟的时间,而数据流之间不会发生参考关系;也就是说,视频流和音频流是通过一个中立的第三方(也就是参考时钟)来实现同步的。

  (2)在播放时基于时间戳对数据流的控制,也就是对数据块早到或晚到采取不同的处理方法。

图中,参考时钟时间在0-10s内播放视频流内容过程中,即使收到了音频流数据块也不能立即播放它,而必须等到参考时钟的时间达到10s之后才可以,否则就会引起音视频不同步问题。

1.2 打时间戳的方法

(1)视频时间戳

  一般这个值依赖于帧率(fps),(1000/fps)为帧间间隔,相当于一个个间隔时间加上去了。

pts = inc++ * (1000/fps); //其中inc是一个静态的,初始值为0,每次打完时间戳加1。

(2)音频时间戳

  依赖于音频的sample rate来计算,(1000 / sample_rate)是每个采样多长时间,(frame_size * 1000 / sample_rate)计算出来一个frame_size长度的音频帧的时长:

pts = inc++ * (frame_size * 1000 / sample_rate);

  从上面打时间戳的方法上就可以看出来,根据音频帧中的时间戳是可以计算出来播放时长,而视频帧的时间戳是计算不出来时长的,就算要同步到外部时钟上,也是需要根据音频帧与外部时钟同步后,视频帧再同步到音频帧上。

  具体在NuPlayer中是如何做的,大致思路是:根据音频帧播放的数目,参考外部时钟每隔一段时间打一个锚点,然后将视频帧根据这个锚点来进行同步。

1.3 NuPlayer中音视频同步方法详解

  在之前的文章中分析过,音视频的同步是在NuPlayer::Renderer中做的,具体的流程在《Android多媒体框架--12:Render渲染器流程分析》中已经分析,这里列出主要有关音视频同步的代码分析,先来看函数流程图:

02.png

  NuPlayerDecoder拿到解码后的音视频数据后queueBuffer给NuPlayerRenderer,在NuPlayerRenderer中通过postDrainAudioQueue_l方法调用AudioTrack进行写入,并且获取“Audio当前播放的时间”,可以看到这里也调用了AudioTrack的getTimeStamp和getPosition方法,同时会利用MediaClock类记录一些锚点时间戳变量。

  NuPlayerRenderer中调用postDrainVideoQueue方法对video数据进行处理,包括计算实际送显时间,利用vsync信号调整送显时间等,这里的调整是利用VideoFrameScheduler类完成的。需要注意的是,实际上NuPlayerRenderer方法中只进行了avsync的调整,真正的播放还要通过onRendereBuffer调用到NuPlayerDecoder中,进而调用MediaCodec的release方法进行播放。

  因为音视频同步的方法是根据音频帧播放的数目,参考外部时钟每隔一段时间打一个锚点,然后将视频帧根据这个锚点来进行同步,所以先来分析音频部分。

2.处理解码之后的数据

2.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.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();
    }
......
    int64_t firstAudioTimeUs;
    int64_t firstVideoTimeUs;
    // 分别获取音视频第一帧的PTS
    CHECK(firstAudioBuffer->meta()
            ->findInt64("timeUs", &firstAudioTimeUs));
    CHECK(firstVideoBuffer->meta()
            ->findInt64("timeUs", &firstVideoTimeUs));

    int64_t diff = firstVideoTimeUs - firstAudioTimeUs;

    ALOGV("queueDiff = %.2f secs", diff / 1E6);

    if (diff > 100000ll) {
        // Audio data starts More than 0.1 secs before video.
        // Drop some audio.
        //这里对音视频帧的第一个pts做一下纠正,保证一开始两者就是同步的。
        (*mAudioQueue.begin()).mNotifyConsumed->post();
        mAudioQueue.erase(mAudioQueue.begin());
        return;
    }

    syncQueuesDone_l();
}

3.Audio Buffer的处理

3.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);

postDrainAudioQueue_l函数发送kWhatDrainAudioQueue命令,跳转到处理函数中:

case kWhatDrainAudioQueue:
{
......
            // 接下来主要的工作在onDrainAudioQueue中完成
            if (onDrainAudioQueue()) {
                // 函数onDrainAudioQueue,当AudioQueue的数据没有处理完的情况下,会返回true
                // 返回true的情况下,需要延时delayUs再次调用onDrainAudioQueue处理AudioQueue
                uint32_t numFramesPlayed;
                //(1) 调用AudioTrack的getPosition获得Audio已经播放的数据帧的个数.
                CHECK_EQ(mAudioSink->getPosition(&numFramesPlayed),
                         (status_t)OK);

                //(2) 已经写入到Audio但是还没有播放的数据(主要是在Audio侧Buffer中的数据)
                // 写入的帧数mNumFramesWritten,减去已经播放的帧数numFramesPlayed
                uint32_t numFramesPendingPlayout =
                    mNumFramesWritten - numFramesPlayed;

                // This is how long the audio sink will have data to
                // play back.
                //(3) delayUs: 需要延时delayUs之后,最新写入Audio的数据才会开始播放
                int64_t delayUs =
                    mAudioSink->msecsPerFrame()
                        * numFramesPendingPlayout * 1000ll;
                //(4) 根据播放的速率 调整delayUs
                if (mPlaybackRate > 1.0f) {
                    delayUs /= mPlaybackRate;
                }

                // Let's give it more data after about half that time
                // has elapsed.
                //(5) 调整delayUs,防止Audio Buffer的数据被清空
                delayUs /= 2;
                // check the buffer size to estimate maximum delay permitted.
                const int64_t maxDrainDelayUs = std::max(
                        mAudioSink->getBufferDurationInUs(), (int64_t)500000 /* half second */);
                ALOGD_IF(delayUs > maxDrainDelayUs, "postDrainAudioQueue long delay: %lld > %lld",
                        (long long)delayUs, (long long)maxDrainDelayUs);
                Mutex::Autolock autoLock(mLock);
                //(6) 调用postDrainAudioQueue_l处理AudioQueue的数据
                // 传入 delayUs
                postDrainAudioQueue_l(delayUs);
            }
            break;
        }

最后又会跳转到 postDrainAudioQueue_l(delayUs);里面了,从而会一直调用kWhatDrainAudioQueue这个循环,但是需要注意的是:这里同时加了一个延时,这就是一个很关键的点,说明播放器在音频的处理循环中是有一定间隔的,并非不停的运转Loop。

  至于怎么设置延时时间的,可以看上面的注释,已经都写清楚了,核心就是音频的采样率是固定的,它的帧数与播放时间有一个直接的关系,可以直接进行转换。

3.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 numFramesPlayed;
    uint32_t prevFramesWritten = mNumFramesWritten;
    //(1) 循环处理mAudioQueue中的节点, 直到mAudioQueue中的buffer被清空
    while (!mAudioQueue.empty()) {
        // 处理当前的头节点
        QueueEntry *entry = &*mAudioQueue.begin();

        mLastAudioBufferDrained = entry->mBufferOrdinal;
        // Buffer为空的情况
        if (entry->mBuffer == NULL) {
            // EOS
......
        }
        // ignore 0-sized buffer which could be EOS marker with no data
        //(2) 尝试更新锚点时间,之后会对锚点时间以及如何更新详细介绍
        // 第一次处理这个节点, 如果需要, 尝试通过媒体时间更新锚点时间
        // mOffset Buffer中已经有mOffset大小的数据被处理
        if (entry->mOffset == 0 && entry->mBuffer->size() > 0) {
            int64_t mediaTimeUs;
            // 从Buffer的数据中, 获得媒体的时间(当前Buffer在媒体文件中的位置,可认为是进度条时间)
            CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
            ALOGV("onDrainAudioQueue: rendering audio at media time %.2f secs",
                    mediaTimeUs / 1E6);
            // 尝试更新锚点时间
            onNewAudioMediaTime(mediaTimeUs);
        }

        //(3) 把数据写入到AudioSink
        // 需要写入copy大小的的数据
        size_t copy = entry->mBuffer->size() - entry->mOffset;//表示buffer中还剩多少数据

        // 调用AudioSink的Write把数据写到AudioTrack
        ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset,
                                            copy, false /* blocking */);

        if (written < 0) {
            ......
        }

        // 根据写入的数据, 更新Offset
        entry->mOffset += written;
        // Buffer中还有remainder的数据需要处理
        size_t remainder = entry->mBuffer->size() - entry->mOffset;
        //(4) Buffer被处理完, 通知Decoder
        if ((ssize_t)remainder < mAudioSink->frameSize()) {
            if (remainder > 0) {
                ALOGW("Corrupted audio buffer has fractional frames, discarding %zu bytes.",
                        remainder);
                entry->mOffset += remainder;
                copy -= remainder;
            }

            // 通知Decoder当前buffer已经被处理完
            // mNotifyConsumed: Decoder传入的消息kWhatRenderBuffer
            entry->mNotifyConsumed->post();
            // 从队列中删掉已经播放的数据
            mAudioQueue.erase(mAudioQueue.begin());

            entry = NULL;
        }

        // 更新copiedFrames:已经写入到Audio的数据
        size_t copiedFrames = written / mAudioSink->frameSize();
        mNumFramesWritten += copiedFrames;//更新mNumFramesWritten 

        {
            Mutex::Autolock autoLock(mLock);
            int64_t maxTimeMedia;
            //(5) 更新MediaClock中的maxTimeMedia:(上面写入的Buffer的最后一帧的媒体时间)
            // 媒体时间, Buffer在媒体文件的位置, 可以理解为进度条的时间
            // 锚点媒体时间戳加上新写入帧数对应的时长,即为媒体时间戳最大值
            // ----------------
            // mAnchorTimeMediaUs:锚点媒体时间, 最新的锚点对应的buffer的MediaTime
            // (mNumFramesWritten - mAnchorNumFramesWritten):上次锚点之后,新写入的帧数
            // 1000LL * mAudioSink->msecsPerFrame():每一帧播放的时间(delay)
            maxTimeMedia =
                mAnchorTimeMediaUs +
                        (int64_t)(max((long long)mNumFramesWritten - mAnchorNumFramesWritten, 0LL)
                                * 1000LL * mAudioSink->msecsPerFrame());
            //更新MediaClock中的maxTimeMedia
            mMediaClock->updateMaxTimeMedia(maxTimeMedia);

            notifyIfMediaRenderingStarted_l();
        }

        if (written != (ssize_t)copy) {
......
        }
    }

    // calculate whether we need to reschedule another write.
    //(6) 如果mAudioQueue还有数据没有处理返回true, 需要重新调用onDrainAudioQueue处理
    bool reschedule = !mAudioQueue.empty()
......
    return reschedule;
}

4.Video Buffer的处理

4.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;
    // 2倍vsync duration
    int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);
    // 利用调整后的realTimeUs再计算一次“还有多久播放这一帧”
    delayUs = realTimeUs - nowUs;
    //(3) 发送消息kWhatDrainVideoQueue调用onDrainVideoQueue
    //如果 delayUs大于2倍的Vsync, 延时delayUs减去2倍的Vsync的时间发送kWhatDrainVideoQueue
    //否则立即发送kWhatDrainVideoQueue, 立即发送kWhatDrainVideoQueue,处理buffer
    msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);
    mDrainVideoQueuePending = true;
}

4.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;

5.AVsync Audio更新锚点时间

5.1 AVsync原理

  系统时间和媒体时间应该是线性关系:

03.png

  计算公式:原理是媒体时间间隔和系统时间间隔成比例。

(mediaTimeUs - anchorTimeMediaUs) = PlaybackRate*(realTimeUs - anchorTimeRealUs)
  • mediaTimeUs:媒体时间,当前Buffer在媒体文件中的位置,可认为是进度条时间。

  • realTimeUs:送显时间,当前Buffer实际显示时间(一般针对video)。

  • anchorTimeMediaUs:锚点媒体时间,锚点上记录的一次媒体时间。

  • anchorTimeRealUs:锚点系统时间,锚点上记录的一次系统时间。

  所以理论上,我们可以根据锚点,计算出任意一点的媒体时间对应的系统时间(Buffer应该播放的时间)。 AVsync 的机制就是通过Audio每隔一段时间更新锚点,Video的Buffer根据锚点和媒体时间计算出应该播放的时(系统时间)

5.2 Audio更新锚点

  更新锚点数据,需要同时更新锚点系统时间(anchorTimeRealUs)和锚点媒体时间(anchorTimeMediaUs)。在3.2节中Audio处理Buffer时执行的onDrainAudioQueue函数,会调用onNewAudioMediaTime更新锚点,并且一个Buffer最多更新一次(mOffset == 0)。

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;
                    }
    } else {
......
    }
    mAnchorNumFramesWritten = mNumFramesWritten;//“锚点写入帧数量”初始化为0
    mAnchorTimeMediaUs = mediaTimeUs;//将锚点媒体时间戳设置为初始audio pts
---------------------------
// (2) 需要分析一下getPendingAudioPlayoutDurationUs
// 计算方法使用 writtenAudioDurationUs - PlayedOutDurationUs
// Calculate duration of pending samples if played at normal rate (i.e., 1.0).
int64_t NuPlayer::Renderer::getPendingAudioPlayoutDurationUs(int64_t nowUs) {
    // (3) writtenAudioDurationUs 已经写入的数据的持续时间
    // mNumFramesWritten * (1000000LL / sampleRate)
    // 写入的帧的个数 * 每一帧的持续时间(us)
    // (1000000LL / sampleRate): sampleRate一秒的采样数, 取倒数每一个采样的持续时间
    int64_t writtenAudioDurationUs = getDurationUsIfPlayedAtSampleRate(mNumFramesWritten);
......
    const int64_t audioSinkPlayedUs = mAudioSink->getPlayedOutDurationUs(nowUs);
    // PlayedOutDurationUs 已经播放时间, 需要从Audio侧获取
    int64_t pendingUs = writtenAudioDurationUs - audioSinkPlayedUs;
......
    return pendingUs;
}
---------------------------
// Calculate duration of played samples if played at normal rate (i.e., 1.0).
//(4) 当前已经播放的数据的持续时间
int64_t MediaPlayerService::AudioOutput::getPlayedOutDurationUs(int64_t nowUs) const
{
......
    // 从Audio侧获取已经获取当前播放的帧,和对应的系统时间
    // 注意ts.mPosition并不是正在播放的帧的位置, 
    // 应该是ts.mTime这个系统时间点正在播放的帧的位置
    // ts.mTime与当前时间nowUs并不相等,会有ms级的差别
    status_t res = mTrack->getTimestamp(ts);
    if (res == OK) {                 // case 1: mixing audio tracks and offloaded tracks.
        numFramesPlayed = ts.mPosition;
        numFramesPlayedAtUs = ts.mTime.tv_sec * 1000000LL + ts.mTime.tv_nsec / 1000;
        //ALOGD("getTimestamp: OK %d %lld", numFramesPlayed, (long long)numFramesPlayedAtUs);
    }
......
    //  numFramesPlayed * 1000000LL / mSampleRateHz: ts.mTime时间点已经播放的时间
    // 最后计算的时候需要考虑ts.mTime与当前时间nowUs之间的差异
    // durationUs  nowUs时间点已经播放的时间(正在播放的媒体时间)
    int64_t durationUs = (int64_t)((int32_t)numFramesPlayed * 1000000LL / mSampleRateHz)
            + nowUs - numFramesPlayedAtUs;
......
    return durationUs;
}
---------------------------
// (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.2.1 更新anchorTimeRealUs

anchorTimeRealUs = nowUs

锚点系统时间被设置为当前系统时间,系统时间直接通过ALooper::GetNowUs()获取。

5.2.2 更新anchorTimeMediaUs

  anchorTimeMediaUs 就应该当前正在播放的媒体时间,计算公式如下:

anchorTimeMediaUs = nowMediaUs

(1)计算当前正在播放的媒体时间(nowMediaUs)

04.png
  • anchorTimeMediaUs:锚点媒体时间,锚点上记录的一次媒体时间。

  • anchorTimeRealUs:锚点系统时间,锚点上记录的一次系统时间。

  • nowMediaUs:媒体时间,当前正在播放的媒体时间。

  • nowUs:系统时间,当前的系统时间。

  • mediaTimeUs:媒体时间,正在写入Buffer在媒体文件中的位置,可认为是进度条时间。

  • realTimeUs:送显时间,写入Buffer实际显示时间(一般针对video)。

当前正在播放的媒体时间 = 正在写入的媒体时间 - 已经写入但是没有播放的数据需要播放的时间。

nowMediaUs = mediaTimeUs - getPendingAudioPlayoutDurationUs

getPendingAudioPlayoutDurationUs:已经写入但是没有播放的数据需要播放的时间(主要是在AudioBuffer里面的数据)

05.png

(2)计算没有播放的数据需要播放的时间(getPendingAudioPlayoutDurationUs)

getPendingAudioPlayoutDurationUs = writtenAudioDurationUs - getPlayedOutDurationUs
  • writtenAudioDurationUs:已经写入的数据的持续时间。

  • getPlayedOutDurationUs:当前已经播放的数据的持续时间。

两者相减就是getPendingAudioPlayoutDurationUs。这样计算的原因,主要是getPendingAudioPlayoutDurationUs计算的过程中,音频还继续在播放过程中,它会继续走一段时间,这时候计算播放时长的话,需要把这段延时加上去。

06.png

(3)计算已经写入的数据的持续时间(writtenAudioDurationUs)

writtenAudioDurationUs = mNumFramesWritten * (1000000LL / sampleRate)
  • mNumFramesWritten:已经写入的数据的帧的个数 * 每一帧多少时间(1000000LL / sampleRate)

(4)计算当前已经播放的数据的持续时间(getPlayedOutDurationUs)

getPlayedOutDurationUs = ts.mPosition * 1000000LL/mSampleRateHz + nowUs - ts.mTime
  • ts.mPosition * 1000000LL/mSampleRateHz:在 ts.mTime时间已经播放的数据的时间。

因为nowUs与ts.mTime不相等, 最后需要根据nowUs进行调整。

5.2.3 锚点更新过程

  这里可以看下锚点的更新过程。

(1)更新前

07.png

(2)更新后

08.png

6.AVsync Video获取显示时间

  AVsync的目的是获得Buffer显示的时间(buffer的系统时间)。我们可以根据媒体时间和系统时间的线性关系计算出显示的时间:

(mediaTimeUs - anchorTimeMediaUs) = PlaybackRate*(nowUs - anchorTimeMediaUs)

  代码回到4.2节中的onDrainVideoQueue()函数,其中会通过getRealTimeUs函数计算出当前Buffer显示的时间realTimeUs(系统时间)。看到下面的代码:

int64_t NuPlayer::Renderer::getRealTimeUs(int64_t mediaTimeUs, int64_t nowUs) {
    int64_t realUs;
    // 直接调用到mMediaClock中的方法
    if (mMediaClock->getRealTimeFor(mediaTimeUs, &realUs) != OK) {

        return nowUs;
    }
    return realUs;
}
----------------------
// outRealUs:返回结果,获得当前Buffer播放的系统时间(应该在这个时间点播放)
// targetMediaUs:传入参数,当前Buffer的媒体时间。
status_t MediaClock::getRealTimeFor(
        int64_t targetMediaUs, int64_t *outRealUs) const {
......
    // 获取当前系统时间
    int64_t nowUs = ALooper::GetNowUs();
    int64_t nowMediaUs;
    // (1) nowMediaUs:返回结果,video正在播放的媒体时间
    // nowUs:传入参数,当前的系统时间
    status_t status =
            getMediaTime_l(nowUs, &nowMediaUs, true /* allowPastMaxTime */);
    if (status != OK) {
        return status;
    }
    // (2) 计算出Buffer的显示时间
    *outRealUs = (targetMediaUs - nowMediaUs) / (double)mPlaybackRate + nowUs;
    return OK;
}
----------------------
// outMediaUs:返回结果,video正在播放的媒体时间, nowUs对应的媒体时间
// realUs:传入参数,当前系统时间
status_t MediaClock::getMediaTime_l(
        int64_t realUs, int64_t *outMediaUs, bool allowPastMaxTime) const {

    //mediaUs 当前Audio正在播放的媒体时间, 对应video正在播放的媒体时间
    int64_t mediaUs = mAnchorTimeMediaUs
            + (realUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
    if (mediaUs > mMaxTimeMediaUs && !allowPastMaxTime) {
        mediaUs = mMaxTimeMediaUs;
    }
    if (mediaUs < mStartingTimeMediaUs) {
        mediaUs = mStartingTimeMediaUs;
    }
    if (mediaUs < 0) {
        mediaUs = 0;
    }
    *outMediaUs = mediaUs;
    return OK;
}

(1)计算当前播放媒体时间(nowMediaUs)

  根据当前的系统时间和锚点时间计算出当前播放媒体时间:

nowMediaUs = mAnchorTimeMediaUs + (realUs - mAnchorTimeRealUs) * mPlaybackRate
  • anchorTimeMediaUs:锚点媒体时间,锚点上记录的一次媒体时间。(已知参数)

  • anchorTimeRealUs:锚点系统时间,锚点上记录的一次系统时间。(已知参数)

  • realUs:系统时间,传入的nowUs参数,当前系统时间。(传入参数)

  • mPlaybackRate:播放速率。(已知参数)

  • nowMediaUs:媒体时间,当前播放的媒体时间。(有待求解的结果)

(2)计算Buffer的显示时间(outRealUs)

  根据当前播放的媒体时间和系统时间,计算出Buffer的显示时间(系统时间)

outRealUs = (targetMediaUs - nowMediaUs) / (double)mPlaybackRate + nowUs
          = (targetMediaUs
            - (mAnchorTimeMediaUs + (nowUs - mAnchorTimeRealUs) * mPlaybackRate))
            / (double)mPlaybackRate + nowUs
  • targetMediaUs:媒体时间,当前Buffer的媒体时间,对应为Buffer的mediaTimeUs。(传入参数)

  • nowMediaUs:媒体时间,当前播放的媒体时间。(第(1)步已经求解出来了)

  • anchorTimeMediaUs:锚点媒体时间,锚点上记录的一次媒体时间。(已知参数)

  • anchorTimeRealUs:锚点系统时间,锚点上记录的一次系统时间。(已知参数)

  • mPlaybackRate:播放速率。(已知参数)

  • nowUs:系统时间,当前的系统时间。(已知参数)

  • outRealUs:系统时间,当前Buffer显示的时间。(有待求解的结果)

09.jpg

如上图所示,根据anchorTimeMediaUs,anchorTimeRealUs,nowUs先求出nowMediaUs,然后再根据nowMediaUs,nowUs和targetMediaUs求出当前写入Buffer显示的时间outRealUs(系统时间),也就是4.2节中的onDrainVideoQueue()函数中需要就算的realTimeUs变量:

realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);

最后总结的计算公式:

realTimeUs = (mediaTimeUs
            - (mAnchorTimeMediaUs + (nowUs - mAnchorTimeRealUs) * mPlaybackRate))
            / (double)mPlaybackRate + nowUs

  这么做是为了音视频同步的精度,因为nowUs一直在变化,而函数的执行同样需要消耗时间,每一步中都会去重新获取nowUs来增加精度。用图来表示,就是nowMediaUs是不断变化的,在它的变化过程中,我们不断的打下锚点,但是锚点是滞后nowMediaUs的:

10.png

而我们希望计算一个比较精确的realTimeUs,所以就采用这种方式来计算:

11.png

通过这种方法大概就是优化了图中阴影位置的时间。

7.vsync调整视频帧

  在4.1节postDrainVideoQueue()已经计算出来video显示的时间,之后就是VideoFrameScheduler::schedule()函数了,这个函数根据vsync来调整视频帧应该显示的时间:

nsecs_t VideoFrameScheduler::schedule(nsecs_t renderTime) {
    nsecs_t origRenderTime = renderTime;

    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
    if (now >= mVsyncRefreshAt) {
        updateVsync();
    }

    // without VSYNC info, there is nothing to do
    if (mVsyncPeriod == 0) {
        ALOGV("no vsync: render=%lld", (long long)renderTime);
        return renderTime;
    }

    // ensure vsync time is well before (corrected) render time
    if (mVsyncTime > renderTime - 4 * mVsyncPeriod) {
        mVsyncTime -=
            ((mVsyncTime - renderTime) / mVsyncPeriod + 5) * mVsyncPeriod;
    }

    // Video presentation takes place at the VSYNC _after_ renderTime.  Adjust renderTime
    // so this effectively becomes a rounding operation (to the _closest_ VSYNC.)
    renderTime -= mVsyncPeriod / 2;

    const nsecs_t videoPeriod = mPll.addSample(origRenderTime);
    if (videoPeriod > 0) {
        // Smooth out rendering
        size_t N = 12;
        nsecs_t fiveSixthDev =
            abs(((videoPeriod * 5 + mVsyncPeriod) % (mVsyncPeriod * 6)) - mVsyncPeriod)
                    / (mVsyncPeriod / 100);
        // use 20 samples if we are doing 5:6 ratio +- 1% (e.g. playing 50Hz on 60Hz)
        if (fiveSixthDev < 12) {  /* 12% / 6 = 2% */
            N = 20;
        }

        nsecs_t offset = 0;
        nsecs_t edgeRemainder = 0;
        for (size_t i = 1; i <= N; i++) {
            offset +=
                (renderTime + mTimeCorrection + videoPeriod * i - mVsyncTime) % mVsyncPeriod;
            edgeRemainder += (videoPeriod * i) % mVsyncPeriod;
        }
        mTimeCorrection += mVsyncPeriod / 2 - offset / (nsecs_t)N;
        renderTime += mTimeCorrection;
        nsecs_t correctionLimit = mVsyncPeriod * 3 / 5;
        edgeRemainder = abs(edgeRemainder / (nsecs_t)N - mVsyncPeriod / 2);
        if (edgeRemainder <= mVsyncPeriod / 3) {
            correctionLimit /= 2;
        }

        // estimate how many VSYNCs a frame will spend on the display
        nsecs_t nextVsyncTime =
            renderTime + mVsyncPeriod - ((renderTime - mVsyncTime) % mVsyncPeriod);
        if (mLastVsyncTime >= 0) {
            size_t minVsyncsPerFrame = videoPeriod / mVsyncPeriod;
            size_t vsyncsForLastFrame = divRound(nextVsyncTime - mLastVsyncTime, mVsyncPeriod);
            bool vsyncsPerFrameAreNearlyConstant =
                periodicError(videoPeriod, mVsyncPeriod) / (mVsyncPeriod / 20) == 0;

            if (mTimeCorrection > correctionLimit &&
                    (vsyncsPerFrameAreNearlyConstant || vsyncsForLastFrame > minVsyncsPerFrame)) {
                // remove a VSYNC
                mTimeCorrection -= mVsyncPeriod / 2;
                renderTime -= mVsyncPeriod / 2;
                nextVsyncTime -= mVsyncPeriod;
                if (vsyncsForLastFrame > 0)
                    --vsyncsForLastFrame;
            } else if (mTimeCorrection < -correctionLimit &&
                    (vsyncsPerFrameAreNearlyConstant || vsyncsForLastFrame == minVsyncsPerFrame)) {
                // add a VSYNC
                mTimeCorrection += mVsyncPeriod / 2;
                renderTime += mVsyncPeriod / 2;
                nextVsyncTime += mVsyncPeriod;
                if (vsyncsForLastFrame < ULONG_MAX)
                    ++vsyncsForLastFrame;
            }
            ATRACE_INT("FRAME_VSYNCS", vsyncsForLastFrame);
        }
        mLastVsyncTime = nextVsyncTime;
    }

    // align rendertime to the center between VSYNC edges
    renderTime -= (renderTime - mVsyncTime) % mVsyncPeriod;
    renderTime += mVsyncPeriod / 2;
    ALOGV("adjusting render: %lld => %lld", (long long)origRenderTime, (long long)renderTime);
    ATRACE_INT("FRAME_FLIP_IN(ms)", (renderTime - now) / 1000000);
    return renderTime;
}

8.参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容