From 11619a40a9daa31acdf007de0daef5533d546d69 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:57:54 -0400 Subject: [PATCH] fix(playback): honor source color range fallback --- .../DolbyVisionColorInfoExtractorsFactory.kt | 46 ++++++++++++--- .../player/DolbyVisionProfile7Transformer.kt | 1 + .../silo/common/player/PlaybackV3Session.kt | 1 + .../silo/common/player/SiloPlayerFactory.kt | 56 +++++++++++-------- .../common/player/VideoPlayerMediaMounter.kt | 2 + .../common/player/VideoPlayerMediaSpec.kt | 1 + ...lbyVisionColorInfoExtractorsFactoryTest.kt | 36 ++++++++++++ .../common/player/PlaybackV3SessionTest.kt | 3 + .../android/ui/screens/player/PlayerScreen.kt | 2 + .../tv/ui/screens/player/TvPlayerScreen.kt | 2 + .../silo/model/catalog/CatalogModels.kt | 1 + .../silo/model/playback/PlaybackModels.kt | 1 + .../silo/model/playback/PlaybackProtocolV3.kt | 11 ++++ .../catalog/CatalogTrackSerializationTest.kt | 3 +- .../model/playback/PlaybackProtocolV3Test.kt | 2 + 15 files changed, 136 insertions(+), 32 deletions(-) diff --git a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactory.kt b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactory.kt index f886b8a25..b622b9f87 100644 --- a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactory.kt +++ b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactory.kt @@ -36,6 +36,7 @@ internal class DolbyVisionColorInfoExtractorsFactory( private val transformMode: DolbyVisionTransformMode = DolbyVisionTransformMode.DISABLED, private val converter: DolbyVisionRpuConverter = NativeDolbyVisionRpuConverter, private val expectedDynamicRange: String? = null, + private val expectedColorRange: String? = null, ) : ExtractorsFactory { override fun createExtractors(): Array = delegate.createExtractors().map(::wrap).toTypedArray() @@ -48,18 +49,25 @@ internal class DolbyVisionColorInfoExtractorsFactory( .toTypedArray() private fun wrap(extractor: Extractor): Extractor = - ColorInfoExtractor(extractor, transformMode, converter, expectedDynamicRange) + ColorInfoExtractor(extractor, transformMode, converter, expectedDynamicRange, expectedColorRange) private class ColorInfoExtractor( private val delegate: Extractor, private val transformMode: DolbyVisionTransformMode, private val converter: DolbyVisionRpuConverter, private val expectedDynamicRange: String?, + private val expectedColorRange: String?, ) : Extractor by delegate { private var output: ColorInfoExtractorOutput? = null override fun init(output: ExtractorOutput) { - val wrapped = ColorInfoExtractorOutput(output, transformMode, converter, expectedDynamicRange) + val wrapped = ColorInfoExtractorOutput( + output, + transformMode, + converter, + expectedDynamicRange, + expectedColorRange, + ) this.output = wrapped delegate.init(wrapped) } @@ -81,16 +89,21 @@ internal class DolbyVisionColorInfoExtractorsFactory( private val transformMode: DolbyVisionTransformMode, private val converter: DolbyVisionRpuConverter, private val expectedDynamicRange: String?, + private val expectedColorRange: String?, ) : ExtractorOutput { private val tracks = mutableMapOf() override fun track(id: Int, type: Int): TrackOutput = tracks.getOrPut(id) { val output = delegate.track(id, type) - if (type == C.TRACK_TYPE_VIDEO && transformMode != DolbyVisionTransformMode.DISABLED) { - DolbyVisionTransformingTrackOutput(output, transformMode, converter) - } else { - ColorInfoTrackOutput(output, expectedDynamicRange) - } + if (type != C.TRACK_TYPE_VIDEO) return@getOrPut output + + val colorInfoOutput = ColorInfoTrackOutput( + output, + expectedDynamicRange, + expectedColorRange, + ) + if (transformMode == DolbyVisionTransformMode.DISABLED) colorInfoOutput + else DolbyVisionTransformingTrackOutput(colorInfoOutput, transformMode, converter) } override fun endTracks() = delegate.endTracks() @@ -105,12 +118,14 @@ internal class DolbyVisionColorInfoExtractorsFactory( private class ColorInfoTrackOutput( private val delegate: TrackOutput, private val expectedDynamicRange: String?, + private val expectedColorRange: String?, ) : TrackOutput { override fun durationUs(durationUs: Long) = delegate.durationUs(durationUs) override fun format(format: Format) { delegate.format( format + .withValidatedColorRange(expectedColorRange) .withValidatedDynamicRangeColorInfo(expectedDynamicRange) .withDolbyVisionHdrColorInfo(), ) @@ -139,6 +154,23 @@ internal class DolbyVisionColorInfoExtractorsFactory( } } +@UnstableApi +internal fun Format.withValidatedColorRange(expectedColorRange: String?): Format { + if (!MimeTypes.isVideo(sampleMimeType)) return this + val expected = when (expectedColorRange?.trim()?.lowercase()) { + "tv" -> C.COLOR_RANGE_LIMITED + "pc" -> C.COLOR_RANGE_FULL + else -> return this + } + val current = colorInfo + if (current != null && current.colorRange != -1) return this + + val repaired = (current?.buildUpon() ?: androidx.media3.common.ColorInfo.Builder()) + .setColorRange(expected) + .build() + return buildUpon().setColorInfo(repaired).build() +} + @UnstableApi internal fun Format.withValidatedDynamicRangeColorInfo(expectedDynamicRange: String?): Format { if (!expectedDynamicRange.equals("hlg", ignoreCase = true) || !MimeTypes.isVideo(sampleMimeType)) { diff --git a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionProfile7Transformer.kt b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionProfile7Transformer.kt index 2fba190a6..c9261c74f 100644 --- a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionProfile7Transformer.kt +++ b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/DolbyVisionProfile7Transformer.kt @@ -24,6 +24,7 @@ internal enum class DolbyVisionTransformMode { internal data class SiloMediaTransformTag( val dolbyVisionMode: DolbyVisionTransformMode, val expectedDynamicRange: String? = null, + val expectedColorRange: String? = null, ) internal class DolbyVisionTransformException( diff --git a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt index daa76ece6..4afffd21e 100644 --- a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt +++ b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt @@ -112,6 +112,7 @@ internal fun PlaybackPlanV3.toSessionResponse( audioCodec = effectiveRecipe.audioCodec, resolution = effectiveRecipe.height?.let { "${it}p" }, hdrFormat = effectiveRecipe.dynamicRange, + colorRange = source.colorRange, subtitleCodec = subtitle.artifact?.format, ), claims = claims, diff --git a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloPlayerFactory.kt b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloPlayerFactory.kt index 9fa1324be..65da3b3b5 100644 --- a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloPlayerFactory.kt +++ b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloPlayerFactory.kt @@ -249,6 +249,7 @@ class SiloPlayerFactory( fun defaultMediaSourceFactory( mode: DolbyVisionTransformMode, expectedDynamicRange: String? = null, + expectedColorRange: String? = null, ) = DefaultMediaSourceFactory( context, @@ -256,6 +257,7 @@ class SiloPlayerFactory( configuredExtractorsFactory(), mode, expectedDynamicRange = expectedDynamicRange, + expectedColorRange = expectedColorRange, ), ) .setDataSourceFactory(dataSourceFactory) @@ -267,12 +269,7 @@ class SiloPlayerFactory( .setLoadErrorHandlingPolicy(mediaLoadErrorHandlingPolicy) val mediaSourceFactory = SiloMediaSourceFactory( defaultFactory = defaultMediaSourceFactory(DolbyVisionTransformMode.DISABLED), - hlgFactory = defaultMediaSourceFactory( - DolbyVisionTransformMode.DISABLED, - expectedDynamicRange = "hlg", - ), - dv81Factory = defaultMediaSourceFactory(DolbyVisionTransformMode.PROFILE7_TO_PROFILE81), - hdr10Factory = defaultMediaSourceFactory(DolbyVisionTransformMode.PROFILE7_TO_HDR10), + correctedFactory = ::defaultMediaSourceFactory, hlsFactory = hlsMediaSourceFactory, dataSourceFactory = dataSourceFactory, subtitleParserFactory = subtitleParserFactory, @@ -396,6 +393,7 @@ class SiloPlayerFactory( durationMs: Long? = null, requestHeaders: Map = emptyMap(), expectedDynamicRange: String? = null, + expectedColorRange: String? = null, transformations: List = emptyList(), runtimeCorrections: List = emptyList(), ): MediaItem { @@ -421,6 +419,7 @@ class SiloPlayerFactory( else -> DolbyVisionTransformMode.DISABLED }, expectedDynamicRange = expectedDynamicRange, + expectedColorRange = expectedColorRange, ), ) @@ -476,21 +475,23 @@ class SiloPlayerFactory( private class SiloMediaSourceFactory( private val defaultFactory: MediaSource.Factory, - private val hlgFactory: MediaSource.Factory, - private val dv81Factory: MediaSource.Factory, - private val hdr10Factory: MediaSource.Factory, + private val correctedFactory: ( + DolbyVisionTransformMode, + String?, + String?, + ) -> MediaSource.Factory, private val hlsFactory: MediaSource.Factory, private val dataSourceFactory: DataSource.Factory, private val subtitleParserFactory: SubtitleParser.Factory, private var loadErrorHandlingPolicy: LoadErrorHandlingPolicy, ) : MediaSource.Factory { + private var drmSessionManagerProvider: DrmSessionManagerProvider? = null + override fun setDrmSessionManagerProvider( drmSessionManagerProvider: DrmSessionManagerProvider, ): MediaSource.Factory { + this.drmSessionManagerProvider = drmSessionManagerProvider defaultFactory.setDrmSessionManagerProvider(drmSessionManagerProvider) - hlgFactory.setDrmSessionManagerProvider(drmSessionManagerProvider) - dv81Factory.setDrmSessionManagerProvider(drmSessionManagerProvider) - hdr10Factory.setDrmSessionManagerProvider(drmSessionManagerProvider) hlsFactory.setDrmSessionManagerProvider(drmSessionManagerProvider) return this } @@ -500,9 +501,6 @@ class SiloPlayerFactory( ): MediaSource.Factory { this.loadErrorHandlingPolicy = loadErrorHandlingPolicy defaultFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) - hlgFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) - dv81Factory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) - hdr10Factory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) hlsFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) return this } @@ -520,15 +518,25 @@ class SiloPlayerFactory( createHlsMediaSource(mediaItem) } else { val tag = localConfiguration.tag as? SiloMediaTransformTag - when (tag?.dolbyVisionMode) { - DolbyVisionTransformMode.PROFILE7_TO_PROFILE81 -> dv81Factory.createMediaSource(mediaItem) - DolbyVisionTransformMode.PROFILE7_TO_HDR10 -> hdr10Factory.createMediaSource(mediaItem) - else -> if (tag?.expectedDynamicRange.equals("hlg", ignoreCase = true)) { - hlgFactory.createMediaSource(mediaItem) - } else { - defaultFactory.createMediaSource(mediaItem) - } - } + mediaSourceFactory(tag).createMediaSource(mediaItem) + } + } + + private fun mediaSourceFactory(tag: SiloMediaTransformTag?): MediaSource.Factory { + if (tag == null || ( + tag.dolbyVisionMode == DolbyVisionTransformMode.DISABLED && + tag.expectedDynamicRange == null && + tag.expectedColorRange == null + ) + ) return defaultFactory + + return correctedFactory( + tag.dolbyVisionMode, + tag.expectedDynamicRange, + tag.expectedColorRange, + ).also { factory -> + drmSessionManagerProvider?.let(factory::setDrmSessionManagerProvider) + factory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) } } diff --git a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaMounter.kt b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaMounter.kt index 8103b96d5..8e488033e 100644 --- a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaMounter.kt +++ b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaMounter.kt @@ -25,6 +25,7 @@ fun mountVideoMedia( durationMs = spec.durationMs, requestHeaders = spec.requestHeaders, expectedDynamicRange = spec.expectedDynamicRange, + expectedColorRange = spec.expectedColorRange, transformations = spec.transformations, runtimeCorrections = spec.runtimeCorrections, ) @@ -54,6 +55,7 @@ fun refreshMountedVideoMedia( durationMs = spec.durationMs, requestHeaders = spec.requestHeaders, expectedDynamicRange = spec.expectedDynamicRange, + expectedColorRange = spec.expectedColorRange, transformations = spec.transformations, runtimeCorrections = spec.runtimeCorrections, ) diff --git a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt index 5ccb8dc87..855b5e6e0 100644 --- a/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt +++ b/android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt @@ -19,6 +19,7 @@ data class VideoPlayerMediaSpec( val audioPassthroughCodecs: List = emptyList(), val requestHeaders: Map = emptyMap(), val expectedDynamicRange: String? = null, + val expectedColorRange: String? = null, val transformations: List = emptyList(), val runtimeCorrections: List = emptyList(), ) { diff --git a/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactoryTest.kt b/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactoryTest.kt index 0e8d26e0a..2fae1ca8c 100644 --- a/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactoryTest.kt +++ b/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/DolbyVisionColorInfoExtractorsFactoryTest.kt @@ -106,4 +106,40 @@ class DolbyVisionColorInfoExtractorsFactoryTest { assertSame(source, source.withValidatedDynamicRangeColorInfo(null)) assertSame(source, source.withValidatedDynamicRangeColorInfo("hdr10")) } + + @Test + fun suppliesLimitedRangeWhenContainerMetadataIsMissing() { + val source = Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build() + + val repaired = source.withValidatedColorRange("tv") + + assertEquals(C.COLOR_RANGE_LIMITED, repaired.colorInfo?.colorRange) + } + + @Test + fun suppliesFullRangeWhenContainerMetadataIsMissing() { + val source = Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build() + + val repaired = source.withValidatedColorRange("pc") + + assertEquals(C.COLOR_RANGE_FULL, repaired.colorInfo?.colorRange) + } + + @Test + fun preservesExplicitContainerRangeOverServerFallback() { + val source = Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setColorInfo(ColorInfo.Builder().setColorRange(C.COLOR_RANGE_FULL).build()) + .build() + + assertSame(source, source.withValidatedColorRange("tv")) + } + + @Test + fun ignoresUnknownOrMissingServerRange() { + val source = Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build() + + assertSame(source, source.withValidatedColorRange("unknown")) + assertSame(source, source.withValidatedColorRange(null)) + } } diff --git a/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt b/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt index 731b6d08f..24a9921a4 100644 --- a/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt +++ b/android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt @@ -5,6 +5,7 @@ import org.siloserver.silo.model.playback.PlaybackEngineKind import org.siloserver.silo.model.playback.PlaybackPlanV3 import org.siloserver.silo.model.playback.PlaybackStreamProtocol import org.siloserver.silo.model.playback.PlaybackStreamV3 +import org.siloserver.silo.model.playback.PlaybackSourceV3 import org.siloserver.silo.model.playback.PlaybackSubtitleArtifactV3 import org.siloserver.silo.model.playback.PlaybackSubtitleDecisionV3 import org.siloserver.silo.model.playback.PlaybackSubtitleModeV3 @@ -73,6 +74,7 @@ class PlaybackV3SessionTest { assertEquals(timeline.seekWindowEndSeconds, converted.seekWindowEndSeconds) assertEquals(timeline.canSeekAnywhere, converted.canSeekAnywhere) assertEquals(timeline.seekRestoration, converted.seekRestoration) + assertEquals("pc", response.playbackPlan?.source?.colorRange) } private fun plan( @@ -93,6 +95,7 @@ class PlaybackV3SessionTest { selectedTracks = SelectedPlaybackTracksV3( subtitle = PlaybackTrackIdentityV3("subtitle", 2), ), + source = PlaybackSourceV3(colorRange = "pc"), subtitle = PlaybackSubtitleDecisionV3( mode = mode, trackId = "subtitle", diff --git a/androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt b/androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt index ae188c2a4..3cc4c5bdb 100644 --- a/androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt +++ b/androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt @@ -463,6 +463,7 @@ fun PlayerScreen( audioPassthroughCodecs = plan.validatedPassthroughCodecs(), requestHeaders = uiState.requestHeaders, expectedDynamicRange = plan?.source?.hdrFormat, + expectedColorRange = plan?.source?.colorRange, transformations = plan?.executableMedia3ClientTransformations().orEmpty(), runtimeCorrections = plan?.runtimeCorrections.orEmpty(), ) @@ -522,6 +523,7 @@ fun PlayerScreen( }, requestHeaders = if (!isLocalMedia) uiState.requestHeaders else emptyMap(), expectedDynamicRange = plan?.source?.hdrFormat, + expectedColorRange = plan?.source?.colorRange, transformations = plan?.executableMedia3ClientTransformations().orEmpty(), runtimeCorrections = plan?.runtimeCorrections.orEmpty(), ) diff --git a/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt b/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt index 647b58e67..f1347c6d5 100644 --- a/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt +++ b/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt @@ -1416,6 +1416,7 @@ fun TvPlayerScreen( audioPassthroughCodecs = plan.validatedPassthroughCodecs(), requestHeaders = state.requestHeaders, expectedDynamicRange = plan?.source?.hdrFormat, + expectedColorRange = plan?.source?.colorRange, transformations = plan?.executableMedia3ClientTransformations().orEmpty(), runtimeCorrections = plan?.runtimeCorrections.orEmpty(), ) @@ -1465,6 +1466,7 @@ fun TvPlayerScreen( audioPassthroughCodecs = plan.validatedPassthroughCodecs(), requestHeaders = state.requestHeaders, expectedDynamicRange = plan?.source?.hdrFormat, + expectedColorRange = plan?.source?.colorRange, transformations = plan?.executableMedia3ClientTransformations().orEmpty(), runtimeCorrections = plan?.runtimeCorrections.orEmpty(), ) diff --git a/shared/src/commonMain/kotlin/org/siloserver/silo/model/catalog/CatalogModels.kt b/shared/src/commonMain/kotlin/org/siloserver/silo/model/catalog/CatalogModels.kt index 6ff02efd1..d8031e1b1 100644 --- a/shared/src/commonMain/kotlin/org/siloserver/silo/model/catalog/CatalogModels.kt +++ b/shared/src/commonMain/kotlin/org/siloserver/silo/model/catalog/CatalogModels.kt @@ -300,6 +300,7 @@ data class VideoTrack( val profile: String? = null, val level: String? = null, @SerialName("bit_depth") val bitDepth: Int? = null, + @SerialName("color_range") val colorRange: String? = null, @SerialName("color_space") val colorSpace: String? = null, @SerialName("color_primaries") val colorPrimaries: String? = null, @SerialName("color_transfer") val colorTransfer: String? = null, diff --git a/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt b/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt index c8756ab66..22a289a85 100644 --- a/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt +++ b/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt @@ -248,6 +248,7 @@ data class PlaybackSourceMetadata( @SerialName("audio_codec") val audioCodec: String? = null, val resolution: String? = null, @SerialName("hdr_format") val hdrFormat: String? = null, + @SerialName("color_range") val colorRange: String? = null, @SerialName("dolby_vision_profile") val dolbyVisionProfile: Int? = null, @SerialName("subtitle_codec") val subtitleCodec: String? = null, ) diff --git a/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt b/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt index d46555e13..81b1cb9ce 100644 --- a/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt +++ b/shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt @@ -113,6 +113,7 @@ data class PlaybackPlanV3( val timeline: PlaybackTimelineV3 = PlaybackTimelineV3(), @SerialName("selected_tracks") val selectedTracks: SelectedPlaybackTracksV3 = SelectedPlaybackTracksV3(), @SerialName("effective_recipe") val effectiveRecipe: PlaybackEffectiveRecipeV3 = PlaybackEffectiveRecipeV3(), + val source: PlaybackSourceV3 = PlaybackSourceV3(), val claims: PlaybackValidationClaims = PlaybackValidationClaims(), val subtitle: PlaybackSubtitleDecisionV3 = PlaybackSubtitleDecisionV3(), val transformations: List = emptyList(), @@ -180,6 +181,16 @@ data class PlaybackEffectiveRecipeV3( @SerialName("audio_layout") val audioLayout: String? = null, ) +/** + * Source fields consumed by the Android renderer. The server's source object + * contains additional planning metadata; SiloJson deliberately ignores those + * keys until a client behavior needs them. + */ +@Serializable +data class PlaybackSourceV3( + @SerialName("color_range") val colorRange: String? = null, +) + @Serializable data class PlaybackSubtitleArtifactV3( val url: String, diff --git a/shared/src/commonTest/kotlin/org/siloserver/silo/model/catalog/CatalogTrackSerializationTest.kt b/shared/src/commonTest/kotlin/org/siloserver/silo/model/catalog/CatalogTrackSerializationTest.kt index c7d079173..883a703fb 100644 --- a/shared/src/commonTest/kotlin/org/siloserver/silo/model/catalog/CatalogTrackSerializationTest.kt +++ b/shared/src/commonTest/kotlin/org/siloserver/silo/model/catalog/CatalogTrackSerializationTest.kt @@ -38,11 +38,12 @@ class CatalogTrackSerializationTest { @Test fun `VideoTrack preserves server Dolby Vision metadata`() { - val source = """{"codec":"hevc","dolby_vision":"Profile 8","dv_profile":8}""" + val source = """{"codec":"hevc","dolby_vision":"Profile 8","dv_profile":8,"color_range":"tv"}""" val track = json.decodeFromString(source) assertEquals("Profile 8", track.dolbyVision) assertEquals(8, track.dolbyVisionProfile) + assertEquals("tv", track.colorRange) } } diff --git a/shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt b/shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt index 8fd97f724..84c3b88ed 100644 --- a/shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt +++ b/shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt @@ -31,6 +31,7 @@ class PlaybackProtocolV3Test { height = 2160, dynamicRange = "dolby_vision_p8_1", ), + source = PlaybackSourceV3(colorRange = "tv"), decisionReason = "original_compatible", requestedMediaFileId = 42, effectiveMediaFileId = 84, @@ -77,6 +78,7 @@ class PlaybackProtocolV3Test { val decoded = SiloJson.decodeFromString(encoded) assertEquals(42, decoded.requestedMediaFileId) assertEquals(84, decoded.effectiveMediaFileId) + assertEquals("tv", decoded.source.colorRange) } @Test