Android 13 - Media框架(10)- NuPlayer::Renderer

2023-11-05

这一节我们来了解 NuPlayer Renderer 是如何工作,avsync 机制是如何运行的。

1、创建 Renderer

void NuPlayer::onStart(int64_t startPositionUs, MediaPlayerSeekMode mode) {
    if (mSource->isRealTime()) {
        flags |= Renderer::FLAG_REAL_TIME;
    }
......
    if (mOffloadAudio) {
        flags |= Renderer::FLAG_OFFLOAD_AUDIO;
    }
......
    sp<AMessage> notify = new AMessage(kWhatRendererNotify, this);
    ++mRendererGeneration;
    notify->setInt32("generation", mRendererGeneration);
    mRenderer = new Renderer(mAudioSink, mMediaClock, notify, flags);
    mRendererLooper = new ALooper;
    mRendererLooper->setName("NuPlayerRenderer");
    mRendererLooper->start(false, false, ANDROID_PRIORITY_AUDIO);
    mRendererLooper->registerHandler(mRenderer);
}

调用 NuPlayer start 方法后会创建 Renderer,传入参数为 callback message、AudioSink、MediaClock 以及 flags。可以看到 NuPlayer 中也用一个 generation 来管理 Renderer 的状态,如不了解 generation 是如何使用的,可以看前面一篇笔记。

接下来分别解释几个参数的意义:

  • AudioSink:它是一个基类,实际传入的是他的子类对象 AudioOutput,实现在 MediaPlayerService.cpp 当中,AudioOutput 中封装的是 AudioTrack,如果想了解 AudioTrack 如何使用,可以参考 AudioOutput。回到这里,在 Renderer 中,decoder 解出 audio data 后会直接将数据写到 AudioOutput 中;
  • MediaClock:它是一个系统时钟,用于记录系统时间戳;
  • flags:创建 Renderer 之前会分析它所使用的 flag,首先判断 Source 是否是 RealTime,Source.isRealTime 默认返回值是 false,只有 RTSPSource 返回 true,这里就可以猜测,如果是直播源,那么 avsync 流程应该会有不一样的地方;接着会判断当前的音频流是否支持 offload mode,offload mode 指的是将 audio compress data 直接写入到 AudioTrack 中,直接解码播放,而普通模式需要将 audio compress data 送到 audio decoder 中解出 PCM 数据,然后再写入到 AudioTrack 中。如果走 offload mode,audio Decoder 将使用 NuPlayerDecoderPassThrough,因此 Renderer 中写入 audio data 的流程也需要做些改变。
status_t NuPlayer::instantiateDecoder(bool audio, sp<DecoderBase> *decoder, bool checkAudioModeChange) {
    if (audio) {
        if (checkAudioModeChange) {
        	// 判断是否需要开启 offload mode
            determineAudioModeChange(format);
        }
        if (mOffloadAudio) {
            mSource->setOffloadAudio(true /* offload */);
            const bool hasVideo = (mSource->getFormat(false /*audio */) != NULL);
            format->setInt32("has-video", hasVideo);
            *decoder = new DecoderPassThrough(notify, mSource, mRenderer);
            ALOGV("instantiateDecoder audio DecoderPassThrough  hasVideo: %d", hasVideo);
        } else {
            *decoder = new Decoder(notify, mSource, mPID, mUID, mRenderer);
            ALOGV("instantiateDecoder audio Decoder");
        }
        mAudioDecoderError = false;
    }
}

void NuPlayer::determineAudioModeChange(const sp<AMessage> &audioFormat) {
    if (canOffload) {
        if (!mOffloadAudio) {
            mRenderer->signalEnableOffloadAudio();
        }
        // open audio sink early under offload mode.
        tryOpenAudioSinkForOffload(audioFormat, audioMeta, hasVideo);
    } else {
        if (mOffloadAudio) {
            mRenderer->signalDisableOffloadAudio();
            mOffloadAudio = false;
        }
    }
}

创建 AudioDecoder 时会调用 determineAudioModeChange 再次判断是否支持 offload mode(第一次判断在 NuPlayer 章节中已经简单提过了),如果支持就会优先使用 offload mode(节约性能),调用 Renderer 的 openAudioSink 方法尝试打开 audio hal,并配置 offload mode;如果不支持 offload mode,则暂时不会创建 AudioTrack。

上一篇 Decoder 中我们提到,Decoder 收到Audio Output Format Changed 事件后会调用 changeAudioFormat 方法,如果不是 offload mode,这里会调用 openAudioSink 创建普通的 AudioTrack。也就是说,普通模式下只有真正解出 audio 数据后 AudioTrack 才会被创建。

ps:如果想了解 AudioTrack 普通模式以及 offload mode 如何使用,可以参考 NuPlayer、Renderer 以及 NuPlayerDecoderPassThrough。

接下来的内容我们暂时只看 AudioTrack 普通模式

2、queueBuffer

