NuPlayer源码分析三:解码模块

本文围绕NuPlayer播放器的解码模块展开分析。该模块使用基类NuPlayerDecoderBase管理,实际解码靠MediaCodec。文中按解码器创建、填充数据到解码队列、渲染解码后的数据、释放解码器这几个步骤,详细剖析了NuPlayer解码模块的工作流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

解码模块


系列文章分为如下几个模块:

NuPlayer的解码模块相对比较简单,统一使用了一个基类NuPlayerDecoderBase管理,该类中包含了一个MediaCodec的对象,实际解码工作全靠MediaCodec。

如果你不会知道MediaCodec是什么,推荐去官网看看:MediaCodec

尽管解码工作都被MediaCodec接管,我还是会按照播放器的一般步骤,来分析一下NuPlayerDecoderBase。步骤如下:

一个Android播放器典型的播放步骤一般是:

  1. 播放器创建。
  2. 设置媒体源文件(本地文件路径、或者Uri)。
  3. 准备媒体数据。
  4. 播放视频。
  5. 停止播放。

对应于解码模块,会稍微简单一些:

  1. NuPlayerDecoderBase:解码器创建
  2. onInputBufferFetched:填充数据到解码队列
  3. onRenderBuffer:渲染解码后的数据
  4. ~Decoder:释放解码器

解码器创建:NuPlayerDecoderBase

当前位置:

  1. NuPlayerDecoderBase:解码器创建
  2. onInputBufferFetched:填充数据到解码队列
  3. onRenderBuffer:渲染解码后的数据
  4. ~Decoder:释放解码器

解码器创建的入口在NuPlayer的NuPlayer::instantiateDecoder函数调用时。NuPlayer在执行start函数后,会通过一系列调用链,触发该函数。来具体分析一下该函数。

// 参数部分:audio true调用者想要创建音频解码器, false 想要创建视频解码器
// 参数部分:*decoder 该函数最终会创建指定解码器,使用该函数将解码器对象地址提供给调用者
status_t NuPlayer::instantiateDecoder(
        bool audio, sp<DecoderBase> *decoder, bool checkAudioModeChange) {
    sp<AMessage> format = mSource->getFormat(audio); // 其实就是GenericSource中的MetaData
    format->setInt32("priority", 0 /* realtime */);

    if (!audio) { // 视频
        // 总要丢掉一些代码的
            mCCDecoder = new CCDecoder(ccNotify); // 创建字幕解码器
    }
	// 创建音/视频解码器
    if (audio) { // 音频
        // ...
            *decoder = new Decoder(notify, mSource, mPID, mUID, mRenderer);
        // ...
    } else { // 视频
		// ...
        *decoder = new Decoder(
                notify, mSource, mPID, mUID, mRenderer, mSurface, mCCDecoder);
		// ...
    }
    (*decoder)->init();
    (*decoder)->configure(format);

    if (!audio) { // 视频
        sp<AMessage> params = new AMessage();
        float rate = getFrameRate();
        if (rate > 0) {
            params->setFloat("frame-rate-total", rate);
        }
		// ...
        if (params->countEntries() > 0) {
            (*decoder)->setParameters(params);
        }
    }
    return OK;
}

删删减减去掉了大量代码,留下来我感兴趣的。

先来说说,Decoder实际上是继承于DecoderBase的。

Decoder前者的定义如下:

struct NuPlayer::Decoder : public DecoderBase {
    Decoder(const sp<AMessage> &notify,
            const sp<Source> &source,
            pid_t pid,
            uid_t uid,
            const sp<Renderer> &renderer = NULL,
            const sp<Surface> &surface = NULL,
            const sp<CCDecoder> &ccDecoder = NULL);
    // 自然又是删掉很多代码
protected:
    virtual ~Decoder();

DecoderBase的定义如下:

struct ABuffer;
struct MediaCodec;
class MediaBuffer;
class MediaCodecBuffer;
class Surface;
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);
protected:
    virtual ~DecoderBase();
    void stopLooper();
    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;
};

可以从DecoderBase的实现看到,它包含了所有解码相关的接口,这些接口往往都和MediaCodec的接口直接相关。可见,它是处在解码的前沿阵地上的。

