Android 13 - Media框架(9)- NuPlayer::Decoder

这一节我们将了解 NuPlayer::Decoder,学习如何将 MediaCodec wrap 成一个强大的 Decoder。这一节会提前讲到 MediaCodec 相关的内容,如果看不大懂可以先跳过此篇。原先觉得 Decoder 部分简单,越读越发现自己的无知,Android 源码真是一个巨大的宝库!
ps:本文中大写的 Decoder 指代的是 NuPlayer::Decoder,小写的 decoder指代 mediacodec 以及底层的真正的解码器。

1、DecoderBase

首先看 NuPlayer::Decoder 的基类 DecoderBase

struct NuPlayer::DecoderBase : public AHandler {explicit DecoderBase(const sp<AMessage> &notify);void configure(const sp<AMessage> &format);void init();void setParameters(const sp<AMessage> &params);// Synchronous call to ensure decoder will not request or send out data.void pause();void setRenderer(const sp<Renderer> &renderer);virtual status_t setVideoSurface(const sp<Surface> &) { return INVALID_OPERATION; }void signalFlush();void signalResume(bool notifyComplete);void initiateShutdown();virtual sp<AMessage> getStats() {return mStats;}
protected:virtual void onMessageReceived(const sp<AMessage> &msg);virtual void onConfigure(const sp<AMessage> &format) = 0;virtual void onSetParameters(const sp<AMessage> &params) = 0;virtual void onSetRenderer(const sp<Renderer> &renderer) = 0;virtual void onResume(bool notifyComplete) = 0;virtual void onFlush() = 0;virtual void onShutdown(bool notifyComplete) = 0;void onRequestInputBuffers();virtual bool doRequestBuffers() = 0;
}

DecoderBase 定义了 NuPlayer 可以调用 Decoder 的所有接口,可以看到接口数量相当稀少,并没有 start、stop、reset、seek 等等方法,这时候可能就会有人有疑问了,我上层调用的这些接口为什么底层却没有了呢?其实在之前的文章中我们已经对部分接口做了解释,这里就不再赘述。

来了解下这些接口都是用来干什么、怎么用的:

  • 构造函数:传入一个 AMessage 对象,用于上抛事件于状态;
  • configure:传入 Source 在 prepare 过程中 parse 出的 format 信息,format 信息包括 mime type、surface、secure、width、height、crypto、csd 等等信息;创建 MediaCodec 实例,配置并启动;
  • init:将自身注册到 ALooper 当中;
  • setParameters:给 decoder 设定上层传下的参数;
  • pause:这个方法其实并没有用,后面详细了解它为什么没有用;
  • setRenderer:设定 render,decoder 解出的数据将送到 render 中做 avsync,如果是 audio 数据将直接写入到 AudioTrack;
  • setVideoSurface:重新设定 surface,audio decoder 并不需要这个方法;
  • signalFlush:flush,刷新 decoder 的 input/ouput 缓冲区;
  • signalResume:恢复 decoder 的解码流程;
  • initiateShutdown:停止解码流程并释放相关资源;
  • getStats:获取当前 Decoder 的状态,例如 format 信息,当前解出的帧数,丢弃的帧数等等信息。
  • 其他:onConfigure 等等方法将由具体的 Decoder 来实现,如果 audio 不走 offload, audio / video decoder 会走相同的流程。

2、Decoder 创建与启动

从 NuPlayer 源码中我们可以知道,调用 start 方法后会创建 Decoder,这里的 Decoder 是继承于 DecoderBase 的,接着调用 Decoder.init 和 Decoder.configure,这里Decoder 就完成启动了:

status_t NuPlayer::instantiateDecoder(bool audio, sp<DecoderBase> *decoder, bool checkAudioModeChange) {sp<AMessage> format = mSource->getFormat(audio);*decoder = new Decoder(notify, mSource, mPID, mUID, mRenderer, mSurface, mCCDecoder);(*decoder)->init();(*decoder)->configure(format);
}

init 方法很简单,就做了把 AHandler 注册到 ALooper 中这一件事。在这里我要抛出2个问题, registerHandler 注册的 this 指代的是谁?

void NuPlayer::DecoderBase::init() {mDecoderLooper->registerHandler(this);
}

后面 onRequestInputBuffers 中这个消息会先经 Decoder::onMessageReceived 处理还是 先经 DecoderBase::onMessageReceived 来处理呢?如果不太确定答案是什么可以搜索多态。

void NuPlayer::DecoderBase::onRequestInputBuffers() {....sp<AMessage> msg = new AMessage(kWhatRequestInputBuffers, this);msg->post(10 * 1000LL);
}

继续往下看,configure 最终会调用到 onConfigure 方法中:

void NuPlayer::Decoder::onConfigure(const sp<AMessage> &format) {++mBufferGeneration;AString mime;CHECK(format->findString("mime", &mime));mIsAudio = !strncasecmp("audio/", mime.c_str(), 6);mComponentName = mime;mComponentName.append(" decoder");mCodec = MediaCodec::CreateByType(mCodecLooper, mime.c_str(), false /* encoder */, NULL /* err */, mPid, mUid, format);int32_t secure = 0;if (format->findInt32("secure", &secure) && secure != 0) {if (mCodec != NULL) {mCodec->getName(&mComponentName);mComponentName.append(".secure");mCodec->release();mCodec = MediaCodec::CreateByComponentName(mCodecLooper, mComponentName.c_str(), NULL /* err */, mPid, mUid);}}err = mCodec->configure(format, mSurface, crypto, 0 /* flags */);rememberCodecSpecificData(format);sp<AMessage> reply = new AMessage(kWhatCodecNotify, this);mCodec->setCallback(reply);err = mCodec->start();
}
  1. 查找 format 中的 mime,必须要有这个;
  2. 调用 CreateByType 创建 MediaCodec 实例,如果 format 中带有 secure 字段,那么就调用 CreateByComponentName 创建 Secure Component;
  3. 调用 configure 配置 MediaCodec,需要传入码流的 format,format 中需要有什么我们后面再看,这里传入的 surface 不为 NULL,因为 NuPlayer 中有判断,如果 surface 为 NULL 就不会为 Video 创建 Codec;
  4. 存储 format 中的 codec specific data(csd buffer),这些 buffer 记录了码流的信息,例如 h264,h265码流中的sps pps 等信息,对于有些 decoder 一定需要传入该信息,而有些 decoder 可以自己从码流中 parse 出来这些信息,具体要看各家的 decoder 实现;
  5. 给 MediaCodec 注册 callback,让它以异步的方式工作;
  6. 调用 start 方法启动 decoder,开启整个数据读取、数据解码 以及 数据渲染流程。

以下内容是个人拙见,讲的比较啰嗦,如不喜欢直接跳过就好。

除了这些,我们还要看一个成员 mBufferGeneration,这个东西是干什么的呢?其实我们之前已经说过 Media 这边用了跟多 generation 的思想或者说是trick,那这里的 generation 是用来干什么的呢?

我们搜索代码中的 mBufferGeneration,发现它的值会在 onConfiguredoFlushonShutdownhandleError 中做修改,这四个方法会有一个共同点,它们都会去操作 MediaCodec,改变 MediaCodec 状态,从而影响到 MediaCodec Buffer 的状态。

我们再看 mBufferGeneration 会在哪里使用:

bool NuPlayer::Decoder::isStaleReply(const sp<AMessage> &msg) {int32_t generation;CHECK(msg->findInt32("generation", &generation));return generation != mBufferGeneration;
}void NuPlayer::Decoder::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatRenderBuffer:{if (!isStaleReply(msg)) {onRenderBuffer(msg);}break;}}
}

video output buffer 在送入 Renderer 做完 avsync 后送回来做渲染时会判断当前的 mBufferGeneration 是否有发生变化,这里这么做有什么用意呢?

我的理解是这样:output buffer 送到 Renderer 中处理完成后,Renderer 会调用渲染相关的方法,但是这个时候 buffer 的状态可能已经发生了变化,例如做了 flush、或者是 shutdown,buffer 不需要再被处理,用 mBufferGeneration 来判断就可以跳过处理步骤。

将一件事情交由其他组件处理时,记录当前的generation ,当事件处理结束并=返回到当前组件时,根据当前的 generation 决定内容是否需要丢弃。Android 在 ACodec、NuPlayer::Source、Renderer 等实现中都用了 generation 技巧来处理状态转换时的事务。

以上是我看第一遍代码时对 generation 的理解,再次翻阅又有了些新的感悟:

NuPlayer 中大量使用了 ALooper、AHandler 异步消息机制,这里的异步是相对与调用者而言的,譬如 NuPlayer 调用 Decoder 的 configure 方法,NuPlayer 调用完就结束了,这时候 Decoder 内的 MediaCodec 对象可能还没有创建,这就是异步。但是对于 Decoder 来说,所有的调用(发送来的消息)都是由 Looper 中的线程一条一条处理,所以 Decoder 内部是同步处理的。

为什么要说这些呢?我们来看看都有谁会给 Decoder 发送消息:NuPlayer、Renderer、MediaCodec 它们都可能同时,或者先后向 Decoder 发送消息,这会引发什么问题呢?以 Renderer 为例子:

    sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);reply->setSize("buffer-ix", index);reply->setInt32("generation", mBufferGeneration);reply->setSize("size", size);

Decoder 收到 MediaCodec 发送的 CB_OUTPUT_AVAILABLE 事件后,会将 mBufferGeneration 存到 msg 中,并且传递给 Renderer,Renderer 做完同步后会将消息重新发送到 Decoder。但是如果同步的过程中,上层调用了 reset,Decoder 也对事件做了处理,那么 Decoder 将不能再去处理 Renderer 发过来的渲染消息(整个流程已经停止,component 已经被释放了)。

请添加图片描述

generation 起着状态记录的作用,当状态发生改变后,依赖该状态的消息将会不再处理。它和一些具体的状态,例如 MediaPlayer.cpp 中的状态使用方法类似,但是 generation 的使用更为简单,它不关注具体是什么状态,只关注影响改变 Decoder 状态的方法是否被调用。

3、Start

我们引用上一小节中关于 start 的描述: 开启整个数据读取、数据解码 以及 数据渲染流程,start 不仅仅是启动了 MediaCodec,还驱动了所有组件的运行,一起来看看吧。

先来看关键的 MediaCodec Callback:

void NuPlayer::Decoder::onMessageReceived(const sp<AMessage> &msg) {switch (msg->what()) {case kWhatCodecNotify:{int32_t cbID;CHECK(msg->findInt32("callbackID", &cbID));switch (cbID) {case MediaCodec::CB_INPUT_AVAILABLE:{int32_t index;CHECK(msg->findInt32("index", &index));handleAnInputBuffer(index);break;}case MediaCodec::CB_OUTPUT_AVAILABLE:{int32_t index;size_t offset;size_t size;int64_t timeUs;int32_t flags;CHECK(msg->findInt32("index", &index));CHECK(msg->findSize("offset", &offset));CHECK(msg->findSize("size", &size));CHECK(msg->findInt64("timeUs", &timeUs));CHECK(msg->findInt32("flags", &flags));handleAnOutputBuffer(index, offset, size, timeUs, flags);break;}}}}
}

MediaCodec 通过 kWhatCodecNotify 将消息发送到 Decoder 来处理,用 callbackID 来区分发送过来的内容,常用的有 CB_INPUT_AVAILABLECB_OUTPUT_AVAILABLECB_ERRORCB_OUTPUT_FORMAT_CHANGED 这四个,我们这里只看 input 和 output。

3.1、CB_INPUT_AVAILABLE

收到 MediaCodec 上抛的 input 事件后,会调用 handleAnInputBuffer 方法,传入参数为 input buffer id。

bool NuPlayer::Decoder::handleAnInputBuffer(size_t index) {// 判断是否处在 处理不连续码流 的状态if (isDiscontinuityPending()) {return false;}sp<MediaCodecBuffer> buffer;mCodec->getInputBuffer(index, &buffer);if (index >= mInputBuffers.size()) {for (size_t i = mInputBuffers.size(); i <= index; ++i) {mInputBuffers.add();mInputBufferIsDequeued.add();mMediaBuffers.editItemAt(i) = NULL;mInputBufferIsDequeued.editItemAt(i) = false;}}mInputBuffers.editItemAt(index) = buffer;mInputBufferIsDequeued.editItemAt(index) = true;// 如果有码流不连续的情况,恢复播放后重新发送csd bufferif (!mCSDsToSubmit.isEmpty()) {sp<AMessage> msg = new AMessage();msg->setSize("buffer-ix", index);sp<ABuffer> buffer = mCSDsToSubmit.itemAt(0);msg->setBuffer("buffer", buffer);mCSDsToSubmit.removeAt(0);if (!onInputBufferFetched(msg)) {handleError(UNKNOWN_ERROR);return false;}return true;}// 如果有 buffer 没有成功写入 mediacodec 的情况,尝试重新写入while (!mPendingInputMessages.empty()) {sp<AMessage> msg = *mPendingInputMessages.begin();if (!onInputBufferFetched(msg)) {break;}mPendingInputMessages.erase(mPendingInputMessages.begin());}// 如果在 尝试重新写入的过程中,把当前 buffer 也顺带处理了,那么就直接返回if (!mInputBufferIsDequeued.editItemAt(index)) {return true;}// 将 buffer 记录到 mDequeuedInputBuffers 中mDequeuedInputBuffers.push_back(index);// 尝试从 source 获取数据,填充数据,并送回 decoderonRequestInputBuffers();return true;
}

由于这里涉及到 Source 数据的获取 和 input buffer 写入两部分内容,同时考虑了数据获取失败和数据写入失败的问题,所以 input buffer 的处理流程看起来会比较复杂,不过我们不要着急,我们一步步解析。

首先来看 handleAnInputBuffer 做的事情(省略了部分注释的内容):

  1. 判断当前是否正在处理码流不连续的情况;
  2. 从 MediaCodec 获取对应索引的 MediaCodecBuffer
  3. 将获取的到的 MediaCodecBuffer 按照索引记录到 mInputBuffers 列表当中;
  4. 创建一个列表 mInputBufferIsDequeued,记录索引对应的 input buffer 是否有出队列;
  5. 每次进行了 flush,需要送 csd buffer 给 decoder,记住是每次!其实不是所有的 decoder flush 之后都要 csd buffer 的,之前被坑过!
  6. 先处理被延迟的 input buffer,为什么延迟后面再说;
  7. 处理延迟 buffer 时可能会把当前的 input buffer 处理掉,如果记录出队列的列表中对应位置为 false 说明已经被处理过了;
  8. 如果 input buffer 没有处理,那么再把它加入到一个没有处理的列表当中 mDequeuedInputBuffers
  9. 调用 onRequestInputBuffers 从 Source 读取数据;

这里涉及四个方法,名字长得比较像,先来介绍下它们是做什么用的:

  • onRequestInputBuffers:从 Source 请求 input data;
  • doRequestBuffers:onRequestInputBuffers 的 内部实现;
  • onInputBufferFetched:成功从Source 获取到数据,填充到 input buffer 并返回给 MediaCodec;
  • fetchInputData:onInputBufferFetched 的内部实现;

我们从 onRequestInputBuffers 看起,这个方法实现在 DecoderBase 中,权限为 protected:

void NuPlayer::DecoderBase::onRequestInputBuffers() {// 判断是否处在 处理不连续码流 的状态if (mRequestInputBuffersPending) {return;}// doRequestBuffers() return true if we should request more data// 从 Source 请求数据,如果失败返回 true,发送一条延时消息,retryif (doRequestBuffers()) {// retry 时不会继续处理 获取数据的调用mRequestInputBuffersPending = true;sp<AMessage> msg = new AMessage(kWhatRequestInputBuffers, this);msg->post(10 * 1000LL);}
}

