MediaCodec是Android系统提供的底层多媒体编解码接口,它直接对接芯片厂商的硬件编解码器,同时也能回退到软件实现。相比FFmpeg软编软解占用大量CPU,MediaCodec能充分利用GPU和专用DSP完成编解码,功耗和性能表现都更优秀,因此直播推流、短视频录制、视频转码等场景几乎都绕不开它。但它也是一个出了名难用的API,状态机严格、缓冲区管理复杂、错误码晦涩,本文将从原理到实战完整梳理它的使用方法。

一、MediaCodec的工作原理与核心概念
理解MediaCodec的第一步是搞清楚它的数据模型。MediaCodec本质上是一个处理器,内部维护了两组缓冲区:输入缓冲区队列和输出缓冲区队列。以解码为例,应用程序从输入队列拿到一个空闲的ByteBuffer,往里面写入压缩数据(比如H264的一帧NALU),调用queueInputBuffer把它交给编解码器;编解码器处理完成后,会通过dequeueOutputBuffer返回解码后的YUV或RGB数据所在的缓冲区索引,应用渲染或读取之后再releaseOutputBuffer归还缓冲区。
第二个关键点是编解码器状态机。MediaCodec有三种状态:未初始化、已配置、执行中,其中执行中又分为刷新、运行、结束流三个子状态。最常踩的坑是在错误的状态调用方法,比如还没configure就start,会直接抛IllegalStateException。还有一个重要概念是CodecException,当底层编解码器发生不可恢复的错误时会抛出这个异常,此时必须release后重新创建实例。
MediaFormat描述了数据的格式信息。解码H264视频时需要传入宽高和名为csd-0、csd-1的Buffer,也就是SPS和PPS参数集,缺少它们解码器无法正确初始化。编码时则通过KEY_MIME指定video/avc,并设置码率、帧率、I帧间隔等参数。这些格式信息在configure阶段决定了编解码器的行为,配置错误往往在运行时才暴露,排查起来比较麻烦。
二、同步模式解码H264实战代码
下面演示最经典的同步模式解码流程。核心循环就是不断轮询输入和输出缓冲区的索引,索引大于等于0表示可用,小于0表示暂时没有可用缓冲区或需要更多时间。代码完整展示了一个解复用加解码的过程:
MediaExtractor extractor = new MediaExtractor();
try {
extractor.setDataSource("/sdcard/test.mp4");
int videoTrack = -1;
MediaFormat format = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat f = extractor.getTrackFormat(i);
String mime = f.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrack = i;
format = f;
break;
}
}
extractor.selectTrack(videoTrack);
MediaCodec codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
codec.configure(format, surface, null, 0);
codec.start();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean eos = false;
while (!eos) {
// 填充输入
int inIndex = codec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buf = codec.getInputBuffer(inIndex);
int size = extractor.readSampleData(buf, 0);
if (size < 0) {
codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
eos = true;
} else {
codec.queueInputBuffer(inIndex, 0, size, extractor.getSampleTime(), 0);
extractor.advance();
}
}
// 读取输出
int outIndex = codec.dequeueOutputBuffer(info, 10000);
if (outIndex >= 0) {
// true表示渲染到surface,不渲染传false
codec.releaseOutputBuffer(outIndex, true);
}
}
codec.stop();
codec.release();
} finally {
extractor.release();
}这段代码有几个细节需要注意。首先configure时传入了Surface,这样解码后的帧会直接渲染到屏幕,性能最好;如果传null则需要手动读取YUV数据自行处理。其次dequeueInputBuffer和dequeueOutputBuffer的超时参数单位是微秒,传0表示不等待立即返回,传-1表示无限等待,生产环境要谨慎使用-1避免ANR。最后,收到BUFFER_FLAG_END_OF_STREAM后编解码器还会输出剩余的几帧,不要提前退出循环。
三、异步模式与硬编码实践
Android 5.0开始官方推荐使用异步模式,通过setCallback注册回调,系统会在缓冲区可用时通知你,不再需要手动轮询。异步模式的代码结构更清晰,也不会因为轮询超时设置不当而丢帧,是现在的主流写法:
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
MediaFormat fmt = MediaFormat.createVideoFormat("video/avc", 1280, 720);
fmt.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
fmt.setInteger(MediaFormat.KEY_BIT_RATE, 4000000);
fmt.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
fmt.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 在这里填充摄像头采集的YUV数据
ByteBuffer buf = codec.getInputBuffer(index);
// buf.put(yuvData); 然后queueInputBuffer
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
ByteBuffer out = codec.getOutputBuffer(index);
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// 这就是csd-0,SPS/PPS数据,推流时必须先发送
info.size = 0;
}
// 处理编码后的H264数据...
codec.releaseOutputBuffer(index, false);
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 检查e.isRecoverable()决定重试还是重建编解码器
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 输出格式确定,可获取实际的csd数据
}
}, handler);
encoder.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();编码场景下有一个必踩的坑:颜色格式。不同厂商设备支持的YUV排布不同,有的用YUV420Planar(I420),有的用YUV420SemiPlanar(NV12),如果直接把摄像头输出的格式塞给期望另一种格式的编码器,画面会出现花屏、绿屏或者颜色错乱。正确做法是通过codec.getCodecInfo().getCapabilitiesForType查询支持的格式,或者在摄像头回调里通过ImageReader拿到具体格式再匹配。另外编码输出的第一帧带BUFFER_FLAG_CODEC_CONFIG标志,它携带的是SPS和PPS,不是有效视频帧,写文件或推流时要单独处理,一般放在文件头或者每个关键帧前面。
性能方面,硬编硬解的优势非常明显。以1080p 30fps为例,软编码在低端机型上可能占用60%以上的CPU且容易发热降频,而硬编码通常只占用5%到15%的CPU。硬编码的缺点是各厂商实现质量参差不齐,某些设备对高分辨率支持有限,可以通过MediaCodecList查询设备支持的编解码器能力范围,必要时做软硬结合的降级方案。
四、常见问题排查与最佳实践
使用MediaCodec时遇到的问题大多集中在几个方面。第一类是configure阶段报错,常见原因是宽高不是16的倍数(部分老设备要求对齐)、码率设置过高超出硬件能力、或者Surface尚未就绪。第二类是缓冲区死锁:同步模式下如果只取输入不取输出,或者只取输出不释放,内部缓冲区耗尽后编解码器会停止工作,表现为没有报错但也不出帧,定位这类问题要检查每次dequeue到的缓冲区是否都正确归还。
第二类高频问题是解码器初始化慢。首次createDecoderByType可能耗时几百毫秒,建议提前创建并预热,比如在页面加载时就完成实例化和configure。切换视频源时优先使用flush而不是release再重建,flush会把编解码器重置到刷新子状态,丢弃待处理数据但保留配置,速度远快于重建。不过flush之后必须从关键帧开始喂数据,否则画面会花屏,这是流媒体场景必须处理的点。
最佳实践方面建议:一是始终在子线程操作MediaCodec,避免阻塞主线程引发ANR;二是Android 5.0以上优先使用异步模式,代码可读性和稳定性都更好;三是封装时对CodecException做好分类处理,判断isRecoverable和isTransient决定重试策略;四是在debug版本打印info.flags和每帧时间戳,方便排查音画不同步和丢帧问题。掌握这些要点后,MediaCodec虽然是底层API,但在实际项目中完全可以构建出稳定高效的视频处理链路。
MediaCodec音视频编解码Android开发修改时间:2026-09-02 20:15:19