3. MediaCodec 编解码:编解码器生命周期、输入输出缓冲区、异步模式、硬件加速

各位好,我是老张。今天我们来聊聊 Android 多媒体开发里最核心、也最让人头疼的一个组件——MediaCodec。说实话,我刚接触它的时候,也被它的状态机和缓冲区管理搞得晕头转向。但一旦你摸清了它的脾气,你会发现它其实非常强大。

这一节,我会带你彻底搞懂 MediaCodec 的四个关键点:生命周期缓冲区流转异步模式硬件加速。嗯,这些都是实战中绕不开的坎。

3.1 编解码器的生命周期:状态机才是灵魂

MediaCodec 内部维护了一个严格的状态机。说白了,它就像一台精密的仪器,每个状态能做什么、不能做什么,都是规定死的。你如果不按流程来,它就会给你抛异常。

我个人习惯把它的生命周期分为三个阶段:停止态执行态释放态。下面这张图可以帮你快速建立整体认知。

MediaCodec 生命周期状态机 停止态 (Stopped) 执行态 (Executing) 释放态 (Released) Uninitialized Configured Error Flushed Running End-of-Stream start() stop() / reset() release() 子状态:Flushed → Running → End-of-Stream

你想想看,一个编解码器从创建到销毁,要经历这么多状态。我早期做项目时,就因为在 stop() 之后没有正确 reset(),导致下次启动时一直卡在 Error 状态。嗯,这里要注意:一旦进入 Error 状态,必须调用 reset() 才能回到 Uninitialized 状态

3.2 输入输出缓冲区:数据流转的核心

MediaCodec 的数据处理模型,说白了就是一个生产者-消费者模式。你往输入缓冲区里塞数据,它从输出缓冲区里吐数据。但这里有个关键点:缓冲区是编解码器管理的,不是你直接 new 出来的

我个人习惯把缓冲区操作总结为四个步骤:

  1. 获取输入缓冲区:调用 dequeueInputBuffer(),拿到一个可写的索引。
  2. 填充数据:往这个索引对应的 ByteBuffer 里写入原始数据。
  3. 提交给编解码器:调用 queueInputBuffer(),告诉它数据准备好了。
  4. 获取输出缓冲区:调用 dequeueOutputBuffer(),拿到解码后的数据。

这里有一个我当年踩过的坑:千万不要在获取到缓冲区后,直接操作它的 position 和 limit。我曾经因为手动修改了 position,导致解码出来的画面全是绿的。后来才发现,MediaCodec 内部对缓冲区的状态有严格依赖。

核心要点:输入输出缓冲区是循环复用的。你用完一个输出缓冲区后,必须调用 releaseOutputBuffer() 把它还给编解码器,否则它会一直占用,最终导致 dequeueOutputBuffer() 返回 INFO_TRY_AGAIN_LATER

3.3 异步模式:告别阻塞,拥抱回调

在 Android 5.0 之前,MediaCodec 只有同步模式。你想想看,在主线程里调用 dequeueOutputBuffer(),如果数据还没准备好,它就会一直阻塞。这体验太糟糕了。

从 API 21 开始,MediaCodec 引入了异步模式。说白了,就是通过 setCallback() 注册一个回调,编解码器会在合适的时机通知你。我个人非常推荐使用异步模式,尤其是在做实时音视频处理时。

下面是一个典型的异步模式代码骨架:

MediaCodec codec = MediaCodec.createDecoderByType(mimeType);
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // 有输入缓冲区可用了,赶紧填数据
        ByteBuffer inputBuffer = codec.getInputBuffer(index);
        // ... 填充数据 ...
        codec.queueInputBuffer(index, 0, size, presentationTimeUs, flags);
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        // 有输出缓冲区可用了,赶紧处理
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
        // ... 处理数据 ...
        codec.releaseOutputBuffer(index, true);
    }

    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
        // 输出格式变了,比如分辨率变化
    }

    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 出错了,需要处理
    }
});
codec.configure(format, surface, null, 0);
codec.start();

我的建议:异步模式下,回调是在 Binder 线程池中执行的。不要在回调里做耗时操作,否则会阻塞编解码器。如果需要更新 UI,记得 post 到主线程。

3.4 硬件加速:让 GPU 帮你干活

MediaCodec 默认会优先使用硬件编解码器。你想想看,软件编解码虽然兼容性好,但 CPU 占用率高、功耗大。在移动设备上,硬件加速几乎是必须的。

如何判断当前设备是否支持硬件加速?很简单,通过 MediaCodecList 来查询:

MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
String codecName = codecList.findDecoderForFormat(format);
if (codecName != null) {
    // 找到了对应的编解码器,大概率是硬件加速的
    MediaCodec codec = MediaCodec.createByCodecName(codecName);
}

这里有个细节:硬件编解码器的名称通常包含 "omx" 和 ".h264" 等字样。比如 OMX.qcom.video.decoder.avc 就是高通平台的硬件 H.264 解码器。

我曾经踩过的坑:有些设备上的硬件编解码器对分辨率有严格限制。比如某款低端平板,只支持 1920x1080 以下的硬件解码。如果你强行输入 4K 视频,它会直接崩溃。所以,在生产环境中,一定要做降级处理:如果硬件解码失败,就回退到软件解码。

另外,硬件加速还有一个隐藏福利:Surface 模式。如果你把解码器的输出配置为一个 Surface,那么解码后的数据会直接渲染到 Surface 上,省去了你手动拷贝 YUV 数据的步骤。这在做视频播放器时特别有用。

3.5 实战避坑指南

最后,我总结几个实战中容易踩的坑,希望能帮你少走弯路:

  • 不要混用同步和异步 API:一旦调用了 setCallback(),就不能再使用 dequeueInputBuffer() 等同步方法,否则会抛 IllegalStateException
  • 及时释放输出缓冲区:如果 releaseOutputBuffer() 调用不及时,会导致编解码器内部缓冲区耗尽,出现卡顿。
  • 处理 EOS 标志:当输入数据全部提交后,记得在最后一个 queueInputBuffer() 中设置 MediaCodec.BUFFER_FLAG_END_OF_STREAM。编解码器会在输出端返回同样的标志。
  • 硬件解码失败时不要慌张:捕获 CodecException,然后尝试使用软件解码器。我一般会准备一个备用的 MediaCodec 实例。

好了,关于 MediaCodec 的核心知识点就讲到这里。这些内容是我在实际项目中反复验证过的,希望能对你有所帮助。如果你在实践中有任何问题,欢迎随时交流。