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. 核心结论
- MediaCodecList 是 Android 官方公开 API,从 API 21 稳定至今,完全满足车载 IVI 的解码器能力探测需求,无需也不应依赖 ExoPlayer 的内部工具类。
- 损坏判定:
MediaExtractor.setDataSource()抛IOException→DAMAGED - 不支持判定:
MediaCodecList查询不到解码器 / Profile/Level 不匹配 →FORMAT_NOT_SUPPORTED - 正常判定:容器解析成功 + 解码器存在 + 能力匹配 →
NORMAL