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

news2024/12/23 9:26:57

这一节我们将了解 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 buffer
    if (!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 获取数据,填充数据,并送回 decoder
    onRequestInputBuffers();
    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,发送一条延时消息,retry
    if (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 buffer
            break;
        }
        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 separately
                    formatChange = !seamlessFormatChange;
                }

                // For format or time change, return EOS to queue EOS input,
                // then wait for EOS on output.
                // 如果不支持无缝切换,那么就要向 decoder 填充 eos
                if (formatChange /* not seamless */) {
                    mFormatChangePending = true;
                    err = ERROR_END_OF_STREAM;
                } else if (timeChange) {
                	// 如果pts不连续,那么就要向 decoder 填充 eos,恢复播放后要 发送 csd buffer
                    rememberCodecSpecificData(newFormat);
                    mTimeChangePending = true;
                    err = ERROR_END_OF_STREAM;
                } else if (seamlessFormatChange) {
                    // reuse existing decoder and don't flush
                    // 如果是无缝切换,那么仍要发送 csd buffer
                    rememberCodecSpecificData(newFormat);
                    continue;
                } else {
                    // This stream is unaffected by the discontinuity
                    return -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 buffer
    mCodec->getOutputBuffer(index, &buffer);

    int64_t frameIndex;
    bool frameIndexFound = buffer->meta()->findInt64("frameIndex", &frameIndex);

    buffer->setRange(offset, size);
    // 设置 pts
    buffer->meta()->clear();
    buffer->meta()->setInt64("timeUs", timeUs);
    if (frameIndexFound) {
        buffer->meta()->setInt64("frameIndex", frameIndex);
    }
	// 判断 output buffer 是否到达 eos
    bool 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 做 avsync
        mRenderer->queueBuffer(mIsAudio, buffer, reply);
        // 如果到达 eos,并且不是因为码流中断,调用queueEOS
        if (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 index
    CHECK(msg->findSize("buffer-ix", &bufferIx));

    if (mCodec == NULL) {
        err = NO_INIT;
    } else if (msg->findInt32("render", &render) && render) { // 判断是否render
        int64_t timestampNs;
        CHECK(msg->findInt64("timestampNs", &timestampNs));	// 获取render时间
        err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
    } else {
    	// 如果是 eos 或者 不render 则直接 drop
        if (!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 sources
    sp<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 now
    notifyResumeCompleteIfNecessary();

    if (mCodec != NULL) {
    	// 释放decoder
        err = mCodec->release();
        // 释放 MediaCodec
        mCodec = NULL;
        // 修改 generation 阻止渲染
        ++mBufferGeneration;

        if (mSurface != NULL) {
            // reconnect to surface as MediaCodec disconnected from it
            status_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 list
    releaseAndResetMediaBuffers();

    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.coloradmin.cn/o/964566.html

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

相关文章

nginx-缓存

disk cache&#xff1a;磁盘缓存数据&#xff0c;有时间延迟&#xff0c;但是非常小&#xff0c;相对于直接请求服务器返回 对于用户来说基本无感知。 memory cache&#xff1a;磁盘缓存数据&#xff0c;基本上没有时间延迟 协商缓存&#xff08;nginx自带功能&#xff0c; 不…

机器人中的数值优化(五)——信赖域方法

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考&#xff0c;主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等&#xff0c;本系列文章篇数较多&#xff0c;不定期更新&#xff0c;上半部分介绍无约束优化&#xff0c;…

18.kthread_worker:内核线程异步传输

目录 kthread_worker 驱动传输数据的方式 同步传输 异步传输 头文件 kthread_worker结构体 kthread_work结构体 kthread_flush_work结构体 init_kthread_worker()函数 为kthread_worker创建内核线程 init_kthread_work()函数 启动工作 刷新工作队列 停止内核线程…

3D点云测量:计算三个平面的交点

文章目录 0. 测试效果1. 基本内容文章目录:3D视觉测量目录微信:dhlddxB站: Non-Stop_0. 测试效果 1. 基本内容 计算三个平面的交点需要找到满足所有三个平面方程的点。三个平面通常由它们的法向量和通过它们的点(或参数形式的方程)来定义。以下是计算三个平面的交点的一般步…

在VScode中使用sftp传输本地文件到服务器端

安装SFTP 在VScode的扩展中安装sftp 注意这里需要在你没连接服务器的状态下安装&#xff0c;即本机需要有sftp 配置传输端口 安装成功后&#xff0c;使用快捷键"ctrlshiftp",输入sftp&#xff0c;选择Config 根据自己的实际情况修改配置文件&#xff0c;主要改h…

设计模式-6--装饰者模式(Decorator Pattern)

一、什么是装饰者模式&#xff08;Decorator Pattern&#xff09; 装饰者模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许你在不修改现有对象的情况下&#xff0c;动态地将新功能附加到对象上。这种模式通过创建一个包装类&#xff0c;…

排序之交换排序

文章目录 前言一、冒泡排序1、冒泡排序基本思想2、冒泡排序的效率 二、快速排序 -- hoare版本1、快速排序基本思想2、快速排序代码实现3、为什么最左边值做key时&#xff0c;右边先走 三、快速排序 -- 挖坑法1、快速排序 -- 挖坑法基本思想2、快速排序 -- 挖坑法代码实现3、为什…

stable diffusion实践操作-随机种子seed

系列文章目录 stable diffusion实践操作 文章目录 系列文章目录前言一、seed是什么&#xff1f;二、使用步骤1.多批次随机生成多张图片2.提取图片seed3. 根据seed 再次培养4 seed使用4.1 复原别人图4.1 轻微修改 三、差异随机种子1. webUI位置2. 什么是差异随机种子3.使用差异…

找redis大key工具rdb_bigkeys

github官网 https://github.com/weiyanwei412/rdb_bigkeys 在centos下安装go [roothadoop102 rdb_bigkeys-master]# wget https://dl.google.com/go/go1.13.5.linux-amd64.tar.gz [roothadoop102 rdb_bigkeys-master]# tar -zxf go1.13.5.linux-amd64.tar.gz -C /usr/local将g…

安装bpftrace和bcc的踩坑记录

最后在Ubuntu22.04使用Ubuntu提供的安装命令完成了安装。这里是记录尝试在Ubuntu18.04和Ubuntu22.04使用源码安装未果的过程。 文章目录 22版本安装bcc准备工具安装命令使用报错&#xff1a;iovisor封装的安装方式ubuntu的安装方式 For Bionic (18.04 LTS)官方提供的源码安装准…

SpringCloudGateway集成SpringDoc

SpringCloudGateway集成SpringDoc 最近在搞Spring版本升级&#xff0c;按客户要求升级Spring版本&#xff0c;原来用着SpringBoot 2.2.X版本&#xff0c;只需要升级SpringBoot 2.X最新版本也就可以满足客户Spring版本安全要求&#xff0c;可是好像最新的SpringBoot 2.X貌似也不…

Laravel chunk和chunkById的坑

在编写定时任务脚本的时候&#xff0c;经常会用到chunk和chunkById的API。 一、前言 数据库引擎为innodb。 表结构简述&#xff0c;只列出了本文用到的字段。 字段类型注释idint(11)IDtypeint(11)类型mark_timeint(10)标注时间&#xff08;时间戳&#xff09; 索引&#x…

手撕 视觉slam14讲 ch13 代码(1)工程框架与代码结构

在学习slam一年之后开始&#xff0c;开始自己理思路&#xff0c;全手敲完成ch13的整个代码 我们按照自己写系统的思路进行&#xff0c;首先确定好SLAM整体系统的流程&#xff0c;见下图&#xff0c;输入为双目图像&#xff0c;之后进入前端位姿估计和后端优化&#xff0c;中间…

滑动窗口实例3(最大连续1的个数Ⅲ)

题目&#xff1a; 给定一个二进制数组 nums 和一个整数 k&#xff0c;如果可以翻转最多 k 个 0 &#xff0c;则返回 数组中连续 1 的最大个数 。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1,0,0,0,1,1,1,1,0], K 2 输出&#xff1a;6 解释&#xff1a;[1,1,1,0,0,1,1…

stable diffusion实践操作-宽高设置以及高清修复

系列文章目录 stable diffusion实践操作 文章目录 系列文章目录前言一、SD宽高怎么设置&#xff1f;1.1 宽高历史 二、高清修复总结 前言 主要介绍SD的宽高设置以及高清修复 一、SD宽高怎么设置&#xff1f; 1.1 宽高历史 SD生成256256图片效果最好。512512是SD一直使用的画…

【管理运筹学】第 7 章 | 图与网络分析(1,图论背景以及基本概念、术语)

文章目录 引言一、图与网络的基本知识1.1 图与网络的基本概念1.1.1 图的定义1.1.2 图中相关术语1.1.3 一些特殊图类1.1.4 图的运算 写在最后 引言 按照正常进度应该学习动态规划了&#xff0c;但我想换换口味&#xff0c;而且动态规划听说也有一定难度&#xff0c;还不一定会考…

设计模式—简单工厂

目录 一、前言 二、简单工厂模式 1、计算器例子 2、优化后版本 3、结合面向对象进行优化&#xff08;封装&#xff09; 3.1、Operation运算类 3.2、客户端 4、利用面向对象三大特性&#xff08;继承和多态&#xff09; 4.1、Operation类 4.2、加法类 4.3、减法类 4…

VTK——使用ICP算法进行模型配准

ICP算法 迭代最近点&#xff08;Iterative Closest Point&#xff0c;ICP&#xff09;算法是一种用于两个三维形状之间几何对齐&#xff08;也叫做配准&#xff09;的计算方法。通常&#xff0c;这两个形状至少有一个是点云数据。ICP算法用于最小化源点云与目标点云之间点到点…

【设计模式】Head First 设计模式——构建器模式 C++实现

设计模式最大的作用就是在变化和稳定中间寻找隔离点&#xff0c;然后分离它们&#xff0c;从而管理变化。将变化像小兔子一样关到笼子里&#xff0c;让它在笼子里随便跳&#xff0c;而不至于跳出来把你整个房间给污染掉。 设计思想 ​ 将一个复杂对象的构建与其表示相分离&…

【两个有序数组合并】

问题描述: 给定两个有序整数数组 A 和 B&#xff0c;将B合并到A中&#xff0c;使得 A 成为一个有序数组。 说明: 初始化 A 和 B 的元素数量分别为 m 和 n。A有足够的空间&#xff08;空间大小大于或等于 m n&#xff09;来保存 B 中的元素。默认升序。 输入输出描述&#xf…