New user preference to force enable or disable common HDR and Dolby Vision formats (#1644)

Description
Adding per-format HDR override preferences (HDR10, HDR10+, Dolby Vision
Profile 5/7/8) to control device compatibility information sent to the
Jellyfin server, so users can force specific formats on or off
independently instead of disabling Dolby Vision as a whole.

Related issues
This is a common problem across the Jellyfin ecosystem, but there isn't
a specific issue for Wholphin. My experience is the issue is FireTV
(model and generation) + TV dependent + media dependent. I tested videos
from
https://kodi.wiki/view/Samples with the same DOVIWithHDR10 flag that
worked while other movies I have with It is a very frustrating user
experience and the current workaround of setting the bitrate really low
to force a transcode is sub-optimal IMO as it lowers the video quality.
This allows the user to maintain full quality while disabling only the
specific format(s) causing problems.

https://github.com/jellyfin/jellyfin-androidtv/issues/4021
https://github.com/jellyfin/jellyfin-androidtv/issues/4892
https://github.com/nova-video-player/aos-AVP/issues/1503

https://www.reddit.com/r/jellyfin/comments/12jqoxh/dolby_vision_video_not_working_on_fire_stick_4k/

Testing

<!-- Describe how this change was tested and on what device(s) -->
FireTV 4K (gen 1) + Vizio
FireTV 4K Max (gen 1) + LG
FireTV 4K Max (gen 2) + Vizio
FireTV (gen 1) + Olevia


AI or LLM usage
Claude code assisted and wrote much of the code in this PR. I've
reviewed it all and run the pre-commit linting and formatting.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
Jeff Whiting 2026-10-13 13:41:53 -06:00 • committed by GitHub
parent f01c5d367c
commit 9f557416ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 468 additions and 37 deletions

View file

@ -456,15 +456,69 @@ sealed interface AppPreference<Pref, T> {
summaryOff = R.string.disabled,
)
val DirectPlayDoviProfile7 =
AppSwitchPreference<AppPreferences>(
title = R.string.force_dovi_profile_7,
defaultValue = false,
getter = { it.playbackPreferences.overrides.directPlayDolbyVisionEL },
val Hdr10Override =
AppChoicePreference<AppPreferences, HdrOverrideMode>(
title = R.string.hdr10_override,
defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO,
getter = { it.playbackPreferences.overrides.hdr10Override },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { directPlayDolbyVisionEL = value }
prefs.updatePlaybackOverrides { hdr10Override = value }
},
summary = R.string.force_dovi_profile_7_summary,
displayValues = R.array.hdr_override_modes,
indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO },
valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 },
)
val Hdr10PlusOverride =
AppChoicePreference<AppPreferences, HdrOverrideMode>(
title = R.string.hdr10_plus_override,
defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO,
getter = { it.playbackPreferences.overrides.hdr10PlusOverride },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { hdr10PlusOverride = value }
},
displayValues = R.array.hdr_override_modes,
indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO },
valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 },
)
val DoviProfile5Override =
AppChoicePreference<AppPreferences, HdrOverrideMode>(
title = R.string.dovi_profile_5,
defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO,
getter = { it.playbackPreferences.overrides.doviProfile5Override },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { doviProfile5Override = value }
},
displayValues = R.array.hdr_override_modes,
indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO },
valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 },
)
val DoviProfile7Override =
AppChoicePreference<AppPreferences, HdrOverrideMode>(
title = R.string.dovi_profile_7,
defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO,
getter = { it.playbackPreferences.overrides.doviProfile7Override },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { doviProfile7Override = value }
},
displayValues = R.array.hdr_override_modes,
indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO },
valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 },
)
val DoviProfile8Override =
AppChoicePreference<AppPreferences, HdrOverrideMode>(
title = R.string.dovi_profile_8,
defaultValue = HdrOverrideMode.HDR_OVERRIDE_AUTO,
getter = { it.playbackPreferences.overrides.doviProfile8Override },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { doviProfile8Override = value }
},
displayValues = R.array.hdr_override_modes,
indexToValue = { HdrOverrideMode.forNumber(it) ?: HdrOverrideMode.HDR_OVERRIDE_AUTO },
valueToIndex = { if (it != HdrOverrideMode.UNRECOGNIZED) it.number else 0 },
)
val DecodeAv1 =
@ -919,6 +973,12 @@ sealed interface AppPreference<Pref, T> {
destination = Destination.Settings(PreferenceScreenOption.MPV),
)
val HdrFormatSettings =
AppDestinationPreference<AppPreferences>(
title = R.string.hdr_formats,
destination = Destination.Settings(PreferenceScreenOption.HDR_FORMATS),
)
val MpvHardwareDecoding =
AppSwitchPreference<AppPreferences>(
title = R.string.mpv_hardware_decoding,
@ -1183,8 +1243,17 @@ private val ExoPlayerSettings =
AppPreference.Ac3Supported,
AppPreference.AssSubtitleMode,
AppPreference.DirectPlayPgs,
AppPreference.DirectPlayDoviProfile7,
AppPreference.DecodeAv1,
AppPreference.HdrFormatSettings,
)
private val HdrFormatSettings =
listOf(
AppPreference.Hdr10Override,
AppPreference.Hdr10PlusOverride,
AppPreference.DoviProfile5Override,
AppPreference.DoviProfile7Override,
AppPreference.DoviProfile8Override,
)
val ExoPlayerPreferences =
@ -1195,6 +1264,14 @@ val ExoPlayerPreferences =
),
)
val HdrFormatPreferences =
listOf(
PreferenceGroup(
title = R.string.hdr_formats,
preferences = HdrFormatSettings,
),
)
private val MpvSettings =
listOf(
AppPreference.MpvHardwareDecoding,

View file

@ -0,0 +1,56 @@
package com.github.damontecres.wholphin.preferences
import com.github.damontecres.wholphin.util.profile.WholphinVideoRangeType
// Adapted from https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/preference/constant/HdrFormat.kt
/**
* HDR formats that the user can override in the device profile.
*/
enum class HdrFormat(
val getter: (AppPreferences) -> HdrOverrideMode,
val rangeTypeNames: (jellyfinTenEleven: Boolean) -> Set<String>,
) {
HDR10(
getter = { it.playbackPreferences.overrides.hdr10Override },
rangeTypeNames = { setOf(WholphinVideoRangeType.HDR10.serialName) },
),
HDR10_PLUS(
getter = { it.playbackPreferences.overrides.hdr10PlusOverride },
rangeTypeNames = { setOf(WholphinVideoRangeType.HDR10_PLUS.serialName) },
),
DOVI_PROFILE_5(
getter = { it.playbackPreferences.overrides.doviProfile5Override },
rangeTypeNames = { setOf(WholphinVideoRangeType.DOVI.serialName) },
),
DOVI_PROFILE_7(
getter = { it.playbackPreferences.overrides.doviProfile7Override },
rangeTypeNames = { jellyfinTenEleven ->
if (jellyfinTenEleven) {
setOf(WholphinVideoRangeType.DOVI_WITH_EL.serialName, WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName)
} else {
emptySet()
}
},
),
DOVI_PROFILE_8(
getter = { it.playbackPreferences.overrides.doviProfile8Override },
rangeTypeNames = { jellyfinTenEleven ->
buildSet {
add(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName)
if (jellyfinTenEleven) add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName)
}
},
),
}
/**
* Collects the video range type names for every [HdrFormat] whose preference is currently set to [mode]
*/
fun AppPreferences.getHdrRangeTypesFor(
mode: HdrOverrideMode,
jellyfinTenEleven: Boolean,
): Set<String> =
HdrFormat.entries
.filter { it.getter(this) == mode }
.flatMapTo(mutableSetOf()) { it.rangeTypeNames(jellyfinTenEleven) }

