第1章 相机与MediaCodec:硬件编码与实时流推送

大家好,我是老张。今天咱们聊聊Android开发里一个挺硬核的话题——MediaCodec。说实话,我刚开始接触这个API的时候,也被它绕得够呛。但搞明白了之后,你会发现它其实就是个“编码黑盒”,你把数据喂进去,它把压缩好的码流吐出来。

嗯,咱们先搞清楚一件事:为什么非得用MediaCodec?

你想想看,手机摄像头采集到的是原始图像数据,一帧1080p的YUV420图像,大概3MB左右。如果30帧每秒,那就是90MB/s的数据量。这谁顶得住?别说网络传输了,本地存储都扛不住。所以必须压缩。

软件编码(比如用FFmpeg的x264)虽然质量好,但功耗高、发热大。我曾在项目里试过纯软件编码,手机烫得能煎鸡蛋。MediaCodec是硬件编码器,功耗低、速度快,说白了就是专门干这活的。

1.1 MediaCodec的基本工作流

MediaCodec的工作模式,我习惯把它理解成一个“三明治”结构:

核心流程: 输入缓冲区 → 编码器处理 → 输出缓冲区

具体来说,分四步走:

  1. 创建编码器 —— 指定编码格式(H.264或H.265)
  2. 配置编码器 —— 设置分辨率、码率、帧率等参数
  3. 启动编码器 —— 开始干活
  4. 循环处理 —— 不断喂数据、取数据

这里有个坑,我一开始踩过:MediaCodec的输入输出是异步的。你不能指望喂一帧进去,立刻就能拿到输出。它内部有缓冲队列,你得按它的节奏来。

1.2 H.264与H.265编码参数

咱们先看一张表,对比一下这两种编码器:

参数 H.264 (AVC) H.265 (HEVC)
压缩率 基准 比H.264高30%-50%
计算复杂度 较低 较高
设备兼容性 几乎全部支持 中高端设备支持
码率节省 同等画质下码率更低

我个人习惯是:如果是做直播推流,优先选H.264。为什么?兼容性好。你想想看,观众那边可能是各种千奇百怪的手机,H.264基本都能硬解。H.265虽然省带宽,但解码器不是所有设备都有。

但如果是录本地视频,或者你确定目标设备都支持H.265,那就果断上H.265。我在一个监控项目里用过H.265,同样的存储空间,录像时长能多存将近一倍。

1.3 关键编码参数设置

配置MediaCodec的时候,有几个参数特别重要:

  • 码率(Bitrate) —— 决定了视频质量。码率越高,画质越好,文件也越大。直播场景一般用1-2Mbps,本地录制可以到5-10Mbps。
  • 帧率(Frame Rate) —— 通常设为30fps。但如果你做的是慢动作录制,可能需要60fps甚至120fps。
  • I帧间隔(Key Frame Interval) —— 这个很关键。直播场景下,我建议设为1-2秒。为什么呢?因为如果网络丢包了,解码器需要等到下一个I帧才能恢复画面。间隔太短,码率浪费;间隔太长,丢包后黑屏时间太久。
  • 编码档次(Profile) —— H.264有Baseline、Main、High三个档次。直播用Baseline就够了,兼容性最好。

我的经验: 码率控制模式建议用VBR(可变码率)。CBR(固定码率)虽然码率稳定,但画面复杂时容易糊。VBR能在保证画质的前提下,尽量节省带宽。

1.4 实时流推送的核心逻辑

实时流推送,说白了就是把编码后的数据打包成流媒体协议,然后发出去。最常见的协议是RTMP和HLS。

这里我画了一张流程图,帮你理清整个链路:

实时流推送核心流程 摄像头采集 YUV原始数据 MediaCodec硬件编码 H.264/H.265码流 推流协议封装 • RTMP • HLS • SRT 编码后的数据通过RTMP等协议推送到流媒体服务器

你看,整个链路其实不复杂。摄像头采集到YUV数据,喂给MediaCodec,编码成H.264/H.265码流,然后打包成RTMP或者HLS发出去。

但这里有个细节:MediaCodec输出的数据是ES流(Elementary Stream),不能直接推。你需要先封装成FLV或者TS格式,再通过RTMP发送。这个封装过程,我一般用MediaMuxer或者自己写个简单的FLV封装器。

1.5 代码示例:创建并配置MediaCodec编码器

光说不练假把式,咱们直接上代码。这是我项目里摘出来的一个编码器初始化片段:

// 创建H.264编码器
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");

// 配置编码参数
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1280, 720);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000);  // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);  // 2秒一个I帧
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 
    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
format.setInteger(MediaFormat.KEY_PROFILE, 
    MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
format.setInteger(MediaFormat.KEY_LEVEL, 
    MediaCodecInfo.CodecProfileLevel.AVCLevel31);

// 配置编码器
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

注意: COLOR_FORMAT这个参数很坑。不同设备支持的YUV格式不一样。我遇到过一台老手机,只支持COLOR_FormatYUV420Planar,不支持SemiPlanar。建议先通过CodecCapabilities查询一下设备支持的格式列表。

1.6 避坑指南

我曾经在一个项目里,因为I帧间隔设得太长(10秒),结果直播时网络一波动,观众端黑屏了将近10秒才恢复。后来改成2秒,体验好多了。

还有一次,我忘了设置KEY_PROFILE,默认用了High Profile。结果推流到某些老旧的解码器上,直接花屏。嗯,从那以后我学乖了,直播场景一律用Baseline。

另外,MediaCodec的输入数据需要保证时间戳是单调递增的。如果你从Camera2回调里拿到的时间戳是纳秒级的,记得转成微秒。不然编码器会报错,甚至直接崩掉。

1.7 小结

好了,这一章咱们把MediaCodec的基本用法、编码参数、实时流推送的核心逻辑都过了一遍。说白了,硬件编码就是个“配置-启动-循环”的过程。参数设置上,码率、帧率、I帧间隔这三个是重中之重。

下一章咱们会深入Camera2的采集流程,看看怎么把摄像头数据喂给MediaCodec。到时候我会分享一个我在实际项目中踩过的坑——Camera2的预览尺寸和编码尺寸不匹配,导致画面拉伸变形。嗯,到时候细聊。


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