Add user-defined AVC/HEVC codec level override (#5520)

This commit is contained in:
John Corser authored and GitHub committed 2026-04-18 13:44:56 +02:00
1 parent 13f1c036ff
commit d21b4e9a35
10 files changed
+282 -4

No files matched your search

@@ -3,10 +3,12 @@ package org.jellyfin.androidtv.preference
import android.content.Context
import androidx.preference.PreferenceManager
import org.jellyfin.androidtv.preference.UserPreferences.Companion.screensaverInAppEnabled
import org.jellyfin.androidtv.preference.constant.AVCLevel
import org.jellyfin.androidtv.preference.constant.AppTheme
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.preference.constant.BackdropBehavior
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.preference.constant.HEVCLevel
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.preference.constant.StillWatchingBehavior
@@ -103,6 +105,16 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var preferExoPlayerFfmpeg = booleanPreference("exoplayer_prefer_ffmpeg", defaultValue = false)
/**
* User defined AVC level override. AUTO uses device-reported capabilities.
*/
var userAVCLevel = enumPreference("user_avc_level", AVCLevel.AUTO)
/**
* User defined HEVC level override. AUTO uses device-reported capabilities.
*/
var userHEVCLevel = enumPreference("user_hevc_level", HEVCLevel.AUTO)
/* Playback - Audio related */
/**
* Preferred behavior for audio streaming.
@@ -0,0 +1,25 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
/**
* User-defined AVC (H.264) codec level override.
* Values correspond to MediaCodecInfo.CodecProfileLevel constants.
*/
enum class AVCLevel(
override val nameRes: Int,
val level: Int?,
) : PreferenceEnum {
AUTO(R.string.auto, null),
LEVEL_6_2(R.string.codec_level_6_2, 62),
LEVEL_6_1(R.string.codec_level_6_1, 61),
LEVEL_6_0(R.string.codec_level_6_0, 60),
LEVEL_5_2(R.string.codec_level_5_2, 52),
LEVEL_5_1(R.string.codec_level_5_1, 51),
LEVEL_5_0(R.string.codec_level_5_0, 50),
LEVEL_4_2(R.string.codec_level_4_2, 42),
LEVEL_4_1(R.string.codec_level_4_1, 41),
LEVEL_4_0(R.string.codec_level_4_0, 40),
}
@@ -0,0 +1,24 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
/**
* User-defined HEVC (H.265) codec level override.
* Values correspond to MediaCodecInfo.CodecProfileLevel constants.
*/
enum class HEVCLevel(
override val nameRes: Int,
val level: Int?,
) : PreferenceEnum {
AUTO(R.string.auto, null),
LEVEL_6_2(R.string.codec_level_6_2, 186),
LEVEL_6_1(R.string.codec_level_6_1, 183),
LEVEL_6_0(R.string.codec_level_6_0, 180),
LEVEL_5_2(R.string.codec_level_5_2, 156),
LEVEL_5_1(R.string.codec_level_5_1, 153),
LEVEL_5_0(R.string.codec_level_5_0, 150),
LEVEL_4_1(R.string.codec_level_4_1, 123),
LEVEL_4_0(R.string.codec_level_4_0, 120),
}
@@ -31,8 +31,11 @@ import org.jellyfin.androidtv.ui.settings.screen.license.SettingsLicensesScreen
import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideChannelOrderScreen
import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideFiltersScreen
import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideOptionsScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackAVCLevelScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackAdvancedScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackAudioBehaviorScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackCodecScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackHEVCLevelScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackInactivityPromptScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackMaxBitrateScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackPlayerScreen
@@ -95,6 +98,9 @@ object Routes {
const val PLAYBACK_REFRESH_RATE_SWITCHING_BEHAVIOR = "/playback/refresh-rate-switching-behavior"
const val PLAYBACK_ZOOM_MODE = "/playback/zoom-mode"
const val PLAYBACK_AUDIO_BEHAVIOR = "/playback/audio-behavior"
const val PLAYBACK_CODEC = "/playback/codec"
const val PLAYBACK_AVC_LEVEL = "/playback/codec/avc-level"
const val PLAYBACK_HEVC_LEVEL = "/playback/codec/hevc-level"
const val TELEMETRY = "/telemetry"
const val DEVELOPER = "/developer"
const val ABOUT = "/about"
@@ -245,6 +251,15 @@ val routes = mapOf<String, RouteComposable>(
Routes.PLAYBACK_AUDIO_BEHAVIOR to {
SettingsPlaybackAudioBehaviorScreen()
},
Routes.PLAYBACK_CODEC to {
SettingsPlaybackCodecScreen()
},
Routes.PLAYBACK_AVC_LEVEL to {
SettingsPlaybackAVCLevelScreen()
},
Routes.PLAYBACK_HEVC_LEVEL to {
SettingsPlaybackHEVCLevelScreen()
},
Routes.TELEMETRY to {
SettingsTelemetryScreen()
},
@@ -0,0 +1,61 @@
package org.jellyfin.androidtv.ui.settings.screen.playback
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.AVCLevel
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.RadioButton
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsPlaybackAVCLevelScreen() {
val router = LocalRouter.current
val userPreferences = koinInject<UserPreferences>()
var userAVCLevel by rememberPreference(userPreferences, UserPreferences.userAVCLevel)
val manualOptions = AVCLevel.entries.filter { it != AVCLevel.AUTO }
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.preference_codecs).uppercase()) },
headingContent = { Text(stringResource(R.string.user_avc_level)) },
captionContent = { Text(stringResource(R.string.codec_level_warning)) },
)
}
item {
ListButton(
headingContent = { Text(stringResource(AVCLevel.AUTO.nameRes)) },
trailingContent = { RadioButton(checked = userAVCLevel == AVCLevel.AUTO) },
onClick = {
userAVCLevel = AVCLevel.AUTO
router.back()
}
)
}
item { ListSection(headingContent = { Text(stringResource(R.string.codec_level_manual)) }) }
items(manualOptions) { level ->
ListButton(
headingContent = { Text(stringResource(level.nameRes)) },
trailingContent = { RadioButton(checked = userAVCLevel == level) },
onClick = {
userAVCLevel = level
router.back()
}
)
}
}
}
@@ -187,6 +187,14 @@ fun SettingsPlaybackAdvancedScreen() {
)
}
item {
ListButton(
headingContent = { Text(stringResource(R.string.preference_codecs)) },
captionContent = { Text(stringResource(R.string.preference_codecs_summary)) },
onClick = { router.push(Routes.PLAYBACK_CODEC) }
)
}
item { ListSection(headingContent = { Text(stringResource(R.string.pref_live_tv_cat)) }) }
item {
@@ -0,0 +1,52 @@
package org.jellyfin.androidtv.ui.settings.screen.playback
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.Routes
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsPlaybackCodecScreen() {
val router = LocalRouter.current
val userPreferences = koinInject<UserPreferences>()
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.pref_playback_advanced).uppercase()) },
headingContent = { Text(stringResource(R.string.preference_codecs)) },
captionContent = { Text(stringResource(R.string.preference_codecs_summary)) },
)
}
item {
var userAVCLevel by rememberPreference(userPreferences, UserPreferences.userAVCLevel)
ListButton(
headingContent = { Text(stringResource(R.string.user_avc_level)) },
captionContent = { Text(stringResource(userAVCLevel.nameRes)) },
onClick = { router.push(Routes.PLAYBACK_AVC_LEVEL) }
)
}
item {
var userHEVCLevel by rememberPreference(userPreferences, UserPreferences.userHEVCLevel)
ListButton(
headingContent = { Text(stringResource(R.string.user_hevc_level)) },
captionContent = { Text(stringResource(userHEVCLevel.nameRes)) },
onClick = { router.push(Routes.PLAYBACK_HEVC_LEVEL) }
)
}
}
}
@@ -0,0 +1,61 @@
package org.jellyfin.androidtv.ui.settings.screen.playback
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.HEVCLevel
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.form.RadioButton
import org.jellyfin.androidtv.ui.base.list.ListButton
import org.jellyfin.androidtv.ui.base.list.ListSection
import org.jellyfin.androidtv.ui.navigation.LocalRouter
import org.jellyfin.androidtv.ui.settings.compat.rememberPreference
import org.jellyfin.androidtv.ui.settings.composable.SettingsColumn
import org.koin.compose.koinInject
@Composable
fun SettingsPlaybackHEVCLevelScreen() {
val router = LocalRouter.current
val userPreferences = koinInject<UserPreferences>()
var userHEVCLevel by rememberPreference(userPreferences, UserPreferences.userHEVCLevel)
val manualOptions = HEVCLevel.entries.filter { it != HEVCLevel.AUTO }
SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.preference_codecs).uppercase()) },
headingContent = { Text(stringResource(R.string.user_hevc_level)) },
captionContent = { Text(stringResource(R.string.codec_level_warning)) },
)
}
item {
ListButton(
headingContent = { Text(stringResource(HEVCLevel.AUTO.nameRes)) },
trailingContent = { RadioButton(checked = userHEVCLevel == HEVCLevel.AUTO) },
onClick = {
userHEVCLevel = HEVCLevel.AUTO
router.back()
}
)
}
item { ListSection(headingContent = { Text(stringResource(R.string.codec_level_manual)) }) }
items(manualOptions) { level ->
ListButton(
headingContent = { Text(stringResource(level.nameRes)) },
trailingContent = { RadioButton(checked = userHEVCLevel == level) },
onClick = {
userHEVCLevel = level
router.back()
}
)
}
}
}
@@ -85,6 +85,8 @@ fun createDeviceProfile(
downMixAudio = userPreferences[UserPreferences.audioBehaviour] == AudioBehavior.DOWNMIX_TO_STEREO,
assDirectPlay = userPreferences[UserPreferences.assDirectPlay],
pgsDirectPlay = userPreferences[UserPreferences.pgsDirectPlay],
userAVCLevel = userPreferences[UserPreferences.userAVCLevel].level,
userHEVCLevel = userPreferences[UserPreferences.userHEVCLevel].level,
)
fun createDeviceProfile(
@@ -94,6 +96,8 @@ fun createDeviceProfile(
downMixAudio: Boolean,
assDirectPlay: Boolean,
pgsDirectPlay: Boolean,
userAVCLevel: Int?,
userHEVCLevel: Int?,
) = buildDeviceProfile {
val allowedAudioCodecs = when {
downMixAudio -> downmixSupportedAudioCodecs
@@ -103,12 +107,12 @@ fun createDeviceProfile(
val supportsHevc = mediaTest.supportsHevc()
val supportsHevcMain10 = mediaTest.supportsHevcMain10()
val hevcMainLevel = mediaTest.getHevcMainLevel()
val hevcMain10Level = mediaTest.getHevcMain10Level()
val hevcMainLevel = userHEVCLevel ?: mediaTest.getHevcMainLevel()
val hevcMain10Level = userHEVCLevel ?: mediaTest.getHevcMain10Level()
val supportsAVC = mediaTest.supportsAVC()
val supportsAVCHigh10 = mediaTest.supportsAVCHigh10()
val avcMainLevel = mediaTest.getAVCMainLevel()
val avcHigh10Level = mediaTest.getAVCHigh10Level()
val avcMainLevel = userAVCLevel ?: mediaTest.getAVCMainLevel()
val avcHigh10Level = userAVCLevel ?: mediaTest.getAVCHigh10Level()
val supportsAV1 = mediaTest.supportsAV1()
val supportsAV1Main10 = mediaTest.supportsAV1Main10()
val supportsVC1 = mediaTest.supportsVc1()
+16
View File
@@ -579,6 +579,22 @@
<string name="filters">Filters</string>
<string name="backdrop_behavior_with_blur">Backdrop with blur</string>
<string name="backdrop_behavior_without_blur">Backdrop without blur</string>
<string name="user_avc_level">H.264 (AVC) level</string>
<string name="user_hevc_level">H.265 (HEVC) level</string>
<string name="codec_level_warning">Changing from auto may cause playback issues if your device doesn\'t actually support the selected level.</string>
<string name="preference_codecs">Media codecs</string>
<string name="preference_codecs_summary">Override device-reported codec capabilities</string>
<string name="codec_level_manual">Manual override</string>
<string name="codec_level_6_2">Level 6.2</string>
<string name="codec_level_6_1">Level 6.1</string>
<string name="codec_level_6_0">Level 6.0</string>
<string name="codec_level_5_2">Level 5.2</string>
<string name="codec_level_5_1">Level 5.1</string>
<string name="codec_level_5_0">Level 5.0</string>
<string name="codec_level_4_2">Level 4.2</string>
<string name="codec_level_4_1">Level 4.1</string>
<string name="codec_level_4_0">Level 4.0</string>
<string name="auto">Auto</string>
<plurals name="seconds">
<item quantity="one">%1$s second</item>
<item quantity="other">%1$s seconds</item>