Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.preference.constant.BackdropBehavior
import org.jellyfin.androidtv.preference.constant.BufferLength
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.preference.constant.HdrOverrideMode
import org.jellyfin.androidtv.preference.constant.HEVCLevel
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
Expand Down Expand Up @@ -137,6 +138,31 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var ac3Enabled = booleanPreference("pref_bitstream_ac3", true)

/**
* HDR10 device profile override.
*/
var hdr10Override = enumPreference("hdr10_override", HdrOverrideMode.AUTO)

/**
* HDR10+ device profile override.
*/
var hdr10PlusOverride = enumPreference("hdr10_plus_override", HdrOverrideMode.AUTO)

/**
* Dolby Vision Profile 5 device profile override.
*/
var doviProfile5Override = enumPreference("dovi_profile_5_override", HdrOverrideMode.AUTO)

/**
* Dolby Vision Profile 7 device profile override.
*/
var doviProfile7Override = enumPreference("dovi_profile_7_override", HdrOverrideMode.AUTO)

/**
* Dolby Vision Profile 8 device profile override.
*/
var doviProfile8Override = enumPreference("dovi_profile_8_override", HdrOverrideMode.AUTO)

/* Live TV */
/**
* Use direct play
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.jellyfin.androidtv.preference.constant

import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.preference.Preference
import org.jellyfin.sdk.model.api.VideoRangeType

/**
* HDR formats that the user can override in the device profile.
*/
enum class HdrFormat(
val nameRes: Int,
val descriptionRes: Int,
val videoRangeTypes: Set<VideoRangeType>,
val preference: Preference<HdrOverrideMode>,
) {
HDR10(
nameRes = R.string.hdr10_override,
descriptionRes = R.string.hdr10_override_description,
videoRangeTypes = setOf(VideoRangeType.HDR10),
preference = UserPreferences.hdr10Override,
),
HDR10_PLUS(
nameRes = R.string.hdr10_plus_override,
descriptionRes = R.string.hdr10_plus_override_description,
videoRangeTypes = setOf(VideoRangeType.HDR10_PLUS),
preference = UserPreferences.hdr10PlusOverride,
),
DOVI_PROFILE_5(
nameRes = R.string.dovi_profile_5,
descriptionRes = R.string.dovi_profile_5_description,
videoRangeTypes = setOf(VideoRangeType.DOVI),
preference = UserPreferences.doviProfile5Override,
),
DOVI_PROFILE_7(
nameRes = R.string.dovi_profile_7,
descriptionRes = R.string.dovi_profile_7_description,
videoRangeTypes = setOf(VideoRangeType.DOVI_WITH_EL, VideoRangeType.DOVI_WITH_ELHDR10_PLUS),
preference = UserPreferences.doviProfile7Override,
),
DOVI_PROFILE_8(
nameRes = R.string.dovi_profile_8,
descriptionRes = R.string.dovi_profile_8_description,
videoRangeTypes = setOf(VideoRangeType.DOVI_WITH_HDR10, VideoRangeType.DOVI_WITH_HDR10_PLUS),
preference = UserPreferences.doviProfile8Override,
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.jellyfin.androidtv.preference.constant

import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum

/**
* How an HDR format should be treated in the device profile, overriding device-reported capabilities.
*/
enum class HdrOverrideMode(
override val nameRes: Int,
) : PreferenceEnum {
AUTO(R.string.hdr_override_auto),
ENABLE(R.string.hdr_override_enable),
DISABLE(R.string.hdr_override_disable),
}
13 changes: 13 additions & 0 deletions app/src/main/java/org/jellyfin/androidtv/ui/settings/routes.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jellyfin.androidtv.ui.settings

import org.jellyfin.androidtv.preference.constant.HdrFormat
import org.jellyfin.androidtv.ui.navigation.RouteComposable
import org.jellyfin.androidtv.ui.settings.screen.SettingsDeveloperScreen
import org.jellyfin.androidtv.ui.settings.screen.SettingsMainScreen
Expand Down Expand Up @@ -34,6 +35,8 @@ import org.jellyfin.androidtv.ui.settings.screen.livetv.SettingsLiveTvGuideOptio
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.SettingsPlaybackHdrOverrideScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackHdrOverridesScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackBufferLengthScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackCodecScreen
import org.jellyfin.androidtv.ui.settings.screen.playback.SettingsPlaybackHEVCLevelScreen
Expand Down Expand Up @@ -99,6 +102,8 @@ 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_BUFFER_LENGTH = "/playback/buffer-length"
const val PLAYBACK_HDR_OVERRIDES = "/playback/hdr-overrides"
const val PLAYBACK_HDR_OVERRIDE = "/playback/hdr-overrides/{format}"
const val PLAYBACK_AUDIO_BEHAVIOR = "/playback/audio-behavior"
const val PLAYBACK_CODEC = "/playback/codec"
const val PLAYBACK_AVC_LEVEL = "/playback/codec/avc-level"
Expand Down Expand Up @@ -253,6 +258,14 @@ val routes = mapOf<String, RouteComposable>(
Routes.PLAYBACK_BUFFER_LENGTH to {
SettingsPlaybackBufferLengthScreen()
},
Routes.PLAYBACK_HDR_OVERRIDES to {
SettingsPlaybackHdrOverridesScreen()
},
Routes.PLAYBACK_HDR_OVERRIDE to { context ->
SettingsPlaybackHdrOverrideScreen(
format = context.parameters["format"]?.let(HdrFormat::valueOf)!!,
)
},
Routes.PLAYBACK_AUDIO_BEHAVIOR to {
SettingsPlaybackAudioBehaviorScreen()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ fun SettingsPlaybackAdvancedScreen() {
)
}

item {
ListButton(
headingContent = { Text(stringResource(R.string.preference_hdr_overrides)) },
captionContent = { Text(stringResource(R.string.preference_hdr_overrides_summary)) },
onClick = { router.push(Routes.PLAYBACK_HDR_OVERRIDES) }
)
}

item {
ListButton(
headingContent = { Text(stringResource(R.string.preference_codecs)) },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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.HdrFormat
import org.jellyfin.androidtv.preference.constant.HdrOverrideMode
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 SettingsPlaybackHdrOverrideScreen(
format: HdrFormat,
) {
val router = LocalRouter.current
val userPreferences = koinInject<UserPreferences>()
var mode by rememberPreference(userPreferences, format.preference)

SettingsColumn {
item {
ListSection(
overlineContent = { Text(stringResource(R.string.preference_hdr_overrides).uppercase()) },
headingContent = { Text(stringResource(format.nameRes)) },
captionContent = { Text(stringResource(format.descriptionRes)) },
)
}

items(HdrOverrideMode.entries) { entry ->
ListButton(
headingContent = { Text(stringResource(entry.nameRes)) },
trailingContent = { RadioButton(checked = mode == entry) },
onClick = {
mode = entry
router.back()
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.jellyfin.androidtv.ui.settings.screen.playback

import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.HdrFormat
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.composable.SettingsColumn
import org.koin.compose.koinInject

@Composable
fun SettingsPlaybackHdrOverridesScreen() {
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_hdr_overrides)) },
captionContent = { Text(stringResource(R.string.preference_hdr_overrides_summary)) },
)
}

items(HdrFormat.entries) { format ->
val mode = userPreferences[format.preference]

ListButton(
headingContent = { Text(stringResource(format.nameRes)) },
captionContent = { Text(stringResource(mode.nameRes)) },
onClick = {
router.push(
route = Routes.PLAYBACK_HDR_OVERRIDE,
parameters = mapOf("format" to format.name),
)
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.media3.common.MimeTypes
import org.jellyfin.androidtv.constant.Codec
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.preference.constant.HdrFormat
import org.jellyfin.androidtv.preference.constant.HdrOverrideMode
import org.jellyfin.sdk.model.ServerVersion
import org.jellyfin.sdk.model.api.CodecType
import org.jellyfin.sdk.model.api.DlnaProfileType
Expand Down Expand Up @@ -74,6 +76,11 @@ private fun UserPreferences.getMaxBitrate(): Int {
return (maxBitrate * 1_000_000).roundToInt()
}

private fun UserPreferences.getHdrRangeTypesFor(mode: HdrOverrideMode): Set<VideoRangeType> =
HdrFormat.entries
.filter { this[it.preference] == mode }
.flatMapTo(mutableSetOf()) { it.videoRangeTypes }

fun createDeviceProfile(
context: Context,
userPreferences: UserPreferences,
Expand All @@ -87,6 +94,8 @@ fun createDeviceProfile(
pgsDirectPlay = userPreferences[UserPreferences.pgsDirectPlay],
userAVCLevel = userPreferences[UserPreferences.userAVCLevel].level,
userHEVCLevel = userPreferences[UserPreferences.userHEVCLevel].level,
forceEnabledHdr = userPreferences.getHdrRangeTypesFor(HdrOverrideMode.ENABLE),
forceDisabledHdr = userPreferences.getHdrRangeTypesFor(HdrOverrideMode.DISABLE),
)

fun createDeviceProfile(
Expand All @@ -98,6 +107,8 @@ fun createDeviceProfile(
pgsDirectPlay: Boolean,
userAVCLevel: Int?,
userHEVCLevel: Int?,
forceEnabledHdr: Set<VideoRangeType>,
forceDisabledHdr: Set<VideoRangeType>
) = buildDeviceProfile {
val allowedAudioCodecs = when {
downMixAudio -> downmixSupportedAudioCodecs
Expand Down Expand Up @@ -442,7 +453,7 @@ fun createDeviceProfile(

if (!mediaTest.supportsAV1HDR10()) add(VideoRangeType.HDR10)
}
}
} - forceEnabledHdr + forceDisabledHdr

val unsupportedRangeTypesHevc = buildSet {
add(VideoRangeType.DOVI_INVALID)
Expand All @@ -467,7 +478,7 @@ fun createDeviceProfile(
add(VideoRangeType.DOVI_WITH_HDR10_PLUS)
add(VideoRangeType.DOVI_WITH_ELHDR10_PLUS)
}
}
} - forceEnabledHdr + forceDisabledHdr

// 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
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,22 @@
<string name="desc_automatic_fav_songs">Automatic playlist of favorite songs</string>
<string name="lbl_new_premieres">New premieres</string>
<string name="lbl_bitstream_ac3">Bitstream Dolby Digital audio</string>
<string name="desc_bitstream_ac3">Requires capable hardware</string>

Check warning

Code scanning / Android Lint

Unused resources Warning

The resource R.string.desc_bitstream_ac3 appears to be unused
<string name="preference_hdr_overrides">HDR formats</string>
<string name="preference_hdr_overrides_summary">Override device-reported HDR capabilities</string>
<string name="hdr_override_auto">Auto (device detection)</string>
<string name="hdr_override_enable">Enable</string>
<string name="hdr_override_disable">Disable</string>
<string name="hdr10_override">HDR10</string>
<string name="hdr10_override_description">Static HDR metadata</string>
<string name="hdr10_plus_override">HDR10+</string>
<string name="hdr10_plus_override_description">Dynamic HDR metadata</string>
<string name="dovi_profile_5">Dolby Vision Profile 5</string>
<string name="dovi_profile_5_description">Single-layer Dolby Vision</string>
<string name="dovi_profile_7">Dolby Vision Profile 7</string>
<string name="dovi_profile_7_description">Dual-layer with Enhancement Layer</string>
<string name="dovi_profile_8">Dolby Vision Profile 8</string>
<string name="dovi_profile_8_description">Single-layer with HDR10 fallback</string>
<string name="desc_audio_night_mode">Levels out audio volume automatically</string>
<string name="lbl_zoom">Zoom</string>
<string name="playback_info">Playback info</string>
Expand Down