Update device profile for Jellyfin 10.11 (#293)

## Details

This PR updates the device profile used by ExoPlayer. It should more
accurately report support for HDR formats.

## Acknowledgements
This PR contains a commit (cd6351912af72d274b0baf50af4d777b986f0f46)
that has code copied and slightly modified from
https://github.com/jellyfin/jellyfin-androidtv `v0.19.4`.

Major acknowledgement & credit to the Jellyfin Android TV developers for
this code!

## Issues
Related to #15
This commit is contained in:
damontecres 2025-11-21 20:16:49 -05:00 committed by GitHub
parent ec852906b1
commit 20ab146c97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 472 additions and 150 deletions

View file

@ -0,0 +1,308 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "a4fd9e29c8cbcd13c24e8644f62e8551",
"entities": [
{
"tableName": "servers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "rowId",
"columnName": "rowId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT"
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"rowId"
]
},
"indices": [
{
"name": "index_users_id_serverId",
"unique": true,
"columnNames": [
"id",
"serverId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name": "index_users_id",
"unique": false,
"columnNames": [
"id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name": "index_users_serverId",
"unique": false,
"columnNames": [
"serverId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys": [
{
"table": "servers",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ItemPlayback",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "rowId",
"columnName": "rowId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourceId",
"columnName": "sourceId",
"affinity": "TEXT"
},
{
"fieldPath": "audioIndex",
"columnName": "audioIndex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subtitleIndex",
"columnName": "subtitleIndex",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"rowId"
]
},
"indices": [
{
"name": "index_ItemPlayback_userId_itemId",
"unique": true,
"columnNames": [
"userId",
"itemId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"userId"
],
"referencedColumns": [
"rowId"
]
}
]
},
{
"tableName": "NavDrawerPinnedItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId",
"itemId"
]
},
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"userId"
],
"referencedColumns": [
"rowId"
]
}
]
},
{
"tableName": "LibraryDisplayInfo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "sort",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "direction",
"columnName": "direction",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId",
"itemId"
]
},
"indices": [
{
"name": "index_LibraryDisplayInfo_userId_itemId",
"unique": true,
"columnNames": [
"userId",
"itemId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"userId"
],
"referencedColumns": [
"rowId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a4fd9e29c8cbcd13c24e8644f62e8551')"
]
}
}

View file

@ -120,11 +120,6 @@ class MainActivity : AppCompatActivity() {
userDto?.configuration ?: DefaultUserConfiguration,
)
val deviceProfile =
remember(appPreferences) {
createDeviceProfile(this@MainActivity, preferences, false)
}
if (isRestoringSession) {
Box(
modifier = Modifier.size(200.dp),
@ -154,6 +149,14 @@ class MainActivity : AppCompatActivity() {
}
}
}
val deviceProfile =
remember(server, appPreferences) {
createDeviceProfile(
this@MainActivity,
preferences,
server?.serverVersion,
)
}
ApplicationContent(
user = user,
server = server,

View file

@ -19,12 +19,13 @@ import java.util.UUID
@Database(
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
version = 6,
version = 7,
exportSchema = true,
autoMigrations = [
AutoMigration(3, 4),
AutoMigration(4, 5),
AutoMigration(5, 6),
AutoMigration(6, 7),
],
)
@TypeConverters(Converters::class)

View file

@ -78,7 +78,7 @@ class ServerRepository
val updatedServer =
try {
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
server.copy(name = sysInfo.serverName)
server.copy(name = sysInfo.serverName, version = sysInfo.version)
} catch (ex: Exception) {
Timber.w(ex, "Exception fetching public system info")
server
@ -148,6 +148,7 @@ class ServerRepository
id = it,
name = authedUser?.serverName,
url = serverUrl,
null,
)
}
if (server != null) {

View file

@ -6,12 +6,14 @@ import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.Relation
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.ServerVersion
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import java.util.UUID
@ -21,7 +23,11 @@ data class JellyfinServer(
@PrimaryKey val id: UUID,
val name: String?,
val url: String,
)
val version: String?,
) {
@get:Ignore
val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) }
}
@Entity(
tableName = "users",

View file

@ -145,6 +145,7 @@ class SwitchServerViewModel
id = id,
name = serverInfo.serverName,
url = serverUrl,
version = serverInfo.version,
)
serverRepository.addAndChangeServer(server)
val quickConnect =
@ -198,6 +199,7 @@ class SwitchServerViewModel
server.id.toUUID(),
server.name,
server.address,
null,
),
)
}

