为什么手动 YUV 视频编码在 Android 设备上失败(以及如何修复)

发布: (2026年1月14日 GMT+8 01:00)
4 min read
原文: Dev.to

Source: Dev.to

问题

许多开发者采用一种天真的做法:手动将 Bitmap 转换为 YUV 并推送给编码器。看似简单——但在生产环境中,这是一片雷区:

  • 不同设备的 stride 假设不同(MediaTek 常会按 16–64 字节对齐)
  • 色度平面顺序各异(平面式 vs. 半平面式)
  • Surface 锁定可能竞争,导致 IllegalArgumentException
  • 编码器输出线程可能无限阻塞,导致应用卡死

结果?崩溃、文件损坏,以及在各设备上都不可靠的视频管线。

为什么天真的手动 YUV 转换会失败

手动 YUV 转换

fun bitmapToYuv420(bitmap: Bitmap, width: Int, height: Int): ByteArray {
    val yuv = ByteArray(width * height * 3 / 2)
    // Naive RGB → YUV conversion without stride handling
    return yuv
}

出错原因

  • 假设 stride == width → 在 MediaTek 设备上失效
  • 忽略平面/交错布局 → 颜色错乱
  • 漏掉硬件对齐要求 → 编码器拒绝帧

即使在你的测试手机上能跑通,也很可能在其他设备上失效。

天真的 Surface 处理

fun recordFrame(bitmap: Bitmap) {
    val canvas = surface.lockCanvas(null)
    canvas.drawBitmap(bitmap, ...)
    surface.unlockCanvasAndPost(canvas)
}

问题

  • 没有并发控制 → 当新帧到来而前一帧仍在绘制时会崩溃
  • 阻塞主线程 → 丢帧或摄像头卡死
  • 缺乏错误恢复 → 整个录制过程失败

生产级解决方案:基于 Surface 的编码

唯一的厂商无关、可投入生产的做法是基于 Surface 的编码(COLOR_FormatSurface)。硬件内部会自行处理 stride、颜色转换和对齐。

关键模式

  • 帧丢弃 – 当编码器忙碌时跳过一帧,以防止 Surface 锁冲突
  • 短超时 – 为 dequeueOutputBuffer() 使用约 100 ms 的超时,以实现快速关闭
  • 资源清理顺序MediaMuxer → MediaCodec → Surface
  • 线程安全 – 为所有操作使用专用的编码器线程
val isEncodingFrame = AtomicBoolean(false)

fun recordFrame(bitmap: Bitmap) {
    if (!isEncodingFrame.compareAndSet(false, true)) return

    try {
        val canvas = encoderSurface.lockCanvas(null)
        canvas.drawBitmap(bitmap, null, dstRect, paint)
        encoderSurface.unlockCanvasAndPost(canvas)
    } finally {
        isEncodingFrame.set(false)
    }
}

这种做法在 Qualcomm、MediaTek、Exynos 设备上均可工作,适用于 Android 10–15,支持长时录制和高 FPS。

权衡与经验教训

  • 帧丢弃 vs. 画质 – 稍微卡顿的视频总好过崩溃
  • 内存 & CPU – Surface 编码将转换任务交给 GPU,降低内存占用
  • 后台处理 – Android 12 及以上若应用被置于后台,系统可能会杀死编码线程;请使用前台服务
  • 测试重点 – 优先在 MediaTek 设备(Vivo、Oppo、Xiaomi)以及高 FPS 场景上进行验证

关键要点

  • 避免手动 YUV 转换——它脆弱且受设备限制
  • 使用基于 Surface 的编码——硬件会自动处理各种怪癖
  • 在多种芯片组和 Android 版本上进行测试——真实世界的信号胜过理论

采用这种方式,你的视频管线将具备生产级的可靠性、可维护性——无需针对特定设备的 hack。

Back to Blog

相关文章

阅读更多 »