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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Extractor> =
delegate.createExtractors().map(::wrap).toTypedArray()
Expand All @@ -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)
}
Expand All @@ -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<Int, TrackOutput>()

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()
Expand All @@ -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(),
)
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,15 @@ class SiloPlayerFactory(
fun defaultMediaSourceFactory(
mode: DolbyVisionTransformMode,
expectedDynamicRange: String? = null,
expectedColorRange: String? = null,
) =
DefaultMediaSourceFactory(
context,
DolbyVisionColorInfoExtractorsFactory(
configuredExtractorsFactory(),
mode,
expectedDynamicRange = expectedDynamicRange,
expectedColorRange = expectedColorRange,
),
)
.setDataSourceFactory(dataSourceFactory)
Expand All @@ -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,
Expand Down Expand Up @@ -396,6 +393,7 @@ class SiloPlayerFactory(
durationMs: Long? = null,
requestHeaders: Map<String, String> = emptyMap(),
expectedDynamicRange: String? = null,
expectedColorRange: String? = null,
transformations: List<String> = emptyList(),
runtimeCorrections: List<String> = emptyList(),
): MediaItem {
Expand All @@ -421,6 +419,7 @@ class SiloPlayerFactory(
else -> DolbyVisionTransformMode.DISABLED
},
expectedDynamicRange = expectedDynamicRange,
expectedColorRange = expectedColorRange,
),
)

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fun mountVideoMedia(
durationMs = spec.durationMs,
requestHeaders = spec.requestHeaders,
expectedDynamicRange = spec.expectedDynamicRange,
expectedColorRange = spec.expectedColorRange,
transformations = spec.transformations,
runtimeCorrections = spec.runtimeCorrections,
)
Expand Down Expand Up @@ -54,6 +55,7 @@ fun refreshMountedVideoMedia(
durationMs = spec.durationMs,
requestHeaders = spec.requestHeaders,
expectedDynamicRange = spec.expectedDynamicRange,
expectedColorRange = spec.expectedColorRange,
transformations = spec.transformations,
runtimeCorrections = spec.runtimeCorrections,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data class VideoPlayerMediaSpec(
val audioPassthroughCodecs: List<String> = emptyList(),
val requestHeaders: Map<String, String> = emptyMap(),
val expectedDynamicRange: String? = null,
val expectedColorRange: String? = null,
val transformations: List<String> = emptyList(),
val runtimeCorrections: List<String> = emptyList(),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -93,6 +95,7 @@ class PlaybackV3SessionTest {
selectedTracks = SelectedPlaybackTracksV3(
subtitle = PlaybackTrackIdentityV3("subtitle", 2),
),
source = PlaybackSourceV3(colorRange = "pc"),
subtitle = PlaybackSubtitleDecisionV3(
mode = mode,
trackId = "subtitle",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down Expand Up @@ -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(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down Expand Up @@ -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(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Loading