Renderer 没有 start 方法,调用 queueBuffer 把 ouput buffer 写入到 Renderer 时 Avsync 就自动开始了。

void NuPlayer::Renderer::queueBuffer(
        bool audio,
        const sp<MediaCodecBuffer> &buffer,
        const sp<AMessage> &notifyConsumed) {
    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();
}

Renderer 中同样也使用了 generation trick,传进来的参数会被封装到新的 AMessage 送到 ALooper 中,最后通过 onQueueBuffer 处理消息:

void NuPlayer::Renderer::onQueueBuffer(const sp<AMessage> &msg) {
    int32_t audio;
    CHECK(msg->findInt32("audio", &audio));
	// 判断 buffer 是否因为 generation变化要 drop
    if (dropBufferIfStale(audio, msg)) {
        return;
    }

    if (audio) {
        mHasAudio = true;
    } else {
        mHasVideo = true;
    }
	// 如果是 video 则需要创建 VideoFrameScheduler,这是用于获取 vsync,这里不做研究
    if (mHasVideo) {
        if (mVideoScheduler == NULL) {
            mVideoScheduler = new VideoFrameScheduler();
            mVideoScheduler->init();
        }
    }

    sp<RefBase> obj;
    CHECK(msg->findObject("buffer", &obj));
    sp<MediaCodecBuffer> buffer = static_cast<MediaCodecBuffer *>(obj.get());

    sp<AMessage> notifyConsumed;
    CHECK(msg->findMessage("notifyConsumed", &notifyConsumed));
	// 将 Message 中的内容重新封装到 QueueEntry
    QueueEntry entry;
    entry.mBuffer = buffer;
    entry.mNotifyConsumed = notifyConsumed;
    entry.mOffset = 0;
    entry.mFinalResult = OK;
    entry.mBufferOrdinal = ++mTotalBuffersQueued;
	// 发消息处理 Queue 中的 entry
    if (audio) {
        Mutex::Autolock autoLock(mLock);
        mAudioQueue.push_back(entry);
        postDrainAudioQueue_l();
    } else {
        mVideoQueue.push_back(entry);
        postDrainVideoQueue();
    }
	// SyncQueue
    Mutex::Autolock autoLock(mLock);
    if (!mSyncQueues || mAudioQueue.empty() || mVideoQueue.empty()) {
        return;
    }

    sp<MediaCodecBuffer> firstAudioBuffer = (*mAudioQueue.begin()).mBuffer;
    sp<MediaCodecBuffer> firstVideoBuffer = (*mVideoQueue.begin()).mBuffer;

    if (firstAudioBuffer == NULL || firstVideoBuffer == NULL) {
        // EOS signalled on either queue.
        syncQueuesDone_l();
        return;
    }

    int64_t firstAudioTimeUs;
    int64_t firstVideoTimeUs;
    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.

        (*mAudioQueue.begin()).mNotifyConsumed->post();
        mAudioQueue.erase(mAudioQueue.begin());
        return;
    }

    syncQueuesDone_l();
}

onQueueBuffer 看起来很长,但是做的内容却并不多:

  1. 将 Message 中的内容重新封装成 QueueEntry,添加到对应的 List 中;
  2. 调用 postDrainAudioQueue_l / postDrainVideoQueue 发送消息处理 List 中的 Entry;
  3. 判断是否需要 SyncQueue;

2.1、SyncQueue

先说 SyncQueue 机制,它应该做起播同步用的,如果起播时 audio pts 和 video pts 差距过大,则通过该机制来 drop output data。

SyncQueue 机制在 Renderer 中尚未启用,功能也没有做完,为什么这么说呢?因为 mSyncQueues 并不会在代码中置 true,另外除了起播时要做 SyncQueue 外,我觉得 flush 之后也需要做一下同步。

SyncQueue 机制的主要思路是当 Video 和 Audio 数据都到达时,判断两队队首元素的 pts,如果 Video 的时间比 Audio 晚,那么就把 Audio 先 drop,同步之后可以将 SyncQueue flag 置为 false。

我们上面说到 onQueueBuffer 会先调用 postDrainAudioQueue_l / postDrainVideoQueue 发送消息,但是如果实际看代码就会发现,一进入这两个方法就会判断是否需要做 SyncQueue,如果需要是不会执行接下来的内容的。

2.2、postDrainAudioQueue_l

void NuPlayer::Renderer::postDrainAudioQueue_l(int64_t delayUs) {
	// 暂停、syncqueue、offload 直接退出
    if (mDrainAudioQueuePending || mSyncQueues || mUseAudioCallback) {
        return;
    }

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

    // FIXME: if paused, wait until AudioTrack stop() is complete before delivering data.
    if (mPaused) {
        const int64_t diffUs = mPauseDrainAudioAllowedUs - ALooper::GetNowUs();
        if (diffUs > delayUs) {
            delayUs = diffUs;
        }
    }
	// 如果暂停了就延时写入audioTrack
    mDrainAudioQueuePending = true;
    sp<AMessage> msg = new AMessage(kWhatDrainAudioQueue, this);
    msg->setInt32("drainGeneration", mAudioDrainGeneration);
    msg->post(delayUs);
}

使用 postDrainAudioQueue_l 发送消息前会先做判断,再决定是否要发送:

  1. 如果调用了暂停,那么需要等待 AudioTrack 完全停止才能做写入操作,所以消息需要做延时;
  2. 如果处在暂停延时或重写处理过程中、或者在 SyncQueue 处理过程中,又或者使用的 offload mode,将直接退出不再发送消息;
void NuPlayer::Renderer::onMessageReceived(const sp<AMessage> &msg) {
	switch (msg->what()) {
        case kWhatDrainAudioQueue:
        {
            mDrainAudioQueuePending = false;
			// 检查 generation
            int32_t generation;
            CHECK(msg->findInt32("drainGeneration", &generation));
            if (generation != getDrainGeneration(true /* audio */)) {
                break;
            }
			// 写入 audiotrack
            if (onDrainAudioQueue()) {
                uint32_t numFramesPlayed;
                CHECK_EQ(mAudioSink->getPosition(&numFramesPlayed),
                         (status_t)OK);
				// ......
				// 重写处理
                postDrainAudioQueue_l(delayUs);
            }
            break;
        }
    }
}

kWhatDrainAudioQueue 的主要步骤如下:

  1. 检查 generation,判断是否需要停止写入数据到 AudioTrack;
  2. 调用 onDrainAudioQueue 将 List 中的数据全部写入到 AudioTrack;
  3. 如果 List 中的数据没有全部写完(ring buffer 写满),那么计算延时时间,调用 postDrainAudioQueue_l 重新发送延时消息等待写入。
bool NuPlayer::Renderer::onDrainAudioQueue() {
    // do not drain audio during teardown as queued buffers may be invalid.
    if (mAudioTornDown) {
        return false;
    }
    // 获取当前已经播放的帧数
	uint32_t numFramesPlayed;
	mAudioSink->getPosition(&numFramesPlayed);
    uint32_t prevFramesWritten = mNumFramesWritten;
    while (!mAudioQueue.empty()) {
        QueueEntry *entry = &*mAudioQueue.begin();

        if (entry->mBuffer == NULL) {
        	// buffer 等于 null 会有两种情况,一种是 mNotifyConsumed 不等 null,另一种是 等于 null
            if (entry->mNotifyConsumed != nullptr) {
                // TAG for re-open audio sink.
                onChangeAudioFormat(entry->mMeta, entry->mNotifyConsumed);
                mAudioQueue.erase(mAudioQueue.begin());
                continue;
            }

            // EOS
            if (mPaused) {
                // Do not notify EOS when paused.
                // This is needed to avoid switch to next clip while in pause.
                ALOGV("onDrainAudioQueue(): Do not notify EOS when paused");
                return false;
            }

            int64_t postEOSDelayUs = 0;
            if (mAudioSink->needsTrailingPadding()) {
                postEOSDelayUs = getPendingAudioPlayoutDurationUs(ALooper::GetNowUs());
            }
            notifyEOS(true /* audio */, entry->mFinalResult, postEOSDelayUs);
            mLastAudioMediaTimeUs = getDurationUsIfPlayedAtSampleRate(mNumFramesWritten);

            mAudioQueue.erase(mAudioQueue.begin());
            entry = NULL;
            if (mAudioSink->needsTrailingPadding()) {
                mAudioSink->stop();
                mNumFramesWritten = 0;
            }
            return false;
        }

        mLastAudioBufferDrained = entry->mBufferOrdinal;

		// 如果偏移量为 0,说明是一个全新的ouput buffer
        if (entry->mOffset == 0 && entry->mBuffer->size() > 0) {
            int64_t mediaTimeUs;
            CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
            // 更新 AudioMediaTime
            onNewAudioMediaTime(mediaTimeUs);
        }

        size_t copy = entry->mBuffer->size() - entry->mOffset;
		// 将数据写入到 AudioTrack
        ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset,
                                            copy, false /* blocking */);
        // 计算写入长度,并且做一些判断
        entry->mOffset += written;
        size_t remainder = entry->mBuffer->size() - entry->mOffset;
        if ((ssize_t)remainder < mAudioSink->frameSize()) {
        	// 如果剩余的数据大于0,并且小于一帧音频的大小,那么就丢弃剩下的数据
            if (remainder > 0) {
                ALOGW("Corrupted audio buffer has fractional frames, discarding %zu bytes.",
                        remainder);
                entry->mOffset += remainder;
                copy -= remainder;
            }

            entry->mNotifyConsumed->post();
            mAudioQueue.erase(mAudioQueue.begin());

            entry = NULL;
        }
		// 记录写入的帧数
        size_t copiedFrames = written / mAudioSink->frameSize();
        mNumFramesWritten += copiedFrames;

        {
        	// 计算最大可播放时间
            Mutex::Autolock autoLock(mLock);
            int64_t maxTimeMedia;
            maxTimeMedia =
                mAnchorTimeMediaUs +
                        (int64_t)(max((long long)mNumFramesWritten - mAnchorNumFramesWritten, 0LL)
                                * 1000LL * mAudioSink->msecsPerFrame());
            mMediaClock->updateMaxTimeMedia(maxTimeMedia);

            notifyIfMediaRenderingStarted_l();
        }

        if (written != (ssize_t)copy) {
            CHECK_EQ(copy % mAudioSink->frameSize(), 0u);
            ALOGV("AudioSink write short frame count %zd < %zu", written, copy);
            break;
        }
    }

    // calculate whether we need to reschedule another write.
    // 当 List 数据不为空,并且 没有暂停或者是AudioTrack还没写满,尝试再次调用 postDrainAudioQueue_l 写入
    bool reschedule = !mAudioQueue.empty()
            && (!mPaused
                || prevFramesWritten != mNumFramesWritten);
    return reschedule;
}

