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> ¬ify);void configure(const sp<AMessage> &format);void init();void setParameters(const sp<AMessage> ¶ms);// 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> ¶ms) = 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();
}
- 查找 format 中的 mime,必须要有这个;
- 调用 CreateByType 创建 MediaCodec 实例,如果 format 中带有 secure 字段,那么就调用 CreateByComponentName 创建 Secure Component;
- 调用 configure 配置 MediaCodec,需要传入码流的 format,format 中需要有什么我们后面再看,这里传入的 surface 不为 NULL,因为 NuPlayer 中有判断,如果 surface 为 NULL 就不会为 Video 创建 Codec;
- 存储 format 中的 codec specific data(csd buffer),这些 buffer 记录了码流的信息,例如 h264,h265码流中的sps pps 等信息,对于有些 decoder 一定需要传入该信息,而有些 decoder 可以自己从码流中 parse 出来这些信息,具体要看各家的 decoder 实现;
- 给 MediaCodec 注册 callback,让它以
异步的方式工作; - 调用 start 方法启动 decoder,开启整个数据读取、数据解码 以及 数据渲染流程。
以下内容是个人拙见,讲的比较啰嗦,如不喜欢直接跳过就好。
除了这些,我们还要看一个成员 mBufferGeneration,这个东西是干什么的呢?其实我们之前已经说过 Media 这边用了跟多 generation 的思想或者说是trick,那这里的 generation 是用来干什么的呢?
我们搜索代码中的 mBufferGeneration,发现它的值会在 onConfigure、doFlush、 onShutdown、handleError 中做修改,这四个方法会有一个共同点,它们都会去操作 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_AVAILABLE、CB_OUTPUT_AVAILABLE、CB_ERROR、CB_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 做的事情(省略了部分注释的内容):
- 判断当前是否正在处理码流不连续的情况;
- 从 MediaCodec 获取对应索引的
MediaCodecBuffer; - 将获取的到的 MediaCodecBuffer 按照索引记录到 mInputBuffers 列表当中;
- 创建一个列表
mInputBufferIsDequeued,记录索引对应的 input buffer 是否有出队列; - 每次进行了 flush,需要送 csd buffer 给 decoder,记住是每次!其实不是所有的 decoder flush 之后都要 csd buffer 的,之前被坑过!
- 先处理被延迟的 input buffer,为什么延迟后面再说;
- 处理延迟 buffer 时可能会把当前的 input buffer 处理掉,如果记录出队列的列表中对应位置为 false 说明已经被处理过了;
- 如果 input buffer 没有处理,那么再把它加入到一个没有处理的列表当中
mDequeuedInputBuffers; - 调用 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;
}
- 获取 output buffer;
- 创建 reply,设置 generation,avsync 完成后 Renderer 通过该消息 callback 到 Decoder;
- 判断 output buffer flag 是否是 eos,如果是则在 reply 中进行标记;
- queue input buffer 时可能设有开始渲染的 pts,output buffer pts 小于该 pts 时直接 drop;
- 将 output buffer 和 reply message 一起送到 Renderer,如果到达 eos,且不是因为码流不连续,还要给 Renderer 送一个 EOS;
3.3、CB_OUTPUT_FORMAT_CHANGED
虽然我们使用 decoder 时都会传 input format 下去,但是 decoder 收到数据后仍会自己解析格式,并且上抛 output format change 事件,上层收到事件后需要做对应的处理。
void NuPlayer::Decoder::handleOutputFormatChange(const sp<AMessage> &format) {if (!mIsAudio) {int32_t width, height;if (format->findInt32("width", &width)&& format->findInt32("height", &height)) {Mutex::Autolock autolock(mStatsLock);mStats->setInt32("width", width);mStats->setInt32("height", height);}sp<AMessage> notify = mNotify->dup();notify->setInt32("what", kWhatVideoSizeChanged);notify->setMessage("format", format);notify->post();} else if (mRenderer != NULL) {uint32_t flags;int64_t durationUs;bool hasVideo = (mSource->getFormat(false /* audio */) != NULL);if (getAudioDeepBufferSetting() // override regardless of source duration|| (mSource->getDuration(&durationUs) == OK&& durationUs > AUDIO_SINK_MIN_DEEP_BUFFER_DURATION_US)) {flags = AUDIO_OUTPUT_FLAG_DEEP_BUFFER;} else {flags = AUDIO_OUTPUT_FLAG_NONE;}sp<AMessage> reply = new AMessage(kWhatAudioOutputFormatChanged, this);reply->setInt32("generation", mBufferGeneration);mRenderer->changeAudioFormat(format, false /* offloadOnly */, hasVideo,flags, mSource->isStreaming(), reply);}
}
如果是 Video Format 发生改变,继续上抛事件即可。如果是 Audio Format 发生改变,Decoder 需要调用 Renderer.changeAudioFormat 来重新打开 AudioTrack,具体如何处理在 Renderer 篇中会简单介绍。
3.4、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", ×tampNs)); // 获取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 也很简单:
- 调用 MediaCodec 的 release 方法,释放 decoder;
- 释放掉 MediaCodec 对象;
- 修改 generation,停止处理 render 事件;
- 释放 buffer list;
- 将 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
相关文章:
Android 13 - Media框架(9)- NuPlayer::Decoder
这一节我们将了解 NuPlayer::Decoder,学习如何将 MediaCodec wrap 成一个强大的 Decoder。这一节会提前讲到 MediaCodec 相关的内容,如果看不大懂可以先跳过此篇。原先觉得 Decoder 部分简单,越读越发现自己的无知,Android 源码真…...
23.09.5 《CLR via C#》 笔记5
第六章 类型和成员基础 类型可以定义0或多个以下成员:常量、字段、实例构造器、类型构造器、方法、操作符重载、转换操作符、属性、事件、类型类型的可见性分为public和internal(默认)C#中,成员的可访问性分为private、protected、internal、protected …...
laravel部署api项目遇到问题总结
laravel线上部署问题 一、Ubuntu远程Mysql 61“Connection refused”二、Ubuntu更新php8三、线上部署Permission denied3.1、部署完之后访问域名出现报错:3.2、The /bootstrap/cache directory must be present and writable. 四、图片访问404五、git部署线上文件 一…...
lintcode 1646 · 合法组合【字符串DFS, vip 中等 好题】
题目 https://www.lintcode.com/problem/1646 给一个单词s,和一个字符串集合str。这个单词每次去掉一个字母,直到剩下最后一个字母。求验证是否存在一种删除的顺序,这个顺序下所有的单词都在str中。例如单词是’abc’,字符串集合是{‘a’,’…...
【多线程】线程安全 问题
线程安全 问题 一. 线程不安全的典型例子二. 线程安全的概念三. 线程不安全的原因1. 线程调度的抢占式执行2. 修改共享数据3. 原子性4. 内存可见性5. 指令重排序 一. 线程不安全的典型例子 class ThreadDemo {static class Counter {public int count 0;void increase() {cou…...
【用unity实现100个游戏之11】复刻经典消消乐游戏
文章目录 前言开始项目开始一、方块网格生成二、方块交换三、添加交换的动画效果四、水平消除检测五、垂直消除检测六、完善删除功能七、效果优化(移动方块后再进行消除检测)八、方块下落十、方块填充十一、后续 源码参考完结 前言 欢迎来到经典消消乐游…...
若依cloud 修改包名等
一、项目的项目名。 先改pom 然后在重命名文件 1、 修改主pom.xml <artifactId>ruoyi-api</artifactId> 缓存 <artifactId>zxf-api</artifactId> <groupId>com.ruoyi</groupId> <groupId>com.zhixiaofeng</groupId> 2、…...
健康系统练习
健康系统 项目建构: 前后端分离,前端vue3,后端Java,springboot做跨域处理,前端将在vscode中 的tomcat下部署,后端将在ideal中集成的tomcat中部署 创建项目工程在ideal中直接选用springi…创建,…...
网络协议从入门到底层原理学习(一)—— 简介及基本概念
文章目录 网络协议从入门到底层原理学习(一)—— 简介及基本概念一、简介1、网络协议的定义2、网络协议组成要素3、广泛的网络协议类型网络通信协议网络安全协议网络管理协议 4、网络协议模型对比图 二、基本概念1、网络互连模型2、计算机之间的通信基础…...
centos密码过期导致navicat无法通过SSH登录阿里云RDS问题
具体错误提示:2013 - Lost connection to server at "hand hake: reading initial communication packet, system error: 0 解决办法:更新SSH服务器密码...
对于pytorch和对应pytorch网站的探索
一、关于网站上面的那个教程: 适合PyTorch小白的官网教程:Learning PyTorch With Examples - 知乎 (zhihu.com) 这个链接也是一样的, 总的来说,里面讲了这么一件事: 如果没有pytorch的分装好的nn.module用来继承的话,需要设计…...
和AI聊天:动态规划
动态规划 动态规划(Dynamic Programming,简称 DP)是一种常用于优化问题的算法。它解决的问题通常具有重叠子问题和最优子结构性质,可以通过将问题分解成相互依赖的子问题来求解整个问题的最优解。 动态规划算法主要分为以下几个步…...
微信小程序——使用插槽slot快捷开发
微信小程序的插槽(slot)是一种组件化的技术,用于在父组件中插入子组件的内容。通过插槽,可以将父组件中的一部分内容替换为子组件的内容,实现更灵活的组件复用和定制。 插槽的使用步骤如下: 在父组件的wx…...
大数据技术之Hadoop:使用命令操作HDFS(四)
目录 一、创建文件夹 二、查看指定目录下的内容 三、上传文件到HDFS指定目录下 四、查看HDFS文件内容 五、下载HDFS文件 六、拷贝HDFS文件 七、HDFS数据移动操作 八、HDFS数据删除操作 九、HDFS的其他命令 十、hdfs web查看目录 十一、HDFS客户端工具 11.1 下载插件…...
静态路由配置实验:构建多路由器网络拓扑实现不同业务网段互通
文章目录 一、实验背景与目的二、实验拓扑三、实验需求四、实验解法1. 配置 IP 地址2. 按照需求配置静态路由,实现连接 PC 的业务网段互通 摘要: 本实验旨在通过配置网络设备的IP地址和静态路由,实现不同业务网段之间的互通。通过构建一组具有…...
Python函数的概念以及定义方式
一. 前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 二. 什么是函数? 假设你现在是一个工人,如果你实现就准备好了工具,等你接收到任务的时候, 直接带上工…...
【数学建模竞赛】超详细Matlab二维三维图形绘制
二维图像绘制 绘制曲线图 g 是表示绿色 b--o是表示蓝色/虚线/o标记 c*是表示蓝绿色(cyan)/*标记 ‘MakerIndices,1:5:length(y) 每五个点取点(设置标记密度) 特殊符号的输入 序号 需求 函数字符结构 示例 1 上角标 ^{ } title( $ a…...
2023国赛数学建模E题思路代码 黄河水沙监测数据分析
E题最大的难度是数据处理,可以做一个假设,假设一定时间内流量跟含沙量不变,那么我们可以对数据进行向下填充,把所有的数据进行合并之后可以对其进行展开特性分析,在研究调水调沙的实际效果时,可以先通过分析…...
窗口延时、侧输出流数据处理
一 、 AllowedLateness API 延时关闭窗口 AllowedLateness 方法需要基于 WindowedStream 调用。AllowedLateness 需要设置一个延时时间,注意这个时间决定了窗口真正关闭的时间,而且是加上WaterMark的时间,例如 WaterMark的延时时间为2s&…...
发送HTTP请求
HTTP请求是一种客户端向服务器发送请求的协议。它是基于TCP/IP协议的应用层协议,用于在Web浏览器和Web服务器之间传输数据。 HTTP请求由以下几个部分组成: 请求行:包含请求方法、请求的URL和HTTP协议的版本。常见的请求方法有GET、POST、PUT、…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
