Android MediaCodec 解码 H264 到 Surface 实战指南:从原理到避坑
·
为什么需要关注视频解码?
在开发视频播放器或实时流应用时,经常遇到画面卡顿、延迟飙升甚至应用崩溃的问题。比如:
- 明明网络带宽足够,但画面总是跳帧
- 手机发热严重时出现绿色马赛克
- 快速切换视频源时引发内存泄漏

视图容器选型:SurfaceView vs TextureView
两种常见方案对比:
- SurfaceView
- 独立绘图表面,性能更好
- 不支持动画变换
-
适合全屏播放场景
-
TextureView
- 支持平移/旋转等动画
- 消耗更多内存
- 适合需要界面融合的场景
// SurfaceView基础用法
val surfaceView = findViewById<SurfaceView>(R.id.surface).apply {
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// 在这里初始化MediaCodec
}
})
}
核心解码流程拆解
1. 初始化MediaCodec
fun initDecoder(surface: Surface): MediaCodec {
// 创建解码器实例
val codec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
// 关键配置参数
val format = MediaFormat().apply {
setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC)
setInteger(MediaFormat.KEY_WIDTH, 1280)
setInteger(MediaFormat.KEY_HEIGHT, 720)
// 必须包含CSD数据(SPS/PPS)
setByteBuffer("csd-0", ByteBuffer.wrap(spsBytes))
setByteBuffer("csd-1", ByteBuffer.wrap(ppsBytes))
}
// 同步模式配置
codec.configure(format, surface, null, 0)
codec.start()
return codec
}
2. 数据输入与输出处理
典型状态机处理流程:
- 获取可用输入缓冲区索引
- 填充H264数据(注意时间戳计算)
- 提交给解码器
- 获取输出缓冲区
- 渲染到Surface

// 同步模式处理示例
fun decodeFrame(codec: MediaCodec, inputData: ByteArray) {
val inputBufferId = codec.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferId >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferId)
inputBuffer?.clear()?.put(inputData)
codec.queueInputBuffer(
inputBufferId,
0,
inputData.size,
computePresentationTimeUs(),
0
)
}
val bufferInfo = MediaCodec.BufferInfo()
val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
when {
outputBufferId >= 0 -> {
// 自动渲染到Surface
codec.releaseOutputBuffer(outputBufferId, true)
}
outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
// 格式变化处理
}
}
}
必须掌握的优化技巧
线程模型设计
建议采用生产者-消费者模式:
val decodeThread = HandlerThread("DecoderThread").apply { start() }
val decodeHandler = Handler(decodeThread.looper)
// 数据输入
fun queueFrame(data: ByteArray) {
decodeHandler.post {
decodeFrame(codec, data)
}
}
关键参数调优
- 设置
KEY_MAX_INPUT_SIZE避免缓冲区溢出 - 使用
KEY_FRAME_RATE提示解码器 - 配置
KEY_OPERATING_RATE适配性能
踩坑经验分享
1. 解码器复用
// 停止时先flush再stop
fun releaseDecoder() {
codec.stop()
codec.release()
surface.release() // 必须释放Surface
}
2. EOS信号处理
// 发送结束标志
codec.queueInputBuffer(
inputBufferId,
0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
// 检测结束标志
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
// 处理播放完成逻辑
}
进阶方向
- 尝试AV1解码:检查
MediaCodecList支持情况 - 实现解码+编码流水线
- 结合OpenGL ES做后处理
完整示例项目已放在GitHub仓库,包含线程同步和异常处理完整实现。
更多推荐


所有评论(0)