Newstrong's 博客

Kratos
专注于用户阅读体验的响应式博客主题
  1. 首页
  2. Android
  3. 正文

Android MediaCodecList 解码器能力探测方案

2026年7月17日 4点热度 0人点赞

1. 背景

在 Android 车载 IVI 视频扫描场景中,区分 FORMAT_NOT_SUPPORTED 与 DAMAGED 的核心瓶颈在于:

  • MediaMetadataRetriever 无法暴露底层解码器错误码
  • MediaExtractor 抛出的异常无法区分"文件损坏"与"格式不支持"

本方案基于 Android 系统稳定的 MediaCodecList API,在应用层实现零额外依赖的解码器能力探测,从而精准判定视频格式是否被车机 SoC 支持。

2. 为什么不推荐 MediaCodecUtil

Media3(ExoPlayer)内部提供了 MediaCodecUtil.getDecoderInfos(),但属于 @UnstableApi,不推荐在量产代码中直接使用。

风险说明
二进制不兼容升级 Media3 版本(如 1.4.0 → 1.6.0)时,方法签名可能改变,编译直接报错
反射风险ProGuard/R8 混淆后可能找不到类
车载认证影响Android Automotive 认证要求尽量使用系统稳定 API,第三方不稳定 API 可能被审核质疑

MediaCodecUtil 本质上是对 MediaCodecList 的封装(增加黑名单过滤、别名映射、Secure Decoder 检测等),功能可通过系统 API + 自建策略等效替代。

3. 系统稳定 API:MediaCodecList

Android 从 API 21(Android 5.0)起提供稳定的 MediaCodecList,功能等价于 MediaCodecUtil,且跨版本稳定、零依赖。

3.1 基础查询:是否存在解码器

import android.media.MediaCodecList
import android.media.MediaCodecInfo
import android.media.MediaFormat

fun hasDecoder(mimeType: String): Boolean {
    val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
    return codecList.codecInfos.any { codecInfo ->
        !codecInfo.isEncoder && codecInfo.supportedTypes.contains(mimeType)
    }
}

// 使用示例
val hasHevc = hasDecoder("video/hevc")  // false = 车机不支持 H.265
val hasAvc  = hasDecoder("video/avc")    // true  = 支持 H.264

3.2 进阶查询:获取解码器能力详情

import android.media.MediaCodecInfo.CodecCapabilities
import android.media.MediaCodecInfo.VideoCapabilities

data class DecoderCapability(
    val name: String,
    val isHardwareAccelerated: Boolean,
    val maxWidth: Int,
    val maxHeight: Int,
    val maxBitrate: Int,
    val supportedProfiles: List<Int>
)

fun queryDecoderCapabilities(mimeType: String): List<DecoderCapability> {
    val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
    val results = mutableListOf<DecoderCapability>()

    for (codecInfo in codecList.codecInfos) {
        if (codecInfo.isEncoder) continue

        val capabilities = try {
            codecInfo.getCapabilitiesForType(mimeType)
        } catch (e: IllegalArgumentException) {
            continue
        }

        val videoCaps = capabilities.videoCapabilities
        val profileLevels = capabilities.profileLevels

        results.add(
            DecoderCapability(
                name = codecInfo.name,
                isHardwareAccelerated = isHardwareAccelerated(codecInfo.name),
                maxWidth = videoCaps?.supportedWidths?.upper ?: 0,
                maxHeight = videoCaps?.supportedHeights?.upper ?: 0,
                maxBitrate = videoCaps?.bitrateRange?.upper ?: 0,
                supportedProfiles = profileLevels.map { it.profile }
            )
        )
    }
    return results
}

// 判断是否为硬件解码器(车载优先使用硬解,避免 CPU 发热)
fun isHardwareAccelerated(decoderName: String): Boolean {
    return when {
        decoderName.contains("google", ignoreCase = true)  -> false
        decoderName.contains("android", ignoreCase = true) -> false
        decoderName.contains(".sw.", ignoreCase = true)    -> false
        else -> true
    }
}

3.3 精确判定:Profile/Level 是否支持