View file

@ -1,10 +1,11 @@
package com.github.damontecres.wholphin.util.profile
// Adapted from https://github.com/jellyfin/jellyfin-androidtv/blob/c775603df457862c495b010550ae0aee1a66c0bc/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt
// Adapted from https://github.com/jellyfin/jellyfin-androidtv/blob/v0.19.4/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt
import android.content.Context
import androidx.media3.common.MimeTypes
import com.github.damontecres.wholphin.preferences.UserPreferences
import org.jellyfin.sdk.model.ServerVersion
import org.jellyfin.sdk.model.api.CodecType
import org.jellyfin.sdk.model.api.DlnaProfileType
import org.jellyfin.sdk.model.api.EncodingContext
@ -48,37 +49,35 @@ val supportedAudioCodecs =
fun createDeviceProfile(
context: Context,
userPreferences: UserPreferences,
disableDirectPlay: Boolean = false,
serverVersion: ServerVersion?,
) = userPreferences.appPreferences.playbackPreferences.let { prefs ->
createDeviceProfile(
mediaTest = MediaCodecCapabilitiesTest(context),
maxBitrate = 100_000_000,
disableDirectPlay = disableDirectPlay,
maxBitrate = prefs.maxBitrate.toInt(),
isAC3Enabled = prefs.overrides.ac3Supported,
downMixAudio = prefs.overrides.downmixStereo,
assDirectPlay = prefs.overrides.directPlayAss,
pgsDirectPlay = prefs.overrides.directPlayPgs,
jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
)
}
fun createDeviceProfile(
mediaTest: MediaCodecCapabilitiesTest,
maxBitrate: Int,
disableDirectPlay: Boolean,
isAC3Enabled: Boolean,
downMixAudio: Boolean,
assDirectPlay: Boolean,
pgsDirectPlay: Boolean,
jellyfinTenEleven: Boolean,
) = buildDeviceProfile {
val allowedAudioCodecs =
when {
downMixAudio -> downmixSupportedAudioCodecs
!isAC3Enabled ->
supportedAudioCodecs
.filterNot {
it == Codec.Audio.EAC3 ||
it == Codec.Audio.AC3
}.toTypedArray()
.filterNot { it == Codec.Audio.EAC3 || it == Codec.Audio.AC3 }
.toTypedArray()
else -> supportedAudioCodecs
}
@ -98,10 +97,6 @@ fun createDeviceProfile(
val maxResolutionAV1 = mediaTest.getMaxResolution(MimeTypes.VIDEO_AV1)
// / HDR capabilities
// Display
val supportsDolbyVisionDisplay = mediaTest.supportsDolbyVision()
val supportsHdr10Display = mediaTest.supportsHdr10()
val supportsHdr10PlusDisplay = mediaTest.supportsHdr10Plus()
// Codecs
// AV1
@ -150,46 +145,44 @@ fun createDeviceProfile(
}
// / Direct play profiles
if (!disableDirectPlay) {
// Video
directPlayProfile {
type = DlnaProfileType.VIDEO
// Video
directPlayProfile {
type = DlnaProfileType.VIDEO
container(
Codec.Container.ASF,
Codec.Container.HLS,
Codec.Container.M4V,
Codec.Container.MKV,
Codec.Container.MOV,
Codec.Container.MP4,
Codec.Container.OGM,
Codec.Container.OGV,
Codec.Container.TS,
Codec.Container.VOB,
Codec.Container.WEBM,
Codec.Container.WMV,
Codec.Container.XVID,
)
container(
Codec.Container.ASF,
Codec.Container.HLS,
Codec.Container.M4V,
Codec.Container.MKV,
Codec.Container.MOV,
Codec.Container.MP4,
Codec.Container.OGM,
Codec.Container.OGV,
Codec.Container.TS,
Codec.Container.VOB,
Codec.Container.WEBM,
Codec.Container.WMV,
Codec.Container.XVID,
)
videoCodec(
Codec.Video.AV1,
Codec.Video.H264,
Codec.Video.HEVC,
Codec.Video.MPEG,
Codec.Video.MPEG2VIDEO,
Codec.Video.VP8,
Codec.Video.VP9,
)
videoCodec(
Codec.Video.AV1,
Codec.Video.H264,
Codec.Video.HEVC,
Codec.Video.MPEG,
Codec.Video.MPEG2VIDEO,
Codec.Video.VP8,
Codec.Video.VP9,
)
audioCodec(*allowedAudioCodecs)
}
audioCodec(*allowedAudioCodecs)
}
// Audio
directPlayProfile {
type = DlnaProfileType.AUDIO
// Audio
directPlayProfile {
type = DlnaProfileType.AUDIO
audioCodec(*allowedAudioCodecs)
}
audioCodec(*allowedAudioCodecs)
}
// / Codec profiles
@ -371,96 +364,101 @@ fun createDeviceProfile(
}
// / HDR exclude list
// TODO Use VideoRangeType enum with Jelylfin 10.11 based SDK
// Display
codecProfile {
type = CodecType.VIDEO
conditions {
if (!supportsDolbyVisionDisplay) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI.serialName
// TODO values not supported on v10.10: https://github.com/jellyfin/jellyfin/blob/v10.10.7/Jellyfin.Data/Enums/VideoRangeType.cs
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithEL"
// if (!supportsHdr10PlusDisplay) {
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithHDR10Plus"
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithELHDR10Plus"
// }
if (!supportsHdr10Display) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI_WITH_HDR10.serialName
}
// TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK
val unsupportedRangeTypesAv1 =
buildSet {
if (jellyfinTenEleven) add("DOVIInvalid")
if (!supportsAV1DolbyVision) {
add(VideoRangeType.DOVI.serialName)
if (!supportsAV1HDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName)
if (jellyfinTenEleven && !supportsAV1HDR10Plus) add("DOVIWithHDR10Plus")
}
if (!supportsHdr10PlusDisplay) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10_PLUS.serialName
if (!supportsHdr10Display) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10.serialName
}
if (!supportsAV1HDR10Plus) {
add(VideoRangeType.HDR10_PLUS.serialName)
if (!mediaTest.supportsAV1HDR10()) add(VideoRangeType.HDR10.serialName)
}
}
}.let {
// Remove codec profile if all HDR types are fully supported
if (it.conditions.isEmpty()) codecProfiles.remove(it)
}
// TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK
val unsupportedRangeTypesHevc =
buildSet {
if (jellyfinTenEleven) add("DOVIInvalid")
if (!supportsHevcDolbyVisionEL) {
if (jellyfinTenEleven) {
add("DOVIWithEL")
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus")
}
if (!supportsHevcDolbyVision) {
add(VideoRangeType.DOVI.serialName)
if (!supportsHevcHDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName)
if (jellyfinTenEleven && !supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) {
add(
"DOVIWithHDR10Plus",
)
}
}
}
if (!supportsHevcHDR10Plus) {
add(VideoRangeType.HDR10_PLUS.serialName)
if (!supportsHevcHDR10) add(VideoRangeType.HDR10.serialName)
}
if (jellyfinTenEleven && KnownDefects.hevcDoviHdr10PlusBug) {
add("DOVIWithHDR10Plus")
add("DOVIWithELHDR10Plus")
}
}
// Display
// Note: The codec profiles use a workaround to create correct behavior
// The notEquals condition will always fail the ConditionProcessor test in the server so we use applyConditions to only have the codec
// profile be active when the media in question uses one of the unsupported range types. The server will then use the value of the
// notEquals in the StreamBuilder to create a correct transcode pipeline
// Codecs
// AV1
codecProfile {
type = CodecType.VIDEO
codec = Codec.Video.AV1
if (unsupportedRangeTypesAv1.isNotEmpty()) {
codecProfile {
type = CodecType.VIDEO
codec = Codec.Video.AV1
conditions {
if (supportsDolbyVisionDisplay && !supportsAV1DolbyVision) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI.serialName
if (supportsHdr10Display && !supportsAV1HDR10) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI_WITH_HDR10.serialName
}
if (supportsHdr10PlusDisplay && !supportsAV1HDR10Plus) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithHDR10Plus"
}
conditions {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals
unsupportedRangeTypesAv1.joinToString(
"|",
)
}
if (supportsHdr10PlusDisplay && !mediaTest.supportsAV1HDR10Plus()) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10_PLUS.serialName
if (supportsHdr10Display && !mediaTest.supportsAV1HDR10()) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10.serialName
}
applyConditions {
ProfileConditionValue.VIDEO_RANGE_TYPE inCollection unsupportedRangeTypesAv1
}
}
}.let {
// Remove codec profile if all HDR types are fully supported
if (it.conditions.isEmpty()) codecProfiles.remove(it)
}
// HEVC
codecProfile {
type = CodecType.VIDEO
codec = Codec.Video.HEVC
if (unsupportedRangeTypesHevc.isNotEmpty()) {
codecProfile {
type = CodecType.VIDEO
codec = Codec.Video.HEVC
conditions {
if (supportsDolbyVisionDisplay && !supportsHevcDolbyVisionEL) {
// TODO
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithEL"
// if (supportsHdr10PlusDisplay && !supportsHevcHDR10Plus) {
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithELHDR10Plus"
// }
if (!supportsHevcDolbyVision) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI.serialName
if (supportsHdr10Display && !supportsHevcHDR10) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI_WITH_HDR10.serialName
}
// if (supportsHdr10PlusDisplay && !supportsHevcHDR10Plus) {
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithHDR10Plus"
// }
}
conditions {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals
unsupportedRangeTypesHevc.joinToString(
"|",
)
}
if (supportsHdr10PlusDisplay && !supportsHevcHDR10Plus) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10_PLUS.serialName
if (supportsHdr10Display && !supportsHevcHDR10) {
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10.serialName
}
applyConditions {
ProfileConditionValue.VIDEO_RANGE_TYPE inCollection unsupportedRangeTypesHevc
}
}
}.let {
// Remove codec profile if all HDR types are fully supported
if (it.conditions.isEmpty()) codecProfiles.remove(it)
}
// Audio channel profile

View file

@ -0,0 +1,19 @@
package com.github.damontecres.wholphin.util.profile
// Copied from https://github.com/jellyfin/jellyfin-androidtv/blob/v0.19.4/app/src/main/java/org/jellyfin/androidtv/util/profile/KnownDefects.kt
import android.os.Build
/**
* List of devie models with known HEVC DoVi/HDR10+ playback issues.
*/
private val modelsWithDoViHdr10PlusBug =
listOf(
"AFTKRT", // Amazon Fire TV 4K Max (2nd Gen)
"AFTKA", // Amazon Fire TV 4K Max (1st Gen)
"AFTKM", // Amazon Fire TV 4K (2nd Gen)
)
object KnownDefects {
val hevcDoviHdr10PlusBug = Build.MODEL in modelsWithDoViHdr10PlusBug
}

View file

@ -1,6 +1,6 @@
package com.github.damontecres.wholphin.util.profile
// Adapted from https://github.com/jellyfin/jellyfin-androidtv/blob/c775603df457862c495b010550ae0aee1a66c0bc/app/src/main/java/org/jellyfin/androidtv/util/profile/MediaCodecCapabilitiesTest.kt
// Copied from https://github.com/jellyfin/jellyfin-androidtv/blob/v0.19.4/app/src/main/java/org/jellyfin/androidtv/util/profile/MediaCodecCapabilitiesTest.kt
import android.content.Context
import android.media.MediaCodecInfo.CodecProfileLevel
@ -8,7 +8,6 @@ import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import android.util.Size
import android.view.Display
import androidx.core.content.ContextCompat
import timber.log.Timber
@ -18,17 +17,6 @@ class MediaCodecCapabilitiesTest(
private val display by lazy { ContextCompat.getDisplayOrDefault(context) }
private val mediaCodecList by lazy { MediaCodecList(MediaCodecList.REGULAR_CODECS) }
@Suppress("DEPRECATION")
private val supportedHdrTypes by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
display.mode.supportedHdrTypes.toList()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
display.hdrCapabilities.supportedHdrTypes.toList()
} else {
emptyList()
}
}
// Map common Dolby Vision Profiles to their corresponding CodecProfileLevel constant
private object DolbyVisionProfiles {
val Profile5: Int by lazy {
@ -340,13 +328,4 @@ class MediaCodecCapabilitiesTest(
return Size(maxWidth, maxHeight)
}
fun supportsDolbyVision(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportedHdrTypes.contains(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)
fun supportsHdr10(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportedHdrTypes.contains(Display.HdrCapabilities.HDR_TYPE_HDR10)
fun supportsHdr10Plus(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && supportedHdrTypes.contains(Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS)
}

View file

@ -0,0 +1,5 @@
# Acknowledgement
This folder/package (`com.github.damontecres.wholphin.util.profile`) contains code copied and modified from https://github.com/jellyfin/jellyfin-androidtv. See the individual files for more details.
Both Jellyfin Android TV and Wholphin are licensed under GPL-2.0. See the project root for full license text.