instantiateDecoder函数中,创建音频和视频的解码器,参数略有不同,创建视频解码器是会多出一个mSurface,提供给MediaCodec以显示视频,mCCDecoder则是字幕相关解码器。来看一下解码器构建函数:

NuPlayer::Decoder::Decoder(
        const sp<AMessage> &notify,
        const sp<Source> &source,
        pid_t pid,
        uid_t uid,
        const sp<Renderer> &renderer,
        const sp<Surface> &surface,
        const sp<CCDecoder> &ccDecoder)
    : DecoderBase(notify),
      mSurface(surface), // 视频播放的surface实体
      mSource(source),
      mRenderer(renderer), // 渲染器
      mCCDecoder(ccDecoder), // 字幕解码器
      mIsAudio(true) { // 是否为音频
    mCodecLooper = new ALooper;
    mCodecLooper->setName("NPDecoder-CL");
    mCodecLooper->start(false, false, ANDROID_PRIORITY_AUDIO);
    mVideoTemporalLayerAggregateFps[0] = mFrameRateTotal;
}

构造函数基本上就是将传递进来的参数,直接保存到自己的各类变量中,功能后续使用。继续看一下接下来对解码器来说比较重要的调用。

再来看看关于解码器的第二各操作:(*decoder)->init();

直接在Decoder类中查找init()并不能找到,因为Decoder继承与DecoderBase,所以这里执行的应该是DecoderBaseinit函数:

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

DecoderBase的构造函数中,已经创建了一套NativeHandler体系,并将Looper启动,只是没有将AHandler的子类对象和ALooper绑定,知道init()函数执行后,这种绑定关系才算结束。也只有这样,DecoderBase中的NativeHandler体系才能够正常工作。有关NativeHandler的详细信息,请参考:NativeHandler系列(一)

接下来看看和解码相关的最重要的一步操作:(*decoder)->configure(format);

configure函数实际上是在DecoderBase中实现,最终调用了DecoderBase的纯虚构函数:onConfigure,让它的子类去实现具体的配置方法:

void NuPlayer::Decoder::onConfigure(const sp<AMessage> &format) {
    AString mime;
    CHECK(format->findString("mime", &mime));
	// 根据需要创建的解码器类型创建解码器
    mCodec = MediaCodec::CreateByType(
            mCodecLooper, mime.c_str(), false /* encoder */, NULL /* err */, mPid, mUid);
    err = mCodec->configure(format, mSurface, crypto, 0 /* flags */); // 配置解码器
    sp<AMessage> reply = new AMessage(kWhatCodecNotify, this);
    mCodec->setCallback(reply); // 设置解码器回调
    err = mCodec->start(); // 启动解码器
}

从简化后的代码可以看到, 在onConfigure函数中,有关MediaCodec的调用都是比较经典的调用方式。分别有,MediaCodec的创建、配置、设置回调通知、启动解码器。

关于MediaCodec还有buffer的入队和出队以及释放函数,相信不久后在其它地方可以见到。

解码器创建部分暂时就这么多,小结一下:

小结解码器创建

  • Decoder继承于DecoderBaseDecoderBase基本上接管了解码工作所有的操作,通过纯虚构(抽象)函数来让子类,也就是Decoder来实现一些具体的操作。
  • DecoderBase解码体系,都是通过MediaCodec来实现解码流程。有鉴于此,它的基本操作函数都是为了MediaCodec的服务。

填充数据到解码队列:onInputBufferFetched

当前位置:

  1. NuPlayerDecoderBase:解码器创建
  2. onInputBufferFetched:填充数据到解码队列
  3. onRenderBuffer:渲染解码后的数据
  4. ~Decoder:释放解码器

MediaCode创建并执行了start函数后,就已经在通过mCodec->setCallback(reply)提供的回调,不断地调用填充数据有关的逻辑,最后实现数据填充的地方是在onInputBufferFetched函数中:

bool NuPlayer::Decoder::onInputBufferFetched(const sp<AMessage> &msg) {
   size_t bufferIx;
   CHECK(msg->findSize("buffer-ix", &bufferIx));
   CHECK_LT(bufferIx, mInputBuffers.size());
   sp<MediaCodecBuffer> codecBuffer = mInputBuffers[bufferIx];

   sp<ABuffer> buffer;
   bool hasBuffer = msg->findBuffer("buffer", &buffer); // 填充通解封装模块获取的数据
   bool needsCopy = true; // 是否需要将数据拷贝给MediaCodec

   if (buffer == NULL /* includes !hasBuffer */) { // 如果已经没有buffer可以提供了。
       status_t err = mCodec->queueInputBuffer(bufferIx, 0, 0, 0, MediaCodec::BUFFER_FLAG_EOS);
		// ...
   } else { // 还有buffer
        if (needsCopy) { // 拷贝给MediaCodec
            // ...
            if (buffer->data() != NULL) {
                codecBuffer->setRange(0, buffer->size());
                // 拷贝到MediaCodec的buffer中
                memcpy(codecBuffer->data(), buffer->data(), buffer->size()); 
            }
        } // needsCopy

        status_t err;
        AString errorDetailMsg;
		// ...
            err = mCodec->queueInputBuffer( // 将buffer加入到MediaCodec的待解码队列中
                    bufferIx,
                    codecBuffer->offset(),
                    codecBuffer->size(),
                    timeUs,
                    flags,
                    &errorDetailMsg);
        // ...
    }   // buffer != NULL
    return true;
}

这个函数的核心,就是调用MediaCodecqueueInputBuffer函数,将填充好的MediaCodecBuffer添加到MediaCodec的输入队列中,等待解码。解释都放在注释里了。来看一下如何取数据的。

渲染解码后的数据:onRenderBuffer

当前位置:

  1. NuPlayerDecoderBase:解码器创建
  2. onInputBufferFetched:填充数据到解码队列
  3. onRenderBuffer:渲染解码后的数据
  4. ~Decoder:释放解码器

onRenderBuffer的执行时机,和onInputBufferFetched几乎是同时的,当MediaCodec的解码outputBuffer队列中有数据时,就会通过回调通知播放器,执行对应的回调函数渲染数据。在NuPlayer这样的回调函数执行链条为:NuPlayer::Decoder::onMessageReceived ==> handleAnOutputBuffer ==> NuPlayer::Decoder::onRenderBuffer

最终执行取出解码数据并渲染的函数便是onRenderBuffer

void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) {
    int32_t render;
    size_t bufferIx;
    CHECK(msg->findSize("buffer-ix", &bufferIx));
	ALOGE("onRenderBuffer");

    if (msg->findInt32("render", &render) && render) {
        int64_t timestampNs;
        CHECK(msg->findInt64("timestampNs", &timestampNs));
        // 触发播放音频数据
        err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
    } else { // 播放视频数据
        mNumOutputFramesDropped += !mIsAudio;
        err = mCodec->releaseOutputBuffer(bufferIx);
    }
	// ...
}

释放解码器:~Decoder

当前位置:

  1. NuPlayerDecoderBase:解码器创建
  2. onInputBufferFetched:填充数据到解码队列
  3. onRenderBuffer:渲染解码后的数据
  4. ~Decoder:释放解码器