View file

@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.RememberedTab
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.ExperimentalPreference
import com.github.damontecres.wholphin.preferences.HdrOverrideMode
import com.github.damontecres.wholphin.preferences.ScreensaverPreference
import com.github.damontecres.wholphin.preferences.update
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
@ -470,5 +471,19 @@ class AppUpgradeHandler
}
}
}
if (previous.isEqualOrBefore(Version.fromString("1.0.10"))) {
// directPlayDolbyVisionEL was replaced by doviProfile7Override; carry the setting forward
appPreferences.updateData {
if (it.playbackPreferences.overrides.directPlayDolbyVisionEL) {
it.updatePlaybackOverrides {
doviProfile7Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE
directPlayDolbyVisionEL = false
}
} else {
it
}
}
}
}
}

View file

@ -4,8 +4,10 @@ import android.content.Context
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AssPlaybackMode
import com.github.damontecres.wholphin.preferences.ExperimentalPreferences
import com.github.damontecres.wholphin.preferences.HdrOverrideMode
import com.github.damontecres.wholphin.preferences.PlaybackOverrides
import com.github.damontecres.wholphin.preferences.enabled
import com.github.damontecres.wholphin.preferences.getHdrRangeTypesFor
import com.github.damontecres.wholphin.util.WholphinDispatchers
import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
@ -61,9 +63,12 @@ class DeviceProfileService
downMixAudio = newConfig.overrides.downmixStereo,
assDirectPlay = newConfig.overrides.assPlaybackMode != AssPlaybackMode.ASS_TRANSCODE,
pgsDirectPlay = newConfig.overrides.directPlayPgs,
dolbyVisionELDirectPlay = newConfig.overrides.directPlayDolbyVisionEL,
decodeAv1 = prefs.overrides.decodeAv1,
preferAc3ForSurround = appPrefs.experimentalPreferences.enabled { preferAc3Surround },
forceEnabledHdr =
appPrefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, newConfig.jellyfinTenEleven),
forceDisabledHdr =
appPrefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, newConfig.jellyfinTenEleven),
jellyfinTenEleven = newConfig.jellyfinTenEleven,
maxResolution = newConfig.overrides.maxResolution,
)

