diff --git a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/7.json b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/7.json new file mode 100644 index 00000000..2eb54dc4 --- /dev/null +++ b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/7.json @@ -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')" + ] + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index fbacf6e8..eca1250c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt index 305a3146..2292d7ea 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt @@ -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) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt index ec385b53..a381464d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt index c9c2f6bf..e7933992 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -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", diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt index d75dafaa..23201235 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt @@ -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, ), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt index 9ab8e87b..2da77741 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/DeviceProfileUtils.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/KnownDefects.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/KnownDefects.kt new file mode 100644 index 00000000..0a936de9 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/KnownDefects.kt @@ -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 +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt b/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt index 1f926621..c607e89c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/MediaCodecCapabilitiesTest.kt @@ -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) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/profile/README.md b/app/src/main/java/com/github/damontecres/wholphin/util/profile/README.md new file mode 100644 index 00000000..31e232be --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/profile/README.md @@ -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.