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

这一节我们来了解 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 modedetermineAudioModeChange(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变化要 dropif (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 中的内容重新封装到 QueueEntryQueueEntry entry;entry.mBuffer = buffer;entry.mNotifyConsumed = notifyConsumed;entry.mOffset = 0;entry.mFinalResult = OK;entry.mBufferOrdinal = ++mTotalBuffersQueued;// 发消息处理 Queue 中的 entryif (audio) {Mutex::Autolock autoLock(mLock);mAudioQueue.push_back(entry);postDrainAudioQueue_l();} else {mVideoQueue.push_back(entry);postDrainVideoQueue();}// SyncQueueMutex::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;}}// 如果暂停了就延时写入audioTrackmDrainAudioQueuePending = 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;// 检查 generationint32_t generation;CHECK(msg->findInt32("drainGeneration", &generation));if (generation != getDrainGeneration(true /* audio */)) {break;}// 写入 audiotrackif (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,另一种是 等于 nullif (entry->mNotifyConsumed != nullptr) {// TAG for re-open audio sink.onChangeAudioFormat(entry->mMeta, entry->mNotifyConsumed);mAudioQueue.erase(mAudioQueue.begin());continue;}// EOSif (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 bufferif (entry->mOffset == 0 && entry->mBuffer->size() > 0) {int64_t mediaTimeUs;CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));// 更新 AudioMediaTimeonNewAudioMediaTime(mediaTimeUs);}size_t copy = entry->mBuffer->size() - entry->mOffset;// 将数据写入到 AudioTrackssize_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 timestampif (mediaTimeUs == mAnchorTimeMediaUs) {return;}// 设置 MediaClock 的开始媒体时间;setAudioFirstAnchorTimeIfNeeded_l(mediaTimeUs);// mNextAudioClockUpdateTimeUs is -1 if we're waiting for audio sink to start// 等待 AudioTrack 启动,获取到第一帧ptsif (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();// 直播流的avsyncif (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 duemsg->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 timeif (mAnchorTimeMediaUs < 0) {mMediaClock->updateAnchor(mediaTimeUs, nowUs, mediaTimeUs);mAnchorTimeMediaUs = mediaTimeUs;}}mNextVideoTimeMediaUs = mediaTimeUs;// 如果没有 audio 则用 video 来计算最大可播放时间if (!mHasAudio) {// smooth out videos >= 10fpsmMediaClock->updateMaxTimeMedia(mediaTimeUs + kDefaultVideoFrameIntervalUs);}// 第一帧 video 到达 或者是 video pts 小于 audio 第一帧 pts,直接postif (!mVideoSampleReceived || mediaTimeUs < mAudioFirstAnchorTimeMediaUs) {msg->post();} else {int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);// 等到 2倍vsync时间前post消息// post 2 display refreshes before rendering is duemMediaClock->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 EOSif (entry->mBuffer == NULL) {// EOSnotifyEOS(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 这里就不再做过多了解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/67356.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Shotcut for Mac:一款强大而易于使用的视频编辑器

随着数码相机的普及&#xff0c;视频编辑已成为我们日常生活的一部分。对于许多专业和非专业用户来说&#xff0c;找到一个易于使用且功能强大的视频编辑器是至关重要的。今天&#xff0c;我们将向您介绍Shotcut——一款专为Mac用户设计的强大视频编辑器。 什么是Shotcut&…

恒运资本:沪指涨逾1%,金融、地产等板块走强,北向资金净买入超60亿元

4日早盘&#xff0c;两市股指盘中强势上扬&#xff0c;沪指、深成指涨超1%&#xff0c;上证50指数涨近2%&#xff1b;两市半日成交约5500亿元&#xff0c;北向资金大举流入&#xff0c;半日净买入超60亿元。 截至午间收盘&#xff0c;沪指涨1.12%报3168.38点&#xff0c;深成指…

164到网络安全面试大全(附答案)

最近有不少小伙伴跑来咨询&#xff1a; 想找网络安全工作&#xff0c;应该要怎么进行技术面试准备&#xff1f;工作不到 2 年&#xff0c;想跳槽看下机会&#xff0c;有没有相关的面试题呢&#xff1f; 为了更好地帮助大家高薪就业&#xff0c;今天就给大家分享两份网络安全工…

基于卡尔曼滤波的锂电池状态估计MATLAB仿真及程序

完整资源请查看主页置顶博客&#xff08;专享优惠&#xff09; 该项目的目的是估计锂电池的荷电状态(SOC)。主要完成了扩展卡尔曼滤波(EKF)的实验、参数辨识和仿真。 项目中包括锂电池模型建立、参数辨识与验证、SOC估计采用扩展卡尔曼滤波(EKF)&#xff0c;使用了两种方式实…

ensp基础命令大全(华为设备命令)

路漫漫其修远兮&#xff0c;吾将上下而求索 今天写一些曾经学习过的网络笔记&#xff0c;希望对您的学习有所帮助。 OSPF,BGP,IS-IS的命令笔记没有写上来&#xff0c;计划单独写&#xff0c;敬请期待&#xff0c;或者您可以在这个网站查查 &#xff1a; 万能查询网站 …

详解Spring Boot中@value的使用方式

如何在Spring Boot中使用value获得配置文件中的内容。 value的常见用法以及案例 Value 注解是 Spring 框架中强大且常用的注解之一&#xff0c;在 Spring 框架中具有以下几个重要的意义&#xff1a; 一、属性值注入&#xff1a; Value注解的主要目的是将属性值注入到 Sprin…

java八股文面试[多线程]——进程与线程的区别

定义 1、进程&#xff1a;进程是一个具有独立功能的程序关于某个数据集合的以此运行活动。 是系统进行资源分配和调度的独立单位&#xff0c;也是基本的执行单元。是一个动态的概念&#xff0c;是一个活动的实体。它不只是程序的代码&#xff0c;还包括当前的活动。 进程结构…

【面试题精讲】什么是websocket?如何与前端通信?

有的时候博客内容会有变动&#xff0c;首发博客是最新的&#xff0c;其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址 系列文章地址 什么是WebSocket&#xff1f; WebSocket是一种在Web应用程序中实现双向通信的协议。它允许在客户端和服务器之间建立持久…

SolVES4.1学习1——安装与使用教程

1、下载并安装 SolVES 4版本是QGIS插件&#xff0c;但实际使用过程中发现在最新版的QGIS安装该插件过程中&#xff0c;会报错或异常。因此需安装特定版本的软件。共需安装如下图软件及Java环境等。 根据官方文档安装好后&#xff0c;可以进行相关操作。 2、设置QGIS环境 QG…

代码随想录二刷day07

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、力扣454. 四数相加 II二、力扣383. 赎金信三、力扣15. 三数之和四、力扣18. 四数之和 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1…

IDEA报错:Plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘ not found

问题&#xff1a; 使用IDEA新建spring boot项目&#xff0c;报错如下&#xff1a; Plugin org.springframework.boot:spring-boot-maven-plugin: not found解决办法&#xff1a; 1.在本地maven仓库中找到spring-boot-maven-plugin的版本号 2.在pom.xml文件中添加对应的版本…

449. 序列化和反序列化二叉搜索树

诸神缄默不语-个人CSDN博文目录 力扣刷题笔记 Python3版代码提示&#xff1a; # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val x # self.left None # self.right None# Your Codec object will…

Java项目中jar war pom包的区别

1、pom&#xff1a;用在父级工程或聚合工程中&#xff0c;用来做jar包的版本控制&#xff0c;必须指明这个聚合工程的打包方式为pom。 <project ...> <modelVersion>4.0.0</modelVersion> <groupId>com.wong.tech</groupId> <artifactI…

主成分分析笔记

主成分分析是指在尽量减少失真的前提下&#xff0c;将高维数据压缩成低微的方式。 减少失真是指最大化压缩后数据的方差。 记 P P P矩阵为 n m n\times m nm&#xff08; n n n行 m m m列&#xff09;的矩阵&#xff0c;表示一共有 m m m组数据&#xff0c;每组数据有 n n n…

Redis 缓存预热+缓存雪崩+缓存击穿+缓存穿透

面试题&#xff1a; 缓存预热、雪萌、穿透、击穿分别是什么&#xff1f;你遇到过那几个情况&#xff1f;缓存预热你是怎么做的&#xff1f;如何造免或者减少缓存雪崩&#xff1f;穿透和击穿有什么区别&#xff1f;他两是一个意思还是载然不同&#xff1f;穿适和击穿你有什么解…

【数据结构-队列】队列介绍

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…

OPPO手机便签数据搬家到华为mate60Pro手机怎么操作

今年8月底&#xff0c;华为上线了本年度的旗舰手机——华为mate60Pro。有不少网友都在抢购这台手机&#xff0c;不过在拿到新手机之后&#xff0c;还有一件重要的事情要做&#xff0c;这就是把旧手机中比较重要的数据&#xff0c;例如图片、短信、通讯录、联系人、便签等数据搬…

网络安全—0基础入门学习手册

前言 一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防…

Android Studio中创建java项目

1.创建普通的android工程 2.创建一个module 3.module类型选择java library 4.填写libary和class的名字 5.生成的工程如图所示 6.然后点击Run --- Edit Configurations... 选择Application选项 设置所需要的参数 选中myjavalib后点击OK。然后打开刚创建的lib的gradle 编辑gradl…

单片机简介

目录 1、单片机 2、CISC和RISC 3、 冯诺依曼结构和哈佛结构​编辑 1、单片机 单片机:Single-Chip Microcomputer,单片微型计算机,是一种集成电路芯片 ------------------------------------------------------------ 电脑&#xff1a; <--------> …