Skip to content

fix(playback): take the media runtime from the v3 plan - #104

Merged
Quick104 merged 1 commit into
mainfrom
fix/playback-source-duration
Jul 26, 2026
Merged

fix(playback): take the media runtime from the v3 plan#104
Quick104 merged 1 commit into
mainfrom
fix/playback-source-duration

Conversation

@Quick104

@Quick104 Quick104 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

A 90-minute movie played back as 0:37 / 1:01.

The v3 plan carries a source descriptor that this client never decoded, and PlaybackV3Session.toSessionResponse() never set durationSeconds at 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 PlayerViewModel is 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_seconds from the plan and carries it into PlaybackSessionResponse, which both MobileVideoPlaybackStarter and TvVideoPlaybackStarter already consume via resolved.durationSeconds ?: effectiveVersion?.duration ?: 0.0. The existing fallback chain is untouched — it simply gains an authoritative first rung.

Nullability is load-bearing. SiloJson sets coerceInputValues, so a non-nullable Double here would silently turn an unknown runtime into 0.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 is Double? 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_seconds to 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

$ ./gradlew :shared:testDebugUnitTest :android-shared:testDebugUnitTest
BUILD SUCCESSFUL

PlaybackV3SessionTest results:

tests="6" skipped="0" failures="0" errors="0"

Three tests added, per the repo guidance to cover critical shared-logic behavior:

  • the runtime reaches the session response
  • an unknown runtime stays null rather than becoming 0.0
  • a plan with no source object at all still decodes, with the runtime reading as unknown

Notes

Not addressed here, and worth separate issues:

  • PlaybackSessionLifecycle writes duration = 0.0 with forceOverwrite = true on stop, and the server clobbers a good stored duration with it.
  • PlayerViewModel does not refresh duration on replan (TvPlayerViewModel does), so a replan that changes the effective file leaves the phone with a stale runtime.
  • timing_origin_seconds is declared in PlaybackProtocolV3.kt and read nowhere in the client; on a re-anchored copy stream every sidecar subtitle cue is offset by the anchor.

AI Disclosure

  • Tool(s): Claude Code
  • Model(s): claude-opus-5[1m]
  • Involvement: fully AI-generated
  • Adversarial review: The design was reviewed by three independent agents before implementation. Two findings changed this client's half specifically. First, the field must be nullable end to end — coerceInputValues would otherwise convert an explicit null to 0.0 and 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 to TvPlayerViewModel scrub clamping and PlayerProgressBar, 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 treating 0 as unknown internally and only gains a trustworthy first source.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Playback sessions now report the resolved media source’s runtime when available.
    • Unknown runtimes remain explicitly unavailable instead of being replaced with an estimated duration.
    • Older playback plans continue to decode safely with an unknown runtime.
  • Tests

    • Added coverage for known, unknown, and legacy playback duration scenarios.

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>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Playback 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.

Changes

Playback runtime propagation

Layer / File(s) Summary
Source descriptor contract
shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
PlaybackPlanV3 includes a defaulted PlaybackSourceDescriptorV3 with optional media file and duration fields.
Session response runtime mapping
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt, android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt
Session responses copy source.durationSeconds; tests verify known, unknown, and legacy JSON behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: taking media runtime from the v3 playback plan.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/playback-source-duration

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt (1)

111-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover decoding of a known duration_seconds value.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2987b4e and 816b903.

📒 Files selected for processing (3)
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt

@Quick104
Quick104 merged commit 7e9fdc1 into main Jul 26, 2026
2 checks passed
@Quick104
Quick104 deleted the fix/playback-source-duration branch July 26, 2026 04:12
cursor Bot pushed a commit to Prairie-Server/prairie-android that referenced this pull request Jul 26, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant