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:
parent
f01c5d367c
commit
9f557416ea
12 changed files with 468 additions and 37 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ enum class PreferenceScreenOption {
|
|||
SCREENSAVER,
|
||||
SKIP_SEGMENTS,
|
||||
EXPERIMENTAL,
|
||||
HDR_FORMATS,
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue