视频优化:解码器选择、硬解与软解、缓冲策略、HDR 优化

视频播放,是 Android 系统里最吃性能的场景之一。我见过不少应用,UI 流畅度做得不错,但一播视频就发热、掉帧、甚至花屏。说白了,视频优化没做好,用户的第一印象就崩了。

今天咱们就聊聊视频这条链路里的几个关键点:解码器怎么选、硬解和软解到底怎么权衡、缓冲策略怎么调、以及 HDR 这个“新贵”该怎么伺候。

解码器选择:MediaCodec 与底层博弈

Android 的视频解码,核心 API 是 MediaCodec。但你别以为调个 API 就完事了。底层解码器的选择,直接决定了功耗和画质。

我个人习惯,在初始化解码器时,先通过 MediaCodecList 遍历所有可用的解码器。你会发现,同一个 MIME 类型(比如 video/avc),系统可能注册了多个解码器:一个硬件解码器(比如 OMX.qcom.video.decoder.avc),一个软件解码器(比如 OMX.google.h264.decoder)。

这里有个坑:不要直接选第一个。我曾经在一个项目里,直接用了 findDecoderForFormat,结果在某个低端芯片上,它给我返回了一个软解实例,播放 1080p 视频直接卡成 PPT。

我的建议:优先选择名称中包含硬件厂商标识(如 qcom、exynos、mtk)的解码器。如果找不到,再 fallback 到 Google 的软解实现。

// 伪代码示例:优先选择硬件解码器
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo info : codecList.getCodecInfos()) {
    if (!info.isEncoder()) {
        for (String type : info.getSupportedTypes()) {
            if (type.equals(mimeType)) {
                // 检查是否是硬件解码器
                if (info.getName().contains("qcom") || info.getName().contains("mtk")) {
                    // 优先使用
                    return info.getName();
                }
            }
        }
    }
}
// 兜底:使用 Google 软解
return "OMX.google." + mimeType + ".decoder";

硬解码与软解码:不是非黑即白

很多人觉得硬解一定比软解好。其实不一定。硬解功耗低、性能好,但兼容性差;软解兼容性好,但吃 CPU。

我遇到过最典型的情况:某款手机硬解 H.265 10bit 视频,画面出现绿色条纹。查了半天,是芯片解码器的一个 bug。最后只能降级到软解。

所以我的策略是:默认硬解,异常降级。具体来说:

  • 启动硬解,监听 MediaCodec.CallbackonError 回调。
  • 如果连续出现 3 次解码错误,自动切换到软解。
  • 在切换时,保留当前播放进度,不要让用户感觉到“断流”。

小技巧:硬解模式下,Surface 的格式建议用 SurfaceView 而不是 TextureView。TextureView 会多一次 GPU 拷贝,增加延迟和功耗。我实测过,同样 4K 视频,TextureView 的帧渲染时间比 SurfaceView 多了 3-5ms。

缓冲策略:别让“水龙头”忽大忽小

视频播放的流畅度,很大程度上取决于缓冲策略。缓冲太小,容易卡顿;缓冲太大,启动慢、内存高。

我习惯把缓冲分为三个阶段:

  1. 启动阶段:快速填充缓冲区,让首帧尽快显示。一般预加载 500ms 的数据就够了。
  2. 稳定阶段:维持一个动态缓冲区。根据网络波动,缓冲区大小在 2s 到 5s 之间浮动。
  3. 低水位阶段:当缓冲区低于 1s 时,暂停解码,等待数据填充到 3s 再继续。

这里有个细节:不要用固定大小的缓冲区。我曾经在一个直播项目里,固定了 4MB 的缓冲区。结果在弱网环境下,缓冲区一直填不满,导致播放器频繁进入“等待-播放-等待”的死循环。

避坑指南:我曾经在调整缓冲大小时,直接修改了 MediaCodecqueueInputBuffer 的时机。结果发现,如果输入队列积压太多,解码器会直接丢帧。正确的做法是:通过 getOutputBuffers 的返回状态来判断解码器的处理能力,而不是盲目塞数据。

HDR 优化:从“能播”到“播好”

HDR 视频,说白了就是让亮的地方更亮,暗的地方更暗,色彩更丰富。但 Android 上的 HDR 支持,碎片化非常严重。

我遇到过最头疼的问题:同一段 HLG 视频,在 Pixel 上色彩正常,在小米上却发灰。原因是小米的 ColorOS 对 HDR 的 tone mapping 实现不同。

我的优化思路分三步:

  • 第一步:检测能力。通过 Display.getHdrCapabilities() 获取设备支持的 HDR 类型(HDR10、HLG、Dolby Vision)。如果设备不支持,直接降级到 SDR。
  • 第二步:设置正确的色彩空间。MediaCodecconfigure 方法中,传入 MediaFormat.KEY_COLOR_TRANSFERMediaFormat.KEY_COLOR_STANDARD。很多开发者漏了这一步,导致解码器用默认的 BT.709 去渲染 BT.2020 的内容,颜色当然不对。
  • 第三步:Surface 的 HDR 适配。如果是 SurfaceView,需要调用 surfaceView.setSecure(true)(针对 DRM 内容),并且确保 Surface 的像素格式是 RGBA_1010102RGBA_FP16
// 设置 HDR 色彩空间
MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);
format.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_HLG);
format.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020);
format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);

核心观点:HDR 优化不是让所有设备都显示 HDR,而是让不支持 HDR 的设备也能“正常”显示 SDR 版本。我建议在应用层做一次 tone mapping 的 fallback:如果设备不支持 HDR,用 OpenGLVulkan 做一次色彩空间转换,把 HDR 内容映射到 SDR 范围。虽然会损失一些动态范围,但至少不会出现“灰蒙蒙”的画面。

知识体系总览

下面这张图,是我对视频优化核心逻辑的总结。你可以把它当作一个检查清单:

视频优化核心 解码器选择 优先硬件解码器 异常降级到软解 硬解 vs 软解 硬解:低功耗、高兼容风险 软解:高兼容、高功耗 缓冲策略 启动:500ms 快速填充 稳定:2s-5s 动态调整 HDR 优化 检测 HDR 能力 → 设置色彩空间 → Surface 适配 不支持 HDR 时:tone mapping 降级到 SDR 核心原则:默认硬解 + 异常降级 + 动态缓冲 + HDR 兜底

嗯,视频优化这块,说白了就是“平衡”二字。平衡功耗与画质,平衡兼容性与性能,平衡启动速度与流畅度。没有银弹,只有根据实际场景不断调优。

我最后想说的是:不要迷信某个“最佳实践”。同样的策略,在骁龙 8 Gen 3 上跑得飞起,在联发科 G99 上可能就翻车。多测试、多埋点、多分析,才是正道。

专注资料整理