Android MediaCodec 笔记

0 阅读6分钟

🎯 一、MediaCodec 的定位:承上启下的“转换器”

MediaCodec 是 Android 多媒体框架中的核心编解码器接口,自 Android 4.1(API 16)引入 。它的主要作用就是为开发者提供一个统一且高效的途径,去调用设备底层(包括硬件和软件)的编解码能力 。

你可以把它理解为一个位于应用层和底层硬件之间的智能“转换器”

  • 承上:为上层 Java/Kotlin 应用提供了 createEncoderByTypeconfigurestart 等简单明了的 API,屏蔽了底层实现的复杂性 。
  • 启下:其内部实现依赖于我们上一轮讨论的 OpenMAX 框架MediaCodec 的调用会经由 StageFright 多媒体框架,最终通过 OpenMAX IL 层与芯片厂商提供的硬件编解码器(如高通、联发科的 DSP/GPU 模块)进行通信 。

通过这种分层设计,MediaCodec 让应用开发者能够轻松利用硬件加速能力,实现高性能、低功耗的音视频处理 。

⚙️ 二、核心工作原理:高效的双缓冲区队列

理解了它的定位,我们来看看它是如何高效工作的。MediaCodec 的核心是一个异步的“生产者-消费者”模型,它内部维护了输入输出两组缓冲区队列 。

image.png

这个流程图清晰地展示了一个完整的数据处理循环 :

  1. 获取空缓冲:客户端(Client,即你的应用)从输入队列中“领取”一个空的输入缓冲区 (dequeueInputBuffer)。
  2. 填充数据:客户端将待处理的原始数据(如编码前的 YUV 视频帧或解码前的 H.264 数据包)填入该缓冲区。
  3. 提交数据:客户端将填满的缓冲区“交还”给 MediaCodec 的输入队列 (queueInputBuffer)。
  4. 核心处理MediaCodec 内部模块取出数据,利用硬件或软件完成编解码工作。
  5. 获取结果:处理完成后,数据被放入输出队列。客户端从输出队列中“领取”一个填满结果的输出缓冲区 (dequeueOutputBuffer)。
  6. 消费并释放:客户端使用完输出数据(如送去渲染或保存)后,将该缓冲区“释放”回输出队列 (releaseOutputBuffer),供 MediaCodec 循环使用。

这种基于环形缓冲区的设计,使得数据的生产和消费可以并行进行,从而最大化编解码效率 。

🔄 三、严格的状态机与生命周期

MediaPlayer 类似,MediaCodec 也拥有一个严格定义的生命周期,这是正确使用它的基础。其生命周期主要包含三大状态:Stopped, Executing, Released

image.png

  • Stopped(停止态):包含 Uninitialized(刚创建)、Configured(已配置)和 Error 三个子状态。
  • Executing(执行态):这是主要的工作状态,包含 Flushed(已刷新)、Running(运行中)和 End-of-Stream(流结束)三个子状态。
  • Released(释放态):调用 release() 后,所有资源被释放,生命周期结束。

理解这个状态机,可以帮助你在错误发生时(进入 Error 子状态)通过 reset() 来恢复,而不是创建一个全新的实例。

💻 四、两种工作模式:同步与异步

MediaCodec 支持两种使用模式,你可以根据场景灵活选择 。

特性同步模式异步模式
适用版本API 16+API 21+ (Android 5.0)
核心APIdequeueInputBuffer
dequeueOutputBuffer
setCallback
工作方式主动轮询缓冲区状态被动接收缓冲区可用回调
优点控制逻辑简单直接代码结构清晰,效率更高,避免主线程阻塞
缺点需自行管理线程循环回调在多线程环境,需注意线程安全

1. 同步模式示例 (典型解码循环)

这是最经典的使用方式,通常在一个单独的线程中循环执行 。

// 假设在子线程中执行
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();

MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (!isFinished) {
    // 1. 处理输入:喂数据
    int inputIndex = codec.dequeueInputBuffer(TIMEOUT_US);
    if (inputIndex >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
        // 从文件/网络读取数据到 inputBuffer ...
        codec.queueInputBuffer(inputIndex, 0, dataSize, presentationTimeUs, flags);
    }

    // 2. 处理输出:取数据
    int outputIndex = codec.dequeueOutputBuffer(info, TIMEOUT_US);
    if (outputIndex >= 0) {
        // 对于视频,如果配置了 surface,这里会自动渲染
        codec.releaseOutputBuffer(outputIndex, true); // true 表示渲染
        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            break; // 播放结束
        }
    } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 输出格式变化,比如获取了 sps/pps 后的新格式
        MediaFormat newFormat = codec.getOutputFormat();
    }
}
codec.stop();
codec.release();

2. 异步模式示例 (API 21+)

异步模式通过 setCallback 设置监听器,让代码更加解耦和高效 。

MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
// 设置异步回调
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // 当有输入缓冲区可用时,在此填充数据
        ByteBuffer inputBuffer = codec.getInputBuffer(index);
        // ... 填充数据
        codec.queueInputBuffer(index, 0, dataSize, pts, flags);
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        // 当有输出缓冲区可用时,在此处理解码后的数据
        // 释放缓冲区并选择是否渲染
        codec.releaseOutputBuffer(index, true /* render */);
    }

    @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(); // 启动后,回调便开始工作

异步模式在 Android 5.0 之后引入,可以配合 Handler 指定回调执行的线程,非常适合对性能要求较高的场景 。

🛠️ 五、工程实践与优化建议

在实际项目中,用好 MediaCodec 还需要注意以下几点:

  1. 选择合适的编解码器:在配置之前,可以通过 MediaCodecList 遍历所有可用的编解码器,并根据设备支持的分辨率、帧率等能力,选择最适合当前硬件的编码器 。
  2. 预计算缓冲区大小:对于视频,特别是 4K 等高分辨率内容,可以预先根据视频的宽高和颜色格式(如 YUV420)计算出所需的缓冲区大小,并预留 10%-20% 的余量,以应对动态分辨率切换,避免运行时频繁申请内存 。
  3. 正确处理 EOS:当输入队列结束时,记得在最后一帧的 flags 中设置 BUFFER_FLAG_END_OF_STREAM。同时,在输出循环中也要检查该标志,以便安全地结束解码并释放资源 。
  4. 善用 Surface 减少拷贝:对于视频解码,如果最终目的是渲染到屏幕,那么在 configure() 时传入一个 Surface(来自 SurfaceViewTextureView)。之后调用 releaseOutputBuffer(index, true) 即可实现零拷贝渲染,这是最高效的方式 。
  5. 硬件解码失败回退:尽管 MediaCodec 旨在使用硬件,但某些设备或格式可能不支持。因此,在编解码器创建或配置失败时,捕获异常并准备一个软件解码方案(如通过 FFmpeg)作为降级策略,是保障应用健壮性的关键 。
  6. 及时释放资源MediaCodec 实例会占用昂贵的硬件资源。务必在 onPause()onDestroy() 等生命周期结束点,调用 stop()release() 来释放它,防止资源泄漏 。