这里把 onDrainAudioQueue 做的事情一一列举:

  1. 调用 AudioSink.getPosition 获取当前已经播放的 Audio 帧数,利用这个帧数与当前写入的帧数可以计算出现在可以写入多少帧音频数据;
  2. 如果 QueueEntry 中的 Buffer 为 NULL,说明收到了 EOS:
    • 2.1. 如果 mNotifyConsumed 不为 NULL,说明是收到了码流不连续时间,需要用新的 format 重新启动 AudioTrack;
    • 2.2. 如果 mNotifyConsumed 为 NULL,说明是上层调用了 queueEOS,这时候需要计算音频剩余可以播放多长时间,然后发送一条延时消息将 EOS 通知到 NuPlayer;
  3. 如果 Buffer 不为NULL 则要将数据拷贝到 AudioTrack,这里又分为两种情况:
    • 4.1. 如果 Buffer 的 offset 不为 0,说明是上次没有拷贝完,这里要接着拷贝;
    • 4.2. 如果 offset 为 0,说明是一块新的 output buffer,需要调用 onNewAudioMediaTime 用新的 timestamp 更新一些内容;
  4. 调用 AudioSink.write 将数据写入到 AudioTrack,如果剩余数据小于一帧音频数据的大小则剩余数据直接 drop,否则留到下次再拷贝;
  5. 更新 MediaClock 的最大媒体时长,并且发送开始 render 的事件 kWhatMediaRenderingStart 给 NuPlayer;
  6. 如果 List 不为空,并且没有暂停或者 AudioTrack的数据还没有写满,返回 true,尝试重新写入。

2.3、onNewAudioMediaTime

之所以把这个函数单拎出来是因为它是 NuPlayer Avsync 机制所用的核心函数之一。Avsync 分为以下四种:

  • free run:不做 Avsync;
  • Audio Master:video 同步到 audio;
  • Video Master:audio 同步到 video;
  • System Master:audio 和 video 都同步到系统时钟;

NuPlayer 用的是 Audio Master。

void NuPlayer::Renderer::onNewAudioMediaTime(int64_t mediaTimeUs) {
    Mutex::Autolock autoLock(mLock);
    // TRICKY: vorbis decoder generates multiple frames with the same
    // timestamp, so only update on the first frame with a given timestamp
    if (mediaTimeUs == mAnchorTimeMediaUs) {
        return;
    }
    // 设置 MediaClock 的开始媒体时间;
    setAudioFirstAnchorTimeIfNeeded_l(mediaTimeUs);

    // mNextAudioClockUpdateTimeUs is -1 if we're waiting for audio sink to start
    // 等待 AudioTrack 启动,获取到第一帧pts
    if (mNextAudioClockUpdateTimeUs == -1) {
        AudioTimestamp ts;
        if (mAudioSink->getTimestamp(ts) == OK && ts.mPosition > 0) {
            mNextAudioClockUpdateTimeUs = 0; // start our clock updates
        }
    }
    int64_t nowUs = ALooper::GetNowUs();
    if (mNextAudioClockUpdateTimeUs >= 0) {
    	// 到达更新时间
        if (nowUs >= mNextAudioClockUpdateTimeUs) {
        	// 获取当前剩余帧数,计算当前已播时长
            int64_t nowMediaUs = mediaTimeUs - getPendingAudioPlayoutDurationUs(nowUs);
            mMediaClock->updateAnchor(nowMediaUs, nowUs, mediaTimeUs);
            mUseVirtualAudioSink = false;
            mNextAudioClockUpdateTimeUs = nowUs + kMinimumAudioClockUpdatePeriodUs;
        }
    } 
    mAnchorNumFramesWritten = mNumFramesWritten;
    mAnchorTimeMediaUs = mediaTimeUs;
}
  1. 设置 MediaClock 的开始媒体时间;
  2. 等待 AudioTrack 启动,获取到第一帧pts;
  3. 根据当前写入帧数计算总时长,减去当前 AudioTrack 已播时长,得到剩余可播时长;
  4. 用本次写入的 timestamp 减去可播时长,得到当前的媒体播放时间;
  5. 用计算到的当前媒体时间 nowMediaUs,当前写入的 timestamp,以及当前的系统时间 更新 MediaClock anchor;

这里涉及到三个时间:

  • nowMediaUs:当前播放位置的媒体时间;
  • mediaTimeUs:当前音频帧的媒体时间;
  • nowUs:当前系统时间;

getPendingAudioPlayoutDurationUs 的主要目标就是通过 AudioTrack 获得当前已经播放到的位置的媒体时间。

最后调用 updateAnchor 更新 MediaClock 中的 anchor time,里面会更新三个值:

  • mAnchorTimeMediaUs:当前播放位置的媒体时间;
  • mAnchorTimeRealUs:当前媒体时间所对应的系统时间;
  • mPlaybackRate:当前播放速率;

并不是每次收到新的 audio timestamp 时都会更新 anchor time 的,
而是间隔 mNextAudioClockUpdateTimeUs 会更新一次。

2.4、postDrainVideoQueue

void NuPlayer::Renderer::postDrainVideoQueue() {
	// 当前正在处理 video output buffer、syncqueue、暂停 直接退出
    if (mDrainVideoQueuePending
            || getSyncQueues()
            || (mPaused && mVideoSampleReceived)) {
        return;
    }

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

    QueueEntry &entry = *mVideoQueue.begin();

    sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this);
    msg->setInt32("drainGeneration", getDrainGeneration(false /* audio */));
	// 收到 EOS 直接发送消息
    if (entry.mBuffer == NULL) {
        // EOS doesn't carry a timestamp.
        msg->post();
        mDrainVideoQueuePending = true;
        return;
    }

    int64_t nowUs = ALooper::GetNowUs();
    // 直播流的avsync
    if (mFlags & FLAG_REAL_TIME) {
        int64_t realTimeUs;
        CHECK(entry.mBuffer->meta()->findInt64("timeUs", &realTimeUs));

        realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;

        int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);

        int64_t delayUs = realTimeUs - nowUs;

        ALOGW_IF(delayUs > 500000, "unusually high delayUs: %lld", (long long)delayUs);
        // post 2 display refreshes before rendering is due
        msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);

        mDrainVideoQueuePending = true;
        return;
    }

    int64_t mediaTimeUs;
    CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));

    {
        Mutex::Autolock autoLock(mLock);
        // 如果 anchor time 小于0,则使用 video timestamp 更新 anchor time
        if (mAnchorTimeMediaUs < 0) {
            mMediaClock->updateAnchor(mediaTimeUs, nowUs, mediaTimeUs);
            mAnchorTimeMediaUs = mediaTimeUs;
        }
    }
    mNextVideoTimeMediaUs = mediaTimeUs;
    // 如果没有 audio 则用 video 来计算最大可播放时间
    if (!mHasAudio) {
        // smooth out videos >= 10fps
        mMediaClock->updateMaxTimeMedia(mediaTimeUs + kDefaultVideoFrameIntervalUs);
    }
	// 第一帧 video 到达 或者是 video pts 小于 audio 第一帧 pts,直接post
    if (!mVideoSampleReceived || mediaTimeUs < mAudioFirstAnchorTimeMediaUs) {
        msg->post();
    } else {
        int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);
		// 等到 2倍vsync时间前post消息
        // post 2 display refreshes before rendering is due
        mMediaClock->addTimer(msg, mediaTimeUs, -twoVsyncsUs);
    }

    mDrainVideoQueuePending = true;
}

video output buffer 的处理要复杂很多:

  1. 如果当前处在等待处理上一个output buffer的过程中,或者正在做syncqueue,又或者是已经暂停,直接返回不处理当前消息;
  2. 收到 EOS 直接将消息 post 出去;
  3. 如果 source 是 RTSPSource,也就是实时流,将以自身的 pts 作为参照计算什么时候渲染;
  4. 如果只有 video,那么会用 video 第一帧 timestamp 来更新 anchor time;
  5. 如果是普通流:
    • 4.1. 第一帧 video 到达 或者 video pts 小于 audio 第一帧的 pts,直接 post 消息;
    • 4.2. 其他情况需要用 MediaClock 计算消息发送时间,到达时间后 post 消息;

为什么要用 MediaClock 计算消息发送时间呢?这是因为要考虑到倍速播放,如果存在倍速,那么消息发送时间就不能简单用加减法得到了。

上面说 Renderer 是用 audio 来同步的,在什么地方可以验证呢?

使用 MediaClock 计算消息处理时间时会用到一个方法 getMediaTime_l,它是用来获取当前媒体播放时间的:

status_t MediaClock::getMediaTime_l(
        int64_t realUs, int64_t *outMediaUs, bool allowPastMaxTime) const {
    if (mAnchorTimeRealUs == -1) {
        return NO_INIT;
    }

    int64_t mediaUs = mAnchorTimeMediaUs
            + (realUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
    *outMediaUs = mediaUs;
    return OK;
}

当前媒体播放时间 = 上次记录的媒体播放时间 + 系统走过时间 * 倍速

mAnchorTimeMediaUs 和 mAnchorTimeRealUs 是用音频播放时间来更新的,所以 video output 的消息时间是根据音频时间算出来的,也就说明video 是同步于 audio 的。

如果想了解更详细的 MediaClock 内容,还请自行阅读。

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

    QueueEntry *entry = &*mVideoQueue.begin();
	// 通知 NuPlayer EOS
    if (entry->mBuffer == NULL) {
        // EOS
        notifyEOS(false /* audio */, entry->mFinalResult);
        mVideoQueue.erase(mVideoQueue.begin());
        entry = NULL;
        setVideoLateByUs(0);
        return;
    }

	// 获取render的系统时间
    int64_t nowUs = ALooper::GetNowUs();
    int64_t realTimeUs;
    int64_t mediaTimeUs = -1;
    if (mFlags & FLAG_REAL_TIME) {
        CHECK(entry->mBuffer->meta()->findInt64("timeUs", &realTimeUs));
    } else {
        CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
		// 计算 render 的系统时间
        realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
    }
    realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;

    bool tooLate = false;
	// 判断video是否晚到
    if (!mPaused) {
        setVideoLateByUs(nowUs - realTimeUs);
        tooLate = (mVideoLateByUs > 40000);
    } 
	// 总是渲染出第一帧
    // Always render the first video frame while keeping stats on A/V sync.
    if (!mVideoSampleReceived) {
        realTimeUs = nowUs;
        tooLate = false;
    }
	// 将消息发送给个 NuPlayer Decoder 处理
    entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000LL);
    entry->mNotifyConsumed->setInt32("render", !tooLate);
    entry->mNotifyConsumed->post();
    mVideoQueue.erase(mVideoQueue.begin());
    entry = NULL;

    mVideoSampleReceived = true;

    if (!mPaused) {
    	// 通知 NuPlayer video render 开始
        if (!mVideoRenderingStarted) {
            mVideoRenderingStarted = true;
            notifyVideoRenderingStart();
        }
        Mutex::Autolock autoLock(mLock);
        notifyIfMediaRenderingStarted_l();
    }
}
  1. 如果 buffer 为 NULL,通知 NuPlayer EOS;
  2. 获取或者计算当前 buffer 应当 render 的系统时间;
  3. 根据当前的系统时间判断 buffer 是否晚到;
  4. 发送消息通知 NuPlayer Decoder 做 render;

