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。结果编码出来的视频全是绿色条纹。后来我加了个格式检测逻辑,才彻底解决。

还有一个容易被忽略的点:不同厂商的编码器行为差异很大。高通、联发科、三星的芯片,对同一参数的响应可能完全不同。我建议在测试阶段,至少覆盖主流芯片平台。

编码器硬件加速核心流程 输入源 输入方式选择 异步模式回调 Surface输入 ByteBuffer输入 硬件编码器 MediaCodec 动态参数调整 限制与规避策略 编码输出 输入/输出 选择决策 异步/回调 核心编码

最后说一句:硬件编码器的调试,一定要看logcat里的OMX或Codec2日志。很多限制和错误,文档里没写,但日志里会告诉你。我习惯在开发阶段把MediaCodec的日志级别调到VERBOSE,能省不少排查时间。

总结一下:异步模式保流畅,Surface输入提性能,动态参数调灵活,硬件限制早规避。这四个点做到位,编码器这块基本就稳了。


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