27、Camera 与 MediaCodec:MediaCodec编码配置、Surface输入模式、编码器与Camera联动、编码参数调优

各位好,今天我们来聊聊 Camera 和 MediaCodec 的联动。说实话,这部分内容在 Android 多媒体开发里算是比较硬核的。我见过不少开发者,Camera 预览跑得挺好,MediaCodec 单独用也没问题,但一要把两者连起来做实时编码,就各种翻车。

为什么会这样?说白了,Camera 和 MediaCodec 是两个独立的模块,它们各自有各自的节奏。Camera 按帧率吐数据,MediaCodec 按自己的节奏消费数据。怎么让它们步调一致?这就是我们今天要解决的核心问题。

核心要点: Camera 与 MediaCodec 的联动,本质上是生产者-消费者模型。Camera 是生产者,MediaCodec 是消费者,Surface 就是它们之间的传送带。

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 模式

我的经验: 比特率设置不要太高。1080p 的视频,8Mbps 就足够了。设太高反而会导致编码器频繁丢帧,画面反而更差。

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 预览尺寸、Surface 尺寸、编码器输入尺寸三者完全一致。否则会出现花屏、绿屏、或者编码失败。

编码器与 Camera 联动

联动这部分,我建议用异步方式。Camera 的回调里不要直接操作编码器,而是通过 Handler 或者回调接口来传递数据。这样能避免死锁。

下面是一个典型的联动流程:

  1. Camera 预览帧到达,通过 Surface 传递给编码器
  2. 编码器内部自动处理数据,输出编码后的帧
  3. 我们在编码器的输出回调中获取编码数据
  4. 将编码数据写入文件或通过网络发送

代码示例:

// 编码器输出回调
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,就是因为手机太烫了。

我的建议: 如果要做长时间录制,建议在编码器配置时降低一些参数。比如 1080p 30fps 降到 720p 30fps,或者降低比特率。这样能减少发热,保证稳定性。

最后,说一个我踩过的坑。有一次我在编码器配置时,把 KEY_PROFILE 设成了 AVCProfileHigh,结果在部分低端设备上编码失败。后来查文档才发现,High 档次不是所有设备都支持的。所以,兼容性优先,能用 Baseline 就别用 High

好了,关于 Camera 和 MediaCodec 的联动,核心内容就这些。记住三点:Surface 模式是首选、参数要匹配、调优要结合实际场景。做到这三点,基本不会出大问题。

Camera 与 MediaCodec 联动架构图 Camera 生产者 输出预览帧 预览数据 Surface 传送带 零拷贝传递 编码输入 MediaCodec 消费者 H.264/H.265编码 编码数据 输出 文件 / 网络 / 显示 编码参数配置 比特率:4-8 Mbps 帧率:30 fps 关键帧间隔:1-2 秒 编码档次:Baseline/Main 配置生效 核心原则:生产者-消费者模型,Surface 作为零拷贝传送带 注意:Camera 预览尺寸、Surface 尺寸、编码器输入尺寸必须一致
公众号:蓝海资料掘金营,微信deep3321