限时福利领取


背景与痛点

在 Android 平台上处理视频解码时,MediaCodec 是开发者最常用的工具之一。但直接将 H264 视频流解码到 Surface 时,往往会遇到一些棘手的问题,比如帧丢失、延迟过高甚至内存泄漏。这些问题的背后,通常是由于对 MediaCodec 的工作机制理解不够深入,或者配置不当导致的。

解码流程示意图

常见痛点包括:

  • 帧率不稳定:解码速度跟不上视频的帧率,导致卡顿或跳帧。
  • 内存泄漏:未正确释放 MediaCodec 或缓冲区资源,导致内存占用持续增加。
  • 同步问题:解码线程与渲染线程未正确同步,导致画面撕裂或延迟。

技术选型对比

在 Android 中,渲染视频流的视图组件主要有 SurfaceViewTextureViewSurfaceTexture。它们各有优劣:

  • SurfaceView:性能最佳,适合高帧率视频,但层级管理较复杂。
  • TextureView:支持动画和变形,但性能略低于 SurfaceView
  • SurfaceTexture:更灵活,可直接与 OpenGL ES 结合使用,适合定制化需求。

对于大多数视频播放场景,SurfaceView 是首选,因为它直接由硬件合成器处理,性能最优。

核心实现

1. 配置 MediaCodec

MediaCodec 的配置是关键。以下是核心步骤:

  1. 创建 MediaCodec 实例并配置解码器。
  2. 设置视频格式(如 MediaFormat.MIMETYPE_VIDEO_AVC)。
  3. 指定颜色格式(推荐 COLOR_FormatSurface 以兼容大多数设备)。
  4. 绑定 Surface 并启动解码器。

关键代码片段(Kotlin):

val mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
mediaCodec.configure(format, surface, null, 0)
mediaCodec.start()

2. 输入与输出缓冲区管理

MediaCodec 通过输入和输出缓冲区与开发者交互。以下是典型流程:

  1. 通过 dequeueInputBuffer 获取输入缓冲区。
  2. 填充 H264 数据到缓冲区。
  3. 通过 queueInputBuffer 提交数据。
  4. 通过 dequeueOutputBuffer 获取解码后的帧。
  5. 渲染到 Surface 后释放缓冲区。

3. Surface 绑定

绑定 Surface 是渲染的关键。通常通过 SurfaceViewTextureView 获取 Surface,并将其传递给 MediaCodec

性能优化

1. 异步模式

MediaCodec 支持异步模式,通过回调机制减少主线程阻塞。配置方法:

mediaCodec.setCallback(object : MediaCodec.Callback() {
    override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
        // 处理输入缓冲区
    }
    override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
        // 处理输出缓冲区
    }
})

2. 硬件加速

确保设备支持硬件解码,可通过 MediaCodecList 查询:

val codecInfo = MediaCodecList(MediaCodecList.ALL_CODECS)
    .codecInfos.find { it.isEncoder && it.supportedTypes.contains(MediaFormat.MIMETYPE_VIDEO_AVC) }

3. 缓冲区复用

避免频繁分配和释放缓冲区,可以在解码完成后复用缓冲区。

避坑指南

  1. 线程同步:确保解码和渲染在独立线程中运行,避免 ANR。
  2. 资源释放:在 onDestroyonPause 中正确释放 MediaCodecSurface
  3. 帧率控制:根据设备性能动态调整解码帧率,避免过载。

完整代码示例

以下是一个完整的解码流水线示例(Kotlin):

// 创建 MediaExtractor 并设置数据源
val extractor = MediaExtractor()
extractor.setDataSource(context, uri, null)

// 选择视频轨道
val trackIndex = extractor.trackIndices.first { 
    extractor.getTrackFormat(it).getString(MediaFormat.KEY_MIME) == MediaFormat.MIMETYPE_VIDEO_AVC 
}
extractor.selectTrack(trackIndex)

// 配置 MediaCodec
val format = extractor.getTrackFormat(trackIndex)
val mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
mediaCodec.configure(format, surfaceView.holder.surface, null, 0)
mediaCodec.start()

// 解码循环
while (true) {
    val inputBufferId = mediaCodec.dequeueInputBuffer(TIMEOUT_US)
    if (inputBufferId >= 0) {
        val inputBuffer = mediaCodec.getInputBuffer(inputBufferId)
        val sampleSize = extractor.readSampleData(inputBuffer!!, 0)
        if (sampleSize > 0) {
            mediaCodec.queueInputBuffer(inputBufferId, 0, sampleSize, extractor.sampleTime, 0)
            extractor.advance()
        }
    }

    val bufferInfo = MediaCodec.BufferInfo()
    val outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
    if (outputBufferId >= 0) {
        mediaCodec.releaseOutputBuffer(outputBufferId, true)
    }
}

结语

通过合理配置 MediaCodec 和优化解码流程,可以显著提升 H264 视频的解码性能。建议你在实际项目中尝试这些优化方法,并对比性能数据。如果有更好的实践或问题,欢迎分享交流!

优化效果对比

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