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 模式。流程是这样的:
- 从编码器拿到输入 buffer 的索引
- 把 YUV 数据拷贝进去
- 把 buffer 还给编码器
- 从输出端拿到编码后的数据
- 写入文件或推流
// 输入
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 到编码器的数据流转。你看一眼就明白了:
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 的结合,核心就是这些。配置参数要准,数据流转要顺,资源释放要干净。做到这三点,编码这块基本就稳了。