Android MediaCodec 编解码
文章目录
- 概述
- 一、核心概念与工作原理
- 1.编码器 (Encoder)
- 2.解码器 (Decoder)
- 3.MediaCodec 工作模型
- 4. MediaCodec 生命周期
- 5. 基本工作流程
- 二、关键组件与 参数
- 1.组件
- 2.参数解析
- 三、常见应用场景
- 四、使用中的关键注意事项与难点
概述
Android MediaCodec 是 Android 系统提供的底层 API,用于访问设备的硬件(或软件)编解码器,实现高效、低功耗的音视频编码和解码。它是构建高性能多媒体应用(如视频播放器、视频录制、直播推流、视频编辑等)的核心组件。
一、核心概念与工作原理
MediaCodec 采用异步的生产者 - 消费者模型,通过输入和输出缓冲区队列处理数据。
1.编码器 (Encoder)
生产者:应用(提供原始数据,如 YUV 格式的视频帧或 PCM 格式的音频采样)。
消费者:MediaCodec 编码器(接收原始数据,输出压缩后的数据,如 H.264 视频流或 AAC 音频流)。
2.解码器 (Decoder)
生产者:应用(提供压缩数据)。
消费者:MediaCodec 解码器(接收压缩数据,输出原始数据)。
3.MediaCodec 工作模型
MediaCodec 采用双缓冲区队列(输入/输出)实现异步数据处理,其架构可分为以下三层:
客户端(Client)
- 输入端:填充待编解码的原始数据(如 YUV 视频帧、PCM 音频)到输入缓冲区队列。
- 输出端:从输出缓冲区队列读取编解码后的数据(如 H.264 流、AAC 音频)并进行渲染或播放。
编解码器(Codec)
- 硬件加速层:优先调用设备专属编解码器(如高通 DSP、ARM Mali),显著降低 CPU 负载。
- 处理逻辑:从输入队列取出数据,执行编码/解码后,将结果存入输出队列,并回收缓冲区供复用。
缓冲区队列(Buffer Queue)
- 输入队列:存储待处理的原始数据缓冲区(ByteBuffer 数组)。
- 输出队列:存储处理后的数据缓冲区,供客户端消费。
4. MediaCodec 生命周期
生命周期执行顺序和各各声明周期详解如下:
-
Uninitialized(未初始化)
创建方式:调用 MediaCodec.createEncoderByType() 或 createDecoderByType() 后进入。
允许操作:
configure(…)
release() → 跳转到 Released
禁止操作:调用 start(), dequeueInputBuffer(), getInputBuffer() 等会抛出 IllegalStateException
⚠️ 此时还未配置参数,不能进行任何数据处理。 -
Configured(已配置)
进入方式:在 Uninitialized 状态下调用 configure(mediaFormat, surface, crypto, flags)。
允许操作:
start() → 跳转到 Executing
release() → 跳转到 Released
禁止操作:调用 dequeueInputBuffer() 等数据操作会抛异常。
📌 注意:如果使用 Surface 输入/输出(如相机或播放器),Surface 必须在 configure 时传入,之后不能更改。 -
Executing(执行中) ← 核心工作状态
进入方式:在 Configured 状态下调用 start()。
子状态:
Flushed(刚启动或调用 flush() 后)
Running(正常处理数据中)
End-of-Stream(收到 EOS 信号,正在清空缓冲区)
➤ Executing - Flushed
刚调用 start() 或 flush() 后进入。
输入/输出缓冲区队列为空。
第一次调用 dequeueInputBuffer() 会返回有效索引。
➤ Executing - Running
正常编解码状态。
可以反复调用:
dequeueInputBuffer() + queueInputBuffer() → 提交数据
dequeueOutputBuffer() + releaseOutputBuffer() → 获取并释放结果
➤ Executing - End-of-Stream
当你调用 queueInputBuffer(…, …, BUFFER_FLAG_END_OF_STREAM) 后进入。
编解码器会继续输出剩余数据,直到 dequeueOutputBuffer() 返回带有 BUFFER_FLAG_END_OF_STREAM 的 buffer。
此时仍需继续处理输出缓冲区,直到收到 EOS。
✅ 在 Executing 状态下可以调用:
flush() → 回到 Flushed 子状态(清空所有缓冲区,用于 seek 或重新开始)
stop() → 跳转到 Uninitialized
release() → 跳转到 Released
4. Released(已释放)
进入方式:在任何状态下调用 release()。
特点:
所有资源被释放,包括底层硬件编解码器实例。
对象不可再使用,任何方法调用都会抛出 IllegalStateException。
GC 会回收 Java 对象,但 native 资源必须手动 release() 才能释放。
✅ 最佳实践:在 Activity/Fragment 销毁、Surface 被销毁、或编码完成时,必须调用 release()
5. 基本工作流程
- 创建 (Create):通过 MediaCodec.createEncoderByType() 或MediaCodec.createDecoderByType() 创建实例。
- 配置 (Configure):使用 MediaFormat 对象设置编解码参数(如分辨率、码率、帧率、颜色格式、MIME类型等),然后调用 configure() 方法。
- 启动 (Start):调用 start() 方法,使编解码器进入运行状态,此时可开始访问输入和输出缓冲区。 处理数据 (Process):
import android.media.MediaCodec;
import android.media.MediaFormat;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;public class H264Encoder {private static final String MIME_TYPE = "video/avc"; // H.264 MIME 类型private MediaCodec mEncoder;private FileOutputStream mOutputStream; // 用于写入 H.264 文件public void configure(int width, int height, int bitrate, int frameRate, int iframeInterval) {try {// 1. 创建编码器mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);// 2. 创建 MediaFormat 并设置参数MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);// 关键参数设置format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); // 推荐使用 Flexibleformat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); // 码率,单位 bpsformat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); // 帧率format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval); // I帧间隔,单位秒format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); // 码率模式,可选 CBR/VBR/CQ// 3. 配置编码器(指定为编码器,传入 null 表示不关联 Surface)mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);// 4. 启动编码器mEncoder.start();// 5. 打开输出文件mOutputStream = new FileOutputStream("output.h264");} catch (IOException e) {e.printStackTrace();}}
}
- 输入:调用 dequeueInputBuffer() 获取可用输入缓冲区索引,通过 getInputBuffer() 获取缓冲区,写入数据后调用 queueInputBuffer() 提交给编解码器。
- 输出:调用 dequeueOutputBuffer() 获取包含处理后数据的输出缓冲区索引,通过getOutputBuffer()获取数据,处理完毕后调用 releaseOutputBuffer() 释放缓冲区。
public void encodeFrame(byte[] yuvData, long presentationTimeUs) {try {// --- 处理输入 ---// 1. 获取输入缓冲区的索引int inputBufferIndex = mEncoder.dequeueInputBuffer(10000); // 超时 10msif (inputBufferIndex >= 0) {ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);inputBuffer.clear();// 2. 将 YUV 数据复制到输入缓冲区// 注意:这里假设 yuvData 的格式与 MediaFormat 中设置的 COLOR_FORMAT 一致!// 如果格式不匹配,需要先进行转换(例如 NV21 -> I420 或 NV12)inputBuffer.put(yuvData);// 3. 提交输入缓冲区给编码器// presentationTimeUs 是此帧的时间戳,单位微秒 (us)// 如果是最后一帧,需要加上 BUFFER_FLAG_END_OF_STREAM 标志mEncoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, presentationTimeUs, 0);}// --- 处理输出 ---MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();int outputBufferIndex;while ((outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, 10000)) >= 0) {if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// 这是编解码器配置信息 (SPS/PPS),通常需要保存并在文件开头或每个 I 帧前写入// 对于 H.264 文件,通常将 SPS/PPS 写在文件最开头ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);byte[] spsPps = new byte[bufferInfo.size];outputBuffer.get(spsPps);// 将 spsPps 写入文件mOutputStream.write(spsPps);// 释放输出缓冲区mEncoder.releaseOutputBuffer(outputBufferIndex, false);continue;}if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {// 编码结束break;}// 获取包含编码后 H.264 数据的输出缓冲区ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);byte[] encodedData = new byte[bufferInfo.size];outputBuffer.get(encodedData);// 将编码后的 H.264 NAL 单元写入文件// 通常需要在每个 NAL 单元前添加起始码 0x00000001mOutputStream.write(new byte[]{0, 0, 0, 1});mOutputStream.write(encodedData);// 释放输出缓冲区mEncoder.releaseOutputBuffer(outputBufferIndex, false);}} catch (IOException e) {e.printStackTrace();}
}
- 结束 (Stop & Release):处理完所有数据后,发送结束信号(在 queueInputBuffer() 时设置
BUFFER_FLAG_END_OF_STREAM),等待并处理完所有输出缓冲区,最后调用 stop() 和 release() 释放资源。
public void stopAndRelease() {try {// 1. 发送结束信号int inputBufferIndex = mEncoder.dequeueInputBuffer(-1); // 阻塞等待if (inputBufferIndex >= 0) {mEncoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);}// 2. 处理所有剩余的输出数据MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();int outputBufferIndex;do {outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, 10000);if (outputBufferIndex >= 0) {if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {break; // 确认结束}ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);byte[] encodedData = new byte[bufferInfo.size];outputBuffer.get(encodedData);mOutputStream.write(new byte[]{0, 0, 0, 1});mOutputStream.write(encodedData);mEncoder.releaseOutputBuffer(outputBufferIndex, false);}} while (outputBufferIndex >= 0 || outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER);// 3. 关闭文件流if (mOutputStream != null) {mOutputStream.close();}// 4. 停止并释放编码器资源mEncoder.stop();mEncoder.release();mEncoder = null;} catch (IOException e) {e.printStackTrace();}
}
二、关键组件与 参数
1.组件
- MediaCodec:核心类,负责编解码操作。
- MediaForma:用于描述媒体数据的格式。在配置编解码器时,必须提供正确的 MediaFormat。
- MediaCodec.BufferInfo:伴随输出缓冲区返回,包含数据大小、时间戳、偏移量和标志位(如 BUFFER_FLAG_CODEC_CONFIG,BUFFER_FLAG_END_OF_STREAM)。
- MediaCodecList:用于查询设备支持的编解码器及其能力。从 Android 10 (API 29) 开始,可以查询编解码器是否为硬件加速 (isHardwareAccelerated()) 以及支持的性能点 (getSupportedPerformancePoints())。
2.参数解析
视频编码关键参数 (Encoder)
这些参数在创建视频编码器(如 H.264, H.265)时配置,决定了输出视频流的特性和质量。
参数键 (Key) | 类型 | 含义说明 | 常用值 / 示例 | 重要性 | 备注 |
---|---|---|---|---|---|
KEY_MIME | String | 编码格式 MIME 类型 | “video/avc” (H.264), “video/hevc” (H.265), “video/x-vnd.on2.vp8” (VP8) | 必须 | 与 createEncoderByType() 一致 |
KEY_WIDTH | int | 视频帧宽度(像素) | 1920, 1280, 720 | 必须 | |
KEY_HEIGHT | int | 视频帧高度(像素) | 1080, 720, 480 | 必须 | |
KEY_COLOR_FORMAT | int | 输入原始数据的颜色格式 | COLOR_FormatYUV420Flexible(推荐), COLOR_FormatYUV420SemiPlanar (NV12) | 必须 | 格式不匹配会导致花屏!需转换相机 NV21 → NV12/I420 |
KEY_BIT_RATE | int | 目标码率(单位:bps) | 2 * 1024 * 1024(2Mbps) | 核心 | 影响画质与体积 |
KEY_FRAME_RATE | int | 目标帧率(FPS) | 30, 25, 60 | 核心 | 应与输入帧率匹配 |
KEY_I_FRAME_INTERVAL | int | I 帧(关键帧)间隔(单位:秒) | 1, 2, 3 | 重要 | 直播推荐 1~3 秒;影响拖动和容错 |
KEY_BITRATE_MODE | int | 码率控制模式 | BITRATE_MODE_CBR, BITRATE_MODE_VBR, BITRATE_MODE_CQ | 重要 | CBR 适合直播,VBR 节省空间 |
KEY_PROFILE | int | 编码 Profile(档次) | AVCProfileBaseline, AVCProfileMain, AVCProfileHigh | 可选 | 不设置则用默认,影响兼容性 |
KEY_LEVEL | int | 编码 Level(级别) | AVCLevel3, AVCLevel4, AVCLevel5 | 可选 | 限制分辨率/码率上限 |
视频解码关键参数 (Decoder)
参数键 (Key) | 类型 | 含义说明 | 常用值 / 示例 | 重要性 | 备注 |
---|---|---|---|---|---|
KEY_MIME | String | 解码格式 MIME 类型 | “video/avc”, “video/hevc” | 必须 | 需与码流一致 |
KEY_WIDTH | int | 视频帧宽度(从码流中解析) | 1920, 1280 | 自动获取 | 通常从容器读取 |
KEY_HEIGHT | int | 视频帧高度(从码流中解析) | 1080, 720 | 自动获取 | |
KEY_COLOR_FORMAT | int | 通常被忽略,输出格式由 Surface 决定 | - | 无关 | 解码到 ByteBuffer 时使用默认格式 |
KEY_MAX_INPUT_SIZE | int | 建议输入缓冲区最大大小(字节) | 根据分辨率估算,如 width * height * 3 / 2 + 1024 | 推荐设置 | 避免 queueInputBuffer 失败 |
音频关键参数
参数键 (Key) | 类型 | 含义说明 | 常用值 / 示例 | 重要性 | 备注 |
---|---|---|---|---|---|
KEY_MIME | String | 音频编码格式 | “audio/mp4a-latm” (AAC), “audio/opus”, “audio/vorbis” | 必须 | |
KEY_SAMPLE_RATE | int | 采样率(Hz) | 44100, 48000, 22050, 16000 | 必须 | |
KEY_CHANNEL_COUNT | int | 声道数 | 1 (单声道), 2 (立体声) | 必须 | |
KEY_BIT_RATE | int | 音频码率(bps) | 128000, 64000, 96000 | 核心 | VBR 模式下为平均/最大码率 |
KEY_AAC_PROFILE | int | AAC 编码 Profile | AACObjectLC(最常用), AACObjectHE, AACObjectHE_PS | 推荐设置 | LC 兼容性好,HE 低码率高效 |
三、常见应用场景
- 视频录制与编码:从相机获取 YUV 数据,使用 MediaCodec 编码为 H.264/H.265 格式,然后封装成 MP4 文件(通常需要配合 MediaMuxer)。
- 视频播放与解码:读取 MP4 文件中的 H.264 数据,使用 MediaCodec 解码为 YUV/RGB 数据,然后渲染到 SurfaceView 或 TextureView 上。
- 直播推流:实时采集音视频数据,编码后通过网络协议(如 RTMP)发送到服务器。
- 视频转码/编辑:解码源视频,进行裁剪、滤镜等处理,再重新编码。
- 音频处理:录制 PCM 音频并编码为 AAC,或解码 AAC 为 PCM 进行播放或分析。
四、使用中的关键注意事项与难点
- YUV 格式匹配:相机输出的格式(通常是 NV21)与编码器期望的格式(通常是 NV12 或 I420)往往不同。必须进行格式转换,否则会出现花屏。可以使用 libyuv 库或 OpenGL ES 进行高效转换。
- SPS/PPS 处理:对于 H.264/H.265 编码,编解码器会在开始时输出包含 SPS/PPS 的 BUFFER_FLAG_CODEC_CONFIG 数据。这些数据是解码的关键,必须保存并在文件开头或流中正确发送。
- 时间戳管理:正确设置每一帧的 presentationTimeUs(单位微秒)对于音视频同步和流畅播放至关重要。
- 异步处理:虽然 MediaCodec 本身是同步 API,但为了不阻塞主线程,通常需要在子线程中进行编解码循环。
- 资源释放:务必在使用完毕后调用 stop() 和 release(),否则可能导致内存泄漏或后续编解码失败。
- 设备兼容性:不同设备支持的编解码器、颜色格式和性能可能不同。建议使用 MediaCodecList 进行查询和适配。