这是 区分 FORMAT_NOT_SUPPORTED 的核心——容器解析成功、头文件合法,但如果视频的 Profile/Level 超出解码器能力(如 4K H.265 Main10 在仅支持 1080p Main 的车机上),应判定为 FORMAT_NOT_SUPPORTED 而非 DAMAGED。

import android.media.MediaCodecInfo.CodecProfileLevel

fun isFormatFullySupported(
    mimeType: String,
    width: Int,
    height: Int,
    bitrate: Int,
    profile: Int,
    level: Int
): Boolean {
    val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)

    for (codecInfo in codecList.codecInfos) {
        if (codecInfo.isEncoder) continue

        val caps = try {
            codecInfo.getCapabilitiesForType(mimeType)
        } catch (e: IllegalArgumentException) {
            continue
        }

        val videoCaps = caps.videoCapabilities ?: continue

        // 1. 分辨率检查
        if (!videoCaps.isSizeSupported(width, height)) continue

        // 2. 码率检查
        if (bitrate > 0 && bitrate > videoCaps.bitrateRange.upper) continue

        // 3. Profile/Level 检查(关键!)
        val hasMatchingProfile = caps.profileLevels.any { pl ->
            pl.profile == profile && pl.level >= level
        }
        if (!hasMatchingProfile) continue

        return true
    }
    return false
}

// H.264 Profile 常量映射
val PROFILE_H264_BASELINE = CodecProfileLevel.AVCProfileBaseline
val PROFILE_H264_MAIN     = CodecProfileLevel.AVCProfileMain
val PROFILE_H264_HIGH     = CodecProfileLevel.AVCProfileHigh

4. 车载 IVI 解码器策略引擎

object VehicleVideoPolicy {

    data class PlatformConfig(
        val supportedContainers: Set<String>,
        val supportedCodecs: Set<String>,
        val maxResolution: Pair<Int, Int>,
        val maxBitrate: Int,
        val requireHardware: Boolean = true
    )

    // 高通 SA8155 示例配置
    val SA8155 = PlatformConfig(
        supportedContainers = setOf("mp4", "mkv", "avi", "ts"),
        supportedCodecs = setOf("video/avc", "video/hevc"),
        maxResolution = 3840 to 2160,
        maxBitrate = 100_000_000,
        requireHardware = true
    )

    // MTK 入门平台示例
    val MT8667 = PlatformConfig(
        supportedContainers = setOf("mp4", "avi"),
        supportedCodecs = setOf("video/avc"),
        maxResolution = 1920 to 1080,
        maxBitrate = 40_000_000,
        requireHardware = true
    )
}

class VideoFormatChecker(private val config: VehicleVideoPolicy.PlatformConfig) {

    /**
     * 返回值:true = 格式受支持(NORMAL),false = 格式不支持(FORMAT_NOT_SUPPORTED)
     */
    fun checkFormatSupport(
        mimeType: String,
        width: Int,
        height: Int,
        bitrate: Int = 0,
        profile: Int = -1,
        level: Int = -1
    ): Boolean {
        // 1. MIME 白名单
        if (mimeType !in config.supportedCodecs) return false

        // 2. 分辨率检查
        if (width > config.maxResolution.first || height > config.maxResolution.second) {
            return false
        }

        // 3. 码率检查
        if (bitrate > 0 && bitrate > config.maxBitrate) return false

        // 4. 解码器存在性
        val codecList = MediaCodecList(MediaCodecList.ALL_CODECS)
        val matchingDecoders = codecList.codecInfos.filter { info ->
            !info.isEncoder && info.supportedTypes.contains(mimeType)
        }
        if (matchingDecoders.isEmpty()) return false

        // 5. 硬件解码强制要求(车载关键:软解发热严重)
        if (config.requireHardware) {
            val hasHardware = matchingDecoders.any { !isSoftwareDecoder(it.name) }
            if (!hasHardware) return false
        }

        // 6. Profile/Level 能力检查
        if (profile != -1 && level != -1) {
            val canHandle = matchingDecoders.any { decoder ->
                val caps = decoder.getCapabilitiesForType(mimeType)
                caps.profileLevels.any { it.profile == profile && it.level >= level }
            }
            if (!canHandle) return false
        }

        return true
    }

