硬编码优化:Surface输入模式、编码器性能调优、低延迟模式、码率自适应
硬编码这块,说实话坑不少。很多开发者觉得用 MediaCodec 就完事了,但真正上线后才发现——卡顿、花屏、延迟高,各种问题全冒出来了。今天我把这几年踩过的坑和优化经验,一次性讲清楚。
Surface 输入模式:为什么我推荐你用这个
MediaCodec 有两种输入方式:ByteBuffer 模式和 Surface 模式。我个人习惯,只要条件允许,一律用 Surface 模式。
为什么?说白了,ByteBuffer 模式需要你把每一帧的 YUV 数据手动塞给编码器。这个过程涉及内存拷贝,而且你还要自己管理帧率、时间戳。Surface 模式就不一样了——你只需要把 Surface 传给 Camera 或者 OpenGL,数据直接走 GPU 管线,零拷贝。
核心区别:
- ByteBuffer 模式:CPU 参与数据搬运,内存拷贝多,功耗高
- Surface 模式:GPU 直接输出到编码器,零拷贝,功耗低
我在项目中遇到过一个问题:用 ByteBuffer 模式做 1080p 30fps 录制,手机发热严重,掉帧明显。换成 Surface 模式后,温度直接降了 5 度,帧率稳如老狗。
Surface 模式的代码其实很简单:
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIME_TYPE_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();
// 把 inputSurface 传给 Camera 或 OpenGL 即可
嗯,这里要注意:Surface 创建后,编码器就接管了输入。你不能再手动往里面塞数据了。
编码器性能调优:别让编码器成为瓶颈
编码器性能调优,说白了就是让编码器跑得又快又稳。我总结了几个关键点:
1. 分辨率与码率的匹配
很多人喜欢把码率设得特别高,觉得画质好。但编码器处理高码率时,压力会成倍增加。我建议的参考值:
| 分辨率 | 推荐码率范围 | 备注 |
|---|---|---|
| 480p | 500-800 kbps | 低码率场景 |
| 720p | 1-2 Mbps | 主流直播场景 |
| 1080p | 2-4 Mbps | 高清录制 |
| 4K | 8-15 Mbps | 注意发热 |
2. 关键帧间隔
KEY_I_FRAME_INTERVAL 这个参数,我建议设成 1 秒。为什么?因为推流场景下,如果关键帧间隔太长,用户从中间开始看,要等好几秒才能出画面。我曾经遇到过设成 5 秒的情况,用户反馈「黑屏时间太长」,后来改成 1 秒就解决了。
3. 编码器复用
不要每次录制都重新创建编码器。创建编码器的开销很大,尤其是第一次。我习惯用一个编码器池,复用已经创建好的实例。这样切换摄像头或者重新录制时,几乎无感。
小技巧:编码器创建后,先调用 start(),然后立刻调用 stop() 和 release(),这样下次创建会快很多。因为系统会缓存一些资源。
低延迟模式:实时推流的关键
低延迟模式,说白了就是让编码器不要攒太多帧再处理。默认情况下,编码器为了效率,会缓存几帧数据,然后一起编码。这在录制视频时没问题,但推流就不行了——延迟会累积到几百毫秒甚至一秒以上。
Android 提供了低延迟模式,通过 MediaFormat 设置:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
format.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
}
这个参数告诉编码器:别攒帧了,来一帧编一帧。代价是压缩效率会稍微下降,码率会略有波动。但推流场景下,延迟比码率更重要。
我曾经在一个直播项目里,没开低延迟模式,用户说「主播嘴都动半天了,声音才出来」。开了之后,延迟从 800ms 降到了 200ms 以内,体验完全不一样。
注意:低延迟模式不是所有设备都支持。建议在运行时检查 SDK 版本,并且做好降级方案。如果不支持,就默认用标准模式。
码率自适应:应对网络波动
码率自适应,说白了就是根据网络状况动态调整编码码率。网络好的时候用高码率,网络差的时候降码率,避免卡顿和花屏。
实现思路其实不复杂:
- 客户端定期统计发送端的吞吐量
- 如果吞吐量下降,就降低码率
- 如果吞吐量恢复,就逐步提高码率
代码实现上,MediaCodec 支持动态调整码率:
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitRate);
encoder.setParameters(params);
嗯,这里要注意:码率调整不要太频繁。我建议每 2-3 秒调整一次,而且每次调整幅度不要超过 30%。否则编码器内部会频繁重置,反而影响画质。
我见过一个项目,码率自适应做得太激进,每秒钟都在调,结果画面一直在「清晰-模糊-清晰-模糊」之间来回跳,用户体验极差。
知识体系总览
下面这张图,把今天讲的核心内容串起来了:
这四个方面,其实是一个整体。Surface 模式解决输入效率,性能调优解决编码稳定性,低延迟模式解决实时性,码率自适应解决网络适应性。缺一个,整个链路都可能出问题。
好了,今天的内容就到这里。这些优化点,你可以在自己的项目里逐个尝试。记住,不要一次性全改,改一个测一个,才能找到最适合你场景的方案。