到这儿 NuPlayer Renderer 大致就了解结束,常用的 pause、resume、flush、getCurrentPosition、setPlaybackSettings、setSyncSettings 以及 offload mode 这里就不再做过多了解。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android 13 - Media框架(10)- NuPlayer::Renderer 的相关文章

  • 使用 Glide 库设置图像加载完成后进度条的可见性

    您好 我想要一个图像进度条 该进度条将在图像加载时显示 但当图像加载完成时 我想将其设置为消失 早些时候我为此使用了毕加索库 但我不知道如何将它与 Glide 库一起使用 我知道有一些资源就绪功能 但我不知道如何使用它 谁能帮我 毕加索图书
  • InAppMessage 一旦显示就会自动消失

    您好 我最近将 InAppMessaging 添加到我的项目中 这似乎很容易集成 但对我来说并没有按预期工作 首先 我将其添加到 build gradle 中 implementation com google firebase fireb
  • 确定视图是否在屏幕上 - Android

    我对这个有点困惑 首先也是最重要的是 以下链接很有用 但是我提出了一些可见性问题 链接 检查视图可见性 https stackoverflow com questions 4628800 android how to check if a
  • C 类型命名约定,_t 或 ALLCAPS

    我一直想知道是否有任何命名约定 例如何时对类型使用全部大写以及何时追加 t 什么时候不使用任何东西 我知道当时 K R 发布了各种有关如何使用 C 的文档 但我找不到任何相关内容 在 C 标准库类型中 t看起来漂亮占主导地位 time t
  • 检测 TextBox 中的 Tab 键按下

    I am trying to detect the Tab key press in a TextBox I know that the Tab key does not trigger the KeyDown KeyUp or the K
  • 无法获取本地或参数的值,因为它在此指令指针处不可用,可能是因为它已被优化掉

    Visual Studio 2010 会删除 没有其他词 不安全块中函数参数之一中的数据 什么可能导致此错误 调试器显示以下消息 Cannot obtain value of local or argument as it is not a
  • 如何增加ofstream的缓冲区大小

    我想增加 C 程序的缓冲区大小 以便它不会过于频繁地写入 默认缓冲区是 8192 字节 我尝试使用 pubsetbuf 将其增加到 200K 原始代码 ofstream fq fastq1 cstr ios out fastq1 is a
  • 是否可以在Linux上将C转换为asm而不链接libc?

    测试平台为Linux 32位 但也欢迎 Windows 32 位上的某些解决方案 这是一个c代码片段 int a 0 printf d n a 如果我使用 gcc 生成汇编代码 gcc S test c 然后我会得到 movl 0 28 e
  • 选择 asp.net CheckBoxList 中的所有项目

    ASP NET 和 C 我想要一个带有 全选 项目的复选框列表 当这个特定项目是 已选择 所有其他都将被选择 也 当选择被删除时 这个项目 也将来自所有人 其他物品 选中 取消选中 任何其他项目只会有一个 对特定项目的影响 无论选择状态如何
  • 获取当前图片在图库中显示的位置

    在我的应用程序中 我有一个图片库 但我想检测当前显示图像的位置 例如 当我启动我的活动时 位置是 0 但是当我在图库中滚动时 我想获取当前显示图像的位置 我尝试过 OnFocusChanged OnItemClicked 但只有当我单击图库
  • 从多个 TextView 中选择文本

    如何在android中从多个文本视图中选择文本 我已经尝试过以下代码 该代码一次仅适用于一个文本视图 我想一次性从许多文本视图中复制文本 android textIsSelectable true 你不能同时这样做 您需要在单个文本视图中设
  • 使用 Unity 在 C# 中发送 http 请求

    如何使用 Unity 在 C 中发送 HTTP GET 和 POST 请求 我想要的是 在post请求中发送json数据 我使用Unity序列化器 所以不需要 新的 我只想在发布数据中传递一个字符串并且能够 将 ContentType 设置
  • 时间:2019-03-17 标签:c#TimerStopConfusion

    我想通过单击按钮时更改文本颜色来将文本框文本设置为 闪烁 我可以让文本按照我想要的方式闪烁 但我希望它在闪烁几次后停止 我不知道如何在计时器触发几次后让它停止 这是我的代码 public Form1 InitializeComponent
  • 解释这段代码的工作原理;子进程如何返回值以及在哪里返回值?

    我不明白子进程如何返回该值以及返回给谁 输出为 6 7 问题来源 http www cs utexas edu mwalfish classes s11 cs372h hw sol1 html http www cs utexas edu
  • 使用 boost 异步发送和接收自定义数据包?

    我正在尝试使用 boost 异步发送和接收自定义数据包 根据我当前的实现 我有一些问题 tcpclient cpp include tcpclient h include
  • Android BLE 扫描永远找不到设备

    几天以来 我尝试在我的应用程序中实现 BLE 连接 我知道我尝试连接的设备功能齐全 因此问题一定是我的代码 我用BluetoothLeScanner startScan 方法 但回调方法永远不会被调用 public void startSc
  • firebase中按范围查询

    我有一个食品价格范围滑块 根据滑块的最小值和最大值 我想显示此范围内的食品 滑块代码 multiSlider setOnThumbValueChangeListener new MultiSlider SimpleChangeListene
  • 运行 xunit 测试时无法将输出打印到控制台窗口

    public class test2InAnotherProject private readonly ITestOutputHelper output public test2InAnotherProject ITestOutputHel
  • 画布:尝试使用回收的位图错误

    我是一个相当新的程序员 所以任何建议将不胜感激 我有一个类 每次调用它时都会在循环中运行 AsyncTask AsyncTask 看起来像这样 public class LoadImageTask extends AsyncTask
  • 在DialogFragment中,onCreate应该做什么?

    我目前正在摆弄 DialogFragment 以学习使用它 我假设相比onCreateView onCreate 可以这样做 public void onCreate Bundle savedInstanceState super onCr

