Skip to content
Merged
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 @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<PlaybackPlanV3>(
"""
{
"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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down