    private fun isSoftwareDecoder(name: String): Boolean {
        return name.contains("google", ignoreCase = true) ||
               name.contains("android", ignoreCase = true)
    }
}

5. 从 MediaExtractor / ExoPlayer Format 获取 Profile/Level

val extractor = MediaExtractor()
extractor.setDataSource(path)

for (i in 0 until extractor.trackCount) {
    val format = extractor.getTrackFormat(i)
    val mime = format.getString(MediaFormat.KEY_MIME)

    if (mime?.startsWith("video/") == true) {
        // MediaFormat 包含 profile/level(API 21+,部分设备可能缺失)
        val profile = format.getInteger(MediaFormat.KEY_PROFILE, -1)
        val level   = format.getInteger(MediaFormat.KEY_LEVEL, -1)
        val width   = format.getInteger(MediaFormat.KEY_WIDTH)
        val height  = format.getInteger(MediaFormat.KEY_HEIGHT)
        val bitrate = format.getInteger(MediaFormat.KEY_BIT_RATE, 0)

        // 精确判定
        val isSupported = VehicleVideoPolicy.SA8155.let { cfg ->
            mime in cfg.supportedCodecs &&
            width <= cfg.maxResolution.first &&
            height <= cfg.maxResolution.second &&
            (bitrate == 0 || bitrate <= cfg.maxBitrate)
        }
    }
}

注意:MediaFormat.KEY_PROFILE 和 KEY_LEVEL 在部分设备上可能无法提取(取决于 MediaExtractor 实现),此时保守处理:仅检查 MIME + 分辨率 + 解码器存在性。

6. MediaCodecList vs MediaCodecUtil 对比

维度MediaCodecList(系统 API)MediaCodecUtil(Media3 内部)
来源Android SDK 内置ExoPlayer 内置
稳定性⭐⭐⭐⭐⭐ 公开 API,跨版本稳定⭐⭐ @UnstableApi,可能变更
车载认证✅ 无风险⚠️ 可能需额外说明
黑名单过滤❌ 需自建✅ ExoPlayer 内置已知问题解码器黑名单
别名映射❌ 仅标准 MIME✅ 自动处理 avc1/hvc1 等 brand 到 MIME 映射
硬件检测✅ 通过命名规则判断✅ 相同逻辑
Secure Decoder✅ 手动过滤✅ 参数化支持

7. 推荐策略

场景推荐 API
量产车载 IVI 代码使用 MediaCodecList(系统 API),避免 @UnstableApi 依赖
需要别名解析(如从 MP4 brand 推断 MIME)自建映射表(参考 CODEC_MIME_MAP)
内部原型验证可临时使用 MediaCodecUtil,但量产前必须迁移
结合 ExoPlayer 播放引擎复用 DefaultRenderersFactory 的 MediaCodecSelector.DEFAULT,查询逻辑仍用系统 API

8. 核心结论

  1. MediaCodecList 是 Android 官方公开 API,从 API 21 稳定至今,完全满足车载 IVI 的解码器能力探测需求,无需也不应依赖 ExoPlayer 的内部工具类。
  2. 损坏判定:MediaExtractor.setDataSource() 抛 IOException → DAMAGED
  3. 不支持判定:MediaCodecList 查询不到解码器 / Profile/Level 不匹配 → FORMAT_NOT_SUPPORTED
  4. 正常判定:容器解析成功 + 解码器存在 + 能力匹配 → NORMAL
标签: 暂无
最后更新:2026年7月17日

newstrong

这个人很懒,什么都没留下

点赞
< 上一篇

归档

  • 2026 年 7 月
  • 2026 年 1 月
  • 2025 年 3 月
  • 2024 年 10 月
  • 2024 年 9 月
  • 2024 年 8 月
  • 2022 年 2 月
  • 2021 年 4 月
  • 2020 年 7 月
  • 2020 年 6 月
  • 2020 年 5 月
  • 2017 年 4 月

分类

  • Android
  • git
  • 性能优化
  • 未分类

COPYRIGHT © 2026 Newstrong's 博客. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

蜀ICP备2024057367