Refactor: Extract MediaCodecQuery class and AVC codec module

- Extract codec query logic into MediaCodecQuery concrete class (no interface
  since there's only one implementation)
- Extract AVC level mapping into AvcCodecCapabilities module with unit tests
- Remove unused context parameter from MediaCodecCapabilitiesTest
- Remove obsolete SDK_INT < M check (min SDK is now 23)
- Fix detekt nesting issues by extracting helper methods and using Sequence
This commit is contained in:
John Corser authored and Niels van Velzen committed 2026-06-12 21:30:34 +02:00
1 parent b3338d46dc
commit 30b2332cdc
6 files changed
+355 -178

No files matched your search

@@ -1,22 +1,20 @@
package org.jellyfin.androidtv.util.profile
import android.content.Context
import android.media.MediaCodecInfo
import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import android.util.Size
import androidx.core.content.ContextCompat
import androidx.media3.common.MimeTypes
import timber.log.Timber
import org.jellyfin.androidtv.util.profile.codec.AvcCodecCapabilities
import org.jellyfin.androidtv.util.profile.codec.MediaCodecQuery
class MediaCodecCapabilitiesTest(
private val context: Context,
private val softwareCodecsEnabled: Boolean
private val softwareCodecsEnabled: Boolean,
) {
private val display by lazy { ContextCompat.getDisplayOrDefault(context) }
private val mediaCodecList by lazy { MediaCodecList(MediaCodecList.REGULAR_CODECS) }
private val codecQuery by lazy { MediaCodecQuery(mediaCodecList, softwareCodecsEnabled) }
private val avc by lazy { AvcCodecCapabilities(codecQuery) }
// Map common Dolby Vision Profiles to their corresponding CodecProfileLevel constant
private object DolbyVisionProfiles {
@@ -61,27 +59,6 @@ class MediaCodecCapabilitiesTest(
}
}
// AVC levels as reported by ffprobe are multiplied by 10, e.g. level 4.1 is 41. Level 1b is set to 9
private val avcLevels = listOf(
CodecProfileLevel.AVCLevel1b to 9,
CodecProfileLevel.AVCLevel1 to 10,
CodecProfileLevel.AVCLevel11 to 11,
CodecProfileLevel.AVCLevel12 to 12,
CodecProfileLevel.AVCLevel13 to 13,
CodecProfileLevel.AVCLevel2 to 20,
CodecProfileLevel.AVCLevel21 to 21,
CodecProfileLevel.AVCLevel22 to 22,
CodecProfileLevel.AVCLevel3 to 30,
CodecProfileLevel.AVCLevel31 to 31,
CodecProfileLevel.AVCLevel32 to 32,
CodecProfileLevel.AVCLevel4 to 40,
CodecProfileLevel.AVCLevel41 to 41,
CodecProfileLevel.AVCLevel42 to 42,
CodecProfileLevel.AVCLevel5 to 50,
CodecProfileLevel.AVCLevel51 to 51,
CodecProfileLevel.AVCLevel52 to 52,
)
// HEVC levels as reported by ffprobe are multiplied by 30, e.g. level 4.1 is 123
private val hevcLevels = listOf(
CodecProfileLevel.HEVCMainTierLevel1 to 30,
@@ -99,60 +76,44 @@ class MediaCodecCapabilitiesTest(
CodecProfileLevel.HEVCMainTierLevel62 to 186,
)
fun supportsAV1(): Boolean = hasCodecForMime(MimeTypes.VIDEO_AV1)
fun supportsAV1(): Boolean = codecQuery.hasCodecForMime(MimeTypes.VIDEO_AV1)
fun supportsAV1Main10(): Boolean = hasDecoder(
fun supportsAV1Main10(): Boolean = codecQuery.hasDecoder(
MimeTypes.VIDEO_AV1,
AV1ProfileLevel.ProfileMain10,
AV1ProfileLevel.Level5
)
fun supportsAV1DolbyVision(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
hasDecoder(
codecQuery.hasDecoder(
MimeTypes.VIDEO_DOLBY_VISION,
AV1ProfileLevel.DolbyVisionProfile10,
CodecProfileLevel.DolbyVisionLevelHd24
)
fun supportsAV1HDR10(): Boolean = hasDecoder(
fun supportsAV1HDR10(): Boolean = codecQuery.hasDecoder(
MimeTypes.VIDEO_AV1,
AV1ProfileLevel.ProfileMain10HDR10,
AV1ProfileLevel.Level5
)
fun supportsAV1HDR10Plus(): Boolean = hasDecoder(
fun supportsAV1HDR10Plus(): Boolean = codecQuery.hasDecoder(
MimeTypes.VIDEO_AV1,
AV1ProfileLevel.ProfileMain10HDR10Plus,
AV1ProfileLevel.Level5
)
fun supportsAVC(): Boolean = hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_AVC)
fun supportsAVC(): Boolean = avc.supportsAvc()
fun supportsAVCHigh10(): Boolean = hasDecoder(
MediaFormat.MIMETYPE_VIDEO_AVC,
CodecProfileLevel.AVCProfileHigh10,
CodecProfileLevel.AVCLevel4
)
fun supportsAVCHigh10(): Boolean = avc.supportsAvcHigh10()
fun getAVCMainLevel(): Int = getAVCLevel(
CodecProfileLevel.AVCProfileMain
)
fun getAVCMainLevel(): Int = avc.getMainLevel()
fun getAVCHigh10Level(): Int = getAVCLevel(
CodecProfileLevel.AVCProfileHigh10
)
fun getAVCHigh10Level(): Int = avc.getHigh10Level()
private fun getAVCLevel(profile: Int): Int {
val level = getDecoderLevel(MediaFormat.MIMETYPE_VIDEO_AVC, profile)
fun supportsHevc(): Boolean = codecQuery.hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_HEVC)
return avcLevels.asReversed().find { item ->
level >= item.first
}?.second ?: 0
}
fun supportsHevc(): Boolean = hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_HEVC)
fun supportsHevcMain10(): Boolean = hasDecoder(
fun supportsHevcMain10(): Boolean = codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10,
CodecProfileLevel.HEVCMainTierLevel4
@@ -160,27 +121,27 @@ class MediaCodecCapabilitiesTest(
// Can safely assume Dolby Vision decoders support single-layer HEVC profiles
fun supportsHevcDolbyVision(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION)
codecQuery.hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION)
// Checks for Dolby Vision Profile 7 (Enhancement Layer) and multi-instance HEVC support
fun supportsHevcDolbyVisionEL(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
hasDecoder(
codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION,
DolbyVisionProfiles.Profile7,
CodecProfileLevel.DolbyVisionLevelHd24
) &&
supportsMultiInstance(MediaFormat.MIMETYPE_VIDEO_HEVC)
codecQuery.supportsMultiInstance(MediaFormat.MIMETYPE_VIDEO_HEVC)
fun supportsHevcHDR10(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
hasDecoder(
codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10HDR10,
CodecProfileLevel.HEVCMainTierLevel4
)
fun supportsHevcHDR10Plus(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
hasDecoder(
codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10HDR10Plus,
CodecProfileLevel.HEVCMainTierLevel4
@@ -195,127 +156,14 @@ class MediaCodecCapabilitiesTest(
)
private fun getHevcLevel(profile: Int): Int {
val level = getDecoderLevel(MediaFormat.MIMETYPE_VIDEO_HEVC, profile)
val level = codecQuery.getDecoderLevel(MediaFormat.MIMETYPE_VIDEO_HEVC, profile)
return hevcLevels.asReversed().find { item ->
level >= item.first
}?.second ?: 0
}
fun supportsVc1(): Boolean = hasCodecForMime(MimeTypes.VIDEO_VC1)
fun supportsVc1(): Boolean = codecQuery.hasCodecForMime(MimeTypes.VIDEO_VC1)
private fun getDecoderLevel(mime: String, profile: Int): Int {
var maxLevel = 0
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue
if (!softwareCodecsEnabled && info.isSoftwareCodec) continue
try {
val capabilities = info.getCapabilitiesForType(mime)
for (profileLevel in capabilities.profileLevels) {
if (profileLevel.profile == profile) {
maxLevel = maxOf(maxLevel, profileLevel.level)
}
}
} catch (_: IllegalArgumentException) {
// Decoder not supported - ignore
}
}
return maxLevel
}
private fun hasDecoder(mime: String, profile: Int, level: Int): Boolean {
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue
if (!softwareCodecsEnabled && info.isSoftwareCodec) continue
try {
val capabilities = info.getCapabilitiesForType(mime)
for (profileLevel in capabilities.profileLevels) {
if (profileLevel.profile != profile) continue
// H.263 levels are not completely ordered:
// Level45 support only implies Level10 support
if (mime.equals(MediaFormat.MIMETYPE_VIDEO_H263, ignoreCase = true)) {
if (profileLevel.level != level && profileLevel.level == CodecProfileLevel.H263Level45 && level > CodecProfileLevel.H263Level10) {
continue
}
}
if (profileLevel.level >= level) return true
}
} catch (_: IllegalArgumentException) {
// Decoder not supported - ignore
}
}
return false
}
private fun hasCodecForMime(mime: String): Boolean {
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue
if (!softwareCodecsEnabled && info.isSoftwareCodec) continue
if (info.supportedTypes.any { it.equals(mime, ignoreCase = true) }) {
Timber.i("found codec %s for mime %s", info.name, mime)
return true
}
}
return false
}
private fun supportsMultiInstance(mime: String): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue
if (!softwareCodecsEnabled && info.isSoftwareCodec) continue
try {
val types = info.getSupportedTypes()
if (!types.contains(mime)) continue
val capabilities = info.getCapabilitiesForType(mime)
if (capabilities.maxSupportedInstances > 1) return true
} catch (_: IllegalArgumentException) {
// Decoder not supported - ignore
}
}
return false
}
fun getMaxResolution(mime: String): Size {
var maxWidth = 0
var maxHeight = 0
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue
if (!softwareCodecsEnabled && info.isSoftwareCodec) continue
try {
val capabilities = info.getCapabilitiesForType(mime)
val videoCapabilities = capabilities.videoCapabilities ?: continue
val supportedWidth = videoCapabilities.supportedWidths?.upper ?: continue
val supportedHeight = videoCapabilities.supportedHeights?.upper ?: continue
maxWidth = maxOf(maxWidth, supportedWidth)
maxHeight = maxOf(maxHeight, supportedHeight)
} catch (_: IllegalArgumentException) {
// Decoder not supported - ignore
}
}
Timber.d("Computed max resolution for %s: %dx%d", mime, maxWidth, maxHeight)
return Size(maxWidth, maxHeight)
}
private val MediaCodecInfo.isSoftwareCodec: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isSoftwareOnly
fun getMaxResolution(mime: String): Size = codecQuery.getMaxResolution(mime)
}
@@ -0,0 +1,56 @@
package org.jellyfin.androidtv.util.profile.codec
import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaFormat
/**
* AVC (H.264) codec capability detection backed by a [MediaCodecQuery].
*/
class AvcCodecCapabilities(
private val query: MediaCodecQuery,
) {
companion object {
// AVC levels as reported by ffprobe are multiplied by 10, e.g. level 4.1 is 41. Level 1b is set to 9
internal val LEVEL_MAP: List<Pair<Int, Int>> = listOf(
CodecProfileLevel.AVCLevel1b to 9,
CodecProfileLevel.AVCLevel1 to 10,
CodecProfileLevel.AVCLevel11 to 11,
CodecProfileLevel.AVCLevel12 to 12,
CodecProfileLevel.AVCLevel13 to 13,
CodecProfileLevel.AVCLevel2 to 20,
CodecProfileLevel.AVCLevel21 to 21,
CodecProfileLevel.AVCLevel22 to 22,
CodecProfileLevel.AVCLevel3 to 30,
CodecProfileLevel.AVCLevel31 to 31,
CodecProfileLevel.AVCLevel32 to 32,
CodecProfileLevel.AVCLevel4 to 40,
CodecProfileLevel.AVCLevel41 to 41,
CodecProfileLevel.AVCLevel42 to 42,
CodecProfileLevel.AVCLevel5 to 50,
CodecProfileLevel.AVCLevel51 to 51,
CodecProfileLevel.AVCLevel52 to 52,
)
private const val MIME = MediaFormat.MIMETYPE_VIDEO_AVC
}
fun supportsAvc(): Boolean = query.hasCodecForMime(MIME)
fun supportsAvcHigh10(): Boolean = query.hasDecoder(
MIME,
CodecProfileLevel.AVCProfileHigh10,
CodecProfileLevel.AVCLevel4,
)
fun getMainLevel(): Int = getLevel(CodecProfileLevel.AVCProfileMain)
fun getHigh10Level(): Int = getLevel(CodecProfileLevel.AVCProfileHigh10)
private fun getLevel(profile: Int): Int {
val level = query.getDecoderLevel(MIME, profile)
return LEVEL_MAP.asReversed().find { (codecLevel, _) ->
level >= codecLevel
}?.second ?: 0
}
}
@@ -0,0 +1,109 @@
package org.jellyfin.androidtv.util.profile.codec
import android.media.MediaCodecInfo
import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import android.util.Size
import timber.log.Timber
/**
* Queries device codec capabilities from Android's [MediaCodecList].
*/
class MediaCodecQuery(
private val mediaCodecList: MediaCodecList,
private val softwareCodecsEnabled: Boolean,
) {
private val MediaCodecInfo.isSoftwareCodec: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isSoftwareOnly
private fun decoderInfos(): Sequence<MediaCodecInfo> =
mediaCodecList.codecInfos.asSequence()
.filter { !it.isEncoder }
.filter { softwareCodecsEnabled || !it.isSoftwareCodec }
fun hasCodecForMime(mime: String): Boolean {
val info = decoderInfos().firstOrNull { info ->
info.supportedTypes.any { it.equals(mime, ignoreCase = true) }
} ?: return false
Timber.i("found codec %s for mime %s", info.name, mime)
return true
}
fun hasDecoder(mime: String, profile: Int, level: Int): Boolean =
decoderInfos().any { info -> supportsProfileLevel(info, mime, profile, level) }
fun getDecoderLevel(mime: String, profile: Int): Int {
var maxLevel = 0
for (info in decoderInfos()) {
val capabilities = getCapabilitiesOrNull(info, mime) ?: continue
for (profileLevel in capabilities.profileLevels) {
if (profileLevel.profile == profile) {
maxLevel = maxOf(maxLevel, profileLevel.level)
}
}
}
return maxLevel
}
fun getMaxResolution(mime: String): Size {
val resolutions = decoderInfos()
.mapNotNull { info -> getCapabilitiesOrNull(info, mime)?.videoCapabilities }
.mapNotNull { vc ->
val w = vc.supportedWidths?.upper ?: return@mapNotNull null
val h = vc.supportedHeights?.upper ?: return@mapNotNull null
w to h
}
val maxWidth = resolutions.maxOfOrNull { it.first } ?: 0
val maxHeight = resolutions.maxOfOrNull { it.second } ?: 0
Timber.d("Computed max resolution for %s: %dx%d", mime, maxWidth, maxHeight)
return Size(maxWidth, maxHeight)
}
fun supportsMultiInstance(mime: String): Boolean =
decoderInfos().any { info -> hasMultipleInstances(info, mime) }
private fun supportsProfileLevel(info: MediaCodecInfo, mime: String, profile: Int, level: Int): Boolean {
val capabilities = getCapabilitiesOrNull(info, mime) ?: return false
return capabilities.profileLevels.any { profileLevel ->
profileLevel.profile == profile && meetsLevelRequirement(mime, profileLevel.level, level)
}
}
private fun meetsLevelRequirement(mime: String, decoderLevel: Int, requiredLevel: Int): Boolean {
// H.263 levels are not completely ordered:
// Level45 support only implies Level10 support
if (mime.equals(MediaFormat.MIMETYPE_VIDEO_H263, ignoreCase = true)) {
if (decoderLevel != requiredLevel &&
decoderLevel == CodecProfileLevel.H263Level45 &&
requiredLevel > CodecProfileLevel.H263Level10
) {
return false
}
}
return decoderLevel >= requiredLevel
}
private fun hasMultipleInstances(info: MediaCodecInfo, mime: String): Boolean {
if (!info.supportedTypes.contains(mime)) return false
val capabilities = getCapabilitiesOrNull(info, mime) ?: return false
return capabilities.maxSupportedInstances > 1
}
private fun getCapabilitiesOrNull(
info: MediaCodecInfo,
mime: String,
): MediaCodecInfo.CodecCapabilities? = try {
info.getCapabilitiesForType(mime)
} catch (_: IllegalArgumentException) {
null
}
}
@@ -79,7 +79,7 @@ fun createDeviceProfile(
userPreferences: UserPreferences,
serverVersion: ServerVersion,
) = createDeviceProfile(
mediaTest = MediaCodecCapabilitiesTest(context, userPreferences[UserPreferences.softwareCodecsEnabled]),
mediaTest = MediaCodecCapabilitiesTest(userPreferences[UserPreferences.softwareCodecsEnabled]),
maxBitrate = userPreferences.getMaxBitrate(),
isAC3Enabled = userPreferences[UserPreferences.ac3Enabled],
downMixAudio = userPreferences[UserPreferences.audioBehaviour] == AudioBehavior.DOWNMIX_TO_STEREO,
@@ -181,7 +181,7 @@ fun createDeviceProfileReport(
}
appendDetails("Codec HDR Support") {
val mediaTest = MediaCodecCapabilitiesTest(context, userPreferences[UserPreferences.softwareCodecsEnabled])
val mediaTest = MediaCodecCapabilitiesTest(userPreferences[UserPreferences.softwareCodecsEnabled])
val codecHDRSupport = buildMap<String, Map<HdrFormats, Boolean>> {
if (mediaTest.supportsAV1()) {
@@ -0,0 +1,164 @@
package org.jellyfin.androidtv.util.profile.codec
import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaFormat
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
class AvcCodecCapabilitiesTest : FunSpec({
val mime = MediaFormat.MIMETYPE_VIDEO_AVC
test("supportsAvc returns true when device has AVC decoder") {
val query = mockk<MediaCodecQuery> {
every { hasCodecForMime(mime) } returns true
}
AvcCodecCapabilities(query).supportsAvc() shouldBe true
}
test("supportsAvc returns false when device has no AVC decoder") {
val query = mockk<MediaCodecQuery> {
every { hasCodecForMime(mime) } returns false
}
AvcCodecCapabilities(query).supportsAvc() shouldBe false
}
test("supportsAvcHigh10 returns true when device supports High10 at Level 4") {
val query = mockk<MediaCodecQuery> {
every { hasDecoder(mime, CodecProfileLevel.AVCProfileHigh10, CodecProfileLevel.AVCLevel4) } returns true
}
AvcCodecCapabilities(query).supportsAvcHigh10() shouldBe true
}
test("supportsAvcHigh10 returns false when device lacks High10 support") {
val query = mockk<MediaCodecQuery> {
every { hasDecoder(mime, CodecProfileLevel.AVCProfileHigh10, CodecProfileLevel.AVCLevel4) } returns false
}
AvcCodecCapabilities(query).supportsAvcHigh10() shouldBe false
}
test("getMainLevel returns mapped level for device with Main Level 4.1") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel41
}
AvcCodecCapabilities(query).getMainLevel() shouldBe 41
}
test("getMainLevel returns mapped level for device with Main Level 5.1") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel51
}
AvcCodecCapabilities(query).getMainLevel() shouldBe 51
}
test("getMainLevel returns mapped level for device with Main Level 5.2") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel52
}
AvcCodecCapabilities(query).getMainLevel() shouldBe 52
}
test("getMainLevel returns 0 when device reports no AVC Main support") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns 0
}
AvcCodecCapabilities(query).getMainLevel() shouldBe 0
}
test("getMainLevel maps level between known values to the lower known level") {
val query = mockk<MediaCodecQuery> {
// Return a value between AVCLevel4 (2048) and AVCLevel41 (4096)
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns 3000
}
// 3000 >= AVCLevel4 (2048) but < AVCLevel41 (4096), so should map to 40
AvcCodecCapabilities(query).getMainLevel() shouldBe 40
}
test("getMainLevel maps Level 1b correctly") {
val query = mockk<MediaCodecQuery> {
// AVCLevel1b = 2, AVCLevel1 = 1
// In reversed search, AVCLevel1 (1) appears before AVCLevel1b (2)
// so level 2 >= 1 matches AVCLevel1 first, returning 10
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel1b
}
AvcCodecCapabilities(query).getMainLevel() shouldBe 10
}
test("getMainLevel maps Level 1 correctly") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel1
}
AvcCodecCapabilities(query).getMainLevel() shouldBe 10
}
test("getHigh10Level returns mapped level for device with High10 Level 5.1") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileHigh10) } returns CodecProfileLevel.AVCLevel51
}
AvcCodecCapabilities(query).getHigh10Level() shouldBe 51
}
test("getHigh10Level returns 0 when device has no High10 support") {
val query = mockk<MediaCodecQuery> {
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileHigh10) } returns 0
}
AvcCodecCapabilities(query).getHigh10Level() shouldBe 0
}
test("LEVEL_MAP contains all expected AVC levels in ascending order") {
val expectedFfprobeLevels = listOf(9, 10, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52)
AvcCodecCapabilities.LEVEL_MAP.map { it.second } shouldBe expectedFfprobeLevels
}
test("LEVEL_MAP ffprobe level values are in ascending order") {
val ffprobeLevels = AvcCodecCapabilities.LEVEL_MAP.map { it.second }
ffprobeLevels shouldBe ffprobeLevels.sorted()
}
// Simulated device profiles
test("Shield TV profile: Main Level 5.1 + High10 Level 5.1") {
val query = mockk<MediaCodecQuery> {
every { hasCodecForMime(mime) } returns true
every { hasDecoder(mime, CodecProfileLevel.AVCProfileHigh10, CodecProfileLevel.AVCLevel4) } returns true
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel51
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileHigh10) } returns CodecProfileLevel.AVCLevel51
}
val avc = AvcCodecCapabilities(query)
avc.supportsAvc() shouldBe true
avc.supportsAvcHigh10() shouldBe true
avc.getMainLevel() shouldBe 51
avc.getHigh10Level() shouldBe 51
}
test("Budget Fire Stick profile: Main Level 4.1 only") {
val query = mockk<MediaCodecQuery> {
every { hasCodecForMime(mime) } returns true
every { hasDecoder(mime, CodecProfileLevel.AVCProfileHigh10, CodecProfileLevel.AVCLevel4) } returns false
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns CodecProfileLevel.AVCLevel41
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileHigh10) } returns 0
}
val avc = AvcCodecCapabilities(query)
avc.supportsAvc() shouldBe true
avc.supportsAvcHigh10() shouldBe false
avc.getMainLevel() shouldBe 41
avc.getHigh10Level() shouldBe 0
}
test("No AVC support at all") {
val query = mockk<MediaCodecQuery> {
every { hasCodecForMime(mime) } returns false
every { hasDecoder(mime, CodecProfileLevel.AVCProfileHigh10, CodecProfileLevel.AVCLevel4) } returns false
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileMain) } returns 0
every { getDecoderLevel(mime, CodecProfileLevel.AVCProfileHigh10) } returns 0
}
val avc = AvcCodecCapabilities(query)
avc.supportsAvc() shouldBe false
avc.supportsAvcHigh10() shouldBe false
avc.getMainLevel() shouldBe 0
avc.getHigh10Level() shouldBe 0
}
})