fix(playback): take the media runtime from the v3 plan - #104
Conversation
The v3 plan carries a source descriptor the client never decoded, and toSessionResponse never set durationSeconds at all. With no runtime from the server, the duration fell through to the catalog value and then to the playback engine's report. On an HLS copy remux the engine reports the window FFmpeg has produced so far, so a feature film displayed as about a minute. Decode source.duration_seconds and carry it into the session response. It stays nullable end to end: SiloJson sets coerceInputValues, so a non-nullable Double would turn an unknown runtime into 0.0 — the value the grow-only ratchet then has no floor to defend against, which is how the engine's window won in the first place. The descriptor and every field default, so a server predating it still decodes and simply reports an unknown runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughPlayback Protocol v3 now models optional source metadata, including runtime duration. Session responses expose that duration, preserving unknown values, and tests cover known, unknown, and legacy plans without source descriptors. ChangesPlayback runtime propagation
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt (1)
111-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover decoding of a known
duration_secondsvalue.The tests cover Kotlin-object propagation and an omitted descriptor, but not the populated JSON field. Decode
"source":{"duration_seconds":5400.0}and assert the session runtime to protect the wire-name and mapping contract together.As per coding guidelines, “add focused tests for shared-logic changes only when behavior is critical or high-risk”; this is a runtime protocol boundary.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt` around lines 111 - 125, Add a focused test alongside planWithoutASourceDescriptorDecodesWithAnUnknownRuntime that decodes a PlaybackPlanV3 JSON payload containing source.duration_seconds set to 5400.0, then assert toSessionResponse(...).durationSeconds equals 5400.0. Ensure the test exercises the duration_seconds wire name and its mapping to the session runtime.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt`:
- Around line 111-125: Add a focused test alongside
planWithoutASourceDescriptorDecodesWithAnUnknownRuntime that decodes a
PlaybackPlanV3 JSON payload containing source.duration_seconds set to 5400.0,
then assert toSessionResponse(...).durationSeconds equals 5400.0. Ensure the
test exercises the duration_seconds wire name and its mapping to the session
runtime.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c3b30869-9936-41ce-808d-8d85308d169a
📒 Files selected for processing (3)
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.ktandroid-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.ktshared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
The v3 plan carries a source descriptor the client never decoded, and toSessionResponse never set durationSeconds at all. With no runtime from the server, the duration fell through to the catalog value and then to the playback engine's report. On an HLS copy remux the engine reports the window FFmpeg has produced so far, so a feature film displayed as about a minute. Decode source.duration_seconds and carry it into the session response. It stays nullable end to end: SiloJson sets coerceInputValues, so a non-nullable Double would turn an unknown runtime into 0.0 — the value the grow-only ratchet then has no floor to defend against, which is how the engine's window won in the first place. The descriptor and every field default, so a server predating it still decodes and simply reports an unknown runtime. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
A 90-minute movie played back as
0:37 / 1:01.The v3 plan carries a
sourcedescriptor that this client never decoded, andPlaybackV3Session.toSessionResponse()never setdurationSecondsat all. With no runtime from the server, duration fell through to the catalog value and then to the playback engine's report.On an HLS copy remux the server intentionally serves FFmpeg's still-growing playlist, so the engine reports only the length produced so far. The grow-only ratchet in
PlayerViewModelis a correct defense —maxOf(state.duration, durationSec)never lets a known runtime shrink — but with a wrong catalog value it had no floor to hold, so the growing window won.What this does
Decodes
source.duration_secondsfrom the plan and carries it intoPlaybackSessionResponse, which bothMobileVideoPlaybackStarterandTvVideoPlaybackStarteralready consume viaresolved.durationSeconds ?: effectiveVersion?.duration ?: 0.0. The existing fallback chain is untouched — it simply gains an authoritative first rung.Nullability is load-bearing.
SiloJsonsetscoerceInputValues, so a non-nullableDoublehere would silently turn an unknown runtime into0.0— the exact value the ratchet then has no floor to defend against, which is how the engine's window won in the first place. The field isDouble?end to end, and the server omits the key rather than sending null.The descriptor and every field default, so a server predating it still decodes and reports an unknown runtime rather than failing the plan.
Scope
Client half of Silo-Server/silo-server#482, which adds
source.duration_secondsto the v3 plan and fixes the underlying scanner rule that let a wrong duration persist.Merge the server PR first. This change is inert without it — the field will simply be absent and behavior is unchanged from today. It is safe to merge in either order, but provides no benefit until the server ships.
Verification
PlaybackV3SessionTestresults:Three tests added, per the repo guidance to cover critical shared-logic behavior:
nullrather than becoming0.0sourceobject at all still decodes, with the runtime reading as unknownNotes
Not addressed here, and worth separate issues:
PlaybackSessionLifecyclewritesduration = 0.0withforceOverwrite = trueon stop, and the server clobbers a good stored duration with it.PlayerViewModeldoes not refresh duration on replan (TvPlayerViewModeldoes), so a replan that changes the effective file leaves the phone with a stale runtime.timing_origin_secondsis declared inPlaybackProtocolV3.ktand read nowhere in the client; on a re-anchored copy stream every sidecar subtitle cue is offset by the anchor.AI Disclosure
coerceInputValueswould otherwise convert an explicit null to0.0and reintroduce the bug, which is why the server omits the key rather than sending null. Second, a proposed rule to represent duration as genuinely "unknown" in the UI was withdrawn: review traced it toTvPlayerViewModelscrub clamping andPlayerProgressBar, where a null/zero runtime kills D-pad scrubbing entirely and shrinks the phone seek bar to a one-second slider — worse than the bug being fixed. This client therefore keeps treating0as unknown internally and only gains a trustworthy first source.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests