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,
|
userDto?.configuration ?: DefaultUserConfiguration,
|
||||||
)
|
)
|
||||||
|
|
||||||
val deviceProfile =
|
|
||||||
remember(appPreferences) {
|
|
||||||
createDeviceProfile(this@MainActivity, preferences, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRestoringSession) {
|
if (isRestoringSession) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(200.dp),
|
modifier = Modifier.size(200.dp),
|
||||||
|
|
@ -154,6 +149,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val deviceProfile =
|
||||||
|
remember(server, appPreferences) {
|
||||||
|
createDeviceProfile(
|
||||||
|
this@MainActivity,
|
||||||
|
preferences,
|
||||||
|
server?.serverVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
ApplicationContent(
|
ApplicationContent(
|
||||||
user = user,
|
user = user,
|
||||||
server = server,
|
server = server,
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,13 @@ import java.util.UUID
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||||
version = 6,
|
version = 7,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
AutoMigration(4, 5),
|
AutoMigration(4, 5),
|
||||||
AutoMigration(5, 6),
|
AutoMigration(5, 6),
|
||||||
|
AutoMigration(6, 7),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class ServerRepository
|
||||||
val updatedServer =
|
val updatedServer =
|
||||||
try {
|
try {
|
||||||
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
|
val sysInfo by apiClient.systemApi.getPublicSystemInfo()
|
||||||
server.copy(name = sysInfo.serverName)
|
server.copy(name = sysInfo.serverName, version = sysInfo.version)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.w(ex, "Exception fetching public system info")
|
Timber.w(ex, "Exception fetching public system info")
|
||||||
server
|
server
|
||||||
|
|
@ -148,6 +148,7 @@ class ServerRepository
|
||||||
id = it,
|
id = it,
|
||||||
name = authedUser?.serverName,
|
name = authedUser?.serverName,
|
||||||
url = serverUrl,
|
url = serverUrl,
|
||||||
|
null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ import androidx.room.ColumnInfo
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Ignore
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.UseSerializers
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import org.jellyfin.sdk.model.ServerVersion
|
||||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -21,7 +23,11 @@ data class JellyfinServer(
|
||||||
@PrimaryKey val id: UUID,
|
@PrimaryKey val id: UUID,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val url: String,
|
val url: String,
|
||||||
)
|
val version: String?,
|
||||||
|
) {
|
||||||
|
@get:Ignore
|
||||||
|
val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) }
|
||||||
|
}
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "users",
|
tableName = "users",
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ class SwitchServerViewModel
|
||||||
id = id,
|
id = id,
|
||||||
name = serverInfo.serverName,
|
name = serverInfo.serverName,
|
||||||
url = serverUrl,
|
url = serverUrl,
|
||||||
|
version = serverInfo.version,
|
||||||
)
|
)
|
||||||
serverRepository.addAndChangeServer(server)
|
serverRepository.addAndChangeServer(server)
|
||||||
val quickConnect =
|
val quickConnect =
|
||||||
|
|
@ -198,6 +199,7 @@ class SwitchServerViewModel
|
||||||
server.id.toUUID(),
|
server.id.toUUID(),
|
||||||
server.name,
|
server.name,
|
||||||
server.address,
|
server.address,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.util.profile
|
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 android.content.Context
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
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.CodecType
|
||||||
import org.jellyfin.sdk.model.api.DlnaProfileType
|
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||||
import org.jellyfin.sdk.model.api.EncodingContext
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
|
|
@ -48,37 +49,35 @@ val supportedAudioCodecs =
|
||||||
fun createDeviceProfile(
|
fun createDeviceProfile(
|
||||||
context: Context,
|
context: Context,
|
||||||
userPreferences: UserPreferences,
|
userPreferences: UserPreferences,
|
||||||
disableDirectPlay: Boolean = false,
|
serverVersion: ServerVersion?,
|
||||||
) = userPreferences.appPreferences.playbackPreferences.let { prefs ->
|
) = userPreferences.appPreferences.playbackPreferences.let { prefs ->
|
||||||
createDeviceProfile(
|
createDeviceProfile(
|
||||||
mediaTest = MediaCodecCapabilitiesTest(context),
|
mediaTest = MediaCodecCapabilitiesTest(context),
|
||||||
maxBitrate = 100_000_000,
|
maxBitrate = prefs.maxBitrate.toInt(),
|
||||||
disableDirectPlay = disableDirectPlay,
|
|
||||||
isAC3Enabled = prefs.overrides.ac3Supported,
|
isAC3Enabled = prefs.overrides.ac3Supported,
|
||||||
downMixAudio = prefs.overrides.downmixStereo,
|
downMixAudio = prefs.overrides.downmixStereo,
|
||||||
assDirectPlay = prefs.overrides.directPlayAss,
|
assDirectPlay = prefs.overrides.directPlayAss,
|
||||||
pgsDirectPlay = prefs.overrides.directPlayPgs,
|
pgsDirectPlay = prefs.overrides.directPlayPgs,
|
||||||
|
jellyfinTenEleven = serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDeviceProfile(
|
fun createDeviceProfile(
|
||||||
mediaTest: MediaCodecCapabilitiesTest,
|
mediaTest: MediaCodecCapabilitiesTest,
|
||||||
maxBitrate: Int,
|
maxBitrate: Int,
|
||||||
disableDirectPlay: Boolean,
|
|
||||||
isAC3Enabled: Boolean,
|
isAC3Enabled: Boolean,
|
||||||
downMixAudio: Boolean,
|
downMixAudio: Boolean,
|
||||||
assDirectPlay: Boolean,
|
assDirectPlay: Boolean,
|
||||||
pgsDirectPlay: Boolean,
|
pgsDirectPlay: Boolean,
|
||||||
|
jellyfinTenEleven: Boolean,
|
||||||
) = buildDeviceProfile {
|
) = buildDeviceProfile {
|
||||||
val allowedAudioCodecs =
|
val allowedAudioCodecs =
|
||||||
when {
|
when {
|
||||||
downMixAudio -> downmixSupportedAudioCodecs
|
downMixAudio -> downmixSupportedAudioCodecs
|
||||||
!isAC3Enabled ->
|
!isAC3Enabled ->
|
||||||
supportedAudioCodecs
|
supportedAudioCodecs
|
||||||
.filterNot {
|
.filterNot { it == Codec.Audio.EAC3 || it == Codec.Audio.AC3 }
|
||||||
it == Codec.Audio.EAC3 ||
|
.toTypedArray()
|
||||||
it == Codec.Audio.AC3
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
else -> supportedAudioCodecs
|
else -> supportedAudioCodecs
|
||||||
}
|
}
|
||||||
|
|
@ -98,10 +97,6 @@ fun createDeviceProfile(
|
||||||
val maxResolutionAV1 = mediaTest.getMaxResolution(MimeTypes.VIDEO_AV1)
|
val maxResolutionAV1 = mediaTest.getMaxResolution(MimeTypes.VIDEO_AV1)
|
||||||
|
|
||||||
// / HDR capabilities
|
// / HDR capabilities
|
||||||
// Display
|
|
||||||
val supportsDolbyVisionDisplay = mediaTest.supportsDolbyVision()
|
|
||||||
val supportsHdr10Display = mediaTest.supportsHdr10()
|
|
||||||
val supportsHdr10PlusDisplay = mediaTest.supportsHdr10Plus()
|
|
||||||
|
|
||||||
// Codecs
|
// Codecs
|
||||||
// AV1
|
// AV1
|
||||||
|
|
@ -150,46 +145,44 @@ fun createDeviceProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
// / Direct play profiles
|
// / Direct play profiles
|
||||||
if (!disableDirectPlay) {
|
// Video
|
||||||
// Video
|
directPlayProfile {
|
||||||
directPlayProfile {
|
type = DlnaProfileType.VIDEO
|
||||||
type = DlnaProfileType.VIDEO
|
|
||||||
|
|
||||||
container(
|
container(
|
||||||
Codec.Container.ASF,
|
Codec.Container.ASF,
|
||||||
Codec.Container.HLS,
|
Codec.Container.HLS,
|
||||||
Codec.Container.M4V,
|
Codec.Container.M4V,
|
||||||
Codec.Container.MKV,
|
Codec.Container.MKV,
|
||||||
Codec.Container.MOV,
|
Codec.Container.MOV,
|
||||||
Codec.Container.MP4,
|
Codec.Container.MP4,
|
||||||
Codec.Container.OGM,
|
Codec.Container.OGM,
|
||||||
Codec.Container.OGV,
|
Codec.Container.OGV,
|
||||||
Codec.Container.TS,
|
Codec.Container.TS,
|
||||||
Codec.Container.VOB,
|
Codec.Container.VOB,
|
||||||
Codec.Container.WEBM,
|
Codec.Container.WEBM,
|
||||||
Codec.Container.WMV,
|
Codec.Container.WMV,
|
||||||
Codec.Container.XVID,
|
Codec.Container.XVID,
|
||||||
)
|
)
|
||||||
|
|
||||||
videoCodec(
|
videoCodec(
|
||||||
Codec.Video.AV1,
|
Codec.Video.AV1,
|
||||||
Codec.Video.H264,
|
Codec.Video.H264,
|
||||||
Codec.Video.HEVC,
|
Codec.Video.HEVC,
|
||||||
Codec.Video.MPEG,
|
Codec.Video.MPEG,
|
||||||
Codec.Video.MPEG2VIDEO,
|
Codec.Video.MPEG2VIDEO,
|
||||||
Codec.Video.VP8,
|
Codec.Video.VP8,
|
||||||
Codec.Video.VP9,
|
Codec.Video.VP9,
|
||||||
)
|
)
|
||||||
|
|
||||||
audioCodec(*allowedAudioCodecs)
|
audioCodec(*allowedAudioCodecs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
directPlayProfile {
|
directPlayProfile {
|
||||||
type = DlnaProfileType.AUDIO
|
type = DlnaProfileType.AUDIO
|
||||||
|
|
||||||
audioCodec(*allowedAudioCodecs)
|
audioCodec(*allowedAudioCodecs)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// / Codec profiles
|
// / Codec profiles
|
||||||
|
|
@ -371,96 +364,101 @@ fun createDeviceProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
// / HDR exclude list
|
// / HDR exclude list
|
||||||
// TODO Use VideoRangeType enum with Jelylfin 10.11 based SDK
|
|
||||||
// Display
|
|
||||||
codecProfile {
|
|
||||||
type = CodecType.VIDEO
|
|
||||||
|
|
||||||
conditions {
|
// TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK
|
||||||
if (!supportsDolbyVisionDisplay) {
|
val unsupportedRangeTypesAv1 =
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI.serialName
|
buildSet {
|
||||||
// TODO values not supported on v10.10: https://github.com/jellyfin/jellyfin/blob/v10.10.7/Jellyfin.Data/Enums/VideoRangeType.cs
|
if (jellyfinTenEleven) add("DOVIInvalid")
|
||||||
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithEL"
|
|
||||||
// if (!supportsHdr10PlusDisplay) {
|
if (!supportsAV1DolbyVision) {
|
||||||
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithHDR10Plus"
|
add(VideoRangeType.DOVI.serialName)
|
||||||
// ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithELHDR10Plus"
|
if (!supportsAV1HDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName)
|
||||||
// }
|
if (jellyfinTenEleven && !supportsAV1HDR10Plus) add("DOVIWithHDR10Plus")
|
||||||
if (!supportsHdr10Display) {
|
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI_WITH_HDR10.serialName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!supportsHdr10PlusDisplay) {
|
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10_PLUS.serialName
|
if (!supportsAV1HDR10Plus) {
|
||||||
if (!supportsHdr10Display) {
|
add(VideoRangeType.HDR10_PLUS.serialName)
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10.serialName
|
|
||||||
}
|
if (!mediaTest.supportsAV1HDR10()) add(VideoRangeType.HDR10.serialName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let {
|
|
||||||
// Remove codec profile if all HDR types are fully supported
|
// TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK
|
||||||
if (it.conditions.isEmpty()) codecProfiles.remove(it)
|
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
|
// Codecs
|
||||||
// AV1
|
// AV1
|
||||||
codecProfile {
|
if (unsupportedRangeTypesAv1.isNotEmpty()) {
|
||||||
type = CodecType.VIDEO
|
codecProfile {
|
||||||
codec = Codec.Video.AV1
|
type = CodecType.VIDEO
|
||||||
|
codec = Codec.Video.AV1
|
||||||
|
|
||||||
conditions {
|
conditions {
|
||||||
if (supportsDolbyVisionDisplay && !supportsAV1DolbyVision) {
|
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI.serialName
|
unsupportedRangeTypesAv1.joinToString(
|
||||||
if (supportsHdr10Display && !supportsAV1HDR10) {
|
"|",
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.DOVI_WITH_HDR10.serialName
|
)
|
||||||
}
|
|
||||||
if (supportsHdr10PlusDisplay && !supportsAV1HDR10Plus) {
|
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals "DOVIWithHDR10Plus"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (supportsHdr10PlusDisplay && !mediaTest.supportsAV1HDR10Plus()) {
|
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10_PLUS.serialName
|
applyConditions {
|
||||||
if (supportsHdr10Display && !mediaTest.supportsAV1HDR10()) {
|
ProfileConditionValue.VIDEO_RANGE_TYPE inCollection unsupportedRangeTypesAv1
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10.serialName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let {
|
|
||||||
// Remove codec profile if all HDR types are fully supported
|
|
||||||
if (it.conditions.isEmpty()) codecProfiles.remove(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HEVC
|
// HEVC
|
||||||
codecProfile {
|
if (unsupportedRangeTypesHevc.isNotEmpty()) {
|
||||||
type = CodecType.VIDEO
|
codecProfile {
|
||||||
codec = Codec.Video.HEVC
|
type = CodecType.VIDEO
|
||||||
|
codec = Codec.Video.HEVC
|
||||||
|
|
||||||
conditions {
|
conditions {
|
||||||
if (supportsDolbyVisionDisplay && !supportsHevcDolbyVisionEL) {
|
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals
|
||||||
// TODO
|
unsupportedRangeTypesHevc.joinToString(
|
||||||
// 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"
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (supportsHdr10PlusDisplay && !supportsHevcHDR10Plus) {
|
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10_PLUS.serialName
|
applyConditions {
|
||||||
if (supportsHdr10Display && !supportsHevcHDR10) {
|
ProfileConditionValue.VIDEO_RANGE_TYPE inCollection unsupportedRangeTypesHevc
|
||||||
ProfileConditionValue.VIDEO_RANGE_TYPE notEquals VideoRangeType.HDR10.serialName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let {
|
|
||||||
// Remove codec profile if all HDR types are fully supported
|
|
||||||
if (it.conditions.isEmpty()) codecProfiles.remove(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio channel profile
|
// 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
|
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.content.Context
|
||||||
import android.media.MediaCodecInfo.CodecProfileLevel
|
import android.media.MediaCodecInfo.CodecProfileLevel
|
||||||
|
|
@ -8,7 +8,6 @@ import android.media.MediaCodecList
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.Display
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -18,17 +17,6 @@ class MediaCodecCapabilitiesTest(
|
||||||
private val display by lazy { ContextCompat.getDisplayOrDefault(context) }
|
private val display by lazy { ContextCompat.getDisplayOrDefault(context) }
|
||||||
private val mediaCodecList by lazy { MediaCodecList(MediaCodecList.REGULAR_CODECS) }
|
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
|
// Map common Dolby Vision Profiles to their corresponding CodecProfileLevel constant
|
||||||
private object DolbyVisionProfiles {
|
private object DolbyVisionProfiles {
|
||||||
val Profile5: Int by lazy {
|
val Profile5: Int by lazy {
|
||||||
|
|
@ -340,13 +328,4 @@ class MediaCodecCapabilitiesTest(
|
||||||
|
|
||||||
return Size(maxWidth, maxHeight)
|
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