mirror of
https://github.com/jellyfin/jellyfin-androidtv.git
synced 2026-10-26 16:45:23 +00:00
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:
1 parent
b3338d46dc
commit
30b2332cdc
6 files changed
+355
-178
No files matched your search
+24
-176
@@ -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
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user