限时福利领取


为什么需要关注视频解码?

在开发视频播放器或实时流应用时,经常遇到画面卡顿、延迟飙升甚至应用崩溃的问题。比如:

  • 明明网络带宽足够,但画面总是跳帧
  • 手机发热严重时出现绿色马赛克
  • 快速切换视频源时引发内存泄漏

视频解码流程示意图

视图容器选型: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. 数据输入与输出处理

典型状态机处理流程:

  1. 获取可用输入缓冲区索引
  2. 填充H264数据(注意时间戳计算)
  3. 提交给解码器
  4. 获取输出缓冲区
  5. 渲染到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) {
    // 处理播放完成逻辑
}

进阶方向

  1. 尝试AV1解码:检查MediaCodecList支持情况
  2. 实现解码+编码流水线
  3. 结合OpenGL ES做后处理

完整示例项目已放在GitHub仓库,包含线程同步和异常处理完整实现。

Logo

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

更多推荐