随机推荐

  • Python爬虫副业真的可行吗?能赚多少?

    首先回答你 python爬虫能当副业 副业的方式比较多 等下我会讲几种 到哪个层次能接单 主要看你是接什么样的单 爬一些资料 视频这种简单的学一两个月就没什么问题 复杂的那就需要系统的学习 爬虫原理 html相关知识 urllib urll
  • python实现爬取微博相册所有图片

    微博相册的批量爬取 文章目录 前言 一 分析实现思路 二 编写代码 1 引入库 2 多进程的编写 3 主函数的编写 结果 前言 微博有相册功能 那么我们如何批量下载相册中的所有照片呢 提示 以下是本篇文章正文内容 下面案例可供参考 一 分析
  • python中统计单词出现的次数_python统计文本中每个单词出现的次数

    python统计文本中每个单词出现的次数 coding utf 8 author zcg import collections import os with open abc txt as file1 打开文本文件 str1 file1 r
  • myBatis入门程序介绍

    前言 Mybaits框架也称为对象关系映射 ORM 框架 它通过描述java对象与数据库表之间映射关系 自动的将java应用程序中的对象持久化到关系型数据库中 这些功能的完成主要依赖两种配置文件 主配置文件 通常叫做 mybaits con
  • 【云原生之Docker实战】使用docker部署mm-wiki文档系统

    云原生之Docker实战 使用docker部署mm wiki文档系统 一 mm wiki介绍 1 mm wiki简介 2 mm wiki特点 二 检查本地docker环境 1 检查本地docker版本 2 检查docker状态 3 检查do
  • 京东到家技术解密

    京东到家技术解密 京东到家相信很多人都听过 是一个短短两年内依靠技术与产品崛起的公司 其中项目的划分 技术的选型又是怎么样子呢 本课程目标用户群体是工作过几年的攻城狮 在项目中取得了一定的成就 但是缺少O2O 电商 新零售领域的工作经验 却
  • oracle存储过程----case条件控制语句的用法

    上一篇 oracle存储过程 赋值 相等 分支的写法 oracle存储过程 case条件控制语句的用法 今天又看到了另一种条件控制的方法case case语句可以分为两种类型 一种是简单的case语句 它给出一个表达式 并把表达式结果同提供
  • Vue + EChart之饼图详解

    Vue EChart之饼图详解 1 安装echarts项目依赖 npm install echarts save 或者 npm install echarts S 2 创建图表
  • 为什么需要三次握手?

    为什么需要三次握手 三次握手保证了数据能在收发双方之间传递接收 三次握手最主要的目的就是 双方确认 自己与对方的发送和接收是正常的 第一次握手 客户端什么也确定不了 服务端确认了对方发送正常 自己接收正常 第二次握手 客户端 确认了自己发送
  • 【20220815】新板子第一次调试的错误大多是硬件的疏忽

    1 看门狗没有按照外设芯片的要求接VDD 而是被硬件工程师NC处理了 2 拨码开关输入没有进行上拉处理 3 连接器P3没有GND端口 Layout没问题 主要是硬件工程师绘制原理图的时候的一些判断上的问题 当他不知道项目需要什么接线方法的时
  • Python3之面向对象

    一 面向对象技术简介 类 class 用来描述具有相同属性和方法的对象集合 定义了该集合中每个对象所共有的属性和方法 对象是类的实例 方法 即类中定义的函数 类变量 类变量即在类中声明的变量 其在整个实例化的对象中是公用的 类变量定义在类中
  • MVSNet (pytorch版) 搭建环境 运行dtu数据集重建 实操教程(图文并茂、超详细)

    文章目录 1 准备工作 1 1 下载源码 1 2 测试集下载 2 配置环境 3 dtu数据集 重建演示 3 1 重建效果查看 4 补充解释 4 1 bash 脚本文件超参数解释 4 2 lists dtu解释 5 Meshlab查看三维点云
  • angular基础5-8【指令&管道&组件通讯&组件生命周期】

    5 指令 Directive 指令是 Angular 提供的操作 DOM 的途径 指令分为属性指令和结构指令 属性指令 修改现有元素的外观或行为 使用 包裹 结构指令 增加 删除 DOM 节点以修改布局 使用 作为指令前缀 5 1 内置指令
  • 小程序用什么开发?

    近年来 随着智能手机的普及和移动互联网的发展 小程序成为了一种备受关注的新型应用 那么 小程序用什么开发呢 首先 小程序可以使用多种技术进行开发 其中比较流行的有两种方式 一种是借助微信开发者工具使用各种前端框架和语言进行开发 另一种是使用
  • Unity通过Addressable + ILRuntime 实现代码和资产的热更新(案例+图文详情+源码)

    Unity通过Addressable ILRuntime 实现代码和资产的热更新 图文详情 源码 前言 一 思路概述 二 Unity主工程部分 1 GameLunch的源码 2 ResMgr的源码 3 ILRuntimeWrapper的源码
  • spark报错:ROW FORMAT SERDE is incompatible with format ‘orc‘

    错误样例 CREATE TABLE wangriyu bugtt1 task no int exec ip int ROW FORMAT SERDE org apache hadoop hive ql io orc OrcSerde STO
  • python中list.copy方法用法浅谈

    本文主要给大家介绍了关于python中list copy方法使用的相关资料 文中还介绍了python list copy 和 copy deepcopy 区别 需要的朋友可以参考下 当我们想复制两个一模一样的列表时 我们可能使用到list
  • 【Xilinx AX7103 MicroBalze学习笔记6】MicroBlaze 自定义 IP 核封装实验

    目录 实验任务 实验框图 创建自定义 IP 封装 IP IP 封装界面配置 硬件设计 Vivado部分 Block Design搭建 添加 IP 库 约束文件 软件设计 SDK部分 往期系列博客 实验任务 本节介绍基于 MicroBlaze
  • Tomcat启动乱码完整解决

    1 问题归属 配置问题 2 问题解决步骤 tomcat conf 目录下的server xml配置 添加 URIEncoding UTF 8
  • Android 13 - Media框架(10)- NuPlayer::Renderer

    这一节我们来了解 NuPlayer Renderer 是如何工作 avsync 机制是如何运行的 1 创建 Renderer void NuPlayer onStart int64 t startPositionUs MediaPlayer