12. Camera与MediaCodec结合:MediaCodec配置、编码器输入输出处理、实现自定义视频编码

好,咱们今天聊点硬核的。

Camera 把画面一帧帧送过来了,但裸数据太大了。一帧 1080p 的 NV21 画面,大概要 3MB 左右。如果按 30fps 算,一秒就是 90MB 数据。这谁顶得住?所以必须编码。

Android 上做视频编码,绕不开 MediaCodec。这玩意儿是系统提供的硬件编码器接口,效率极高。我最早接触它是在 Android 4.1 时代,那时候坑还不少,现在成熟多了。

核心思路:Camera 的预览回调(或 Surface)作为生产者,MediaCodec 作为消费者。中间用队列或 Surface 桥接,实现零拷贝或低拷贝的数据流转。

12.1 MediaCodec 的两种工作模式

MediaCodec 有两种玩法:

  • 同步模式(Buffer 模式):你手动去拿输入输出 buffer,填数据、取数据。控制力强,适合做滤镜、裁剪等预处理。
  • 异步模式(Surface 模式):直接把 Surface 丢给编码器,Camera 的预览数据直接喂进去。性能最好,但没法做逐帧处理。

我个人习惯,如果只是单纯录制,用 Surface 模式。如果要做美颜或特效,那就用 Buffer 模式,在编码前插一道处理工序。

12.2 编码器配置:关键参数怎么设

配置 MediaCodec 其实就几步,但参数设不对,后面全是坑。

MediaFormat format = MediaFormat.createVideoFormat(
    MediaFormat.MIME_TYPE_AVC,  // H.264
    width, height
);

format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);

MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

这里有几个参数我特别想强调一下:

参数 说明 我的建议
KEY_BIT_RATE 码率,单位 bps 1080p 建议 8~12Mbps,720p 建议 4~6Mbps
KEY_FRAME_RATE 帧率 和 Camera 的帧率保持一致,别乱设
KEY_I_FRAME_INTERVAL 关键帧间隔,单位秒 1 秒一个关键帧比较稳妥,方便 seek
KEY_COLOR_FORMAT 颜色格式 Surface 模式用 COLOR_FormatSurface,Buffer 模式用 COLOR_FormatYUV420Flexible

注意:不是所有设备都支持 COLOR_FormatYUV420Flexible。我曾经在一台老平板上踩过坑,编码器只认 COLOR_FormatYUV420Planar。建议运行时先查一下 CodecCapabilities。

12.3 编码器输入输出处理

配置好编码器后,调用 start() 就开始干活了。但怎么把数据喂进去,怎么把编码后的数据取出来,这里头有讲究。

12.3.1 Surface 模式:最省心的方式

如果你用 Surface 模式,Camera 直接预览到编码器的 Surface 上。代码大概这样:

Surface inputSurface = encoder.createInputSurface();
// 把这个 Surface 设给 Camera 的预览目标
camera.setPreviewTexture(new SurfaceTexture(10));
// 或者用 Camera2 的话:
captureSession.setRepeatingRequest(request, null, handler);

数据流是:Camera → Surface → 编码器。全程硬件处理,CPU 几乎不参与。我做过测试,这种方式比 Buffer 模式省电 30% 左右。

12.3.2 Buffer 模式:灵活但费劲

如果你需要处理每一帧,那就得用 Buffer 模式。流程是这样的:

  1. 从编码器拿到输入 buffer 的索引
  2. 把 YUV 数据拷贝进去
  3. 把 buffer 还给编码器
  4. 从输出端拿到编码后的数据
  5. 写入文件或推流
// 输入
int inputIndex = encoder.dequeueInputBuffer(timeoutUs);
if (inputIndex >= 0) {
    ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
    inputBuffer.clear();
    inputBuffer.put(yuvData);
    encoder.queueInputBuffer(inputIndex, 0, yuvData.length, pts, 0);
}

// 输出
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(info, timeoutUs);
if (outputIndex >= 0) {
    ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
    // 处理编码后的数据,比如写入 mp4
    muxer.writeSampleData(trackIndex, outputBuffer, info);
    encoder.releaseOutputBuffer(outputIndex, false);
}

小技巧:PTS(显示时间戳)一定要正确。我见过很多人直接用 System.currentTimeMillis(),这是错的。应该用帧序号乘以帧间隔,比如第 0 帧是 0,第 1 帧是 33ms(30fps 时)。否则播放器会抽风。

12.4 实现自定义视频编码

说白了,自定义编码就是把上面这些串起来,形成一个完整的编码器类。我一般会封装成下面这样:

public class VideoEncoder {
    private MediaCodec encoder;
    private MediaMuxer muxer;
    private int trackIndex;
    private boolean isEncoding;
    private long frameIndex;

    public void start(int width, int height, int bitRate, String outputPath) {
        // 配置编码器
        // 创建 MediaMuxer
        // 启动编码器
    }

    public void feedFrame(byte[] yuvData, long timestamp) {
        // 喂数据
    }

    public void stop() {
        // 停止编码器
        // 释放资源
    }
}

这里有个细节:编码器停止时,要等它把缓冲区里的数据都吐出来。我习惯用 signalEndOfInputStream() 通知编码器,然后循环取输出直到 INFO_END_OF_STREAM

坑:MediaMuxer 必须在编码器输出第一个有效数据包之后才能添加轨道。否则会 crash。我早期写代码时就被这个坑过,查了半天才发现是时序问题。

12.5 核心流程图

下面这张图,是我自己总结的 Camera 到编码器的数据流转。你看一眼就明白了:

Camera → MediaCodec 编码数据流 Camera 预览回调 / Surface YUV 数据 预处理 滤镜 / 裁剪 / 旋转 (可选步骤) 处理后的 YUV MediaCodec H.264 / H.265 编码 输入 Buffer / Surface ES 流 MP4 关键说明: 1. 虚线框表示预处理步骤,非必须。纯录制可跳过。 2. Surface 模式下,Camera 直接输出到编码器 Surface,跳过预处理。 3. Buffer 模式下,YUV 数据经过 CPU 拷贝,适合做图像处理。 4. 编码后的 ES 流通过 MediaMuxer 封装为 MP4 文件。

12.6 避坑指南

做编码这块,我踩过的坑比走过的路还多。挑几个典型的说说:

  • 颜色格式不匹配:Camera 输出的是 NV21,但编码器可能只认 I420。需要做格式转换。我一般用 RenderScript 或 libyuv 来做,纯 Java 转太慢了。
  • 编码器超时dequeueInputBuffer 的超时时间别设太长。我习惯设 10ms,然后循环重试。设成 -1 的话,一旦卡住就死锁了。
  • PTS 乱序:B 帧会导致输出顺序和输入顺序不一致。如果你用 H.264 的 Baseline Profile,就没有 B 帧,省心很多。
  • 内存泄漏:编码器用完一定要 release()。我见过有人只调 stop() 不调 release(),结果跑几次就 OOM 了。

我的经验:调试编码器时,先把输出写到文件里,用 ffplay 或 VLC 播放。如果花屏或绿屏,八成是颜色格式或 PTS 的问题。别问我怎么知道的……

嗯,关于 Camera 和 MediaCodec 的结合,核心就是这些。配置参数要准,数据流转要顺,资源释放要干净。做到这三点,编码这块基本就稳了。


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