第1章 相机与MediaCodec:硬件编码与实时流推送
大家好,我是老张。今天咱们聊聊Android开发里一个挺硬核的话题——MediaCodec。说实话,我刚开始接触这个API的时候,也被它绕得够呛。但搞明白了之后,你会发现它其实就是个“编码黑盒”,你把数据喂进去,它把压缩好的码流吐出来。
嗯,咱们先搞清楚一件事:为什么非得用MediaCodec?
你想想看,手机摄像头采集到的是原始图像数据,一帧1080p的YUV420图像,大概3MB左右。如果30帧每秒,那就是90MB/s的数据量。这谁顶得住?别说网络传输了,本地存储都扛不住。所以必须压缩。
软件编码(比如用FFmpeg的x264)虽然质量好,但功耗高、发热大。我曾在项目里试过纯软件编码,手机烫得能煎鸡蛋。MediaCodec是硬件编码器,功耗低、速度快,说白了就是专门干这活的。
1.1 MediaCodec的基本工作流
MediaCodec的工作模式,我习惯把它理解成一个“三明治”结构:
核心流程: 输入缓冲区 → 编码器处理 → 输出缓冲区
具体来说,分四步走:
- 创建编码器 —— 指定编码格式(H.264或H.265)
- 配置编码器 —— 设置分辨率、码率、帧率等参数
- 启动编码器 —— 开始干活
- 循环处理 —— 不断喂数据、取数据
这里有个坑,我一开始踩过: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发出去。
但这里有个细节: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