美文网首页
Android多媒体框架--13:MediaClock分析与音视

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

作者: DarcyZhou | 来源:发表于2023-04-27 08:14 被阅读0次

    "本文转载自:[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.参考资料

    相关文章

      网友评论

          本文标题:Android多媒体框架--13:MediaClock分析与音视

          本文链接:https://www.haomeiwen.com/subject/fmnzddtx.html