🎯 一、MediaCodec 的定位:承上启下的“转换器”
MediaCodec 是 Android 多媒体框架中的核心编解码器接口,自 Android 4.1(API 16)引入 。它的主要作用就是为开发者提供一个统一且高效的途径,去调用设备底层(包括硬件和软件)的编解码能力 。
你可以把它理解为一个位于应用层和底层硬件之间的智能“转换器” :
- 承上:为上层 Java/Kotlin 应用提供了
createEncoderByType、configure、start等简单明了的 API,屏蔽了底层实现的复杂性 。 - 启下:其内部实现依赖于我们上一轮讨论的 OpenMAX 框架。
MediaCodec的调用会经由 StageFright 多媒体框架,最终通过 OpenMAX IL 层与芯片厂商提供的硬件编解码器(如高通、联发科的 DSP/GPU 模块)进行通信 。
通过这种分层设计,MediaCodec 让应用开发者能够轻松利用硬件加速能力,实现高性能、低功耗的音视频处理 。
⚙️ 二、核心工作原理:高效的双缓冲区队列
理解了它的定位,我们来看看它是如何高效工作的。MediaCodec 的核心是一个异步的“生产者-消费者”模型,它内部维护了输入和输出两组缓冲区队列 。
这个流程图清晰地展示了一个完整的数据处理循环 :
- 获取空缓冲:客户端(Client,即你的应用)从输入队列中“领取”一个空的输入缓冲区 (
dequeueInputBuffer)。 - 填充数据:客户端将待处理的原始数据(如编码前的 YUV 视频帧或解码前的 H.264 数据包)填入该缓冲区。
- 提交数据:客户端将填满的缓冲区“交还”给
MediaCodec的输入队列 (queueInputBuffer)。 - 核心处理:
MediaCodec内部模块取出数据,利用硬件或软件完成编解码工作。 - 获取结果:处理完成后,数据被放入输出队列。客户端从输出队列中“领取”一个填满结果的输出缓冲区 (
dequeueOutputBuffer)。 - 消费并释放:客户端使用完输出数据(如送去渲染或保存)后,将该缓冲区“释放”回输出队列 (
releaseOutputBuffer),供MediaCodec循环使用。
这种基于环形缓冲区的设计,使得数据的生产和消费可以并行进行,从而最大化编解码效率 。
🔄 三、严格的状态机与生命周期
和 MediaPlayer 类似,MediaCodec 也拥有一个严格定义的生命周期,这是正确使用它的基础。其生命周期主要包含三大状态:Stopped, Executing, Released 。
- Stopped(停止态):包含
Uninitialized(刚创建)、Configured(已配置)和Error三个子状态。 - Executing(执行态):这是主要的工作状态,包含
Flushed(已刷新)、Running(运行中)和End-of-Stream(流结束)三个子状态。 - Released(释放态):调用
release()后,所有资源被释放,生命周期结束。
理解这个状态机,可以帮助你在错误发生时(进入 Error 子状态)通过 reset() 来恢复,而不是创建一个全新的实例。
💻 四、两种工作模式:同步与异步
MediaCodec 支持两种使用模式,你可以根据场景灵活选择 。
| 特性 | 同步模式 | 异步模式 |
|---|---|---|
| 适用版本 | API 16+ | API 21+ (Android 5.0) |
| 核心API | dequeueInputBufferdequeueOutputBuffer | 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 还需要注意以下几点:
- 选择合适的编解码器:在配置之前,可以通过
MediaCodecList遍历所有可用的编解码器,并根据设备支持的分辨率、帧率等能力,选择最适合当前硬件的编码器 。 - 预计算缓冲区大小:对于视频,特别是 4K 等高分辨率内容,可以预先根据视频的宽高和颜色格式(如 YUV420)计算出所需的缓冲区大小,并预留 10%-20% 的余量,以应对动态分辨率切换,避免运行时频繁申请内存 。
- 正确处理 EOS:当输入队列结束时,记得在最后一帧的
flags中设置BUFFER_FLAG_END_OF_STREAM。同时,在输出循环中也要检查该标志,以便安全地结束解码并释放资源 。 - 善用 Surface 减少拷贝:对于视频解码,如果最终目的是渲染到屏幕,那么在
configure()时传入一个Surface(来自SurfaceView或TextureView)。之后调用releaseOutputBuffer(index, true)即可实现零拷贝渲染,这是最高效的方式 。 - 硬件解码失败回退:尽管
MediaCodec旨在使用硬件,但某些设备或格式可能不支持。因此,在编解码器创建或配置失败时,捕获异常并准备一个软件解码方案(如通过 FFmpeg)作为降级策略,是保障应用健壮性的关键 。 - 及时释放资源:
MediaCodec实例会占用昂贵的硬件资源。务必在onPause()或onDestroy()等生命周期结束点,调用stop()和release()来释放它,防止资源泄漏 。