22、编码器硬件加速:MediaCodec异步模式、Surface输入 vs ByteBuffer输入、编码器参数动态调整、硬件编码器限制与规避
编码器硬件加速,说白了就是让GPU或专用DSP来干编码的活,而不是CPU硬扛。我最早接触这个模块时,总觉得MediaCodec的API文档写得挺清楚,但真到了项目里,坑一个接一个。今天咱们就把这块掰开揉碎了聊。
MediaCodec异步模式:别再阻塞主线程了
早期的MediaCodec用法,大家习惯用同步方式——循环调用dequeueInputBuffer和dequeueOutputBuffer。这种方式简单,但有个致命问题:它阻塞线程。我在项目中遇到过,主线程一旦被编码器卡住,掉帧、ANR就都来了。
我个人习惯,从Android 5.0开始就全面转向异步模式。核心思路是注册回调,让编码器主动通知你“有输入槽位了”或“输出帧准备好了”。
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
codec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 拿到输入缓冲区,塞数据
ByteBuffer inputBuffer = codec.getInputBuffer(index);
// 填充数据...
codec.queueInputBuffer(index, 0, size, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
// 拿到编码后的数据
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
// 处理或写入文件...
codec.releaseOutputBuffer(index, false);
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 错误处理,别忽略
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 编码器动态改了参数,比如分辨率或码率
}
});
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
嗯,这里要注意:回调是在编码器内部线程调用的,别在里面做耗时操作。我曾经见过同事在onOutputBufferAvailable里直接写文件,结果编码器卡死——正确做法是把数据丢到队列里,让另一个线程去写。
Surface输入 vs ByteBuffer输入:选哪个?
这是个经典选择题。我直接说结论:能用Surface就别用ByteBuffer。
| 对比维度 | Surface输入 | ByteBuffer输入 |
|---|---|---|
| 性能 | 极高,零拷贝 | 需要CPU拷贝,开销大 |
| 适用场景 | 相机预览、屏幕录制、视频编辑 | 图像处理后的数据、非实时场景 |
| 内存占用 | 低,共享内存 | 高,需要额外缓冲区 |
| 灵活性 | 低,只能输入图像 | 高,可以输入任意数据 |
为什么Surface性能好?因为编码器直接操作GPU纹理,数据不用经过CPU。你想想看,如果走ByteBuffer,你得先把图像数据从GPU读回CPU,再塞给编码器——这一来一回,带宽和延迟都上去了。
我在项目中做过一个屏幕录制模块,最初用ByteBuffer方式,1080p 30fps就掉帧。后来改成Surface输入,4K 60fps都稳如老狗。说白了,硬件加速的精髓就是减少数据搬运。
核心原则:只要你的数据源是图像(相机、屏幕、视频帧),就优先用Surface输入。只有当你需要对图像做CPU处理(比如加滤镜、人脸检测)时,才考虑ByteBuffer。
编码器参数动态调整:别重启编码器
很多场景下,我们需要在编码过程中调整参数。比如视频通话时,网络变差,得降低码率;或者用户切换摄像头,分辨率要变。
最笨的办法是销毁编码器重新创建。但这样会丢帧,而且有卡顿。我建议用MediaCodec的setParameters方法,它可以在不重启编码器的情况下动态调整。
// 动态调整码率
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, 500000); // 500kbps
codec.setParameters(params);
// 动态调整帧率(部分芯片支持)
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); // 请求关键帧
codec.setParameters(params);
不过要注意,不是所有参数都能动态调。我踩过的坑是:分辨率不能动态改,必须重新configure。码率和帧率大部分芯片支持,但具体效果取决于驱动实现。
小技巧:动态调整码率时,建议配合网络带宽估计模块。我习惯用渐进式调整,每次变化不超过20%,避免编码器内部震荡。
硬件编码器限制与规避:知己知彼
硬件编码器不是万能的。我整理了几个常见限制和应对方案:
- 分辨率限制:大部分芯片只支持16的倍数。我曾经遇到过1920x1088这种奇葩分辨率,编码器直接报错。规避方法:对齐到16的倍数,不足的用黑边填充。
- 码率范围:低端芯片最低码率可能只能到100kbps,再低就花屏。规避方法:设置码率时加个下限检查。
- 关键帧间隔:有些芯片强制关键帧间隔不能太短。规避方法:用setParameters请求关键帧,而不是依赖配置。
- 并发编码数:大部分芯片同时只能跑2-3个编码器。规避方法:做好资源管理,用完及时release。
- 颜色格式:Surface输入默认是COLOR_FormatSurface,ByteBuffer输入常见的是COLOR_FormatYUV420Flexible。规避方法:用MediaCodecInfo查询支持的格式。
曾经踩过的坑:我在一个项目里,用ByteBuffer输入YUV数据,但编码器只支持NV12,我传的是NV21。结果编码出来的视频全是绿色条纹。后来我加了个格式检测逻辑,才彻底解决。
还有一个容易被忽略的点:不同厂商的编码器行为差异很大。高通、联发科、三星的芯片,对同一参数的响应可能完全不同。我建议在测试阶段,至少覆盖主流芯片平台。
最后说一句:硬件编码器的调试,一定要看logcat里的OMX或Codec2日志。很多限制和错误,文档里没写,但日志里会告诉你。我习惯在开发阶段把MediaCodec的日志级别调到VERBOSE,能省不少排查时间。
总结一下:异步模式保流畅,Surface输入提性能,动态参数调灵活,硬件限制早规避。这四个点做到位,编码器这块基本就稳了。
公众号:蓝海资料掘金营,微信deep3321