View file

@ -38,6 +38,7 @@ enum class PreferenceScreenOption {
SCREENSAVER,
SKIP_SEGMENTS,
EXPERIMENTAL,
HDR_FORMATS,
;
companion object {

View file

@ -51,6 +51,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences
import com.github.damontecres.wholphin.preferences.ExperimentalPreference
import com.github.damontecres.wholphin.preferences.HdrFormatPreferences
import com.github.damontecres.wholphin.preferences.MpvPreferences
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.preferences.ScreensaverPreference
@ -155,6 +156,7 @@ fun PreferencesContent(
PreferenceScreenOption.SCREENSAVER -> screensaverPreferences
PreferenceScreenOption.SKIP_SEGMENTS -> SkipSegmentPreferences
PreferenceScreenOption.EXPERIMENTAL -> experimentalPreferences
PreferenceScreenOption.HDR_FORMATS -> HdrFormatPreferences
}
val screenTitle =
when (preferenceScreenOption) {
@ -165,6 +167,7 @@ fun PreferencesContent(
PreferenceScreenOption.SCREENSAVER -> R.string.screensaver_settings
PreferenceScreenOption.SKIP_SEGMENTS -> R.string.skip_behavior
PreferenceScreenOption.EXPERIMENTAL -> R.string.experimental_settings
PreferenceScreenOption.HDR_FORMATS -> R.string.hdr_formats
}
var visible by remember { mutableStateOf(false) }
@ -835,6 +838,7 @@ fun PreferencesPage(
PreferenceScreenOption.SCREENSAVER,
PreferenceScreenOption.SKIP_SEGMENTS,
PreferenceScreenOption.EXPERIMENTAL,
PreferenceScreenOption.HDR_FORMATS,
-> {
PreferencesContent(
initialPreferences,

View file

@ -12,7 +12,6 @@ import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.MediaStreamProtocol
import org.jellyfin.sdk.model.api.ProfileConditionValue
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import org.jellyfin.sdk.model.api.VideoRangeType
import org.jellyfin.sdk.model.deviceprofile.DeviceProfileBuilder
import org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile
@ -71,10 +70,11 @@ fun createDeviceProfile(
downMixAudio: Boolean,
assDirectPlay: Boolean,
pgsDirectPlay: Boolean,
dolbyVisionELDirectPlay: Boolean,
decodeAv1: Boolean,
jellyfinTenEleven: Boolean,
preferAc3ForSurround: Boolean,
forceEnabledHdr: Set<String>,
forceDisabledHdr: Set<String>,
maxResolution: Int,
) = buildDeviceProfile {
val allowedAudioCodecs =
@ -467,60 +467,56 @@ fun createDeviceProfile(
// / HDR exclude list
// TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK
val unsupportedRangeTypesAv1 =
buildSet {
if (jellyfinTenEleven) add("DOVIInvalid")
if (jellyfinTenEleven) add(WholphinVideoRangeType.DOVI_INVALID.serialName)
if (!decodeAv1) {
if (!supportsAV1DolbyVision) {
add(VideoRangeType.DOVI.serialName)
if (!supportsAV1HDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName)
if (jellyfinTenEleven && !supportsAV1HDR10Plus) add("DOVIWithHDR10Plus")
add(WholphinVideoRangeType.DOVI.serialName)
if (!supportsAV1HDR10) add(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName)
if (jellyfinTenEleven && !supportsAV1HDR10Plus) add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName)
}
if (!supportsAV1HDR10Plus) {
add(VideoRangeType.HDR10_PLUS.serialName)
add(WholphinVideoRangeType.HDR10_PLUS.serialName)
if (!mediaTest.supportsAV1HDR10()) add(VideoRangeType.HDR10.serialName)
if (!mediaTest.supportsAV1HDR10()) add(WholphinVideoRangeType.HDR10.serialName)
}
}
}
} - forceEnabledHdr + forceDisabledHdr
// TODO Use VideoRangeType enum with Jellyfin 10.11 based SDK
val unsupportedRangeTypesHevc =
buildSet {
if (jellyfinTenEleven) add("DOVIInvalid")
if (jellyfinTenEleven) add(WholphinVideoRangeType.DOVI_INVALID.serialName)
if (!supportsHevcDolbyVisionEL) {
if (!dolbyVisionELDirectPlay) {
if (jellyfinTenEleven) {
add("DOVIWithEL")
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus")
if (jellyfinTenEleven) {
add(WholphinVideoRangeType.DOVI_WITH_EL.serialName)
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) {
add(WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName)
}
}
if (!supportsHevcDolbyVision) {
add(VideoRangeType.DOVI.serialName)
if (!supportsHevcHDR10) add(VideoRangeType.DOVI_WITH_HDR10.serialName)
add(WholphinVideoRangeType.DOVI.serialName)
if (!supportsHevcHDR10) add(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName)
if (jellyfinTenEleven && !supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) {
add(
"DOVIWithHDR10Plus",
)
add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName)
}
}
}
if (!supportsHevcHDR10Plus) {
add(VideoRangeType.HDR10_PLUS.serialName)
if (!supportsHevcHDR10) add(VideoRangeType.HDR10.serialName)
add(WholphinVideoRangeType.HDR10_PLUS.serialName)
if (!supportsHevcHDR10) add(WholphinVideoRangeType.HDR10.serialName)
}
if (jellyfinTenEleven && KnownDefects.hevcDoviHdr10PlusBug) {
add("DOVIWithHDR10Plus")
add("DOVIWithELHDR10Plus")
add(WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName)
add(WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName)
}
}
} - forceEnabledHdr + forceDisabledHdr
// Display
// Note: The codec profiles use a workaround to create correct behavior

View file

@ -0,0 +1,27 @@
package com.github.damontecres.wholphin.util.profile
import org.jellyfin.sdk.model.api.VideoRangeType
/**
* Complete list of video range types, including ones not yet present in the pinned Jellyfin SDK's
* [VideoRangeType] enum (added server-side in Jellyfin 10.11).
*
* TODO Remove this and use [VideoRangeType] directly once the SDK is upgraded to a version built against Jellyfin 10.11+
*/
enum class WholphinVideoRangeType(
val serialName: String,
) {
UNKNOWN(VideoRangeType.UNKNOWN.serialName),
SDR(VideoRangeType.SDR.serialName),
HDR10(VideoRangeType.HDR10.serialName),
HLG(VideoRangeType.HLG.serialName),
DOVI(VideoRangeType.DOVI.serialName),
DOVI_WITH_HDR10(VideoRangeType.DOVI_WITH_HDR10.serialName),
DOVI_WITH_HLG(VideoRangeType.DOVI_WITH_HLG.serialName),
DOVI_WITH_SDR(VideoRangeType.DOVI_WITH_SDR.serialName),
HDR10_PLUS(VideoRangeType.HDR10_PLUS.serialName),
DOVI_INVALID("DOVIInvalid"),
DOVI_WITH_EL("DOVIWithEL"),
DOVI_WITH_EL_HDR10_PLUS("DOVIWithELHDR10Plus"),
DOVI_WITH_HDR10_PLUS("DOVIWithHDR10Plus"),
}

View file

@ -54,10 +54,15 @@ message PlaybackOverrides{
bool direct_play_ass = 3 [deprecated = true];
bool direct_play_pgs = 4;
MediaExtensionStatus media_extensions_enabled = 5;
bool direct_play_dolby_vision_e_l = 6;
bool direct_play_dolby_vision_e_l = 6 [deprecated = true];
bool decode_av1 = 7;
AssPlaybackMode ass_playback_mode = 8;
int32 max_resolution = 9;
HdrOverrideMode dovi_profile_5_override = 10;
HdrOverrideMode dovi_profile_7_override = 11;
HdrOverrideMode dovi_profile_8_override = 12;
HdrOverrideMode hdr10_override = 13;
HdrOverrideMode hdr10_plus_override = 14;
}
message PlaybackPreferences {
@ -224,6 +229,12 @@ message MusicPreferences {
bool show_backdrop = 4;
}
enum HdrOverrideMode{
HDR_OVERRIDE_AUTO = 0;
HDR_OVERRIDE_ENABLE = 1;
HDR_OVERRIDE_DISABLE = 2;
}
message ExperimentalPreferences {
bool enabled = 1;
bool video_tunneling_enabled = 2;

View file

@ -173,7 +173,7 @@
<string name="video_scale">Video Scale</string>
<string name="video">Video</string>
<string name="videos">Videos</string>
<string name="watch_live">Watch live</string>g
<string name="watch_live">Watch live</string>
<plurals name="years_old">
<item quantity="zero">%1$d years old</item>
<item quantity="one">%1$d year old</item>
@ -413,6 +413,19 @@
<string name="image_cache_size">Image disk cache size (MB)</string>
<string name="disable_audio_offload">Disable audio hardware offload</string>
<string name="disable_audio_offload_summary">Disabling offloading audio decoding may fix playback freezes on some devices</string>
<string name="hdr_formats">HDR formats</string>
<string name="hdr10_override">HDR10</string>
<string name="hdr10_plus_override">HDR10+</string>
<string name="dovi_profile_5">Dolby Vision Profile 5</string>
<string name="dovi_profile_7">Dolby Vision Profile 7</string>
<string name="dovi_profile_8">Dolby Vision Profile 8</string>
<string name="force_enable">Force enable</string>
<string name="force_disable">Force disable</string>
<string-array name="hdr_override_modes">
<item>@string/automatic</item>
<item>@string/force_enable</item>
<item>@string/force_disable</item>
</string-array>
<string name="view_options">View options</string>
<string name="columns">Columns</string>
<string name="spacing">Spacing</string>

View file

@ -0,0 +1,85 @@
package com.github.damontecres.wholphin.test
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.HdrOverrideMode
import com.github.damontecres.wholphin.preferences.PlaybackOverrides
import com.github.damontecres.wholphin.preferences.getHdrRangeTypesFor
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.util.profile.WholphinVideoRangeType
import org.junit.Assert
import org.junit.Test
class TestHdrFormat {
private fun appPreferences(block: PlaybackOverrides.Builder.() -> Unit): AppPreferences =
AppPreferences.getDefaultInstance().updatePlaybackOverrides(block)
@Test
fun `Test all auto returns empty sets`() {
val prefs = appPreferences { }
Assert.assertEquals(emptySet<String>(), prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true))
Assert.assertEquals(emptySet<String>(), prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true))
}
@Test
fun `Test HDR10 override`() {
val prefs = appPreferences { hdr10Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE }
Assert.assertEquals(
setOf(WholphinVideoRangeType.HDR10.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true),
)
Assert.assertEquals(emptySet<String>(), prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true))
}
@Test
fun `Test Dolby Vision Profile 5 override`() {
val prefs = appPreferences { doviProfile5Override = HdrOverrideMode.HDR_OVERRIDE_DISABLE }
Assert.assertEquals(
setOf(WholphinVideoRangeType.DOVI.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true),
)
}
@Test
fun `Test Dolby Vision Profile 7 override requires Jellyfin 10_11`() {
val prefs = appPreferences { doviProfile7Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE }
Assert.assertEquals(
emptySet<String>(),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = false),
)
Assert.assertEquals(
setOf(WholphinVideoRangeType.DOVI_WITH_EL.serialName, WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true),
)
}
@Test
fun `Test Dolby Vision Profile 8 override`() {
val prefs = appPreferences { doviProfile8Override = HdrOverrideMode.HDR_OVERRIDE_DISABLE }
Assert.assertEquals(
setOf(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = false),
)
Assert.assertEquals(
setOf(WholphinVideoRangeType.DOVI_WITH_HDR10.serialName, WholphinVideoRangeType.DOVI_WITH_HDR10_PLUS.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true),
)
}
@Test
fun `Test multiple formats set to the same mode are unioned`() {
val prefs =
appPreferences {
hdr10Override = HdrOverrideMode.HDR_OVERRIDE_ENABLE
hdr10PlusOverride = HdrOverrideMode.HDR_OVERRIDE_ENABLE
doviProfile5Override = HdrOverrideMode.HDR_OVERRIDE_DISABLE
}
Assert.assertEquals(
setOf(WholphinVideoRangeType.HDR10.serialName, WholphinVideoRangeType.HDR10_PLUS.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_ENABLE, jellyfinTenEleven = true),
)
Assert.assertEquals(
setOf(WholphinVideoRangeType.DOVI.serialName),
prefs.getHdrRangeTypesFor(HdrOverrideMode.HDR_OVERRIDE_DISABLE, jellyfinTenEleven = true),
)
}
}

View file

@ -0,0 +1,141 @@
package com.github.damontecres.wholphin.util.profile
import io.mockk.every
import io.mockk.mockk
import org.jellyfin.sdk.model.api.CodecType
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.ProfileConditionValue
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/**
* Verifies that forceEnabledHdr/forceDisabledHdr correctly override the device's own
* codec-capability detection when building the unsupported video range type list.
*/
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [30])
class TestDeviceProfileUtils {
private fun unsupportedRangeTypes(
profile: DeviceProfile,
codec: String,
): Set<String> =
profile.codecProfiles
.filter { it.type == CodecType.VIDEO && it.codec == codec }
.firstNotNullOf { codecProfile ->
codecProfile.applyConditions.firstOrNull { it.property == ProfileConditionValue.VIDEO_RANGE_TYPE }?.value
}.split("|")
.toSet()
private fun buildProfile(
mediaTest: MediaCodecCapabilitiesTest,
jellyfinTenEleven: Boolean = false,
decodeAv1: Boolean = false,
forceEnabledHdr: Set<String> = emptySet(),
forceDisabledHdr: Set<String> = emptySet(),
): DeviceProfile =
createDeviceProfile(
mediaTest = mediaTest,
maxBitrate = 100_000_000,
isAC3Enabled = true,
downMixAudio = false,
assDirectPlay = true,
pgsDirectPlay = true,
decodeAv1 = decodeAv1,
jellyfinTenEleven = jellyfinTenEleven,
preferAc3ForSurround = false,
forceEnabledHdr = forceEnabledHdr,
forceDisabledHdr = forceDisabledHdr,
maxResolution = 0,
)
@Test
fun `Test HEVC Profile 7 unsupported when device does not support it`() {
val mediaTest = mockk<MediaCodecCapabilitiesTest>(relaxed = true)
every { mediaTest.supportsHevcDolbyVisionEL() } returns false
every { mediaTest.supportsHevcDolbyVision() } returns true
every { mediaTest.supportsHevcHDR10Plus() } returns true
val profile = buildProfile(mediaTest, jellyfinTenEleven = true)
Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.HEVC).contains(WholphinVideoRangeType.DOVI_WITH_EL.serialName))
}
@Test
fun `Test force enable overrides HEVC Profile 7 device support check`() {
val mediaTest = mockk<MediaCodecCapabilitiesTest>(relaxed = true)
every { mediaTest.supportsHevcDolbyVisionEL() } returns false
every { mediaTest.supportsHevcDolbyVision() } returns true
every { mediaTest.supportsHevcHDR10Plus() } returns true
val profile =
buildProfile(
mediaTest,
jellyfinTenEleven = true,
forceEnabledHdr =
setOf(
WholphinVideoRangeType.DOVI_WITH_EL.serialName,
WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName,
),
)
Assert.assertFalse(unsupportedRangeTypes(profile, Codec.Video.HEVC).contains(WholphinVideoRangeType.DOVI_WITH_EL.serialName))
}
@Test
fun `Test force disable overrides HEVC Profile 7 device support check`() {
val mediaTest = mockk<MediaCodecCapabilitiesTest>(relaxed = true)
every { mediaTest.supportsHevcDolbyVisionEL() } returns true
every { mediaTest.supportsHevcDolbyVision() } returns true
every { mediaTest.supportsHevcHDR10Plus() } returns true
val profile =
buildProfile(
mediaTest,
jellyfinTenEleven = true,
forceDisabledHdr =
setOf(
WholphinVideoRangeType.DOVI_WITH_EL.serialName,
WholphinVideoRangeType.DOVI_WITH_EL_HDR10_PLUS.serialName,
),
)
Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.HEVC).contains(WholphinVideoRangeType.DOVI_WITH_EL.serialName))
}
@Test
fun `Test AV1 Profile 5 unsupported when device does not support it`() {
val mediaTest = mockk<MediaCodecCapabilitiesTest>(relaxed = true)
every { mediaTest.supportsAV1DolbyVision() } returns false
val profile = buildProfile(mediaTest, decodeAv1 = false)
Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.AV1).contains(WholphinVideoRangeType.DOVI.serialName))
}
@Test
fun `Test force enable overrides AV1 Profile 5 device support check`() {
val mediaTest = mockk<MediaCodecCapabilitiesTest>(relaxed = true)
every { mediaTest.supportsAV1DolbyVision() } returns false
val profile =
buildProfile(
mediaTest,
decodeAv1 = false,
forceEnabledHdr = setOf(WholphinVideoRangeType.DOVI.serialName),
)
Assert.assertFalse(unsupportedRangeTypes(profile, Codec.Video.AV1).contains(WholphinVideoRangeType.DOVI.serialName))
}
@Test
fun `Test force disable overrides AV1 Profile 5 device support check`() {
val mediaTest = mockk<MediaCodecCapabilitiesTest>(relaxed = true)
every { mediaTest.supportsAV1DolbyVision() } returns true
val profile =
buildProfile(
mediaTest,
decodeAv1 = false,
forceDisabledHdr = setOf(WholphinVideoRangeType.DOVI.serialName),
)
Assert.assertTrue(unsupportedRangeTypes(profile, Codec.Video.AV1).contains(WholphinVideoRangeType.DOVI.serialName))
}
}