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
|
|
@ -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