Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package org.jellyfin.androidtv.util.profile

import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import android.util.Size
import androidx.media3.common.MimeTypes
import org.jellyfin.androidtv.util.profile.codec.Av1CodecCapabilities
import org.jellyfin.androidtv.util.profile.codec.AvcCodecCapabilities
import org.jellyfin.androidtv.util.profile.codec.HevcCodecCapabilities
import org.jellyfin.androidtv.util.profile.codec.MediaCodecQuery

class MediaCodecCapabilitiesTest(
Expand All @@ -15,93 +14,18 @@ class MediaCodecCapabilitiesTest(
private val mediaCodecList by lazy { MediaCodecList(MediaCodecList.REGULAR_CODECS) }
private val codecQuery by lazy { MediaCodecQuery(mediaCodecList, softwareCodecsEnabled) }
private val avc by lazy { AvcCodecCapabilities(codecQuery) }
private val hevc by lazy { HevcCodecCapabilities(codecQuery) }
private val av1 by lazy { Av1CodecCapabilities(codecQuery) }

// Map common Dolby Vision Profiles to their corresponding CodecProfileLevel constant
private object DolbyVisionProfiles {
val Profile5: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
CodecProfileLevel.DolbyVisionProfileDvheStn else -1
}
val Profile7: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
CodecProfileLevel.DolbyVisionProfileDvheDtb else -1
}
val Profile8: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1)
CodecProfileLevel.DolbyVisionProfileDvheSt else -1
}
}

// Some devices (e.g., Fire OS) may support AV1 below the official API level
// Use the platform constant if the API level is met; otherwise fall back to the literal value
// Reference:
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/media/java/android/media/MediaCodecInfo.java
private object AV1ProfileLevel {
val ProfileMain10: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
CodecProfileLevel.AV1ProfileMain10 else 0x2
}
val ProfileMain10HDR10: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
CodecProfileLevel.AV1ProfileMain10HDR10 else 0x1000
}
val ProfileMain10HDR10Plus: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
CodecProfileLevel.AV1ProfileMain10HDR10Plus else 0x2000
}
val DolbyVisionProfile10: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
CodecProfileLevel.DolbyVisionProfileDvav110 else 0x400
}
val Level5: Int by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
CodecProfileLevel.AV1Level5 else 0x1000
}
}

// 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,
CodecProfileLevel.HEVCMainTierLevel2 to 60,
CodecProfileLevel.HEVCMainTierLevel21 to 63,
CodecProfileLevel.HEVCMainTierLevel3 to 90,
CodecProfileLevel.HEVCMainTierLevel31 to 93,
CodecProfileLevel.HEVCMainTierLevel4 to 120,
CodecProfileLevel.HEVCMainTierLevel41 to 123,
CodecProfileLevel.HEVCMainTierLevel5 to 150,
CodecProfileLevel.HEVCMainTierLevel51 to 153,
CodecProfileLevel.HEVCMainTierLevel52 to 156,
CodecProfileLevel.HEVCMainTierLevel6 to 180,
CodecProfileLevel.HEVCMainTierLevel61 to 183,
CodecProfileLevel.HEVCMainTierLevel62 to 186,
)

fun supportsAV1(): Boolean = codecQuery.hasCodecForMime(MimeTypes.VIDEO_AV1)

fun supportsAV1Main10(): Boolean = codecQuery.hasDecoder(
MimeTypes.VIDEO_AV1,
AV1ProfileLevel.ProfileMain10,
AV1ProfileLevel.Level5
)

fun supportsAV1DolbyVision(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
codecQuery.hasDecoder(
MimeTypes.VIDEO_DOLBY_VISION,
AV1ProfileLevel.DolbyVisionProfile10,
CodecProfileLevel.DolbyVisionLevelHd24
)

fun supportsAV1HDR10(): Boolean = codecQuery.hasDecoder(
MimeTypes.VIDEO_AV1,
AV1ProfileLevel.ProfileMain10HDR10,
AV1ProfileLevel.Level5
)

fun supportsAV1HDR10Plus(): Boolean = codecQuery.hasDecoder(
MimeTypes.VIDEO_AV1,
AV1ProfileLevel.ProfileMain10HDR10Plus,
AV1ProfileLevel.Level5
)
fun supportsAV1(): Boolean = av1.supportsAv1()

fun supportsAV1Main10(): Boolean = av1.supportsAv1Main10()

fun supportsAV1DolbyVision(): Boolean = av1.supportsAv1DolbyVision()

fun supportsAV1HDR10(): Boolean = av1.supportsAv1HDR10()

fun supportsAV1HDR10Plus(): Boolean = av1.supportsAv1HDR10Plus()

fun supportsAVC(): Boolean = avc.supportsAvc()

Expand All @@ -111,57 +35,21 @@ class MediaCodecCapabilitiesTest(

fun getAVCHigh10Level(): Int = avc.getHigh10Level()

fun supportsHevc(): Boolean = codecQuery.hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_HEVC)

fun supportsHevcMain10(): Boolean = codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10,
CodecProfileLevel.HEVCMainTierLevel4
)

// Can safely assume Dolby Vision decoders support single-layer HEVC profiles
fun supportsHevcDolbyVision(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
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 &&
codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION,
DolbyVisionProfiles.Profile7,
CodecProfileLevel.DolbyVisionLevelHd24
) &&
codecQuery.supportsMultiInstance(MediaFormat.MIMETYPE_VIDEO_HEVC)

fun supportsHevcHDR10(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10HDR10,
CodecProfileLevel.HEVCMainTierLevel4
)

fun supportsHevcHDR10Plus(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
codecQuery.hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10HDR10Plus,
CodecProfileLevel.HEVCMainTierLevel4
)

fun getHevcMainLevel(): Int = getHevcLevel(
CodecProfileLevel.HEVCProfileMain
)

fun getHevcMain10Level(): Int = getHevcLevel(
CodecProfileLevel.HEVCProfileMain10
)

private fun getHevcLevel(profile: Int): Int {
val level = codecQuery.getDecoderLevel(MediaFormat.MIMETYPE_VIDEO_HEVC, profile)

return hevcLevels.asReversed().find { item ->
level >= item.first
}?.second ?: 0
}
fun supportsHevc(): Boolean = hevc.supportsHevc()

fun supportsHevcMain10(): Boolean = hevc.supportsHevcMain10()

fun supportsHevcDolbyVision(): Boolean = hevc.supportsHevcDolbyVision()

fun supportsHevcDolbyVisionEL(): Boolean = hevc.supportsHevcDolbyVisionEL()

fun supportsHevcHDR10(): Boolean = hevc.supportsHevcHDR10()

fun supportsHevcHDR10Plus(): Boolean = hevc.supportsHevcHDR10Plus()

fun getHevcMainLevel(): Int = hevc.getMainLevel()

fun getHevcMain10Level(): Int = hevc.getMain10Level()

fun supportsVc1(): Boolean = codecQuery.hasCodecForMime(MimeTypes.VIDEO_VC1)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.jellyfin.androidtv.util.profile.codec

import android.media.MediaCodecInfo.CodecProfileLevel
import android.os.Build
import androidx.media3.common.MimeTypes

