diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt index 9a526916b..8a7a67241 100644 --- a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt +++ b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt @@ -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. diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/constant/AVCLevel.kt b/app/src/main/java/org/jellyfin/androidtv/preference/constant/AVCLevel.kt new file mode 100644 index 000000000..afb59e7c7 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/preference/constant/AVCLevel.kt @@ -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), +} diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/constant/HEVCLevel.kt b/app/src/main/java/org/jellyfin/androidtv/preference/constant/HEVCLevel.kt new file mode 100644 index 000000000..004a599cc --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/preference/constant/HEVCLevel.kt @@ -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), +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/settings/routes.kt b/app/src/main/java/org/jellyfin/androidtv/ui/settings/routes.kt index 27aca5461..87a01da35 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/settings/routes.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/settings/routes.kt @@ -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( 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() }, diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAVCLevelScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAVCLevelScreen.kt new file mode 100644 index 000000000..a5c40341e --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAVCLevelScreen.kt @@ -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() + + 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() + } + ) + } + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAdvancedScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAdvancedScreen.kt index 4a48a5d29..330170a68 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAdvancedScreen.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackAdvancedScreen.kt @@ -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 { diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackCodecScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackCodecScreen.kt new file mode 100644 index 000000000..b8e5d1293 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackCodecScreen.kt @@ -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() + + 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) } + ) + } + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackHEVCLevelScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackHEVCLevelScreen.kt new file mode 100644 index 000000000..60bc02431 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/playback/SettingsPlaybackHEVCLevelScreen.kt @@ -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() + + 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() + } + ) + } + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt b/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt index 50bc66034..eb0f20a5f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt @@ -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() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f047d09d2..56d0fb234 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -579,6 +579,22 @@ Filters Backdrop with blur Backdrop without blur + H.264 (AVC) level + H.265 (HEVC) level + Changing from auto may cause playback issues if your device doesn\'t actually support the selected level. + Media codecs + Override device-reported codec capabilities + Manual override + Level 6.2 + Level 6.1 + Level 6.0 + Level 5.2 + Level 5.1 + Level 5.0 + Level 4.2 + Level 4.1 + Level 4.0 + Auto %1$s second %1$s seconds