7、MediaCodec基础:MediaCodec架构、异步与同步模式、编解码器配置

各位同学,今天我们来聊聊Android音视频开发中最核心的一个组件——MediaCodec。说实话,我当年刚接触这个API的时候,也被它那套状态机搞得晕头转向。但只要你理解了它的设计思路,就会发现它其实非常优雅。

7.1 MediaCodec的整体架构

MediaCodec说白了,就是一个音视频数据的"加工厂"。你给它原始数据,它给你吐出编码或解码后的结果。这个工厂内部有一套标准的流水线作业流程。

核心概念:MediaCodec采用输入输出队列模型。你往输入队列塞数据,它处理完以后,你去输出队列取结果。就这么简单。

我画了一张图,帮你理解这个架构:

MediaCodec 架构示意图 输入缓冲区队列 Input Buffers MediaCodec 核心 编解码引擎 + 状态机 输出缓冲区队列 Output Buffers dequeueInputBuffer dequeueOutputBuffer 应用层(生产者) 应用层(消费者) queueInputBuffer releaseOutputBuffer 内部状态机 Uninitialized → Configured → Executing → Flushed → End of Stream → Released

你看,整个流程非常清晰。应用层作为生产者,把原始数据塞进输入队列。MediaCodec核心处理完后,把结果放到输出队列。应用层再去取。嗯,这里要注意,队列是循环复用的,不是无限大的。

7.2 同步模式 vs 异步模式

MediaCodec支持两种工作模式:同步和异步。我刚开始用的时候,一直搞不清楚该选哪个。后来踩过坑才明白,这俩各有各的适用场景。

7.2.1 同步模式

同步模式是最传统的用法。说白了,就是你在主线程或者工作线程里,循环调用dequeueInputBuffer和dequeueOutputBuffer。代码写起来很直观:

MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();

// 同步模式:循环处理
while (!isDone) {
    // 获取输入缓冲区
    int inputIndex = codec.dequeueInputBuffer(10000); // 超时10ms
    if (inputIndex >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
        // 填充数据...
        codec.queueInputBuffer(inputIndex, 0, size, pts, flags);
    }

    // 获取输出缓冲区
    BufferInfo info = new BufferInfo();
    int outputIndex = codec.dequeueOutputBuffer(info, 10000);
    if (outputIndex >= 0) {
        // 处理输出(比如渲染到Surface)
        codec.releaseOutputBuffer(outputIndex, true);
    }
}

我的经验:同步模式适合简单的播放器场景。我在做一个短视频App时,就用同步模式,代码逻辑很清晰,调试也方便。但要注意,dequeueInputBuffer的超时时间别设太长,否则容易卡住UI。

7.2.2 异步模式

异步模式是Android 5.0引入的。它用回调的方式通知你:有输入缓冲区可用了,或者有输出缓冲区可用了。我个人更喜欢这种模式,因为它更符合事件驱动的思想。

MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);

// 设置异步回调
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        ByteBuffer inputBuffer = codec.getInputBuffer(index);
        // 填充数据...
        codec.queueInputBuffer(index, 0, size, pts, flags);
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
        // 处理输出
        codec.releaseOutputBuffer(index, true);
    }

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

    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 出错了
    }
});

codec.start();

避坑指南:我曾经在异步模式下犯过一个低级错误——在回调里直接做耗时操作。记住,回调是在Binder线程里执行的,千万别在里面做I/O或者复杂计算。否则会阻塞整个编解码流程。

为什么会这样?因为MediaCodec的回调线程和编解码引擎是共享同一个线程池的。你在回调里卡住了,编解码引擎也卡住了。

7.3 编解码器配置详解

配置MediaCodec,说白了就是告诉它:你要处理什么格式的数据,输出到哪里,期望什么效果。我见过很多新手在configure这一步就翻车了。

7.3.1 核心配置参数

参数 说明 示例值
MIME类型 指定音视频编码格式 "video/avc"、"audio/mp4a-latm"
宽高 视频分辨率 1920x1080
比特率 编码时的目标码率 2000000 (2Mbps)
帧率 视频帧率 30
颜色格式 像素数据格式 COLOR_FormatYUV420SemiPlanar
I帧间隔 关键帧间隔(编码器) 1 (全I帧) 或 30 (每30帧一个I帧)

7.3.2 配置代码示例

我们来看一个完整的配置流程。以H.264编码器为例:

// 创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");

// 配置MediaFormat
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1280, 720);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000);       // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);          // 30fps
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);     // 每2秒一个I帧
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);

// 配置编码器
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

关键点:解码器配置时,最后一个参数是0(解码)。编码器配置时,最后一个参数是CONFIGURE_FLAG_ENCODE。这个别搞反了,我见过有人传错参数,结果编解码器直接罢工。

7.3.3 解码器配置的特殊之处

解码器配置和编码器不太一样。解码器通常需要指定输出Surface(如果你要做直接渲染的话):

// 创建解码器
MediaCodec decoder = MediaCodec.createDecoderByType("video/avc");

// 配置MediaFormat
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
// 解码器不需要设置比特率、帧率等编码参数
// 但可以设置一些解码相关的参数
format.setInteger(MediaFormat.KEY_MAX_WIDTH, 1920);
format.setInteger(MediaFormat.KEY_MAX_HEIGHT, 1080);

// 配置解码器,输出到Surface
decoder.configure(format, surface, null, 0);  // 最后一个参数0表示解码
decoder.start();

我的建议:如果你只是做解码渲染,强烈建议用Surface模式。这样解码器直接输出到Surface,省去了你手动处理YUV数据的麻烦。我在做视频播放器时,用Surface模式性能提升了至少30%。

7.4 状态机与生命周期

MediaCodec内部有一套严格的状态机。你想想看,如果不按状态来操作,API会直接抛异常。我刚开始学的时候,经常在错误的状态调用错误的方法,然后一脸懵逼地看异常日志。

状态流转是这样的:

  • Uninitialized:刚创建完,啥也没干
  • Configured:调用了configure,但还没start
  • Executing:调用了start,正在干活
  • Flushed:调用了flush,清空所有缓冲区
  • End of Stream:收到了EOS标志,处理完最后的数据
  • Released:调用了release,资源释放

避坑指南:我曾经在Executing状态下直接调了release,结果导致内存泄漏。正确的做法是先stop,再release。虽然API文档说可以直接release,但为了保险起见,我建议你按部就班来。

7.5 缓冲区管理技巧

缓冲区管理是MediaCodec使用中最容易出问题的地方。我总结了几条经验:

  1. 及时释放:输出缓冲区用完就release,别占着茅坑不拉屎。缓冲区数量有限,你不释放,编解码器就没法继续干活。
  2. 不要缓存引用:ByteBuffer对象是复用的,别把它存起来以后再用。我见过有人把getInputBuffer返回的ByteBuffer存到列表里,结果数据全乱了。
  3. 注意PTS:Presentation Time Stamp一定要正确设置。尤其是编码时,PTS乱了,播放端就会卡顿或者花屏。
  4. 处理EOS:在输入队列塞入BUFFER_FLAG_END_OF_STREAM后,要等到输出队列也收到这个标志,才算真正结束。

好了,关于MediaCodec的基础知识就讲到这里。这些内容虽然基础,但非常重要。你把这些搞明白了,后面学音视频合成、实时处理、硬编硬解这些高级话题,就会轻松很多。


公众号:蓝海资料掘金营,微信deep3321