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..d045d087d 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 @@ -134,6 +134,12 @@ internal fun PlaybackPlanV3.toSessionResponse( position = timeline.sourceStartSeconds, streamUrl = stream.url, audioTrackIndex = selectedTracks.audio?.index ?: 0, + // The server's runtime for the effective file, or null when it does + // not know. Callers must keep null as null rather than substituting + // the engine's duration: on an HLS copy remux the engine reports the + // window produced so far, so adopting it shows a feature film as a + // couple of minutes. + durationSeconds = source.durationSeconds, subtitleUrls = subtitles, playbackPlan = legacyPlan, ) 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..4d563a6ae 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 @@ -3,6 +3,7 @@ package org.siloserver.silo.common.player import org.siloserver.silo.model.playback.PlaybackDelivery import org.siloserver.silo.model.playback.PlaybackEngineKind import org.siloserver.silo.model.playback.PlaybackPlanV3 +import org.siloserver.silo.model.playback.PlaybackSourceDescriptorV3 import org.siloserver.silo.model.playback.PlaybackStreamProtocol import org.siloserver.silo.model.playback.PlaybackStreamV3 import org.siloserver.silo.model.playback.PlaybackSubtitleArtifactV3 @@ -11,8 +12,10 @@ import org.siloserver.silo.model.playback.PlaybackSubtitleModeV3 import org.siloserver.silo.model.playback.PlaybackTimelineV3 import org.siloserver.silo.model.playback.PlaybackTrackIdentityV3 import org.siloserver.silo.model.playback.SelectedPlaybackTracksV3 +import org.siloserver.silo.network.SiloJson import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNull class PlaybackV3SessionTest { @Test @@ -75,12 +78,61 @@ class PlaybackV3SessionTest { assertEquals(timeline.seekRestoration, converted.seekRestoration) } + @Test + fun sourceRuntimeReachesTheSessionResponse() { + val response = plan( + mode = PlaybackSubtitleModeV3.OFF, + format = "", + url = "", + source = PlaybackSourceDescriptorV3(mediaFileId = 482, durationSeconds = 5400.0), + ).toSessionResponse("session", "profile", 482) + + assertEquals(5400.0, response.durationSeconds) + } + + // A server that does not know the runtime must leave the client knowing it + // does not know. Substituting 0.0 here is what let the playback engine's + // growing-HLS-window duration win and show a feature film as a minute. + @Test + fun unknownSourceRuntimeStaysUnknown() { + val response = plan( + mode = PlaybackSubtitleModeV3.OFF, + format = "", + url = "", + source = PlaybackSourceDescriptorV3(mediaFileId = 482), + ).toSessionResponse("session", "profile", 482) + + assertNull(response.durationSeconds) + } + + // Servers predating the descriptor omit it entirely; decoding must not fail + // and the runtime must read as unknown rather than zero. + @Test + fun planWithoutASourceDescriptorDecodesWithAnUnknownRuntime() { + val decoded = SiloJson.decodeFromString( + """ + { + "plan_id": "plan", + "delivery": "original_http", + "engine": "media3_direct", + "stream": {"url": "/stream/session", "protocol": "http_progressive"}, + "decision_reason": "test" + } + """.trimIndent(), + ) + + assertNull(decoded.source.durationSeconds) + assertNull(decoded.toSessionResponse("session", "profile", 482).durationSeconds) + } + private fun plan( mode: PlaybackSubtitleModeV3, format: String, url: String, timeline: PlaybackTimelineV3 = PlaybackTimelineV3(), + source: PlaybackSourceDescriptorV3 = PlaybackSourceDescriptorV3(), ) = PlaybackPlanV3( + source = source, planId = "plan", delivery = PlaybackDelivery.ORIGINAL_HTTP, engine = PlaybackEngineKind.MEDIA3_DIRECT, 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 6de30d3c2..70d92606d 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 @@ -134,6 +134,30 @@ data class PlaybackPlanV3( @SerialName("decision_reason") val decisionReason: String, @SerialName("requested_media_file_id") val requestedMediaFileId: Int? = null, @SerialName("effective_media_file_id") val effectiveMediaFileId: Int? = null, + val source: PlaybackSourceDescriptorV3 = PlaybackSourceDescriptorV3(), +) + +/** + * Facts about the media file the plan resolved to, as opposed to the transport + * carrying it. Defaulted throughout so a server that predates the descriptor + * still decodes. + */ +@Serializable +data class PlaybackSourceDescriptorV3( + @SerialName("media_file_id") val mediaFileId: Int? = null, + /** + * Full runtime of the source, or null when the server does not know it. + * + * Null must survive as null: `SiloJson` sets `coerceInputValues`, so a + * non-nullable `Double` here would silently become 0.0 — the very value + * this field exists to stop the player inventing. + * + * This is the whole file, never `total - sourceStartSeconds`, and it is + * never adjusted by `timelineOffsetSeconds`. Do not substitute the + * engine's reported duration for it: on an HLS copy remux the engine + * reports the window produced so far, not the runtime. + */ + @SerialName("duration_seconds") val durationSeconds: Double? = null, ) @Serializable