5、MediaCodec编码基础:MediaCodec工作流程、输入输出缓冲区、编码器配置、异步模式

各位同学,今天我们来啃一块硬骨头——MediaCodec。说实话,我刚接触Android音视频开发那会儿,最怵的就是这玩意儿。API看着简单,但坑是真不少。不过别怕,今天我把它的底裤扒干净,咱们一起看透它。

5.1 MediaCodec是什么?

MediaCodec是Android官方提供的底层编解码器接口。说白了,它就是个音视频数据的"翻译官"——把原始数据(比如摄像头采集的YUV帧)转成压缩格式(比如H.264),或者反过来。

我个人习惯把它理解成一个"黑盒子":你往里面塞原始数据,它吐出来压缩后的数据。至于里面怎么编的,你不用管,但怎么塞、怎么取,这个流程你必须门儿清。

5.2 核心工作流程:状态机驱动

MediaCodec内部是个严格的状态机。我刚开始学的时候没注意这个,结果各种IllegalStateException砸脸。嗯,这里要注意——它只有三种状态:

  • Stopped状态:刚创建或已释放,啥也干不了
  • Executing状态:正在干活,包括Flushed、Running、End-of-Stream三个子状态
  • Released状态:彻底拜拜

你想想看,如果你在Stopped状态去调用dequeueOutputBuffer,那肯定报错。所以写代码前,先画个状态图贴在工位上。

核心原则:任何操作前,先确认当前状态是否允许。我见过太多人栽在这个坑里。

5.3 输入输出缓冲区:生产者-消费者模型

MediaCodec使用了一组输入缓冲区和输出缓冲区。这其实是个经典的生产者-消费者模型:

  • 输入缓冲区:你往里面填原始数据(生产者)
  • 输出缓冲区:你从里面取编码后的数据(消费者)

具体操作流程是这样的:

  1. 调用dequeueInputBuffer()获取一个空闲的输入缓冲区索引
  2. 把原始数据塞进去,调用queueInputBuffer()还给MediaCodec
  3. MediaCodec开始编码,完成后把结果放到输出缓冲区
  4. 调用dequeueOutputBuffer()获取输出缓冲区索引
  5. 取出编码后的数据,处理完调用releaseOutputBuffer()释放

我在项目中遇到过一个问题:如果生产速度远大于消费速度,缓冲区会爆掉。所以一定要控制好节奏,别一股脑往里塞。

小技巧:dequeueInputBuffer的超时参数别设太大,我一般用1000微秒。设成-1会一直阻塞,容易出问题。

5.4 编码器配置:关键参数详解

配置编码器是重中之重。我见过有人把参数写错,结果编码出来的视频全是马赛克。来看一个典型的H.264编码器配置:

MediaFormat format = MediaFormat.createVideoFormat(
    MediaFormat.MIMETYPE_VIDEO_AVC,  // H.264
    width, height
);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000);  // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔1秒

这里有几个参数我要重点说一下:

参数 说明 我的建议
KEY_COLOR_FORMAT 颜色格式,Surface模式用COLOR_FormatSurface 能用Surface就用Surface,性能好很多
KEY_BIT_RATE 码率,单位bps 720p视频建议1.5-2Mbps,1080p建议4-8Mbps
KEY_FRAME_RATE 帧率 直播用24或30,别设太高,手机扛不住
KEY_I_FRAME_INTERVAL 关键帧间隔,单位秒 直播推流建议1-2秒,方便seek

避坑指南:我曾经把KEY_I_FRAME_INTERVAL设成0,结果编码器一个关键帧都不生成,推流出去播放器死活拖不动进度条。后来查了半天文档才发现,0表示不强制插关键帧,全靠编码器自己决定。

5.5 异步模式:告别阻塞烦恼

早期MediaCodec只有同步模式,你得自己写循环去轮询缓冲区。代码又臭又长,还容易卡UI线程。Android 5.0之后引入了异步模式,用回调来处理,清爽多了。

异步模式的核心是两个回调:

  • onInputBufferAvailable:编码器告诉你"我有空闲输入缓冲区了,快来填数据"
  • onOutputBufferAvailable:编码器告诉你"我编码完了,快来取结果"

来看代码怎么写:

MediaCodec codec = MediaCodec.createByCodecName("OMX.google.h264.encoder");
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // 获取输入缓冲区,填入数据
        ByteBuffer inputBuffer = codec.getInputBuffer(index);
        // ... 填入数据 ...
        codec.queueInputBuffer(index, 0, data.length, pts, flags);
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index,
                                        MediaCodec.BufferInfo info) {
        // 获取输出缓冲区,取出编码数据
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
        // ... 处理编码数据 ...
        codec.releaseOutputBuffer(index, false);
    }

    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 处理错误
    }

    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
        // 输出格式变了,比如H.264的SPS/PPS来了
    }
});

codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();

我个人习惯用异步模式,代码结构清晰,不容易出bug。但要注意一点:回调是在编码器的工作线程里执行的,别在里面做耗时操作,否则会阻塞编码流程。

经验之谈:onOutputFormatChanged这个回调特别重要。H.264的SPS/PPS就是通过它传出来的。我第一次做推流时没处理这个回调,结果播放器一直黑屏,折腾了两天才发现是少了SPS/PPS。

5.6 知识体系总览

说了这么多,咱们用一张图把MediaCodec的核心知识串起来:

MediaCodec 核心知识体系 状态机驱动 Stopped → Executing → Released 输入输出缓冲区 生产者-消费者模型 编码器配置 码率/帧率/关键帧间隔 同步 vs 异步模式 回调驱动,告别轮询 核心要点 1. 状态机是基础,操作前先确认状态 2. 缓冲区操作要配对:dequeue → queue → dequeue → release 3. 配置参数要合理,特别是码率和关键帧间隔 4. 异步模式是首选,但注意回调线程不要阻塞

这张图把MediaCodec的四个核心模块串起来了。你写代码的时候,脑子里要有这张图,每一步操作都知道自己在哪个环节。

好了,MediaCodec的基础就讲到这里。记住,这东西就是个工具,多写几遍就熟了。我当年也是被它虐了无数遍,现在闭着眼睛都能写出来。你们也行的。