class Av1CodecCapabilities(
private val query: MediaCodecQuery,
) {
companion object {
private const val MIME_AV1 = MimeTypes.VIDEO_AV1
private const val MIME_DOLBY_VISION = MimeTypes.VIDEO_DOLBY_VISION

// Fallback values from AOSP MediaCodecInfo.java for pre-Q/R devices
// Some devices (e.g., Fire OS) support AV1 below the official API level
internal const val AV1_PROFILE_MAIN10 = 0x2
internal const val AV1_PROFILE_MAIN10_HDR10 = 0x1000
internal const val AV1_PROFILE_MAIN10_HDR10_PLUS = 0x2000
internal const val AV1_LEVEL5 = 0x1000
internal const val DV_PROFILE_DVAV1_10 = 0x400
}

private val profileMain10: Int
get() = if (DeviceSdk.sdkInt >= Build.VERSION_CODES.Q) CodecProfileLevel.AV1ProfileMain10 else AV1_PROFILE_MAIN10

private val profileMain10HDR10: Int
get() = if (DeviceSdk.sdkInt >= Build.VERSION_CODES.Q) CodecProfileLevel.AV1ProfileMain10HDR10 else AV1_PROFILE_MAIN10_HDR10

private val profileMain10HDR10Plus: Int
get() = if (DeviceSdk.sdkInt >= Build.VERSION_CODES.Q) CodecProfileLevel.AV1ProfileMain10HDR10Plus else AV1_PROFILE_MAIN10_HDR10_PLUS

private val dolbyVisionProfile10: Int
get() = if (DeviceSdk.sdkInt >= Build.VERSION_CODES.R) CodecProfileLevel.DolbyVisionProfileDvav110 else DV_PROFILE_DVAV1_10

private val level5: Int
get() = if (DeviceSdk.sdkInt >= Build.VERSION_CODES.Q) CodecProfileLevel.AV1Level5 else AV1_LEVEL5

fun supportsAv1(): Boolean = query.hasCodecForMime(MIME_AV1)

fun supportsAv1Main10(): Boolean = query.hasDecoder(
MIME_AV1,
profileMain10,
level5,
)

fun supportsAv1DolbyVision(): Boolean =
DeviceSdk.sdkInt >= Build.VERSION_CODES.N &&
query.hasDecoder(
MIME_DOLBY_VISION,
dolbyVisionProfile10,
CodecProfileLevel.DolbyVisionLevelHd24,
)

fun supportsAv1HDR10(): Boolean = query.hasDecoder(
MIME_AV1,
profileMain10HDR10,
level5,
)

fun supportsAv1HDR10Plus(): Boolean = query.hasDecoder(
MIME_AV1,
profileMain10HDR10Plus,
level5,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.jellyfin.androidtv.util.profile.codec

import android.os.Build

// Wraps Build.VERSION.SDK_INT so tests can mock it via mockkObject — the static final field
// can't be set reflectively on JDK 17+.
internal object DeviceSdk {
val sdkInt: Int get() = Build.VERSION.SDK_INT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.jellyfin.androidtv.util.profile.codec

import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaFormat
import android.os.Build

class HevcCodecCapabilities(
private val query: MediaCodecQuery,
) {
companion object {
// HEVC levels as reported by ffprobe are multiplied by 30, e.g. level 4.1 is 123
internal val LEVEL_MAP: List<Pair<Int, Int>> = listOf(
CodecProfileLevel.HEVCMainTierLevel1 to 30,
CodecProfileLevel.HEVCMainTierLevel2 to 60,
CodecProfileLevel.HEVCMainTierLevel21 to 63,
CodecProfileLevel.HEVCMainTierLevel3 to 90,
CodecProfileLevel.HEVCMainTierLevel31 to 93,
CodecProfileLevel.HEVCMainTierLevel4 to 120,
CodecProfileLevel.HEVCMainTierLevel41 to 123,
CodecProfileLevel.HEVCMainTierLevel5 to 150,
CodecProfileLevel.HEVCMainTierLevel51 to 153,
CodecProfileLevel.HEVCMainTierLevel52 to 156,
CodecProfileLevel.HEVCMainTierLevel6 to 180,
CodecProfileLevel.HEVCMainTierLevel61 to 183,
CodecProfileLevel.HEVCMainTierLevel62 to 186,
)

private const val MIME_HEVC = MediaFormat.MIMETYPE_VIDEO_HEVC
private const val MIME_DOLBY_VISION = MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION
}

fun supportsHevc(): Boolean = query.hasCodecForMime(MIME_HEVC)

fun supportsHevcMain10(): Boolean = query.hasDecoder(
MIME_HEVC,
CodecProfileLevel.HEVCProfileMain10,
CodecProfileLevel.HEVCMainTierLevel4,
)

fun supportsHevcDolbyVision(): Boolean =
DeviceSdk.sdkInt >= Build.VERSION_CODES.N &&
query.hasCodecForMime(MIME_DOLBY_VISION)

fun supportsHevcDolbyVisionEL(): Boolean =
DeviceSdk.sdkInt >= Build.VERSION_CODES.N &&
query.hasDecoder(
MIME_DOLBY_VISION,
CodecProfileLevel.DolbyVisionProfileDvheDtb,
CodecProfileLevel.DolbyVisionLevelHd24,
) &&
query.supportsMultiInstance(MIME_HEVC)

fun supportsHevcHDR10(): Boolean =
DeviceSdk.sdkInt >= Build.VERSION_CODES.N &&
query.hasDecoder(
MIME_HEVC,
CodecProfileLevel.HEVCProfileMain10HDR10,
CodecProfileLevel.HEVCMainTierLevel4,
)

fun supportsHevcHDR10Plus(): Boolean =
DeviceSdk.sdkInt >= Build.VERSION_CODES.Q &&
query.hasDecoder(
MIME_HEVC,
CodecProfileLevel.HEVCProfileMain10HDR10Plus,
CodecProfileLevel.HEVCMainTierLevel4,
)

fun getMainLevel(): Int = getLevel(CodecProfileLevel.HEVCProfileMain)

fun getMain10Level(): Int = getLevel(CodecProfileLevel.HEVCProfileMain10)

private fun getLevel(profile: Int): Int {
val level = query.getDecoderLevel(MIME_HEVC, profile)

return LEVEL_MAP.asReversed().find { (codecLevel, _) ->
level >= codecLevel
}?.second ?: 0
}
}
Loading