9. MediaCodec编码实战:H.264/H.265硬编码、编码参数配置、实时编码
各位同学,今天我们来聊聊MediaCodec编码实战。说实话,编码这块儿在Android音视频开发里,算是比较硬核的部分了。我当年刚接触时也被各种参数搞得头晕,但一旦摸透了,你会发现它其实很有规律。
9.1 硬编码 vs 软编码:你该怎么选?
先问个问题:为什么我们要用硬编码?说白了,硬编码就是让GPU或专门的硬件模块去干活,软编码则是CPU硬扛。我做过一个项目,用软编码在低端机上跑1080p视频,手机直接烫得能煎鸡蛋。换成硬编码后,温度降了十几度,帧率还稳了。
| 对比项 | 硬编码 | 软编码 |
|---|---|---|
| CPU占用 | 低(5%-15%) | 高(30%-80%) |
| 功耗 | 低 | 高 |
| 编码质量 | 中等(取决于芯片) | 高(可精细控制) |
| 灵活性 | 低(参数受限) | 高(可自定义算法) |
| 适用场景 | 实时推流、录制 | 离线转码、专业制作 |
我个人习惯是:实时场景一律硬编码,离线场景才考虑软编码。你想想看,用户正在直播,你让CPU满载,画面卡顿不说,电量也撑不住。
9.2 MediaCodec编码器初始化
初始化编码器,其实就三步:创建编码器、配置参数、启动。但每一步都有坑,我一个个说。
9.2.1 创建编码器
Android提供了两种方式创建编码器。我建议用第二种,更灵活。
// 方式一:直接指定MIME类型
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
// 方式二:通过MediaCodecList查找(推荐)
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
String codecName = codecList.findEncoderForFormat(mediaFormat);
MediaCodec encoder = MediaCodec.createByCodecName(codecName);
为什么要用第二种?因为有些设备上,你指定的编码器可能不支持你想要的参数。通过MediaCodecList查找,能确保找到可用的编码器。我曾经在某个国产平板上踩过这个坑,直接指定"video/avc"结果编码出来的视频全是花屏。
9.2.2 配置编码参数
配置参数是编码的核心。MediaFormat就像一张配置表,你往里面填参数,编码器按这个干活。
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
format.setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000); // 4Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); // 30fps
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔1秒
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // 输入格式
format.setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); // H.264 High Profile
format.setInteger(MediaFormat.KEY_LEVEL,
MediaCodecInfo.CodecProfileLevel.AVCLevel41); // Level 4.1
关键参数说明:
- BIT_RATE:码率,直接影响画质和文件大小。1080p一般2-8Mbps
- FRAME_RATE:帧率,30fps是主流,60fps对硬件要求高
- I_FRAME_INTERVAL:关键帧间隔,直播场景建议1秒,点播可以2-3秒
- COLOR_FORMAT:颜色格式,Surface输入效率最高
9.2.3 启动编码器
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
嗯,这里要注意:configure的第三个参数是MediaCrypto,编码时传null就行。第四个参数必须传CONFIGURE_FLAG_ENCODE,否则编码器不会启动。
9.3 H.264 vs H.265:选哪个?
这个问题我经常被问到。直接说结论:
- H.264:兼容性最好,几乎所有设备都支持。码率控制成熟,画质稳定。
- H.265:压缩率更高,同等画质下码率能省30%-50%。但编码更耗电,老设备可能不支持。
我个人的经验是:如果是做直播或短视频,优先用H.264。因为用户设备千奇百怪,H.264最保险。如果是做本地录制或点播,可以考虑H.265,能省不少存储空间。
// H.265编码器创建
MediaFormat format = MediaFormat.createVideoFormat("video/hevc", 1920, 1080);
// 其他参数配置与H.264类似
format.setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.HEVCProfileMain);
9.4 实时编码流程
实时编码的核心是循环:不断从输入队列取数据,编码后放入输出队列。我画了一张流程图,帮你理解这个过程。
看到这个循环了吗?这就是编码器的核心工作模式。我写个完整的代码示例:
// 实时编码循环
while (!isStopRequested) {
// 1. 获取输入缓冲区
int inputIndex = encoder.dequeueInputBuffer(10000); // 10秒超时
if (inputIndex >= 0) {
ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
// 填充YUV数据到inputBuffer
// ...
encoder.queueInputBuffer(inputIndex, 0, dataSize, presentationTimeUs, 0);
}
// 2. 获取输出缓冲区
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 没有数据,稍后重试
continue;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 输出格式变化,通常在编码开始时触发
MediaFormat newFormat = encoder.getOutputFormat();
// 处理格式变化(如初始化muxer)
} else if (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
// 处理编码后的数据(写入文件或发送网络)
// ...
encoder.releaseOutputBuffer(outputIndex, false);
}
}
小技巧:dequeueInputBuffer的超时时间不要设太长,10毫秒就够了。设太长会导致编码延迟增加,实时场景下很致命。
9.5 编码参数调优实战
参数配置不是一成不变的,得根据场景调。我分享几个实战经验:
9.5.1 码率控制模式
MediaCodec支持三种码率控制模式:
// CBR:恒定码率,适合直播
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
// VBR:可变码率,适合本地录制
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
// CQ:恒定质量,适合专业场景
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);
我个人习惯:直播用CBR,保证码率稳定不卡顿;本地录制用VBR,画质更好;CQ模式慎用,有些设备不支持。
9.5.2 编码质量与性能平衡
有些设备上,你设了高码率但编码器跑不动,就会丢帧。我建议先做一次性能探测:
// 获取编码器能力
MediaCodecInfo codecInfo = new MediaCodecList(MediaCodecList.REGULAR_CODECS)
.findCodecByName(codecName);
MediaCodecInfo.CodecCapabilities capabilities =
codecInfo.getCapabilitiesForType("video/avc");
// 检查支持的码率范围
int maxBitrate = capabilities.getVideoCapabilities().getBitrateRange().getUpper();
int maxFrameRate = capabilities.getVideoCapabilities().getSupportedFrameRates().getUpper();
// 根据能力设置参数
int targetBitrate = Math.min(4_000_000, maxBitrate); // 不超过设备上限
注意:不要盲目设高参数。我曾经在某个低端机上设了4K@60fps编码,结果编码器直接崩溃。一定要先检查设备能力,再设参数。
9.6 常见问题与避坑指南
做编码开发,总会遇到各种问题。我整理了几个高频问题:
- 编码出来的视频花屏:通常是输入数据格式不对。检查YUV数据排列方式,Android上默认是NV12或NV21。
- 编码延迟越来越高:可能是输出缓冲区没及时释放。记得每次dequeueOutputBuffer后都要releaseOutputBuffer。
- 关键帧不生效:有些设备上I_FRAME_INTERVAL设了1但实际不生效。可以手动请求关键帧:
encoder.setParameters(new Bundle().putInt("request-sync", 0))。 - 编码器崩溃:多半是参数超出设备能力。用MediaCodecList查一下支持的Profile和Level。
我曾经遇到过一个奇葩问题:某款手机上编码出来的视频时间戳全乱套。排查了半天,发现是presentationTimeUs传的是系统时间,而不是相对时间。记住,时间戳要从0开始递增,单位是微秒。
9.7 性能优化建议
最后,分享几个性能优化的经验:
- 使用Surface输入:比ByteBuffer输入效率高很多,尤其是Camera预览直接编码的场景。
- 控制编码帧率:不要超过Camera的实际帧率,否则编码器会积压。
- 合理设置缓冲区数量:默认的缓冲区数量可能不够,可以尝试增加:
format.setInteger("max-input-size", width * height * 3 / 2)。 - 异步模式:Android 5.0+支持异步编码,用Callback代替循环,代码更简洁,性能也更好。
// 异步编码示例
encoder.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 填充数据
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
// 处理编码数据
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 错误处理
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 格式变化
}
});
嗯,编码这块儿内容不少,但核心就是理解编码器的工作流程,然后根据场景调参数。多动手写代码,遇到问题别慌,先查设备能力,再查数据格式,基本能解决90%的问题。