mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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:
parent
ec852906b1
commit
20ab146c97
10 changed files with 472 additions and 150 deletions
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue