27、Camera 与 MediaCodec:MediaCodec编码配置、Surface输入模式、编码器与Camera联动、编码参数调优
各位好,今天我们来聊聊 Camera 和 MediaCodec 的联动。说实话,这部分内容在 Android 多媒体开发里算是比较硬核的。我见过不少开发者,Camera 预览跑得挺好,MediaCodec 单独用也没问题,但一要把两者连起来做实时编码,就各种翻车。
为什么会这样?说白了,Camera 和 MediaCodec 是两个独立的模块,它们各自有各自的节奏。Camera 按帧率吐数据,MediaCodec 按自己的节奏消费数据。怎么让它们步调一致?这就是我们今天要解决的核心问题。
MediaCodec 编码配置
先说说 MediaCodec 的配置。我个人习惯用 MediaFormat 来构建编码参数,而不是直接操作 MediaCodec 的底层配置。这样更清晰,也更容易调试。
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, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔
MediaCodec encoder = MediaCodec.createEncoderByType(
MediaFormat.MIMETYPE_VIDEO_AVC);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
这里有个坑,我提醒一下。COLOR_FormatSurface 这个参数,很多人会忽略。如果你不用 Surface 输入模式,那 Camera 的数据就得通过 ByteBuffer 手动塞给编码器。效率低不说,还容易丢帧。所以,一定要用 Surface 模式。
Surface 输入模式
Surface 输入模式,说白了就是让 Camera 直接把数据渲染到一个 Surface 上,然后 MediaCodec 从这个 Surface 上取数据编码。这样省去了内存拷贝,效率高很多。
具体怎么做?看代码:
// 从编码器获取输入 Surface
Surface inputSurface = encoder.createInputSurface();
// 把这个 Surface 设置给 Camera
Camera camera = Camera.open();
Camera.Parameters params = camera.getParameters();
// ... 其他参数设置
camera.setPreviewTexture(inputSurface); // 关键步骤
camera.startPreview();
嗯,这里要注意一点。Camera 的 setPreviewTexture 方法,在 Camera2 API 里对应的是 createCaptureSession 时传入的 Surface。如果你用的是 Camera2,代码会稍微复杂一些,但核心逻辑是一样的。
我曾经在项目里遇到过一个问题:编码器输出的视频总是花屏。排查了半天,发现是 Surface 的尺寸和 Camera 预览尺寸不匹配。你想想看,Camera 输出的是 1920x1080,但 Surface 是 1280x720,编码器拿到的是被拉伸过的数据,不花屏才怪。
编码器与 Camera 联动
联动这部分,我建议用异步方式。Camera 的回调里不要直接操作编码器,而是通过 Handler 或者回调接口来传递数据。这样能避免死锁。
下面是一个典型的联动流程:
- Camera 预览帧到达,通过 Surface 传递给编码器
- 编码器内部自动处理数据,输出编码后的帧
- 我们在编码器的输出回调中获取编码数据
- 将编码数据写入文件或通过网络发送
代码示例:
// 编码器输出回调
encoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputBufferAvailable(
MediaCodec codec, int index, MediaCodec.BufferInfo info) {
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
// 处理编码后的数据
// 写入文件或发送网络
codec.releaseOutputBuffer(index, false);
}
@Override
public void onInputBufferAvailable(
MediaCodec codec, int index) {
// Surface 模式下不需要手动输入数据
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 错误处理
}
@Override
public void onOutputFormatChanged(
MediaCodec codec, MediaFormat format) {
// 输出格式变化,比如编码器重新配置
}
});
这里有个细节:Surface 模式下,onInputBufferAvailable 回调基本不会触发。因为数据是通过 Surface 自动传递的,不需要我们手动塞。我第一次用的时候还傻傻地等这个回调,结果等了半天没反应。
编码参数调优
调优这部分,我踩过的坑最多。直接上干货:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 比特率 | 4-8 Mbps (1080p) | 太低画面模糊,太高浪费带宽 |
| 帧率 | 30 fps | 大部分场景够用,60fps 对编码器压力大 |
| 关键帧间隔 | 1-2 秒 | 太短增加码率,太长影响 seek 操作 |
| 编码档次 | Baseline 或 Main | High 档次兼容性差,部分设备不支持 |
我个人习惯,在调优时先跑一遍基准测试。用固定的 Camera 画面(比如拍一张白纸),然后调整参数看编码后的画质和码率。这样能快速找到最优参数组合。
还有一个容易被忽略的点:编码器的性能受温度影响很大。手机发热后,编码器会自动降频,导致编码速度变慢。我曾经在户外测试时,发现编码帧率从 30fps 掉到了 15fps,就是因为手机太烫了。
最后,说一个我踩过的坑。有一次我在编码器配置时,把 KEY_PROFILE 设成了 AVCProfileHigh,结果在部分低端设备上编码失败。后来查文档才发现,High 档次不是所有设备都支持的。所以,兼容性优先,能用 Baseline 就别用 High。
好了,关于 Camera 和 MediaCodec 的联动,核心内容就这些。记住三点:Surface 模式是首选、参数要匹配、调优要结合实际场景。做到这三点,基本不会出大问题。