硬编码优化: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 版本,并且做好降级方案。如果不支持,就默认用标准模式。

码率自适应:应对网络波动

码率自适应,说白了就是根据网络状况动态调整编码码率。网络好的时候用高码率,网络差的时候降码率,避免卡顿和花屏。

实现思路其实不复杂:

  1. 客户端定期统计发送端的吞吐量
  2. 如果吞吐量下降,就降低码率
  3. 如果吞吐量恢复,就逐步提高码率

代码实现上,MediaCodec 支持动态调整码率:

Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitRate);
encoder.setParameters(params);

嗯,这里要注意:码率调整不要太频繁。我建议每 2-3 秒调整一次,而且每次调整幅度不要超过 30%。否则编码器内部会频繁重置,反而影响画质。

我见过一个项目,码率自适应做得太激进,每秒钟都在调,结果画面一直在「清晰-模糊-清晰-模糊」之间来回跳,用户体验极差。

知识体系总览

下面这张图,把今天讲的核心内容串起来了:

硬编码优化核心知识体系 Surface 输入模式 零拷贝 GPU 直出 降低 CPU 负载 减少内存拷贝 推荐首选方案 编码器性能调优 分辨率码率匹配 关键帧间隔 1s 编码器复用池 避免频繁创建 低延迟模式 来一帧编一帧 延迟降至 200ms API 26+ 支持 需做降级方案 码率自适应 动态调整码率 应对网络波动 2-3s 调整一次 幅度不超过 30% 优化目标 低延迟 + 低功耗 + 高画质 + 抗网络波动 实践建议 1. 优先使用 Surface 模式 + 低延迟模式 2. 码率自适应要平滑调整,避免画质频繁跳变

这四个方面,其实是一个整体。Surface 模式解决输入效率,性能调优解决编码稳定性,低延迟模式解决实时性,码率自适应解决网络适应性。缺一个,整个链路都可能出问题。

好了,今天的内容就到这里。这些优化点,你可以在自己的项目里逐个尝试。记住,不要一次性全改,改一个测一个,才能找到最适合你场景的方案。

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