可以看到 onRequestInputBuffers 就是封装了 doRequestBuffers,所以它们的作用是相同的,但是一个是基类的方法,一个是子类的方法。这么设计有什么用呢?我的理解是这样:每个 Decoder 都需要从 Source 获取数据,所以把获取数据的方法 onRequestInputBuffers 定义在基类当中,但是每个 Decoder 获取数据的方式或者流程又不一样,所以把 doRequestBuffers 放到子类中实现。子类调用父类的 onRequestInputBuffers 方法使用父类定义的数据读取流程,流程中调用子类的数据读取实现,这样就一举两得,既统一了读取流程又区分了读取方式。

另外这里还有 mRequestInputBuffersPending 的用法值得学习,如果从 Source 获取数据失败了,那么需要做延时等待,并且重新尝试获取,等待的过程中我们并不希望外部能够再调用到 doRequestBuffers 获取数据,所以将 mRequestInputBuffersPending 置为 true,表示等待的状态,这个状态只有处理 retry 消息时才能够解除。

bool NuPlayer::Decoder::doRequestBuffers() {if (isDiscontinuityPending()) {return false;}status_t err = OK;while (err == OK && !mDequeuedInputBuffers.empty()) {size_t bufferIx = *mDequeuedInputBuffers.begin();sp<AMessage> msg = new AMessage();msg->setSize("buffer-ix", bufferIx);err = fetchInputData(msg);if (err != OK && err != ERROR_END_OF_STREAM) {// if EOS, need to queue EOS bufferbreak;}mDequeuedInputBuffers.erase(mDequeuedInputBuffers.begin());if (!mPendingInputMessages.empty()|| !onInputBufferFetched(msg)) {mPendingInputMessages.push_back(msg);}}return err == -EWOULDBLOCK&& mSource->feedMoreTSData() == OK;
}

doRequestBuffers 里有个循环,会把当前 mDequeuedInputBuffers 中的所有 input buffer 都处理掉。这里有个问题,什么时候 mDequeuedInputBuffers 中 buffer 的数量会大于 1 呢?从 source 读取数据失败时会直接返回,没能调用 erase 方法,这时候 mDequeuedInputBuffers 的数量会大于1。

status_t NuPlayer::Decoder::fetchInputData(sp<AMessage> &reply) {sp<ABuffer> accessUnit;bool dropAccessUnit = true;do {// 从 Source 获取数据status_t err = mSource->dequeueAccessUnit(mIsAudio, &accessUnit);// 判断返回值,如果是 EWOULDBLOCK 那么说明没读到数据,如果是其他的返回值则说名读到数据if (err == -EWOULDBLOCK) {return err;} else if (err != OK) {// 如果 error 不等于 OK,说明码流出现了一些情况if (err == INFO_DISCONTINUITY) {int32_t type;// 获取码流不连续的原因CHECK(accessUnit->meta()->findInt32("discontinuity", &type));bool formatChange =(mIsAudio &&(type & ATSParser::DISCONTINUITY_AUDIO_FORMAT))|| (!mIsAudio &&(type & ATSParser::DISCONTINUITY_VIDEO_FORMAT));bool timeChange = (type & ATSParser::DISCONTINUITY_TIME) != 0;ALOGI("%s discontinuity (format=%d, time=%d)",mIsAudio ? "audio" : "video", formatChange, timeChange);bool seamlessFormatChange = false;sp<AMessage> newFormat = mSource->getFormat(mIsAudio);// 如果是格式变化if (formatChange) {// 判断当前播放的码流格式是否支持无缝切换seamlessFormatChange =supportsSeamlessFormatChange(newFormat);// treat seamless format change separatelyformatChange = !seamlessFormatChange;}// For format or time change, return EOS to queue EOS input,// then wait for EOS on output.// 如果不支持无缝切换,那么就要向 decoder 填充 eosif (formatChange /* not seamless */) {mFormatChangePending = true;err = ERROR_END_OF_STREAM;} else if (timeChange) {// 如果pts不连续,那么就要向 decoder 填充 eos,恢复播放后要 发送 csd bufferrememberCodecSpecificData(newFormat);mTimeChangePending = true;err = ERROR_END_OF_STREAM;} else if (seamlessFormatChange) {// reuse existing decoder and don't flush// 如果是无缝切换,那么仍要发送 csd bufferrememberCodecSpecificData(newFormat);continue;} else {// This stream is unaffected by the discontinuityreturn -EWOULDBLOCK;}}// reply should only be returned without a buffer set// when there is an error (including EOS)CHECK(err != OK);reply->setInt32("err", err);return ERROR_END_OF_STREAM;}// 以下是 drop 机制dropAccessUnit = false;if (!mIsAudio && !mIsEncrypted) {// 如果视频流慢了 100ms,视频为avc,并且不是参考帧,那么就drop掉当前读取的内容if (mRenderer->getVideoLateByUs() > 100000LL&& mIsVideoAVC&& !IsAVCReferenceFrame(accessUnit)) {dropAccessUnit = true;} if (dropAccessUnit) {++mNumInputFramesDropped;}}} while (dropAccessUnit);reply->setBuffer("buffer", accessUnit);return OK;
}

fetchInputData 不仅仅是获取了数据,还对码流的异常情况做了处理。dequeueAccessUnit 有 四种返回值:

  • OK:获取到有效数据;
  • -EWOULDBLOCK:未能读取到数据;
  • INFO_DISCONTINUITY:码流不连续;
  • ERROR_END_OF_STREAM:读到文件末尾;

返回值为 OK 和 ERROR_END_OF_STREAM 属于正常情况;-EWOULDBLOCK 会直接返回并尝试 retry;INFO_DISCONTINUITY 说明码流出现了不连续的情况,可能是调用了 selectTrack 或者是 seek。

码流不连续分为两种情况:

  • 码流的格式发生变化,error 为 DISCONTINUITY_VIDEO_FORMAT,格式变化分别宽高变化和mime type变化两种,可能出现在 selectTrack 调用之后;
  • 码流的pts不连续,error 为 DISCONTINUITY_TIME,可能出现在 seek 调用后,或者是码流播放结束 pts 回跳时。

一是码流的格式发生变化 DISCONTINUITY_VIDEO_FORMAT引发的 flush,可能出现在 selectTrack 时;另外一种是码流 pts 回绕 DISCONTINUITY_TIME,这种情况出现在直播的回播中比较多。

如果是格式发生变化,那么会判断当前播放的码流是否支持 无缝切换(adaptive-playback),如果支持则不对该事件做处理,如果不支持把返回值设置为 ERROR_END_OF_STREAM。

如果是 pts不连续 则会直接将返回值设置为 ERROR_END_OF_STREAM。由于设置了 ERROR_END_OF_STREAM,那么重新开始播放之后需要先填充 csd buffer。

fetchInputData 还为 AVC 格式的码流设计了一套 drop 机制,如果视频流慢于音频100ms,并且当前帧不是参考帧,那么就 drop 掉该帧。

fetchInputData 调用成功后就该调用 onInputBufferFetched,把获取到的数据填充到 input buffer 中并且送回到 MediaCodec,这里比较简单,就是做了数据拷贝而已,要看的只有 EOS 一点。EOS 有两种情况,一种是 buffer 为空,说明当前已经收到 ERROR_END_OF_STREAM;另一种是 buffer 不为空,返回值为 OK,但是 bufferMeta 中有 eos 信息。

如果是码流结束,eos 信息送出后 fetchInputData 将不会读到任何数据。

如果是因为码流不连续发送了 eos,input buffer 处理流程将会被 isDiscontinuityPending 中断,等到前面的数据都解码渲染完成,再处理 Discontinuity 事件,处理完成后才会写入下一个序列的数据,这部分我们放到下一小节来看。

bool NuPlayer::Decoder::isDiscontinuityPending() const {return mFormatChangePending || mTimeChangePending;
}

3.2、CB_OUTPUT_AVAILABLE

output buffer 的处理流程相对 input 来说会简单很多,主要是调用了 handleAnOutputBuffer 方法:

bool NuPlayer::Decoder::handleAnOutputBuffer(size_t index,size_t offset,size_t size,int64_t timeUs,int32_t flags) {sp<MediaCodecBuffer> buffer;// 获取 output buffermCodec->getOutputBuffer(index, &buffer);int64_t frameIndex;bool frameIndexFound = buffer->meta()->findInt64("frameIndex", &frameIndex);buffer->setRange(offset, size);// 设置 ptsbuffer->meta()->clear();buffer->meta()->setInt64("timeUs", timeUs);if (frameIndexFound) {buffer->meta()->setInt64("frameIndex", frameIndex);}// 判断 output buffer 是否到达 eosbool eos = flags & MediaCodec::BUFFER_FLAG_EOS;// we do not expect CODECCONFIG or SYNCFRAME for decoder// 创建 reply,设置 generation,avsync完成后 renderer 通过该消息 callback 回来sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);reply->setSize("buffer-ix", index);reply->setInt32("generation", mBufferGeneration);reply->setSize("size", size);// 如果出现 eos 则在 reply 中也进行标记if (eos) {ALOGV("[%s] saw output EOS", mIsAudio ? "audio" : "video");buffer->meta()->setInt32("eos", true);reply->setInt32("eos", true);}mNumFramesTotal += !mIsAudio;// 判断 input buffer 有没有设定起播时间if (mSkipRenderingUntilMediaTimeUs >= 0) {if (timeUs < mSkipRenderingUntilMediaTimeUs) {ALOGV("[%s] dropping buffer at time %lld as requested.",mComponentName.c_str(), (long long)timeUs);reply->post();if (eos) {notifyResumeCompleteIfNecessary();if (mRenderer != NULL && !isDiscontinuityPending()) {mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);}}return true;}mSkipRenderingUntilMediaTimeUs = -1;}// wait until 1st frame comes out to signal resume complete// 播放停止后重新恢复播放,等待第一帧到达后上抛消息,在seek时用到notifyResumeCompleteIfNecessary();if (mRenderer != NULL) {// send the buffer to renderer.// 将 ouput buffer 送到 renderer 做 avsyncmRenderer->queueBuffer(mIsAudio, buffer, reply);// 如果到达 eos,并且不是因为码流中断,调用queueEOSif (eos && !isDiscontinuityPending()) {mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);}}return true;
}
  1. 获取 output buffer;
  2. 创建 reply,设置 generation,avsync 完成后 Renderer 通过该消息 callback 到 Decoder;
  3. 判断 output buffer flag 是否是 eos,如果是则在 reply 中进行标记;
  4. queue input buffer 时可能设有开始渲染的 pts,output buffer pts 小于该 pts 时直接 drop;
  5. 将 output buffer 和 reply message 一起送到 Renderer,如果到达 eos,且不是因为码流不连续,还要给 Renderer 送一个 EOS;

3.3、kWhatRenderBuffer

上一节我们讲到 Renderer 做完 avsync 后会以消息的形式 callback 给 Decoder:

        case kWhatRenderBuffer:{if (!isStaleReply(msg)) {onRenderBuffer(msg);}break;}

isStaleReply 我们在上面已经做过解释了,这里不再赘述,主要来看 onRenderBuffer:

void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) {status_t err;int32_t render;size_t bufferIx;int32_t eos;size_t size;// 查找要渲染的output buffer indexCHECK(msg->findSize("buffer-ix", &bufferIx));if (mCodec == NULL) {err = NO_INIT;} else if (msg->findInt32("render", &render) && render) { // 判断是否renderint64_t timestampNs;CHECK(msg->findInt64("timestampNs", &timestampNs));	// 获取render时间err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);} else {// 如果是 eos 或者 不render 则直接 dropif (!msg->findInt32("eos", &eos) || !eos ||!msg->findSize("size", &size) || size) {mNumOutputFramesDropped += !mIsAudio;}err = mCodec->releaseOutputBuffer(bufferIx);}// 如果是因为码流不连续造成的eos,则处理不连续事件if (msg->findInt32("eos", &eos) && eos&& isDiscontinuityPending()) {finishHandleDiscontinuity(true /* flushOnTimeChange */);}
}

onRenderBuffer 主要是用来处理 Video 的,Renderer 确定该帧要渲染,那么就调用 renderOutputBufferAndRelease,否则调用 releaseOutputBuffer。

如果 reply message 中包含有 eos,那么会判断是否因为码流不连续而造成的 eos。我们要注意的是,Renderer 真正执行到 EOS 时,事件并不会发送到 Decoder中,Decoder 只处理 buffer 事件。

这里我们回过头来看 finishHandleDiscontinuity 是如何处理码流异常的:

void NuPlayer::Decoder::finishHandleDiscontinuity(bool flushOnTimeChange) {ALOGV("finishHandleDiscontinuity: format %d, time %d, flush %d",mFormatChangePending, mTimeChangePending, flushOnTimeChange);// If we have format change, pause and wait to be killed;// If we have time change only, flush and restart fetching.if (mFormatChangePending) {mPaused = true;} else if (mTimeChangePending) {if (flushOnTimeChange) {doFlush(false /* notifyComplete */);signalResume(false /* notifyComplete */);}}// Notify NuPlayer to either shutdown decoder, or rescan sourcessp<AMessage> msg = mNotify->dup();msg->setInt32("what", kWhatInputDiscontinuity);msg->setInt32("formatChange", mFormatChangePending);msg->post();mFormatChangePending = false;mTimeChangePending = false;
}

从注释中我们可以看到针对 format change 和 time change 处理方式是不一样的:

  • format change:暂停 buffer 处理流程,等待重启 decoder;
  • time change:flush 然后调用 resume 恢复;

format change 中提到一个暂停,将 mPaused 置为 true,onMessageReceived 将不会再处理送来的 buffer,要注意的是,这个 pause 并不是用于播放暂停。format change 的事件要送到 NuPlayer 中:

            if (what == DecoderBase::kWhatInputDiscontinuity) {int32_t formatChange;CHECK(msg->findInt32("formatChange", &formatChange));ALOGV("%s discontinuity: formatChange %d",audio ? "audio" : "video", formatChange);if (formatChange) {mDeferredActions.push_back(new FlushDecoderAction(audio ? FLUSH_CMD_SHUTDOWN : FLUSH_CMD_NONE,audio ? FLUSH_CMD_NONE : FLUSH_CMD_SHUTDOWN));}mDeferredActions.push_back(new SimpleAction(&NuPlayer::performScanSources));processDeferredActions();} 

NuPlayer 会执行 FlushDecoderAction,并且进行 shutdown 释放当前 decoder,然后再重新调用 performScanSources 为新的 format 创建 decoder。

4、signalFlush

void NuPlayer::Decoder::doFlush(bool notifyComplete) {if (mCCDecoder != NULL) {mCCDecoder->flush();}if (mRenderer != NULL) {mRenderer->flush(mIsAudio, notifyComplete);mRenderer->signalTimeDiscontinuity();}status_t err = OK;if (mCodec != NULL) {err = mCodec->flush();mCSDsToSubmit = mCSDsForCurrentFormat; // copy operator++mBufferGeneration;}if (err != OK) {ALOGE("failed to flush [%s] (err=%d)", mComponentName.c_str(), err);handleError(err);// finish with posting kWhatFlushCompleted.// we attempt to release the buffers even if flush fails.}releaseAndResetMediaBuffers();mPaused = true;
}void NuPlayer::Decoder::onFlush() {doFlush(true);if (isDiscontinuityPending()) {// This could happen if the client starts seeking/shutdown// after we queued an EOS for discontinuities.// We can consider discontinuity handled.finishHandleDiscontinuity(false /* flushOnTimeChange */);}sp<AMessage> notify = mNotify->dup();notify->setInt32("what", kWhatFlushCompleted);notify->post();
}

flush 比较简单,就不多说废话啦。主要工作是调用 Renderer 的 flush,重置 Renderer 的状态,调用 MediaCodec 的 flush,刷新 input buffer 和 output buffer 缓冲区,注意这个方法调用会修改 mBufferGeneration,最后将 Decoder 存储的 buffer 列表都清空。

我们在上一节看到 finishHandleDiscontinuity 中调用的是 doFlush,所以是不会有 kWhatFlushCompleted 事件发送到 NuPlayer 的。

5、initiateShutdown

void NuPlayer::Decoder::onShutdown(bool notifyComplete) {status_t err = OK;// if there is a pending resume request, notify complete nownotifyResumeCompleteIfNecessary();if (mCodec != NULL) {// 释放decodererr = mCodec->release();// 释放 MediaCodecmCodec = NULL;// 修改 generation 阻止渲染++mBufferGeneration;if (mSurface != NULL) {// reconnect to surface as MediaCodec disconnected from itstatus_t error = nativeWindowConnect(mSurface.get(), "onShutdown");ALOGW_IF(error != NO_ERROR,"[%s] failed to connect to native window, error=%d",mComponentName.c_str(), error);}mComponentName = "decoder";}// 释放 buffer listreleaseAndResetMediaBuffers();if (err != OK) {ALOGE("failed to release [%s] (err=%d)", mComponentName.c_str(), err);handleError(err);// finish with posting kWhatShutdownCompleted.}if (notifyComplete) {sp<AMessage> notify = mNotify->dup();notify->setInt32("what", kWhatShutdownCompleted);notify->post();// 停止处理 buffer 事件mPaused = true;}
}

shutdown 也很简单:

  1. 调用 MediaCodec 的 release 方法,释放 decoder;
  2. 释放掉 MediaCodec 对象;
  3. 修改 generation,停止处理 render 事件;
  4. 释放 buffer list;
  5. 将 mPaused 置为 true,停止处理 buffer 事件;

6、signalResume

void NuPlayer::Decoder::onResume(bool notifyComplete) {mPaused = false;if (notifyComplete) {mResumePending = true;}if (mCodec == NULL) {ALOGE("[%s] onResume without a valid codec", mComponentName.c_str());handleError(NO_INIT);return;}mCodec->start();
}

flush 之后要调用 signalResume 才能启动 MediaCodec 恢复解码流程,核心就是调用 MediaCodec 的 start 方法。这里的 mResumePending 是在 decoder 送过来第一帧 ouput buffer 时来判断是否需要发送 kWhatResumeCompleted 给 NuPlayer 的。

7、总结

没有其他的,感悟是 Android ALooper 机制领悟的还不够深刻,设计模式也不会,接下来会继续加强这部分的学习!

以上内容如果有错误请不要吝啬指导。

如果觉得对您有帮助,还请不要吝啬点赞、收藏与关注哦,您的支持是我更新的最大动力。

如需阅读其他 Android Media 框架内容,还请移步 https://blog.csdn.net/qq_41828351?spm=1000.2115.3001.5343

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

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

相关文章

IDEA设置文件编码

IDEA设置文件编码 File->Settings->Editor->File Encodings 均设置为utf-8 新项目 设置 文件编码 点击New Projects Setup 再点击Settings for New Projects File->Settings->Editor->File Encodings 均设置为utf-8

【Linux】文件

Linux 文件 什么叫文件C语言视角下文件的操作文件的打开与关闭文件的写操作文件的读操作 & cat命令模拟实现 文件操作的系统接口open & closewriteread 文件描述符进程与文件的关系重定向问题Linux下一切皆文件的认识文件缓冲区缓冲区的刷新策略 stuout & stderr 什…

windows笔记本远程连接如何打开任务管理器?

参考素材&#xff1a; https://jingyan.baidu.com/article/8275fc86a97f5207a03cf6cd.html https://www.anyviewer.cn/how-to/ctrl-alt-delete-remote-desktop-6540.html 网上查了很多方法&#xff0c;都说ctrlaltend可以解决这个问题。 但是笔记本键盘上没有end键。 继续查了一…

【MATLAB第71期】基于MATLAB的Abcboost自适应决策树多输入单输出回归预测及多分类预测模型(更新中)

【MATLAB第71期】基于MATLAB的Abcboost自适应决策树多输入单输出回归预测及多分类预测模型&#xff08;更新中&#xff09; 一、效果展示&#xff08;多分类预测&#xff09; 二、效果展示&#xff08;回归预测&#xff09; 三、代码获取 CSDN后台私信回复“71期”即可获取下…

c++:QT day2 信号和槽

1.多态&#xff1a; 静态多态&#xff1a;函数的重载 动态多态&#xff1a;程序运行 多态的实现:父类的指针或引用&#xff0c;指向或初始化子类的对象&#xff0c;调用子类对父类重写的函数&#xff0c;进而展开子类的功能 2.虚函数&#xff1a;用virtua关键字修饰的函数是虚函…

【算法】leetcode 105 从前序与中序遍历序列构造二叉树

题目 输入某二叉树的前序遍历和中序遍历的结果&#xff0c;请构建该二叉树并返回其根节点。 假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 示例 1: Input: preorder [3,9,20,15,7], inorder [9,3,15,20,7] Output: [3,9,20,null,null,15,7]示例 2: Input: pr…

SpringBoot自定义消息总线

一、前言 在现代的分布式系统中&#xff0c;消息传递已成为一个非常流行的模式。它使得系统内的不同部分可以松耦合地通信&#xff0c;从而实现更高效、更可靠的应用程序。本博客将介绍SpringBoot如何提供简单易用的消息传递机制&#xff0c;并展示如何自定义消息总线以满足特定…

kafka详解二

kafka详解二 1、 offset 1.1 offset介绍 老版本 Consumer 的位移管理是依托于 Apache ZooKeeper 的&#xff0c;它会自动或手动地将位移数据提交到 ZooKeeper 中保存。当 Consumer 重启后&#xff0c;它能自动从 ZooKeeper 中读取位移数据&#xff0c;从而在上次消费截止的地…

【探索SpringCloud】服务发现-Nacos服务端数据结构和模型

前言 上一文中&#xff0c;我们从官方的图示了解到Nacos的服务数据结构。但我关心的是&#xff0c;Nacos2.x不是重构了吗&#xff1f;怎么还是这种数据结构&#xff1f;我推测&#xff0c;必然是为了对Nacos1.x的兼容&#xff0c;实际存储应该不是这样的。于是&#xff0c;沿着…

vue的第2篇 第一个vue程序

一 环境的搭建 1.1常见前端开发ide 1.2 安装vs.code 1.下载地址&#xff1a;Visual Studio Code - Code Editing. Redefined 2.进行安装 1.2.1 vscode的中文插件安装 1.在搜索框输入“chinese” 2.安装完成重启&#xff0c;如下变成中文 1.2.2 修改工作区的颜色 选中[浅色]…

opencv 提取选中区域内指定hsv颜色的水印

基于《QT 插件化图像算法研究平台》做的功能插件。提取选中区域内指定hsv颜色的水印。 《QT 插件化图像算法研究平台》有个HSV COLOR PICK功能&#xff0c;可以很直观、方便地分析出水印 的hsv颜色&#xff0c;比如, 蓝色&#xff1a;100,180,0,255,100,255。 然后利用 opencv …

Django(10)-项目实战-对发布会管理系统进行测试并获取测试覆盖率

在发布会签到系统中使用django开发了发布会签到系统&#xff0c; 本文对该系统进行测试。 django.test django.test是Django框架中的一个模块&#xff0c;提供了用于编写和运行测试的工具和类。 django.test模块包含了一些用于测试的类和函数&#xff0c;如&#xff1a; Tes…

每日一题 1372二叉树中的最长交错路径

题目 给你一棵以 root 为根的二叉树&#xff0c;二叉树中的交错路径定义如下&#xff1a; 选择二叉树中 任意 节点和一个方向&#xff08;左或者右&#xff09;。如果前进方向为右&#xff0c;那么移动到当前节点的的右子节点&#xff0c;否则移动到它的左子节点。改变前进方…

vscode 清除全部的console.log

在放页面的大文件夹view上面右键点击在文件夹中查找 console.log.*$ 注意&#xff1a;要选择使用正则匹配 替换为 " " (空字符串)

跨模态可信感知

文章目录 跨模态可信感知综述摘要引言跨协议通信模式PCP网络架构 跨模态可信感知跨模态可信感知的概念跨模态可信感知的热点研究场景目前存在的挑战可能改进的方案 参考文献 跨模态可信感知综述 摘要 随着人工智能相关理论和技术的崛起&#xff0c;通信和感知领域的研究引入了…

ELK日志收集系统(四十九)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、概述 二、组件 1. elasticsearch 2. logstash 2.1 工作过程 2.2 INPUT 2.3 FILETER 2.4 OUTPUTS 3. kibana 三、架构类型 3.1 ELK 3.2 ELKK 3.3 ELFK 3.5 EF…

VScode远程连接主机

一、前期准备 1、Windows安装VSCode&#xff1b; 2、在VSCode中安装PHP Debug插件&#xff1b; 3、安装好Docker 4、在容器中安装Xdebug ①写一个展现phpinfo的php文件 <?php phpinfo(); ?>②在浏览器上打开该文件 ③复制所有信息丢到Xdebug: Installation instr…

【C进阶】深度剖析数据在内存中的存储

目录 一、数据类型的介绍 1.类型的意义&#xff1a; 2.类型的基本分类 二、整形在内存中的存储 1.原码 反码 补码 2.大小端介绍 3.练习 三、浮点型在内存中的存储 1.一个例子 2.浮点数存储规则 一、数据类型的介绍 前面我们已经学习了基本的内置类型以及他们所占存储…

封装(个人学习笔记黑马学习)

1、格式 #include <iostream> using namespace std;const double PI 3.14;//设计一个圆类&#xff0c;求圆的周长 class Circle {//访问权限//公共权限 public://属性//半径int m_r;//行为//获取圆的周长double calculateZC() {return 2 * PI * m_r;} };int main() {//通…

Linux 学习笔记(1)——系统基本配置与开关机命令

目录 0、起步 0-1&#xff09;命令使用指引 0-2&#xff09;查看历史的命令记录 0-3&#xff09;清空窗口内容 0-4&#xff09;获取本机的内网 IP 地址 0-5&#xff09;获取本机的公网ip地址 0-6&#xff09;在window的命令行窗口中远程连接linux 0-7&#xff09;修改系…