NuPlayer::Decoder::~Decoder() {
    // Need to stop looper first since mCodec could be accessed on the mDecoderLooper.
    stopLooper(); // 停止looper
    if (mCodec != NULL) {
        mCodec->release(); // release掉MediaCodec
    }
    releaseAndResetMediaBuffers(); // 清理buffer
}
<think>好的,我现在需要帮助用户了解NuPlayer::restartAudio的功能或解决与其相关的问题。首先,我得回忆一下Android多媒体框架中NuPlayer的相关知识。根据提供的引用,NuPlayerAndroid原生的播放器,替代了之前的AwesomePlayer。它的结构包含Source、Decoder和Renderer大组件,这些组件是在接口调用时逐步创建的。 用户提到的restartAudio方法,可能涉及音频重新启动的逻辑。通常,在媒体播放过程中,如果遇到音频输出问题,比如设备切换、音频焦点丢失或解码错误,可能需要重新启动音频输出。根据引用[1],AudioOutput被创建后通过setAudioSink设置给NuPlayer的mAudioSink。这可能意味着restartAudio会重新初始化或重新连接AudioOutput,确保音频可以继续播放。 接下来,我需要考虑可能的问题场景。例如,当音频会话ID改变、音频设备断开或重新连接时,NuPlayer需要重新配置音频输出。此外,如果音频解码器遇到错误,可能需要重启音频管道。此时,restartAudio可能会调用mAudioSink->restart(),重新创建AudioOutput实例,并更新到Decoder或Renderer组件中。 在解决问题方面,用户可能遇到音频中断、无法恢复播放或音频设备切换失败的情况。这时候需要检查日志,查看是否有相关的错误信息,比如AudioSink的错误状态或初始化失败。同时,需要确认音频会话ID是否正确传递,以及AudioOutput的配置参数是否有效。引用[3]提到了JNI层的设置,可能涉及Surface和AudioAttributes的配置是否正确,这也可能影响音频的重启。 另外,需要参考引用[4]中关于NuPlayer Source的内容,确认在Source组件中是否有影响音频数据流的因素,比如缓冲区的处理或数据源的稳定性。如果Source出现问题,可能导致音频解码中断,进而触发restartAudio的调用。 总结来说,restartAudio的功能主要是在音频输出异常时重新初始化音频相关组件,确保播放的连续性。解决问题时需要检查音频配置、设备状态、解码错误以及数据源的稳定性,结合日志和代码路径进行分析。</think>### NuPlayer::restartAudio的功能解析 NuPlayerAndroid原生多媒体框架中负责媒体播放的核心组件,其`restartAudio`方法主要用于**音频输出的异常恢复或配置更新**。以下是具体功能分析: 1. **音频设备重连** 当音频输出设备发生变更(如蓝牙耳机断开/重连、HDMI切换)时,`restartAudio`会重新初始化$mAudioSink$(通过`setAudioSink`接口[^1]),确保音频数据正确路由到新设备。 2. **音频会话管理** 若音频会话ID(AudioSessionId)因系统策略调整需要更新(例如通话优先级抢占),该方法会重新创建`AudioOutput`对象并绑定新的会话ID: ```cpp mAudioOutput = new AudioOutput(newSessionId, ...); mDecoder->setAudioSink(mAudioOutput); ``` 3. **解码异常恢复** 当音频解码器(如NuPlayerDecoder)遇到硬解码器崩溃或缓冲区下溢时,`restartAudio`会触发解码器重启流程,并与Renderer重新同步时间戳[^4]。 --- ### 常见问题排查指南 #### 问题1:音频播放中断后无法恢复 - **可能原因**:`mAudioSink`未正确重新初始化 - **解决步骤**: 1. 检查`MediaPlayerService::Client`中是否成功创建新的`AudioOutput`实例 2. 验证音频属性参数(采样率、声道布局)是否与设备支持格式匹配 3. 通过`dumpsys media.nuplayer`查看`mAudioSink`状态 #### 问题2:设备切换后无声音 - **关键日志**: ```log AudioTrack: Invalid audio session ID / AudioFlinger could not create track ``` - **修复方向**: - 检查`IPCThreadState::self()->getCallingUid()`是否获取到有效UID - 确认`mAudioAttributes`包含正确的`USAGE`和`CONTENT_TYPE`参数 #### 问题3:音频/视频不同步 - **关联代码路径**: ```python NuPlayer::onResume() -> restartAudio() -> notifyVideoRenderingStart() # 需同步更新视频渲染时间基准 ``` - **调试验证**:通过`adb shell media_metrics`检查AV同步差值(`a-v sync offset`) --- ### 实现原理示意图 $$ \begin{array}{ccc} \text{NuPlayer} & \xrightarrow{\text{setAudioSink}} & \text{AudioOutput} \\ \downarrow \text{restartAudio()} & & \downarrow \text{restart()} \\ \text{DecoderBase} & \leftrightarrow & \text{AudioTrack} \end{array} $$ ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值