Android MediaCodec编解码器缓冲区大小不匹配问题咨询
问题
在Android平台使用C++的MediaCodec库实现转码流水线,原本H265解码器运行在surface模式,移除配置时的surface参数改为buffer模式后,解码输出缓冲区已有数据,计划将其复制到H264编码器的输入缓冲区中。
目前遇到的问题是:解码器输出缓冲区大小(3133440字节)大于编码器输入缓冲区大小(3110400字节),且二者颜色格式均为21(YUV420)。对于1080p的YUV420视频,理论总缓冲区大小应为3110400字节(Y平面2073600字节、U/V平面各518400字节)。
编码器配置代码片段:
AMediaFormat* avc_format = AMediaFormat_new(); AMediaFormat_setString(avc_format, AMEDIAFORMAT_KEY_MIME, "video/avc"); AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_WIDTH, 1920); AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_HEIGHT, 1080); AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_BIT_RATE, 17000000); AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1); AMediaFormat_setFloat(avc_format, AMEDIAFORMAT_KEY_FRAME_RATE, 30.0); AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_COLOR_FORMAT, 21); avc_encoder_ = AMediaCodec_createCodecByName("OMX.qcom.video.encoder.avc"); AMediaCodec_configure( avc_encoder_, avc_format, NULL, NULL, AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
获取缓冲区的代码片段:
ssize_t codec_out_buf_idx = AMediaCodec_dequeueOutputBuffer(hevc_decoder_, &info, timeout); ssize_t bufidx = AMediaCodec_dequeueInputBuffer(avc_encoder_, timeout); if (bufidx >= 0) { uint8_t* buf = AMediaCodec_getInputBuffer(avc_encoder_, bufidx, &bufsize); size_t codec_out_bufsize = 0; uint8_t* codec_out_buf = AMediaCodec_getOutputBuffer(hevc_decoder_, codec_out_buf_idx, &codec_out_bufsize); if (codec_out_bufsize > bufsize) { return; } }
解码器输出格式变更回调显示参数为:宽1920、高1080、帧率30、颜色格式21、MIME类型video/raw。尝试过不同颜色格式但均确保与编码器匹配,请问缓冲区大小差异的原因是什么?是否有更优实现方式?
原因分析与解决方案
缓冲区大小差异的原因
- 内存字节对齐优化:MediaCodec底层硬件编解码器(如你使用的高通OMX编码器)会对缓冲区做内存对齐处理,通常按16、32或64字节对齐。解码器输出缓冲区可能因硬件解码的内部要求,采用了更高的对齐标准,导致实际分配的缓冲区比理论值大——比如Y平面按32字节对齐后,总字节数向上取整,加上UV平面的对齐冗余,最终总大小超过理论值。
- 颜色格式的隐性实现差异:虽然都标记为颜色格式21(
OMX_COLOR_FormatYUV420Planar),不同硬件厂商的实现可能存在细微差别。部分解码器会在YUV平面末尾预留少量冗余字节用于硬件临时处理,这部分冗余不影响有效像素数据,但会增加缓冲区总大小。 - 解码器输出缓冲区策略:buffer模式下,解码器可能会分配比实际需求更大的缓冲区,以兼容潜在的分辨率波动(即使你的视频是固定1080p,也可能存在这种预留逻辑)。
更优实现方式
只复制有效像素数据
解码器输出的有效数据就是标准1080p YUV420的3110400字节,可直接忽略缓冲区中超出的部分,仅复制有效数据到编码器输入缓冲区。提交输入时需指定有效数据大小,而非整个缓冲区大小:if (bufidx >= 0 && codec_out_buf_idx >= 0) { uint8_t* enc_buf = AMediaCodec_getInputBuffer(avc_encoder_, bufidx, &bufsize); size_t dec_bufsize = 0; uint8_t* dec_buf = AMediaCodec_getOutputBuffer(hevc_decoder_, codec_out_buf_idx, &dec_bufsize); // 1080p YUV420的有效数据总大小 const size_t valid_size = 1920 * 1080 * 3 / 2; if (bufsize >= valid_size) { memcpy(enc_buf, dec_buf, valid_size); // 提交时指定有效数据大小 AMediaCodec_queueInputBuffer(avc_encoder_, bufidx, 0, valid_size, info.presentationTimeUs, 0); } }Surface直通转码(推荐)
回到Surface模式的思路,直接将解码器的输出Surface作为编码器的输入Surface,完全跳过CPU内存复制步骤——这是Android MediaCodec转码的最优路径,硬件可直接在Surface间传递纹理数据,性能提升显著。
实现逻辑:- 创建
SurfaceTexture,基于它生成同一个Surface,同时作为解码器的输出Surface和编码器的输入Surface - 解码器配置时传入该Surface,编码器配置时也传入此Surface(需保留
AMEDIACODEC_CONFIGURE_FLAG_ENCODE标志) - 通过
SurfaceTexture::updateTexImage()同步帧数据,完成转码流程
- 创建
强制同步缓冲区对齐参数
从解码器输出格式中获取stride(行字节数)和slice_height参数,设置到编码器的格式中,强制编码器使用与解码器一致的缓冲区规格:// 从解码器输出格式中读取对齐参数 int stride, slice_height; AMediaFormat_getInt32(decoder_output_format, AMEDIAFORMAT_KEY_STRIDE, &stride); AMediaFormat_getInt32(decoder_output_format, AMEDIAFORMAT_KEY_SLICE_HEIGHT, &slice_height); // 同步到编码器格式 AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_STRIDE, stride); AMediaFormat_setInt32(avc_format, AMEDIAFORMAT_KEY_SLICE_HEIGHT, slice_height);注意:不是所有硬件编码器都支持自定义这两个参数,需要测试兼容性。
内容的提问来源于stack exchange,提问作者Sumanth Murali

