Skip to content

feat(playback): adopt platform-neutral protocol v3 - #200

Merged
Quick104 merged 17 commits into
mainfrom
codex/playback-v3-neutral-android
Aug 11, 2026
Merged

feat(playback): adopt platform-neutral protocol v3#200
Quick104 merged 17 commits into
mainfrom
codex/playback-v3-neutral-android

Conversation

@Quick104

@Quick104 Quick104 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Part of Silo-Server/silo-server#135

The Android phone/TV playback stack still depended on the pre-neutral v3 draft: client-derived attempt behavior, platform-shaped capability claims, and recovery paths that could lose track intent or restart indefinitely. The client also lacked a strict executable binding to the server's canonical contract corpus.

Approach

  • Adopt the server-owned neutral v3 DTOs, capability evidence, delivery plans, opaque attempt-key echoing, route events, terminal decisions, and intent replans across phone, TV, Cast, and audiobook playback.
  • Harden lifecycle ownership so terminal/stale startup failures stop cleanly and seek, quality, track, renewal, and route changes retain the server-returned session, effective file, timeline, inventory, and intent.
  • Treat the server subtitle inventory, available-quality menu, source duration, and attempt identity as authoritative; Android enriches or echoes those values but does not synthesize protocol identities or menu rows.
  • Keep Shield-specific playback corrections inside execution policy: DV/color-range fallbacks, passthrough suppression, PGS SUP extraction, startup buffering, and deep-link/load ownership.
  • Keep the TV diagnostics crash-report prompt focusable after playback failures.
  • Vendor all nine server fixtures from server PR #567, pin their exact server SHA, and decode the full typed planner/replan/protocol matrix.

Coordinated release train:

Review follow-up

  • Addressed all 32 inline review threads across the original and final CodeRabbit rounds, including Cast cleanup/timeline/session ownership, audiobook source timelines, capability snapshot coherence, lifecycle recovery races, PCM retry preservation, DV codec detection, subtitle caching/path handling, and Media3 listener ownership.
  • Closed additional neutral-v3 gaps found during the follow-up review: staged Cast track replans, authoritative effective-edition/track adoption, exact subtitle_ready.track handling, source-duration rules, server-ordered quality menus, and shared phone/TV subtitle identity migration.
  • Closed the final exact-head audit gaps: Cast load recovery now resets only after a confirmed receiver load while staying bounded across consecutive failures, and phone/TV catalog, Media3, auto-selection, persistence, and formatting paths share one subtitle-accessibility classifier.
  • Closed the final CodeRabbit follow-up: unknown-duration progress bars no longer render false intro/chapter decorations, and malformed realtime scalar tests retain valid control fields independently.
  • Added focused regression coverage for protocol cursor identities, lifecycle and seek adoption, Cast/audiobook timelines, authoritative subtitle inventories, adapted editions, legacy downloaded-subtitle migration, unknown durations, malformed realtime payloads, and Cast recovery budgeting.
  • Merged current main (908466c108c00104ba76e965cc294aae30b4c4de) and re-ran the review and validation on the resolved head.
  • Final independent Standards and Spec review passes report zero findings at 5508a810.

Risks and follow-up

  • Requires the server PR to land in the same release train; this client deliberately does not implement a compatibility window for removed draft-v3 fields.
  • The server remains the one-way contract authority. Android echoes opaque identities and vendors fixtures but never derives them.
  • Physical NVIDIA Shield validation was rerun on the final PR APK against shared development. It covered exact-file 4K Dolby Vision-to-HDR10 remux, seek reanchor/resume, unsupported HLG terminal handling plus unpinned SDR edition adaptation, authoritative subtitle replan/selection, persisted first-frame/stopped route telemetry, and signed-in relaunch persistence.

Testing

  • ./gradlew test lint :androidApp:assembleDebug :androidTvApp:assembleDebug: passed (371 tasks).
  • Focused shared, Android-shared, phone, TV, Cast, audiobook, seek/replan, subtitle identity, and focus suites: passed.
  • All nine vendored JSON fixtures are byte-identical to server 79e3e761ad391b1aa9f2c280eeceeb23df9d3c81.
  • Final arm64 TV APK installed in place on an NVIDIA Shield with app data preserved: exact-file 4K Dolby Vision rendered through an HEVC/AAC HDR10 remux; two server seek reanchors resumed at the exact source time; an exact unsupported HLG pin returned typed no_alternate_version; unpinned playback adapted to 1080p SDR H.264/E-AC-3; Arabic sidecar selection replanned, remounted, and was selected by Media3; clean signed-in relaunch passed.
  • git diff --check: passed.

AI Disclosure

  • Tool(s): OpenAI Codex; Claude Code
  • Model(s): GPT-5 family; Claude Fable 5
  • Involvement: AI-assisted implementation, testing, and adversarial review
  • Adversarial review: Independent Standards and Spec passes reviewed the exact final tree; both returned zero findings after the follow-up fixes.

Summary by CodeRabbit

  • New Features

    • Added server-planned playback with improved quality options, progress restoration, seeking, and recovery.
    • Added authoritative subtitle inventories, downloaded subtitle restoration, burn-in, streaming WebVTT, and real-time subtitle updates.
    • Improved Dolby Vision playback, casting, audiobook playback, and TV deep-link handling.
    • Added support for unknown video durations without misleading progress information.
  • Bug Fixes

    • Improved playback session handling, teardown, subtitle synchronization, and startup-stall recovery.
    • Improved subtitle selection accuracy and accessibility labeling.
    • Improved diagnostics prompt focus and dismissal behavior.

The server now owns the playback protocol as a platform-neutral contract, and
this client's job shrinks to speaking it. Most of this change is deletion: the
pieces below existed because the wire format was shaped around Media3, and a
neutral contract makes them redundant rather than merely unused.

- Attempt keys are server-minted. The Kotlin FNV-1a implementation and the
  fixtures that pinned its output are gone; `plan_attempt_key` arrives on the
  plan, is stored opaquely, and is echoed on the next replan. `attempted_plan_keys`
  carries what the server gave us, never anything computed here.
- Engines become deliveries. `PlaybackEngineKind` and the `media3_*` capability
  envelope are replaced by the three neutral delivery classes — `original_http`,
  `progressive`, `hls` — each self-describing its containers, codecs, subtitle
  support, and transformations. `PlaybackExecutionPlan` survives only as a
  player-facing projection built from the plan, not as a wire type.
- Android-shaped facts move to `platform_details`. The `Build` dump is a
  free-form bag the server reads for quirk matching and support diagnostics
  rather than a set of platform-specific fields on a shared type.
- Capability evidence is stated, not implied. This client probes `MediaCodecList`
  for concrete profile/level/bit-depth tuples, so it advertises `exact` on both
  video and audio — the only tier the server validates strictly against, and the
  only one that earns audio passthrough. Cast advertises `declared`.
- Output identity travels nested under `client_playback_context.output` as an
  opaque `output_context_id`; Android's audio route generation counter is
  exactly the equality-comparable token the server wants.
- Track and quality changes are intents, not failures. They now send
  `track_change` and `quality_change` instead of routing through
  replan-with-failure or a legacy endpoint, so the server can tell a user
  choice from a playback problem.

`PlaybackProtocolV3ConformanceTest` is the drift gate: it reads the server's
golden fixtures, vendored byte-identically under `playback/v3/`, and proves this
client both decodes every field the server sends and encodes requests in the
shape the server expects. Fields the client deliberately does not model are an
explicit allow-list, so a field going unread fails the build naming its JSON
path. Attempt keys are asserted only by echo — there is no hash here to check
them with, which is the point.

Two tests went with their subjects: the transcode-fallback suite (the endpoint
is deleted server-side) and the styled-subtitle burn-in suite (burn-in is now a
server plan decision). `PlaybackSessionLifecycle` loses a `SessionState.Loading`
nobody observed and a `ProfileRepository` it never called.

Verified: 3,231 unit tests across shared, android-shared, androidApp, and
androidTvApp — 0 failures, 0 errors.

Part of the coordinated playback v3 release train; there is no compatibility
window, and a client that does not declare `protocol_version: 3` now gets 426.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request migrates Android playback to protocol V3. It replaces engine-based plans with delivery capabilities, server-minted plan keys, opaque output contexts, authoritative subtitle inventories, caller-owned recovery, and updated mobile, TV, audiobook, and Cast flows.

Changes

Playback V3 contract

Layer / File(s) Summary
Protocol models and validation
shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/*, shared/src/commonMain/kotlin/org/siloserver/silo/network/api/*
V3 uses delivery capabilities, subtitle inventories, opaque plan keys, output context IDs, explicit operations, and progress persistence. Legacy engine and transcode models are removed.
Protocol conformance coverage
shared/src/commonTest/*, shared/src/androidUnitTest/*, shared/src/commonTest/resources/playback/v3/*
Tests and fixtures cover V3 decoding, serialization, delivery selection, subtitle validation, attempt-key echoing, route events, errors, and planner matrices.

Session and player execution

Layer / File(s) Summary
Session ownership and replanning
android-shared/.../PlaybackSessionLifecycle.kt, android-shared/.../PlaybackSessionManager.kt
Lifecycle adoption replaces lifecycle-managed starts. Replans and seek recovery use server plan keys, output context IDs, local mutations, explicit operations, and scoped passthrough suppression.
Android playback flows
android-shared/.../AudiobookPlayerViewModel.kt, androidApp/.../PlayerViewModel.kt, androidTvApp/.../TvPlayerViewModel.kt
Players apply server stream plans, preserve recovery parameters, invalidate stale loads, stop terminal sessions, and clear stale ownership state.
Capability and media handling
android-shared/.../PlaybackCapabilityDetector.kt, PlaybackV3Session.kt, CastPlaybackPreparer.kt, SiloLoadControl.kt, subtitle/*
Capability metadata, subtitle inventory projection, Cast preparation, Dolby Vision buffering, subtitle streaming, seek maps, and subtitle mounting use the updated playback contract.

Application integration

Layer / File(s) Summary
Dependency injection and subtitle identity
androidApp/.../di/*, androidTvApp/.../di/*, androidApp/.../screens/player/*, androidTvApp/.../screens/player/*
Audiobook playback receives qualified session dependencies. Mobile and TV subtitle handling uses server delivery metadata and shared subtitle identity utilities.
TV navigation and diagnostics
androidTvApp/.../navigation/*, TvDiagnosticsPromptScreen.kt
Playback deep links compare content, file, quality, audio, and subtitle selections. The diagnostics prompt uses a full-screen dialog with deferred focus restoration.
Playback evidence and documentation
.agents/skills/test-shield-playback/*, docs/playback/*, docs/superpowers/plans/*
Evidence queries and guidance use delivery fields, optional codenames, exact HDR selection, and neutral V3 server validation requirements.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlayerViewModel
  participant PlaybackSessionManager
  participant PlaybackApi
  participant PlaybackServer
  participant Media3Player
  PlayerViewModel->>PlaybackSessionManager: start or replan with capabilities
  PlaybackSessionManager->>PlaybackApi: send V3 request
  PlaybackApi->>PlaybackServer: request playback decision
  PlaybackServer-->>PlaybackApi: return plan and planAttemptKey
  PlaybackApi-->>PlaybackSessionManager: return ready or terminal result
  PlaybackSessionManager-->>PlayerViewModel: adopt plan or publish recovery
  PlayerViewModel->>Media3Player: start stream at plan position
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: rxwatcher

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adopting the platform-neutral playback protocol v3.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/playback-v3-neutral-android

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

@Quick104

Quick104 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Code review — 10 findings

High-effort review: 8 independent finder angles (line-by-line, removed-behavior audit, cross-file trace, reuse, simplification, efficiency, altitude, conventions) produced 29 candidates; after dedup, each of the 18 surviving candidates was independently verified against the code (and, where relevant, the companion silo-server repo). 13 confirmed, 2 plausible, 3 refuted. Ranked most-severe first.


1. Blank plan_attempt_key bricks playback on older v3 servers — CONFIRMED

shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt:480

validateForMedia3 now hard-fails any plan whose plan_attempt_key is blank with a non-retryable Terminal, while the client-side derivation fallback (the FNV-1a planAttemptKey(outputRouteGeneration) on main) was deleted and every legacy start path removed. A v3 server that predates server-minted plan_attempt_key passes the only gate left (the unchanged playback_plan_v3 feature check), then every video/TV/cast/audiobook start fails with "The server returned no plan-attempt identity" — despite a fully playable plan. Mixed-generation deployments are expected: this same PR keeps transitional-sidecar support for "current mainline servers", but there's no feature flag distinguishing key-minting servers and no derivation fallback.

2. track_change/quality_change replans are rejected by the deployed server generation — CONFIRMED

shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt:400

The old contract's doc promised explicit operations are "negotiated independently through client/server features so an older server never has to infer new semantics." The new client sends the new operations unconditionally — replanOperationForClassification (PlaybackSessionManager.kt:2415) with call sites at PlayerViewModel.kt:1643, TvPlayerViewModel.kt:2243, TvSubtitleTransactionAdapter.kt:2312 — with no serverFeatures check. silo-server main's ReplanRequestV3.Validate() (internal/playback/protocol_v3.go:652) accepts only failure_recovery/seek_failure_recovery/seek_reanchor; its default branch returns "invalid replan operation". The new operations exist only on the unmerged codex/playback-v3-neutral-server branch — so against currently deployed servers, every mid-playback subtitle/audio/quality change fails. (The seek-reanchor failure-omission half of this concern was refuted: server main explicitly accepts it.)

3. Audiobook part starts clobber the shared video session manager — CONFIRMED

android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt:561

Audiobook starts now run through the DI-singleton PlaybackSessionManager.startVideoSessionV3 (same single {} injected into the video VMs — AndroidModule.kt:254/457, AndroidTvModule.kt:174/220). That call unconditionally runs beginContentReset() (draining/stopping any video's staged replan candidates), installs the audiobook as the single activeVideoAttempt, and resets the process-global PassthroughSuppressionRegistry via beginAttempt. If a video attempt coexists (background audiobook via the shared SiloPlaybackService, async video teardown, replan in flight), subsequent video seek re-anchors/replans read the audiobook's attempt (or 409), and the video sink's PCM-retry suppression is cleared mid-playback. CastPlaybackPreparer in this same PR documents exactly this hazard and shields itself with a throwaway manager + PassthroughSuppressionScope.None; the audiobook path (previously stateless startSession) got no such isolation.

4. Cast start position is in source space, receiver needs stream space — CONFIRMED

android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt:150

The cast media spec feeds plan.timeline.sourceStartSeconds to SiloCastSessionManager.loadPendingMedia, which calls setCurrentTime(spec.positionSeconds * 1000) — a stream-local position. Casting forces HLS, and transcode HLS is anchored at the resume point (the repo's own tests model sourceStartSeconds = streamOriginSeconds = 90, playerStartSeconds = 0). Resuming a movie at 40:00 seeks the receiver 2400s into a stream whose 0 already maps to 40:00 — doubling the offset or overshooting end-of-stream. Every other engine-start consumer in this PR uses playerStartSeconds, and the pre-PR cast path used the transcode response's playerStartSeconds.

5. Cast subtitle menu ignores the transitional sidecar fallback — CONFIRMED

android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt:179

castSubtitleTracks filters plan.subtitle.inventory only. Current mainline servers send the transitional sidecars list with an empty inventory (PlaybackProtocolV3.kt:316: "Transitional sidecar set used by current mainline servers"), which native playback in this same PR handles via inventorySubtitles.ifEmpty { transitionalSidecars } (PlaybackV3Session.kt:128). Casting against such a server yields an empty CC menu and silently drops the phone-selected subtitle; the removed v2 path built the menu from session.subtitleUrls.

6. Single-file audiobook start lacks the stale-start guard; strands a server session — CONFIRMED

android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt:512

startSingleFileSession's Success branch calls applyStartedSession unconditionally after the network round-trip. stopPlaybackSession (line 1111) bumps loadGeneration precisely so an in-flight load "can't resurrect a session", but only the sibling loadTrack path (line 468) re-checks generation != loadGeneration || isClosing and stops the superseded session. Stopping/backing out during a single-file start writes streamUrl/sessionId back into the stopped VM, and nothing stops the freshly allocated server session — it lingers holding a concurrent-stream slot (and possibly a transcode) until server expiry.

7. One unknown subtitle-delivery value kills playback non-retryably — CONFIRMED

shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt:576

hasValidSubtitleInventory marks invalid: any entry whose delivery isn't exactly sidecar/burn_in_only (else -> true), a sidecar with a blank url, or a burn_in_only row with a present url. validateForMedia3 escalates one bad entry to Terminal("invalid_playback_plan", retryable = false) for the whole playback. delivery is a plain String defaulting to "" — the vocabulary is only closed by a doc comment — so a newer server adding a third delivery kind for one track terminates video playback outright instead of dropping that track.

8. Part sessions no longer suppress server-side progress persistence — PLAUSIBLE

android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt:556

The always-on disable_progress_persistence flag (main: PlaybackModels.kt:428, set true for every part session) was deleted with StartPlaybackRequest, and PlaybackStartRequestV3 has no equivalent — while the VM still reports part-local positions via reportSessionProgress/retireActiveSession. Whole-book resume correctness now rests entirely on the new doc comment's claim that the server derives suppression "from the file's presentation-part count" — unverifiable from this repo. If a single-file part reads as part-count 1, part-local positions (e.g. 240s into file 3 of 10) race the whole-book global-position writes and can snap the user's cross-device resume point back to minutes into the book — the exact regression the flag existed to prevent.

9. Legacy cover-art video tracks can make audiobooks unplayable — PLAUSIBLE

android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt:564

main spliced AUDIOBOOK_COVER_ART_CODECS (mjpeg/png/jpeg/bmp/gif) into codecsVideo; the PR sends bare capabilityDetector.detect() with qualityPreference = original. The companion server branch stops scanning cover art as video tracks and self-heals most legacy rows, but its own commit message concedes rows with pre-existing color_range metadata "need a rescan". Such an unhealed row makes IsAudioOnly() false, routes the audiobook into the video planner, mjpeg fails the exact-evidence video gate, and the plan resolves terminal — a book that played on v2 won't play until a rescan.

10. clientFeatures default silently drops the passthrough feature — CONFIRMED (simplification)

shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt:398

PlaybackStartRequestV3 and PlaybackReplanRequestV3 default clientFeatures = PLAYBACK_START_CLIENT_FEATURES_V3, which omits LAYOUT_AWARE_PASSTHROUGH_FEATURE — the flag playbackClientFeaturesV3(context) adds conditionally and which gates the server's validated-passthrough grant. All four production construction sites override the default (PlaybackSessionManager.kt:337/848/1768/2036), so today it's dead code that exists only as a trap: a future call site leaning on it compiles cleanly and silently downgrades passthrough-capable devices. Make clientFeatures required, or route construction through one factory.


Below the cap — five additional confirmed cleanup findings were cut by the 10-finding limit: the ~30-line Terminal-replan teardown block duplicated between PlayerViewModel and TvPlayerViewModel; the "sidecar"/"burn_in_only" wire strings independently declared in 5 files (with the serverDelivery → SubtitleIdentity mapping pasted identically into mobile and TV); the replan operation parameter being derivable from classification at every call site (silent failure_recovery mislabel if forgotten); the TV subtitle stack round-tripping the contract-opaque outputContextId through toLongOrNull(); and capability detection running twice per start (detect() + detectPlaybackContext(), which calls detect() internally) at every v3 start site.

Refuted during verification (not findings): the CONVERT-subtitle artifact-row drop (unreachable — Android always sends fidelity=preserve, and reachable CONVERT inventory URLs are already .vtt), a falsy-zero playerStartSeconds seek loss (server always populates the timeline; 0.0 is a correct answer for anchored streams), and a swallowed terminal error on null sessionId (all snapshot callers guard it; the unguarded redrive can't receive Terminal).

🤖 Generated with Claude Code

@Quick104

Quick104 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Resolved the Quick104 playback-v3 review at Android head 8566d96.

  • Compatibility: neutral v3 now requires the server-advertised neutral_playback_v3_contract_v1 token before using server-minted opaque attempt keys or intent replans. The client remains echo-only and does not restore hashing.
  • Session correctness: audiobook playback now uses an isolated manager/lifecycle, rejects stale single-file starts, and sends client-owned progress persistence with an explicit file-local start.
  • Cast/subtitles: Cast uses player-local time, falls back to transitional sidecars only when authoritative inventory is empty, and unknown subtitle delivery values no longer terminate unrelated playback.
  • Contract hygiene: client_features is required, replan operations are derived internally, terminal teardown ordering is shared, output_context_id remains opaque, capability snapshots are reused, and subtitle delivery constants are centralized.
  • Legacy attached-picture normalization is handled by the coordinated server change rather than falsifying Android codec capability.

Vendored all nine server fixtures byte-for-byte and pinned SOURCE to server 30ddc0d09255816e44cd9874ff79a785d8a6fd8e. Typed conformance now covers the neutral feature token, progress_persistence, and draft-v3 HTTP 426 upgrade vector.

Validation: focused protocol/conformance/session/Cast/audiobook tests passed; full ./gradlew test passed; shared/android-shared/phone/TV debug lint plus phone/TV vital release lint passed; fixture parity and git diff --check passed. The first full test pass hit an unrelated coroutine Main-dispatcher ordering race in two unchanged ServerSetupPersistenceTest release cases; the exact class passed in isolation and the complete full suite then passed.

PR remains draft.

@Quick104
Quick104 marked this pull request as ready for review August 9, 2026 05:53

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt (1)

795-809: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Convert StartParams.subtitleTrackIndex before renewal

StartParams.subtitleTrackIndex stores a server track index, but loadContent expects a catalog ordinal. A renewal can select the wrong subtitle or fall back to persisted/automatic selection. Convert the index before calling loadContent, while preserving -1 as Off.

🤖 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
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`
around lines 795 - 809, Update the renewal handling in
sessionLifecycle.missingSessionEvents before calling loadContent: convert
renewal.startParams.subtitleTrackIndex from the server track index to the
catalog ordinal expected by loadContent, preserving -1 as the Off value. Pass
the converted value through initialSubtitleTrackIndex while leaving the other
renewal parameters unchanged.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt (1)

185-198: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The audio codec list has a fabricated fallback path while the evidence tier stays exact.

detectPlatformSoftwareAudioCodecs() returns a hard-coded listOf("aac", "mp3") when the MediaCodecList construction fails (Line 430). That value is assumed, not probed. The comment on Lines 185-190 states the tier must drop when a path fabricates part of the list, and only exact evidence earns audio passthrough. Consider tracking whether the probe succeeded and reporting audioEvidence accordingly.

♻️ Sketch of the fix
-        val softwareAudio = advertisedAudioDecodeCodecs(
-            platformCodecs = detectPlatformSoftwareAudioCodecs(),
+        val platformCodecs = detectPlatformSoftwareAudioCodecs()
+        val softwareAudio = advertisedAudioDecodeCodecs(
+            platformCodecs = platformCodecs,
             ffmpegAvailable = ffmpegAvailable,
             isTv = TvModeDetector.isTv(context),
         )

Then derive audioEvidence from a probe-success flag set inside detectPlatformSoftwareAudioCodecs() instead of the constant.

🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt`
around lines 185 - 198, Track whether detectPlatformSoftwareAudioCodecs()
successfully constructs and probes MediaCodecList, including exposing that
success flag to its caller. In the capability construction around codecProbe and
softwareAudio, derive audioEvidence from this flag, using exact evidence only
for successful probes and the appropriate lower tier for the hard-coded
fallback; leave videoEvidence and codec lists unchanged.
shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt (1)

545-562: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This re-anchor fixture does not match what production sends.

PlaybackSessionManager.reanchorActiveVideoSession builds its re-anchor request without a failure block. This test supplies failure = PlaybackFailureV3(SEEK_REANCHOR_V3_OPERATION). The serialization the test covers is therefore not the serialization production emits. I raise the root concern on the manager.

🤖 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
`@shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt`
around lines 545 - 562, Update the reanchor fixture in PlaybackProtocolV3Test so
PlaybackReplanRequestV3 matches the request built by
PlaybackSessionManager.reanchorActiveVideoSession: remove the failure field from
this test request. Keep the remaining re-anchor serialization fields unchanged.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionManager.kt (1)

894-918: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Skip a blank committed key so it does not enter attemptedPlanKeys.

This block records the server cursor before validateForMedia3() runs. PlaybackPlanV3.planAttemptKey defaults to "", and validation is what rejects a blank key. So a malformed response writes committedKey = "" into ServerPlanCursor.attemptedPlanKeys, and the next replan sends an empty string inside attempted_plan_keys.

The loop detector is unaffected, because nextKey always comes from a validated plan. The effect is a malformed field on the wire that the server folds into its exclusion set.

🐛 Proposed fix
-            result.data.playbackPlan?.let { committedPlan ->
+            result.data.playbackPlan
+                ?.takeIf { it.planAttemptKey.isNotBlank() }
+                ?.let { committedPlan ->
                 val committedKey = committedPlan.planAttemptKey
🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionManager.kt`
around lines 894 - 918, When updating the cursor in the committed-plan block,
only add committedKey to attemptedPlanKeys when it is non-blank; otherwise
preserve attemptedKeys unchanged. Keep the existing cursor update and
compareAndSet behavior intact, while ensuring blank
PlaybackPlanV3.planAttemptKey values never reach the serialized exclusion set.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt (1)

946-955: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve clientPlaybackContext during renewal. Production adoption paths can store StartParams with a null context, and both renewal paths rebuild the context instead of reusing the negotiated one. Make the context non-null and pass it through recovery to preserve the exact route and capabilities promised by the KDoc.

🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt`
around lines 946 - 955, Update StartParams.clientPlaybackContext to be non-null,
requiring callers to provide the negotiated context. In both renewal/recovery
paths that reconstruct StartParams, reuse and propagate the original
clientPlaybackContext rather than rebuilding it or allowing null, preserving the
exact route and capabilities documented for playback.
🧹 Nitpick comments (9)
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt (1)

1475-1499: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use one Dolby Vision policy source for subtitle transactions.

subtitlePlaybackContext uses state-flow values, but other playback paths use playerSettingsStore.dolbyVisionPolicySnapshot(). Their eager defaults differ: dvProfile7Hdr10Fallback starts as false, while the store flow defaults to true. Share one resolved policy snapshot so capability data remains consistent during startup and replanning.

🤖 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
`@androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt`
around lines 1475 - 1499, Update subtitlePlaybackContext to obtain a single
resolved Dolby Vision policy snapshot from
playerSettingsStore.dolbyVisionPolicySnapshot(), rather than constructing
DolbyVisionPolicy.Snapshot from the separate state-flow values. Pass that shared
snapshot to both capabilityDetector.detect and
capabilityDetector.detectPlaybackContext, preserving consistent startup and
replanning behavior.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractor.kt (1)

72-83: 🚀 Performance & Scalability | 🔵 Trivial

Consider indexing the SUP stream.

IndexSeekMap uses (positions, timesUs, durationUs), and C.TIME_UNSET is supported by MergingMediaSource. With one entry, every seek resolves to byte zero. Each nonzero seek or reprepare therefore rescans the full SUP stream. Add seek points if this cost is unacceptable.

🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractor.kt`
around lines 72 - 83, Update the seek map used by PgsSupExtractor to index the
SUP stream with additional byte-position/time entries, rather than relying
solely on the single zero-offset entry. Populate the IndexSeekMap from suitable
parsed PGS timestamps and stream positions while preserving C.TIME_UNSET
duration support and correct seek behavior.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt (1)

94-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the URL guard.

orEmpty().takeIf { ... }.orEmpty() performs two null coalescings for one condition. A single conditional expression is clearer and keeps the same result.

♻️ Proposed refactor
-                url = item.url.orEmpty().takeIf {
-                    subtitle.mode != PlaybackSubtitleModeV3.BURN_IN &&
-                        item.delivery == SUBTITLE_DELIVERY_SIDECAR
-                }.orEmpty(),
+                url = if (
+                    subtitle.mode != PlaybackSubtitleModeV3.BURN_IN &&
+                    item.delivery == SUBTITLE_DELIVERY_SIDECAR
+                ) item.url.orEmpty() else "",
🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt`
around lines 94 - 97, Update the url assignment in the PlaybackV3Session
subtitle mapping to replace the chained item.url.orEmpty().takeIf(...).orEmpty()
with a single conditional expression using the existing subtitle.mode and
item.delivery conditions, preserving an empty string when the guard fails.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt (1)

76-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Lower the log level for this per-selection diagnostic.

calculateTargetBufferBytes runs on every track selection change. Log.i keeps buffer internals in release logcat. Log.d matches the diagnostic intent.

🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt`
around lines 76 - 80, In calculateTargetBufferBytes, lower the per-selection
diagnostic from Log.i to Log.d while preserving the existing TAG and message
contents.
shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt (2)

398-404: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the neutral server feature set in this decode fixture.

The embedded JSON advertises only PLAYBACK_PLAN_V3_FEATURE. The test does not call validateForMedia3(), so it passes. But the fixture now depicts a response that validateForMedia3 classifies as Incompatible. Include NEUTRAL_PLAYBACK_V3_CONTRACT_FEATURE so the fixture stays a valid example.

♻️ Proposed fixture update
-            """{"protocol_version":3,"server_features":["$PLAYBACK_PLAN_V3_FEATURE"],"outcome":"playable",""" +
+            """{"protocol_version":3,"server_features":["$PLAYBACK_PLAN_V3_FEATURE",""" +
+                """"$NEUTRAL_PLAYBACK_V3_CONTRACT_FEATURE"],"outcome":"playable",""" +
🤖 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
`@shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt`
around lines 398 - 404, Update the embedded JSON fixture in the playback-plan
decode test to advertise both PLAYBACK_PLAN_V3_FEATURE and
NEUTRAL_PLAYBACK_V3_CONTRACT_FEATURE. Keep the existing decoding and
planAttemptKey assertion unchanged so the fixture remains a valid
Media3-compatible response.

225-250: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a case for a selected subtitle with a null index.

The ?: 0 at Line 238 shows PlaybackTrackIdentityV3.index is nullable. No test covers a plan where the server selects a subtitle by trackId only. With the current hasValidSubtitleInventory, that plan returns a non-retryable Terminal. I raise the production-side concern on PlaybackProtocolV3.kt Lines 509-513.

💚 Proposed test
`@Test`
fun subtitleSelectedByTrackIdAloneStaysPlayable() {
    val result = PlaybackDecisionResponseV3(
        protocolVersion = PLAYBACK_PROTOCOL_V3,
        serverFeatures = neutralServerFeatures,
        outcome = PlaybackDecisionOutcome.PLAYABLE,
        playbackPlan = plan.copy(
            selectedTracks = SelectedPlaybackTracksV3(
                subtitle = PlaybackTrackIdentityV3("file:42:subtitle:0", null),
            ),
            subtitle = PlaybackSubtitleDecisionV3(
                inventory = listOf(
                    PlaybackSubtitleInventoryItemV3(
                        trackId = "file:42:subtitle:0",
                        combinedIndex = 0,
                        source = "external",
                        delivery = SUBTITLE_DELIVERY_SIDECAR,
                        url = "/stream/session-1/subtitles/0.vtt",
                    ),
                ),
            ),
        ),
    ).validateForMedia3()

    assertIs<PlaybackV3Validation.Playable>(result)
}
🤖 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
`@shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt`
around lines 225 - 250, Add a test alongside
unknownSelectedSubtitleDeliveryRequestsASelectionPreservingReplan covering a
selected subtitle identified by trackId with a null index; provide matching
subtitle inventory and valid sidecar delivery data, then assert
validateForMedia3() returns PlaybackV3Validation.Playable rather than Terminal.
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt (1)

552-565: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The KDoc overstates the ordering guarantee.

The KDoc states the recheck happens "only after the lifecycle reporter is fully stopped". stop() calls reporterJob?.cancel() at Line 496 and does not join it. Job.cancel() is asynchronous, so the reporter coroutine can still be suspended inside sessionManager.reportProgress when stop() returns.

The behavior is still correct, because stop() sets lastAdoptedSessionId = null under the mutex and ownsProgressReply then rejects the late reply. Correct the KDoc to name that mechanism. A reader who trusts the current wording may remove the ownership guard.

♻️ Proposed KDoc correction
     /**
-     * Retires a terminal playback attempt and rechecks screen ownership only
-     * after the lifecycle reporter is fully stopped. Phone and TV must share
-     * this ordering: publishing the terminal first lets the next progress tick
-     * observe the retired server session as a 404 and start a fresh attempt.
+     * Retires a terminal playback attempt, then rechecks screen ownership.
+     *
+     * [stop] cancels the reporter without joining it, so a reporter call may
+     * still be in flight here. [stop] clears `lastAdoptedSessionId` under the
+     * mutex first, so `ownsProgressReply` discards any late reply. Phone and TV
+     * must share this ordering: publishing the terminal first lets the next
+     * progress tick observe the retired server session as a 404 and start a
+     * fresh attempt.
      */
🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt`
around lines 552 - 565, Update the KDoc for stopTerminalSessionIfCurrent to
remove the claim that the lifecycle reporter is fully stopped before rechecking
ownership. Document that stop() cancels the reporter and clears
lastAdoptedSessionId under the mutex, while ownsProgressReply rejects any late
progress reply, preserving the terminal-first ordering without implying
cancellation is joined.
shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt (1)

433-437: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document which operations may omit failure, or validate it.

The KDoc states failure is absent only for INTENT_V3_OPERATIONS. SEEK_REANCHOR_V3_OPERATION is not in that set, yet PlaybackSessionManager.reanchorActiveVideoSession builds a request with operation = SEEK_REANCHOR_V3_OPERATION and no failure. Either widen this KDoc to cover seek re-anchor, or make the re-anchor path send a failure block. I raise the behavioral half of this on the manager.

🤖 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
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt`
around lines 433 - 437, Update the KDoc for PlaybackProtocolV3.failure to
explicitly include SEEK_REANCHOR_V3_OPERATION among operations that may omit
failure, matching the request built by
PlaybackSessionManager.reanchorActiveVideoSession; leave failure nullable and
preserve the existing intent-operation documentation.
shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt (1)

77-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use opaque values in output-context tests.

"7" and "9" also fit the removed numeric route-generation representation. Use nonnumeric output context IDs. Assert the nested start-request value.

  • shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt#L77-L90: use a value such as "tv:hdmi:primary" and assert client_playback_context.output.output_context_id.
  • shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt#L132-L145: use and assert a nonnumeric route-event output context ID.

As per coding guidelines, add focused tests for shared logic only when behavior is critical or high risk.

🤖 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
`@shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt`
around lines 77 - 90, Update
shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt:77-90
to use a nonnumeric output context ID such as "tv:hdmi:primary" and assert it at
client_playback_context.output.output_context_id in the nested start request;
also update
shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt:132-145
to use and assert a nonnumeric route-event output context ID, preserving the
existing request assertions.

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.

Inline comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt`:
- Around line 110-123: Update the session cleanup around buildCastMediaSpec in
CastPlaybackPreparer so castSession.stopSession(sessionId) runs for every
failure, including non-cancellation exceptions, while preserving propagation of
the original exception and cancellation behavior. Use a catch covering Throwable
or an equivalent success-flag finally block, and retain NonCancellable cleanup.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt`:
- Around line 387-401: Update androidPlatformDetails to enforce the documented
128-character value limit by declaring MAX_PLATFORM_DETAIL_CHARS = 128 and using
the existing putBounded helper for Build.ID, Build.DISPLAY, and the joined
SUPPORTED_ABIS value. Preserve the current keys and conditional inclusion
behavior while truncating oversized values before insertion.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt`:
- Around line 730-753: Update handleSessionMissing and the recoveryJob
declaration to make recovery-job ownership thread-safe: ensure recoveryJob is
cross-thread visible, and when the launched recovery coroutine completes, clear
the field only if it still references that same job. Keep recoveryJob assignment
and cleanup synchronized consistently with cancelRecoveryJobs,
adoptActiveSessionIfCurrent, and stop so an older job cannot null or hide a
newer one.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionManager.kt`:
- Around line 2342-2368: Update recordLocalMutation so the
passthroughSuppression.beginAttempt token changes only for audio-route
mutations, specifically the PCM mutation, and remains unchanged for
transport_reopen. Preserve mutation recording and duplicate checks, while
ensuring transport reopening cannot clear blocked layouts or reset the
single-retry state.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt`:
- Around line 48-64: Update the Dolby Vision detection in the sizingTracks
mapping used by SiloLoadControl to reuse the existing codec-string detection
logic alongside the sampleMimeType check, recognizing HEVC formats with dvhe.*,
dvh1.*, dva1.*, or dvav.* codecs. Add regression coverage for these codec-based
Dolby Vision formats while preserving the existing MIME-based detection.

In
`@android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerStagedReplanTest.kt`:
- Line 149: Rename the affected Kotlin test functions to camelCase, preserving
their test behavior: update the three tests in
android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerStagedReplanTest.kt
at lines 149-149, 821-821, and 1319-1319, plus the three tests in
android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/SiloLoadControlTest.kt
at lines 10-10, 22-22, and 34-34.

In `@docs/superpowers/plans/2026-08-02-instant-external-srt-switching-android.md`:
- Line 8: Remove the blank line within the block quote in the document’s
introductory quoted section, keeping consecutive quoted lines contiguous or
closing the quote before the blank line so markdownlint MD028 passes.

In
`@shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3ConformanceTest.kt`:
- Around line 499-512: Vendor the generated playback/v3/conformance_matrix.json
fixture required by conformanceMatrix(), and add its path to
shared/src/commonTest/resources/playback/v3/SOURCE so the resource is included
in the fixture corpus. Preserve the existing
conformanceMatrixDecodesAndRoundTripsEveryGeneratedScenario expectations.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt`:
- Around line 509-513: Update the selected-subtitle lookup in
PlaybackProtocolV3.kt lines 509-513 to match the stable trackId and only compare
combinedIndex when selected.index is non-null; apply the same nullable-index
relaxation to the selected-membership check in hasValidSubtitleInventory. Add a
PlaybackProtocolV3Test.kt lines 225-250 case selecting a subtitle by trackId
with a null index and assert validation returns Playable.

---

Outside diff comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt`:
- Around line 185-198: Track whether detectPlatformSoftwareAudioCodecs()
successfully constructs and probes MediaCodecList, including exposing that
success flag to its caller. In the capability construction around codecProbe and
softwareAudio, derive audioEvidence from this flag, using exact evidence only
for successful probes and the appropriate lower tier for the hard-coded
fallback; leave videoEvidence and codec lists unchanged.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt`:
- Around line 946-955: Update StartParams.clientPlaybackContext to be non-null,
requiring callers to provide the negotiated context. In both renewal/recovery
paths that reconstruct StartParams, reuse and propagate the original
clientPlaybackContext rather than rebuilding it or allowing null, preserving the
exact route and capabilities documented for playback.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionManager.kt`:
- Around line 894-918: When updating the cursor in the committed-plan block,
only add committedKey to attemptedPlanKeys when it is non-blank; otherwise
preserve attemptedKeys unchanged. Keep the existing cursor update and
compareAndSet behavior intact, while ensuring blank
PlaybackPlanV3.planAttemptKey values never reach the serialized exclusion set.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`:
- Around line 795-809: Update the renewal handling in
sessionLifecycle.missingSessionEvents before calling loadContent: convert
renewal.startParams.subtitleTrackIndex from the server track index to the
catalog ordinal expected by loadContent, preserving -1 as the Off value. Pass
the converted value through initialSubtitleTrackIndex while leaving the other
renewal parameters unchanged.

In
`@shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt`:
- Around line 545-562: Update the reanchor fixture in PlaybackProtocolV3Test so
PlaybackReplanRequestV3 matches the request built by
PlaybackSessionManager.reanchorActiveVideoSession: remove the failure field from
this test request. Keep the remaining re-anchor serialization fields unchanged.

---

Nitpick comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt`:
- Around line 552-565: Update the KDoc for stopTerminalSessionIfCurrent to
remove the claim that the lifecycle reporter is fully stopped before rechecking
ownership. Document that stop() cancels the reporter and clears
lastAdoptedSessionId under the mutex, while ownsProgressReply rejects any late
progress reply, preserving the terminal-first ordering without implying
cancellation is joined.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt`:
- Around line 94-97: Update the url assignment in the PlaybackV3Session subtitle
mapping to replace the chained item.url.orEmpty().takeIf(...).orEmpty() with a
single conditional expression using the existing subtitle.mode and item.delivery
conditions, preserving an empty string when the guard fails.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt`:
- Around line 76-80: In calculateTargetBufferBytes, lower the per-selection
diagnostic from Log.i to Log.d while preserving the existing TAG and message
contents.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractor.kt`:
- Around line 72-83: Update the seek map used by PgsSupExtractor to index the
SUP stream with additional byte-position/time entries, rather than relying
solely on the single zero-offset entry. Populate the IndexSeekMap from suitable
parsed PGS timestamps and stream positions while preserving C.TIME_UNSET
duration support and correct seek behavior.

In
`@androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt`:
- Around line 1475-1499: Update subtitlePlaybackContext to obtain a single
resolved Dolby Vision policy snapshot from
playerSettingsStore.dolbyVisionPolicySnapshot(), rather than constructing
DolbyVisionPolicy.Snapshot from the separate state-flow values. Pass that shared
snapshot to both capabilityDetector.detect and
capabilityDetector.detectPlaybackContext, preserving consistent startup and
replanning behavior.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt`:
- Around line 433-437: Update the KDoc for PlaybackProtocolV3.failure to
explicitly include SEEK_REANCHOR_V3_OPERATION among operations that may omit
failure, matching the request built by
PlaybackSessionManager.reanchorActiveVideoSession; leave failure nullable and
preserve the existing intent-operation documentation.

In
`@shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt`:
- Around line 398-404: Update the embedded JSON fixture in the playback-plan
decode test to advertise both PLAYBACK_PLAN_V3_FEATURE and
NEUTRAL_PLAYBACK_V3_CONTRACT_FEATURE. Keep the existing decoding and
planAttemptKey assertion unchanged so the fixture remains a valid
Media3-compatible response.
- Around line 225-250: Add a test alongside
unknownSelectedSubtitleDeliveryRequestsASelectionPreservingReplan covering a
selected subtitle identified by trackId with a null index; provide matching
subtitle inventory and valid sidecar delivery data, then assert
validateForMedia3() returns PlaybackV3Validation.Playable rather than Terminal.

In
`@shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt`:
- Around line 77-90: Update
shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt:77-90
to use a nonnumeric output context ID such as "tv:hdmi:primary" and assert it at
client_playback_context.output.output_context_id in the nested start request;
also update
shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt:132-145
to use and assert a nonnumeric route-event output context ID, preserving the
existing request assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a2209e9-2567-4ac3-85cc-94723b651df9

📥 Commits

Reviewing files that changed from the base of the PR and between 1d17358 and 8566d96.

📒 Files selected for processing (71)
  • .agents/skills/test-shield-playback/references/playback-evidence.md
  • .agents/skills/test-shield-playback/scripts/shield-test
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/di/PlayerInfraModule.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/di/PlayerModule.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionManager.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SubtitleMountResolver.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/audio/PassthroughSuppressionRegistry.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractor.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/PlaybackContainerPolicy.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/AudiobookPlayerTeardownSourceTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackColorRangeFallbackTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackPublicationSettlementIntegrationTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycleLoggingTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycleTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerSeekReanchorTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerStagedReplanTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerTranscodeFallbackTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/SiloLoadControlTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/StyledSubtitleBurnInTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparerTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractorTest.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleAutoSelection.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileVideoPlaybackStarter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModelLoadOwnershipIntegrationTest.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/di/AndroidTvModule.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/navigation/TvAppNavigation.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/navigation/TvAudiobookRouting.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerSubtitlePolicy.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleIdentity.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleTransactionAdapter.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvVideoPlaybackStarter.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/diagnostics/TvDiagnosticsPromptScreen.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/navigation/TvAudiobookRoutingTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/SubtitleTransactionIntegrationTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleSettlementOwnershipTest.kt
  • docs/playback/01-media3-only-player-architecture.md
  • docs/playback/02-migration-compatibility-validation.md
  • docs/playback/04-implementation-status-and-dv-handoff.md
  • docs/playback/README.md
  • docs/superpowers/plans/2026-08-02-instant-external-srt-switching-android.md
  • shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3ConformanceTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/domain/ManagePlaybackUseCase.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/network/api/PlaybackApi.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/PlaybackRepository.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackModelsV2SerializationTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackSessionModelsTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt
  • shared/src/commonTest/resources/playback/v3/SOURCE
  • shared/src/commonTest/resources/playback/v3/attempt_keys.json
  • shared/src/commonTest/resources/playback/v3/capability_response.json
  • shared/src/commonTest/resources/playback/v3/conformance_matrix.json
  • shared/src/commonTest/resources/playback/v3/decision_response.json
  • shared/src/commonTest/resources/playback/v3/error_response.json
  • shared/src/commonTest/resources/playback/v3/replan_request.json
  • shared/src/commonTest/resources/playback/v3/route_event.json
  • shared/src/commonTest/resources/playback/v3/start_request.json
  • shared/src/commonTest/resources/playback/v3/subtitle_inventory.json
💤 Files with no reviewable changes (6)
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerTranscodeFallbackTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackModelsV2SerializationTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackColorRangeFallbackTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/StyledSubtitleBurnInTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/PlaybackRepository.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/network/api/PlaybackApi.kt

Comment thread docs/superpowers/plans/2026-08-02-instant-external-srt-switching-android.md Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dbf13b65f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .agents/skills/test-shield-playback/scripts/shield-test Outdated

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt (1)

797-810: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep renewal subtitle indexes separate from route subtitle indexes.

Lines 801-807 pass StartParams.subtitleTrackIndex into initialSubtitleTrackIndex. applyCoordinatorStateToUi interprets initialSubtitleTrackIndex as a catalog subtitle ordinal at lines 1148-1164. A V3 renewal index uses the combined external-then-embedded index space. An embedded subtitle can therefore resolve to a different track or no track after session renewal.

Carry renewal StartParams separately, or bypass catalog-ordinal resolution for renewal loads and use the server-selected subtitle identity.

🤖 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
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`
around lines 797 - 810, Update the missing-session renewal flow in
sessionLifecycle.missingSessionEvents so renewal subtitle selection does not
pass StartParams.subtitleTrackIndex as initialSubtitleTrackIndex. Preserve the
V3 combined external-then-embedded index semantics by carrying the renewal
StartParams separately or using its server-selected subtitle identity, bypassing
applyCoordinatorStateToUi’s catalog-ordinal resolution while keeping other
renewal parameters unchanged.
🤖 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.

Inline comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/ReplayableSubtitleDataSource.kt`:
- Around line 70-110: Refactor ReplayableSubtitleDataSource.open so
synchronized(cache) only covers the cache lookup and final cache publication.
Perform upstream.open, readAllFromUpstream, and related close/state handling
outside the cache monitor, then reacquire the lock to publish the newly created
ReplayableSubtitleEntry and open the replay, preserving cache-hit and
non-whole-resource behavior.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/PlaybackStartupStallDetector.kt`:
- Around line 142-176: Update the client-transform deadline logic in
PlaybackStartupStallDetector so clientTransformProgressAtMs is refreshed only by
decoder output progress, not currentPositionMs advances that can be driven by
audio. Preserve seek/timeline reset handling as needed, but remove the
startedProgressMs position-based refresh for this local-transform clock, and add
a regression test covering repeated position advances after firstFrameRendered
that still produces DV7_TRANSFORM_STALL_CLASSIFICATION.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt`:
- Around line 98-102: Update subtitleCodecFromUrl to isolate the final path
segment after removing the query and fragment, then extract the extension only
from that segment. Preserve the existing blank-extension-to-null behavior so
canonicalSubtitleCodecFamily receives no codec when the path has no extension.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt`:
- Around line 385-397: Update the Media3 listener DisposableEffect around its
existing keys to include videoBackend, ensuring the effect is disposed and
recreated whenever the selected backend changes. Keep the existing listener
setup, cleanup, and onTracksChanged behavior unchanged.

---

Outside diff comments:
In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`:
- Around line 797-810: Update the missing-session renewal flow in
sessionLifecycle.missingSessionEvents so renewal subtitle selection does not
pass StartParams.subtitleTrackIndex as initialSubtitleTrackIndex. Preserve the
V3 combined external-then-embedded index semantics by carrying the renewal
StartParams separately or using its server-selected subtitle identity, bypassing
applyCoordinatorStateToUi’s catalog-ordinal resolution while keeping other
renewal parameters unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ddba68e-5fc8-44a2-90c9-97410808fd53

📥 Commits

Reviewing files that changed from the base of the PR and between 8566d96 and 4dbf13b.

📒 Files selected for processing (36)
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/Playability.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/ReplayableSubtitleDataSource.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloPlayerFactory.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/backend/Media3VideoPlaybackBackend.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/seek/PlaybackTimelineSeekPolicy.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/subtitle/StreamingWebvttExtractor.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/PlaybackStartupStallDetector.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/VideoTrackSelectionCoordinator.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetectorDolbyVisionTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycleTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/ReplayableSubtitleDataSourceTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/SiloLoadControlTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/VideoPlayerSubtitleMountTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/backend/Media3VideoPlaybackBackendLifecycleTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/seek/PlaybackTimelineSeekPolicyTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/subtitle/StreamingWebvttExtractorTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/video/PlaybackStartupStallDetectorTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/video/VideoTrackSelectionCoordinatorTest.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileVideoPlaybackStarter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapterTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerBackendLifecycleSourceTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModelLoadOwnershipIntegrationTest.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleTransactionAdapter.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerBackendLifecycleSourceTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleTransactionAdapterTest.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleTransactionAdapter.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt

# Conflicts:
#	androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt
#	androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/diagnostics/TvDiagnosticsPromptScreen.kt
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Quick104

Copy link
Copy Markdown
Contributor Author

Review follow-up is complete at 06a8f1c543b4590a2776694c9e767473b005117d.

  • Addressed and resolved all 16 outstanding inline review threads.
  • Closed the additional Neutral V3 lifecycle, timeline, capability snapshot, effective-edition, source-duration, quality-menu, Cast intent, and authoritative-subtitle gaps found during the follow-up audit.
  • Merged current main (908466c108c00104ba76e965cc294aae30b4c4de) and re-reviewed the resolved tree; independent Standards and Spec passes both report zero findings.
  • Verified all nine Android fixtures are byte-identical to server PR #567 head 79e3e761ad391b1aa9f2c280eeceeb23df9d3c81.
  • Passed ./gradlew test lint :androidApp:assembleDebug :androidTvApp:assembleDebug (371 tasks), focused regression suites, git diff --check, and Shield helper syntax/control-plane checks.

The configured Shield was unavailable for the final physical playback rerun; prior PGS and DV7-to-DV8.1/TrueHD device coverage remains documented in the PR description.

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudioCapabilityManager.kt (1)

73-114: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Serialize the snapshot update; the compound write is not atomic.

publishCapabilities and bumpOutputRouteGeneration each perform a multi-step update: read _capabilities, increment generationCounter, write playbackRouteSnapshot, then write _capabilities and _outputRouteGeneration. The steps are not guarded as one unit.

Three distinct callbacks reach these methods on different threads:

  • AudioCapabilitiesReceiver.Listener (line 133).
  • The SpatializerBridge callback (line 154), delivered on a platform executor.
  • DisplayManager.DisplayListener (lines 126-128), delivered on the main looper.

Two concurrent callers can interleave. The later-incremented generation can be written to playbackRouteSnapshot first, and the earlier one can overwrite it. _outputRouteGeneration and playbackRouteSnapshot.routeGeneration can then disagree, and playbackRouteSnapshot.capabilities can disagree with _capabilities. That contradicts the documented guarantee of "one atomically published planning view".

routeSnapshotInitialized is also a plain field. A second thread is not guaranteed to observe the write, so the initialization guard on line 83 can fail.

🔒️ Proposed fix using a single lock
+    private val routeLock = Any()
     `@Volatile`
     private var playbackRouteSnapshot = AudioPlaybackRouteSnapshot(
         sinkType = "unknown",
         routeGeneration = 0L,
         capabilities = AudioPassthroughCapabilities(),
     )
-    private var routeSnapshotInitialized = false
+    private var routeSnapshotInitialized = false
 
-    private fun publishCapabilities(next: AudioPassthroughCapabilities) {
+    private fun publishCapabilities(next: AudioPassthroughCapabilities) = synchronized(routeLock) {
         val changed = _capabilities.value != next
         if (!changed && routeSnapshotInitialized) return
-    private fun bumpOutputRouteGeneration() {
+    private fun bumpOutputRouteGeneration() = synchronized(routeLock) {
         val generation = generationCounter.incrementAndGet()

Note that synchronized changes the return type of the expression-bodied form; keep the block bodies and add synchronized(routeLock) { ... } inside instead if you prefer Unit returns.

🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/AudioCapabilityManager.kt`
around lines 73 - 114, Serialize the compound snapshot updates in
publishCapabilities and bumpOutputRouteGeneration using a shared routeLock,
wrapping each method body in synchronized(routeLock) while preserving
block-bodied Unit returns. Guard all reads and writes of playbackRouteSnapshot,
_capabilities, _outputRouteGeneration, generationCounter, and
routeSnapshotInitialized within the lock so each planning view is published
consistently and initialization is safely visible across callback threads.
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileFreshSubtitleRestore.kt (1)

25-51: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve local downloaded subtitles during authoritative restore.

When authoritativeInventory is true, prepareMobileFreshSubtitleRestore returns only mountedSubtitles. PlayerViewModel publishes that list directly, so local downloaded subtitles disappear during fresh playback and persisted SubtitleIdentity.Downloaded selections cannot resolve. Merge local downloads before publishing the fresh state.

🤖 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
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileFreshSubtitleRestore.kt`
around lines 25 - 51, Update prepareMobileFreshSubtitleRestore so
authoritativeInventory still loads and merges local downloaded subtitles before
producing subtitleTracks; do not discard downloaded when the flag is true.
Preserve mountedSubtitles as the authoritative base while ensuring persisted
SubtitleIdentity.Downloaded selections can resolve in the published fresh state.
🧹 Nitpick comments (7)
android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt (1)

419-445: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Do not cache the fallback probe.

detectPlatformSoftwareAudioCodecs caches the result of a failed enumeration. The cache is process-wide and never invalidated. If MediaCodecList throws once, every later detect() call reuses exact = false and the AAC/MP3 fallback list for the whole process lifetime. The device then reports CAPABILITY_EVIDENCE_PLATFORM_ATTESTED audio permanently, and the server never grants passthrough on that run. Cache only the exact probe so a later call can retry.

♻️ Proposed change
-        cachedPlatformSoftwareAudioProbe = probe
+        if (probe.exact) {
+            cachedPlatformSoftwareAudioProbe = probe
+        }
         return probe
🤖 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/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt`
around lines 419 - 445, Update detectPlatformSoftwareAudioCodecs so only
successfully enumerated probes with exact = true are assigned to
cachedPlatformSoftwareAudioProbe; return the AAC/MP3 non-exact fallback without
caching when MediaCodecList enumeration fails, allowing subsequent calls to
retry.
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt (1)

172-175: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The same onPlayerError override was added to both audiobook screens with a fully qualified PlaybackException. Both files already import the other Media3 types they use, including Player, so the inline fully qualified name is the one inconsistency shared by the two copies. The error handling itself is correct in both places.

  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt#L172-L175: add import androidx.media3.common.PlaybackException and change the parameter type to PlaybackException.
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/audiobook/TvAudiobookPlayerScreen.kt#L203-L206: apply the identical import and parameter-type change.
🤖 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
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt`
around lines 172 - 175, The onPlayerError overrides use an inconsistent fully
qualified PlaybackException type. In
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt:172-175
and
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/audiobook/TvAudiobookPlayerScreen.kt:203-206,
import androidx.media3.common.PlaybackException and use the unqualified type in
each onPlayerError parameter, preserving the existing error handling.
shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReadyTest.kt (1)

50-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the delivery-validation branches.

The tests cover a valid sidecar row and an index gap. They do not cover the delivery rules in applyAuthoritativeSubtitleReadyTrack: sidecar with a blank URL must return null, and burn_in_only with a non-blank URL must return null. Both branches decide whether a malformed row enters playback state.

♻️ Proposed test
+    `@Test`
+    fun mismatchedDeliveryAndUrlIsRejected() {
+        val sidecarWithoutUrl = decodePlaybackSubtitleReady(
+            buildJsonObject {
+                putJsonObject("track") {
+                    put("track_id", "file:9:subtitle:0")
+                    put("combined_index", 0)
+                    put("source", "embedded")
+                    put("delivery", "sidecar")
+                }
+            },
+        )
+        assertNull(applyAuthoritativeSubtitleReadyTrack(emptyList(), sidecarWithoutUrl))
+
+        val burnInWithUrl = decodePlaybackSubtitleReady(
+            buildJsonObject {
+                putJsonObject("track") {
+                    put("track_id", "file:9:subtitle:0")
+                    put("combined_index", 0)
+                    put("source", "embedded")
+                    put("delivery", "burn_in_only")
+                    put("url", "/stream/s/subtitles/0.vtt")
+                }
+            },
+        )
+        assertNull(applyAuthoritativeSubtitleReadyTrack(emptyList(), burnInWithUrl))
+    }
🤖 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
`@shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReadyTest.kt`
around lines 50 - 66, Add tests in PlaybackSubtitleReadyTest covering both
delivery-validation branches in applyAuthoritativeSubtitleReadyTrack: a sidecar
track with a blank URL and a burn_in_only track with a non-blank URL must each
return null, while preserving the existing valid sidecar and index-gap coverage.

Source: Coding guidelines

androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt (1)

284-285: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

authoritativeSubtitleReadyRows grows without bound across sessions. Both players cache subtitle-ready rows in a map keyed by (sessionId, subtitleId) and never remove entries. Each replan, seek recovery, 404 renewal, and auto-advance episode mints a new session id, so entries for retired sessions accumulate for the ViewModel's whole life while only the current session id is ever read.

  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt#L284-L285: drop entries whose session id no longer matches the active session. Clear the map in resetPlaybackRecoveryState() or at the start of loadContent, alongside pendingAuthoritativeSubtitleDownloadId.
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt#L1336-L1337: apply the same removal in resetSeekRecoveryForContentChange() or prepareSessionExit(), so retired-session rows do not survive a content change.
🤖 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
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`
around lines 284 - 285, Clear authoritativeSubtitleReadyRows whenever playback
recovery or content changes reset the active session, alongside
pendingAuthoritativeSubtitleDownloadId. In
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt:284-285,
update resetPlaybackRecoveryState() or loadContent; in
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt:1336-1337,
update resetSeekRecoveryForContentChange() or prepareSessionExit() so
retired-session rows are removed.
android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/AudiobookPlayerTeardownSourceTest.kt (2)

26-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an ordering assertion; the test name claims ordering but the body checks only presence.

The test is named "reports both timelines through the retained lifecycle without blocking". Lines 29-43 assert only that nine strings appear somewhere in onClearedSource. They do not prove that reportPosition runs before stopAsync, which is the behavior that protects the persisted position.

The sibling test on lines 50-51 already uses the correct indexOf comparison. Apply the same pattern here.

💚 Proposed ordering assertion
         assertTrue(onClearedSource.contains("playbackSessionLifecycle.stopAsync("))
+        assertTrue(
+            "reportPosition must run before stopAsync",
+            onClearedSource.indexOf("playbackSessionLifecycle.reportPosition(") <
+                onClearedSource.indexOf("playbackSessionLifecycle.stopAsync("),
+        )
     }
🤖 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/AudiobookPlayerTeardownSourceTest.kt`
around lines 26 - 44, Add an indexOf-based ordering assertion to the test
`onCleared reports both timelines through the retained lifecycle without
blocking`, verifying that `playbackSessionLifecycle.reportPosition(` appears
before `playbackSessionLifecycle.stopAsync(` in `onClearedSource`, matching the
sibling test’s pattern.

18-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the source slices; a missing delimiter silently widens the slice.

substringAfter returns the whole receiver when the delimiter is absent. substringBefore returns the whole remainder when its delimiter is absent. Neither throws.

Three changes break these slices without failing any test:

  • A rename of startSingleFileSession, startPartSession, or retireActiveSession.
  • A modifier change, for example private suspend fun to private fun.
  • A reorder that no longer places startPartSession directly before retireActiveSession.

After any of them the slice widens to cover unrelated code, the substring assertions still find their text elsewhere in the file, and the test reports success while guarding nothing.

Assert that each delimiter exists before slicing.

💚 Proposed helper that fails on a missing delimiter
+    private fun slice(after: String, before: String): String {
+        require(viewModelSource.contains(after)) { "Missing anchor: $after" }
+        val tail = viewModelSource.substringAfter(after)
+        require(tail.contains(before)) { "Missing anchor: $before" }
+        return tail.substringBefore(before)
+    }
+
     private val singleFileStartSource = slice(
         after = "private suspend fun startSingleFileSession(",
         before = "private suspend fun startPartSession(",
     )
 
     private val partStartSource = slice(
         after = "private suspend fun startPartSession(",
         before = "private suspend fun retireActiveSession(",
     )

Apply the same helper to onClearedSource on lines 14-16.

🤖 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/AudiobookPlayerTeardownSourceTest.kt`
around lines 18 - 25, Make the source-slicing test fail when delimiters are
missing by introducing a helper that validates both boundary markers before
applying substringAfter and substringBefore. Use it for singleFileStartSource,
partStartSource, and onClearedSource, preserving the existing delimiter strings
and slice ranges.
android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparerTest.kt (1)

16-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen both assertions so they discriminate against a trivial implementation.

Line 18 asserts only the absence of the removed token. It does not prove that the neutral v3 contract token is advertised, which is the stated requirement of this migration.

Line 29 sets playerStartSeconds = 0.0, which is also the default of PlaybackTimelineV3. The assertion on line 33 passes even if castPlayerStartPosition returns a constant 0.0 or the timeline default. A non-zero player start separates the player-local value from both the source position and the default.

💚 Proposed test changes
     `@Test`
     fun castContextDoesNotAdvertiseThePreNeutralSidecarFeature() {
+        val features = playbackClientFeaturesV3(chromecastPlaybackContext("test"))
         assertFalse(
-            "external_text_sidecar_set_v1" in
-                playbackClientFeaturesV3(chromecastPlaybackContext("test")),
+            "external_text_sidecar_set_v1" in features,
         )
+        // Replace with the actual neutral v3 contract token constant.
+        assertTrue(NEUTRAL_V3_CONTRACT_FEATURE in features)
     }
 
     `@Test`
     fun castUsesPlayerLocalStartInsteadOfSourceTimelinePosition() {
         val plan = plan(
             timeline = PlaybackTimelineV3(
                 sourceStartSeconds = 90.0,
-                playerStartSeconds = 0.0,
+                playerStartSeconds = 12.5,
             ),
         )
 
-        assertEquals(0.0, castPlayerStartPosition(plan, requested = 90.0))
+        assertEquals(12.5, castPlayerStartPosition(plan, requested = 90.0))
     }
🤖 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/cast/CastPlaybackPreparerTest.kt`
around lines 16 - 34, Strengthen
castContextDoesNotAdvertiseThePreNeutralSidecarFeature by additionally asserting
that the neutral v3 contract token is present in
playbackClientFeaturesV3(chromecastPlaybackContext("test")). Update
castUsesPlayerLocalStartInsteadOfSourceTimelinePosition to use a non-zero
playerStartSeconds distinct from sourceStartSeconds and assert that value is
returned by castPlayerStartPosition, proving it does not use a constant or
default timeline value.
🤖 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.

Inline comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt`:
- Around line 244-249: Guard the stale-result cleanup in the collect block
around playback renewal by wrapping
playbackSessionManager.stopSession(start.session.sessionId) in runCatching,
matching the existing handling near the other stale-result cleanup. Also ensure
failures anywhere in this renewal observer’s collect body cannot cancel the
coroutine, while preserving the existing renewal and stale-result behavior.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt`:
- Around line 916-923: Update beginOutageRecovery to synchronize all outageJob
reads and writes using the existing recoveryJobLock, including the active-job
check and coroutine publication. Update cancelRecoveryJobs to cancel and clear
outageJob inside the same lock, preserving the existing recoveryJob
synchronization and preventing missed cancellation during teardown.

In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt`:
- Line 49: Update the selected subtitle handling around
resolvedSelectedSubtitleIndex() so an unresolved optional
selectedTracks.subtitle never defaults to inventory index 0. Preserve the
CONVERT/RENDER artifact using a distinct non-colliding selection representation,
or reject the incomplete plan before deduplication, ensuring the selected
subtitle remains mountable when inventory index 0 exists.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastSessionManager.kt`:
- Around line 678-683: Update the subtitle ID selection in the session state
construction so a non-null plannedSubtitles value is authoritative: return the
selected subtitle’s receiver track ID when one is selected, otherwise return
null, without consulting subtitleOptions or activeIds. Only use the
receiver-derived fallback when plannedSubtitles itself is null.
- Around line 136-159: Update the progress-reporting logic in progressListener
to track whether reportProgress is already in flight, and skip launching another
report while the existing coroutine is pending. Set the in-flight state before
launching and clear it in a finally block so it resets on both success and
failure, while preserving the existing elapsed-time throttle and progress
values.
- Around line 389-401: Bound the recovery loop in the cast load-failure branch
of SiloCastSessionManager by tracking attempts for the pending playback spec and
stopping recovery after a finite maximum. Increment and check the counter before
calling recoverFromLoadFailure or prepareMedia(replacement), reset it when
starting a new spec or successful preparation, and preserve the existing pending
cleanup/stop behavior when recovery is exhausted or returns null.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapter.kt`:
- Around line 865-868: Guard the adapted versionId assignment in the
transaction-building code using the same media-file comparison as
withRebasedDownloads: only emit "adapted:<id>" when
playback.effectiveMediaFileId differs from liveContext.mediaFileId; otherwise
preserve liveContext.versionId. Update the versionId expression near mediaFileId
while leaving the existing fallback behavior intact.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileVideoPlaybackStarter.kt`:
- Around line 372-374: Preserve the nullable duration from
resolved.durationSeconds in the VideoPlaybackStartResult.Ready path instead of
defaulting it to 0.0, and carry that nullability through the player UI state.
Update PlayerProgressBar consumers as needed so unknown durations do not produce
synthetic one-second progress or false −0:00 remaining time.
- Around line 315-320: Update the StartParams construction in
MobileVideoPlaybackStarter to set subtitleTrackIndex from
initialTracks.subtitleTrackIndex when present, otherwise use
readyV3.plan.resolvedSelectedSubtitleIndex(). Preserve explicit -1 for subtitles
Off and null when the plan selected no subtitle.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`:
- Around line 3409-3421: Update the pending-authoritative-subtitle handling
around pendingAuthoritativeSubtitleDownloadId so it is cleared only after added
resolves to a valid mobile subtitle identity and
mobileSubtitleTransactions.selectFromRefresh is invoked. Preserve the pending
marker when added is null or mobileSubtitleIdentity cannot resolve, allowing a
later ready event to complete auto-selection.

In
`@androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt`:
- Around line 1536-1544: The launchSubtitleTransaction method must serialize
subtitlePlaybackContext(state) and the subsequent transaction() as one atomic
operation. Add or reuse a Mutex around both updatePlaybackContext and
transaction execution, ensuring all subtitle transaction callers share the same
lock so suspended context construction cannot allow a newer request to be
overwritten.

In
`@androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvVideoPlaybackStarter.kt`:
- Around line 284-290: Update the duration handling in TvVideoPlaybackStarter so
an unknown duration remains unknown instead of being converted to 0.0. In
updateScrubPreview(), apply the upper-bound clamp only when duration is
positive; when it is zero or otherwise unknown, clamp the preview only to a
lower bound of zero so auto-seek and scrubber nudges can move.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentity.kt`:
- Around line 180-190: Update subtitleLabelIndicatesHearingImpaired so the
ambiguous bare “hi” token no longer matches language-only labels such as “hi” or
“EN - HI”; retain the unambiguous “cc” and “sdh” detection and existing phrase
checks. Hoist the hearing-impaired token Regex to a reusable declaration outside
the function so it is compiled once, then reuse it from
subtitleLabelIndicatesHearingImpaired.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReady.kt`:
- Around line 31-36: Update the PlaybackSubtitleReady construction to safely
cast each payload field to JsonPrimitive before reading content, intOrNull, or
other values, so object and array fields resolve to null instead of throwing.
Preserve valid primitive parsing and ensure malformed subtitle_ready events do
not terminate collection or trigger reconnects.

---

Outside diff comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudioCapabilityManager.kt`:
- Around line 73-114: Serialize the compound snapshot updates in
publishCapabilities and bumpOutputRouteGeneration using a shared routeLock,
wrapping each method body in synchronized(routeLock) while preserving
block-bodied Unit returns. Guard all reads and writes of playbackRouteSnapshot,
_capabilities, _outputRouteGeneration, generationCounter, and
routeSnapshotInitialized within the lock so each planning view is published
consistently and initialization is safely visible across callback threads.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileFreshSubtitleRestore.kt`:
- Around line 25-51: Update prepareMobileFreshSubtitleRestore so
authoritativeInventory still loads and merges local downloaded subtitles before
producing subtitleTracks; do not discard downloaded when the flag is true.
Preserve mountedSubtitles as the authoritative base while ensuring persisted
SubtitleIdentity.Downloaded selections can resolve in the published fresh state.

---

Nitpick comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt`:
- Around line 419-445: Update detectPlatformSoftwareAudioCodecs so only
successfully enumerated probes with exact = true are assigned to
cachedPlatformSoftwareAudioProbe; return the AAC/MP3 non-exact fallback without
caching when MediaCodecList enumeration fails, allowing subsequent calls to
retry.

In
`@android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/AudiobookPlayerTeardownSourceTest.kt`:
- Around line 26-44: Add an indexOf-based ordering assertion to the test
`onCleared reports both timelines through the retained lifecycle without
blocking`, verifying that `playbackSessionLifecycle.reportPosition(` appears
before `playbackSessionLifecycle.stopAsync(` in `onClearedSource`, matching the
sibling test’s pattern.
- Around line 18-25: Make the source-slicing test fail when delimiters are
missing by introducing a helper that validates both boundary markers before
applying substringAfter and substringBefore. Use it for singleFileStartSource,
partStartSource, and onClearedSource, preserving the existing delimiter strings
and slice ranges.

In
`@android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparerTest.kt`:
- Around line 16-34: Strengthen
castContextDoesNotAdvertiseThePreNeutralSidecarFeature by additionally asserting
that the neutral v3 contract token is present in
playbackClientFeaturesV3(chromecastPlaybackContext("test")). Update
castUsesPlayerLocalStartInsteadOfSourceTimelinePosition to use a non-zero
playerStartSeconds distinct from sourceStartSeconds and assert that value is
returned by castPlayerStartPosition, proving it does not use a constant or
default timeline value.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt`:
- Around line 172-175: The onPlayerError overrides use an inconsistent fully
qualified PlaybackException type. In
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt:172-175
and
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/audiobook/TvAudiobookPlayerScreen.kt:203-206,
import androidx.media3.common.PlaybackException and use the unqualified type in
each onPlayerError parameter, preserving the existing error handling.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt`:
- Around line 284-285: Clear authoritativeSubtitleReadyRows whenever playback
recovery or content changes reset the active session, alongside
pendingAuthoritativeSubtitleDownloadId. In
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt:284-285,
update resetPlaybackRecoveryState() or loadContent; in
androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt:1336-1337,
update resetSeekRecoveryForContentChange() or prepareSessionExit() so
retired-session rows are removed.

In
`@shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReadyTest.kt`:
- Around line 50-66: Add tests in PlaybackSubtitleReadyTest covering both
delivery-validation branches in applyAuthoritativeSubtitleReadyTrack: a sidecar
track with a blank URL and a burn_in_only track with a non-blank URL must each
return null, while preserving the existing valid sidecar and index-gap coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 45357666-94f0-4dc5-a121-6e87fe6ebf35

📥 Commits

Reviewing files that changed from the base of the PR and between 4dbf13b and 06a8f1c.

📒 Files selected for processing (78)
  • .agents/skills/test-shield-playback/scripts/shield-test
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudioCapabilityManager.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackCapabilityDetector.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionManager.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/ReplayableSubtitleDataSource.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SubtitleManager.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SubtitleMountResolver.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractor.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/EpisodeSelectionHandoff.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/PlaybackStartupStallDetector.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/VideoPlaybackStartRequest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/AudiobookPlayerTeardownSourceTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackPlanningSnapshotRegistryTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycleTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerSeekReanchorTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionManagerStagedReplanTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/SiloLoadControlTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/SubtitleManagerTrackSelectionTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparerTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractorTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/video/PlaybackStartupStallDetectorTest.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastSessionManager.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/audiobook/AudiobookPlayerScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileFreshSubtitleRestore.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleAutoSelection.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileVideoPlaybackStarter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlaybackRealtimeController.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/MobileFreshSubtitleRestoreTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleAutoSelectionTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapterTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModelLoadOwnershipIntegrationTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/SubtitleTrackSelectionTest.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/audiobook/TvAudiobookPlayerScreen.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/detail/TvPlaybackFormatting.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlaybackRealtimeController.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerSubtitlePolicy.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleIdentity.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleRemountReselection.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleTransactionAdapter.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvVideoPlaybackStarter.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/diagnostics/TvDiagnosticsPromptScreen.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/SubtitleRemountReselectionTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/SubtitleTransactionIntegrationTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlaybackExitSnapshotTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlaybackQualityOptionsTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerSubtitleIntegrationPolicyTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleRefreshOwnershipTest.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvSubtitleTransactionAdapterTest.kt
  • docs/playback/01-media3-only-player-architecture.md
  • docs/superpowers/plans/2026-08-02-instant-external-srt-switching-android.md
  • shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3ConformanceTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackSubtitleChoices.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentity.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReady.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/SubtitleCodecFamily.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackSubtitleChoicesTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentityTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReadyTest.kt
  • shared/src/commonTest/resources/playback/v3/SOURCE
  • shared/src/commonTest/resources/playback/v3/capability_response.json
  • shared/src/commonTest/resources/playback/v3/conformance_matrix.json
  • shared/src/commonTest/resources/playback/v3/decision_response.json
🚧 Files skipped from review as they are similar to previous changes (20)
  • shared/src/commonTest/resources/playback/v3/SOURCE
  • shared/src/commonTest/resources/playback/v3/capability_response.json
  • docs/superpowers/plans/2026-08-02-instant-external-srt-switching-android.md
  • .agents/skills/test-shield-playback/scripts/shield-test
  • shared/src/commonTest/resources/playback/v3/decision_response.json
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/VideoPlayerMediaSpec.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/network/api/PlaybackApiTest.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/ReplayableSubtitleDataSource.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerScreen.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/subtitle/PgsSupExtractorTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModelLoadOwnershipIntegrationTest.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloLoadControl.kt
  • docs/playback/01-media3-only-player-architecture.md
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerScreen.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/PlaybackStartupStallDetector.kt
  • shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3ConformanceTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackModels.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycleTest.kt

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastSessionManager.kt (1)

403-414: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear local playback state after terminal load recovery.

Line 412 clears pending directly when recovery returns null. This bypasses the progress-job cancellation in finalizePending. It also retains the previous fileId, title, subtitle state, and position because syncCastState falls back to cached state when pending is null at Lines 685-690.

Clear the active playback fields and cancel the progress job on this terminal path. Preserve the connected Cast route state. Otherwise, the phone can show an ended item and suppress a later auto-stage for that file.

🤖 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
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastSessionManager.kt`
around lines 403 - 414, Update the terminal recovery branch in
SiloCastSessionManager’s load-failure handling to use finalizePending instead of
assigning pending = null directly. Ensure it cancels the progress job and clears
the active file, title, subtitle, and position fields while preserving the
connected Cast route state, so subsequent sync and auto-stage behavior see no
ended local item.
🤖 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.

Inline comments:
In
`@android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt`:
- Around line 453-457: In CastPlaybackPreparer.kt:453-457, add a mutex-protected
success method that resets loadFailureRecoveryAttempts, and invoke it from
SiloCastSessionManager.kt:398-402 only after the receiver confirms the current
pending spec loaded successfully; do not reset the budget on failed or unrelated
loads.

In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerProgressBar.kt`:
- Around line 74-89: Gate the intro and chapter timeline decoration calculations
and rendering on hasKnownDuration in the PlayerProgressBar composable. When
duration is unknown, skip both intro tint and chapter tick decorations entirely;
preserve their existing positioning and rendering when the duration is known.

In
`@shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReadyTest.kt`:
- Around line 70-84: Update malformedScalarFieldsDoNotAbortRealtimeDecoding to
include valid control values for the other playback subtitle fields and assert
they decode successfully while the malformed scalar becomes null. Add separate
test cases for malformed session_id, file_id, and subtitle_id so each field’s
isolated failure behavior is verified without allowing whole-payload discarding
to pass.

---

Outside diff comments:
In
`@androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastSessionManager.kt`:
- Around line 403-414: Update the terminal recovery branch in
SiloCastSessionManager’s load-failure handling to use finalizePending instead of
assigning pending = null directly. Ensure it cancels the progress job and clears
the active file, title, subtitle, and position fields while preserving the
connected Cast route state, so subsequent sync and auto-stage behavior see no
ended local item.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf49e3a0-6198-4cd2-b494-584b65562325

📥 Commits

Reviewing files that changed from the base of the PR and between 06a8f1c and 37beb2d.

📒 Files selected for processing (24)
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/cast/CastPlaybackPreparer.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/VideoPlaybackStartResult.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/video/VideoPlayerUiState.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PlaybackV3SessionTest.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/cast/SiloCastSessionManager.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileVideoPlaybackStarter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerProgressBar.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapterTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerProgressBarTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModelLoadOwnershipIntegrationTest.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvVideoPlaybackStarter.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvScrubPreviewPolicyTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentity.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReady.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentityTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReadyTest.kt
🚧 Files skipped from review as they are similar to previous changes (13)
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvVideoPlaybackStarter.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleReady.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentityTest.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackV3Session.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/playback/PlaybackSubtitleIdentity.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3Test.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileVideoPlaybackStarter.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/MobileSubtitleTransactionAdapter.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/playback/PlaybackProtocolV3.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/player/PlayerViewModel.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/AudiobookPlayerViewModel.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerViewModel.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt

@Quick104

Copy link
Copy Markdown
Contributor Author

Final review and remediation are complete at 5508a810.

  • Addressed and resolved all 32 inline review threads; the unresolved-thread count is now 0.
  • Final independent Standards and Spec reviews on the exact head returned 0 findings.
  • Full local gate passed: ./gradlew test lint :androidApp:assembleDebug :androidTvApp:assembleDebug (371 tasks), plus the focused regression suites and git diff --check.
  • GitHub CodeRabbit, Lint, and Unit tests are green; the branch is mergeable on current main.
  • All nine neutral-v3 fixtures remain byte-identical to server PR #567 at 79e3e761.
  • Final arm64 TV APK was installed in place on a signed-in NVIDIA Shield without clearing app data. Device validation passed for exact-file 4K Dolby Vision-to-HDR10 remux, rendered first-frame decoder telemetry, two server seek reanchors with exact source-time resume, typed terminal handling for an unsupported exact HLG pin, unpinned adaptation to a playable 1080p SDR edition, Arabic sidecar replan/remount/selection, clean stop telemetry, and signed-in relaunch persistence.

The PR description has been updated with the final validation evidence. The app is stopped cleanly and left on the Shield Home screen.

@Quick104
Quick104 merged commit fc73d3f into main Aug 11, 2026
3 checks passed
@Quick104
Quick104 deleted the codex/playback-v3-neutral-android branch August 11, 2026 01:26
RXWatcher added a commit to RXWatcher/silo-android that referenced this pull request Aug 11, 2026
The ceiling is derived so STEADY-STATE crossing lands near
TRAVERSE_TARGET_SECONDS, but a hold does not start at the ceiling — it doubles
every 900ms to reach it, and the early rungs cover almost nothing. A three-hour
film spends 8.1s ramping and covers only ~920s of itself in that time, so the
real cost is ~17.8s, not the ~10.5s the docs claimed.

The test claimed to check this and could not: it computed duration / topRate,
arithmetic the implementation never performs, so it reported 10.55s against a
15s tolerance and passed while the real behaviour was 17.75s.

TvSeekRateLadder.traverseSeconds now models the ramp the code actually runs,
the test asserts against it, and the documentation states the honest envelope:
~10.6s for a 22-minute episode to ~17.8s for a three-hour film. The property
worth keeping is the SPREAD — under 2x across runtimes, versus 41s vs 338s
before the ceiling was derived — not the absolute number, and that is now what
is asserted. Mutation-checked: changing the ramp cadence fails it.

Also pins the unknown-duration case. Protocol v3 (Silo-Server#200) declares duration
server-side and deliberately refuses a Media3/catalog fallback, so an omitted
duration now reaches the ladder as 0 and lands on MIN_TOP_RATE. That is more
reachable than it was before v3, so it is worth a test.

androidTvApp 1003 -> 1006, all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RXWatcher added a commit to RXWatcher/silo-android that referenced this pull request Aug 11, 2026
The ceiling is derived so STEADY-STATE crossing lands near
TRAVERSE_TARGET_SECONDS, but a hold does not start at the ceiling — it doubles
every 900ms to reach it, and the early rungs cover almost nothing. A three-hour
film spends 8.1s ramping and covers only ~920s of itself in that time, so the
real cost is ~17.8s, not the ~10.5s the docs claimed.

The test claimed to check this and could not: it computed duration / topRate,
arithmetic the implementation never performs, so it reported 10.55s against a
15s tolerance and passed while the real behaviour was 17.75s.

TvSeekRateLadder.traverseSeconds now models the ramp the code actually runs,
the test asserts against it, and the documentation states the honest envelope:
~10.6s for a 22-minute episode to ~17.8s for a three-hour film. The property
worth keeping is the SPREAD — under 2x across runtimes, versus 41s vs 338s
before the ceiling was derived — not the absolute number, and that is now what
is asserted. Mutation-checked: changing the ramp cadence fails it.

Also pins the unknown-duration case. Protocol v3 (Silo-Server#200) declares duration
server-side and deliberately refuses a Media3/catalog fallback, so an omitted
duration now reaches the ladder as 0 and lands on MIN_TOP_RATE. That is more
reachable than it was before v3, so it is worth a test.

androidTvApp 1003 -> 1005, all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RXWatcher added a commit that referenced this pull request Aug 11, 2026
#209)

* fix(tv): make the seek rate mean what the chip says, and stop it running away

Hold-to-seek advanced 2.0 seconds of content on every 100ms tick, so the rate
on the chip was a twentieth of the truth: "8×" moved at 160× real time, and the
top speed of "32×" moved at 640×, crossing a 45-minute episode in four seconds.
That is the whole reason it felt ungovernable rather than merely quick — the
viewer aims with the number on screen, and the number was wrong by 20×.

A rate is now exactly its own multiple of real time: rate × tick seconds per
tick. 8× means 8×.

Two things fell out of fixing that. The ramp reached the top speed after three
seconds of holding, so a press meant to nudge forward a few seconds crossed the
scene; the milestones are now 1.5s / 3s / 5s. And a sustained hold now stops at
16× — reaching 32× takes a deliberate repeat-press, so holding cannot fall into
the fastest speed by accident.

1× is dropped from the ladder. It scans at exactly playback speed, so the first
press looked like nothing had happened.

Speeds and ramp move into TvSeekRateLadder as pure functions, and the test
asserts the property that was violated: a rate advances exactly that multiple
of real time. A magic multiplier in the tick now fails a test instead of
shipping another chip that lies.

Verified: :androidTvApp:testDebugUnitTest 983 tests, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tv): derive the seek ceiling from runtime so the end is reachable

The honest-rate fix made the labels true but left a fixed 32x ceiling, and a
fixed ceiling cannot serve both ends of this control. Nudging past an intro
wants single digits. Reaching the end of a 45-minute episode at 32x takes 84
seconds of holding, and a three-hour film takes five and a half minutes — that
is not a seek.

The top of the ladder now comes from the item's runtime, targeting about ten
seconds to cross the whole thing:

  22-min episode   256x    5.2s to cross   (was 41s)
  45-min episode   512x    5.3s            (was 84s)
  90-min film     1024x    5.3s            (was 169s)
  3h film         1024x   10.5s            (was 338s)

The ramp follows from the same place: it keeps doubling every 900ms until it
reaches that item's ceiling, so a long film goes on accelerating past the point
where a short episode has already topped out. Reaching the top takes 6-8s of
deliberate holding, and the first step is still 4x, so the aimable half of the
control is untouched.

An unknown runtime falls back to 32x rather than guessing — live content and
un-probed files both arrive as zero duration.

Two corrections to the previous commit's tests. The traverse assertion caught a
real bug: 512x was too low a cap for a three-hour film, which crossed in 21s
against a 10s target, so the cap is 1024x. And the reverse-bump test asserted
semantics the key handlers do not use — delta is a direction along the signed
ladder, not "faster" — so it now pins the property that actually matters: a
bump never crosses zero and flips direction mid-seek.

Verified: :androidTvApp:testDebugUnitTest 985 tests, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tv): make the traversal claim honest about the ramp

The ceiling is derived so STEADY-STATE crossing lands near
TRAVERSE_TARGET_SECONDS, but a hold does not start at the ceiling — it doubles
every 900ms to reach it, and the early rungs cover almost nothing. A three-hour
film spends 8.1s ramping and covers only ~920s of itself in that time, so the
real cost is ~17.8s, not the ~10.5s the docs claimed.

The test claimed to check this and could not: it computed duration / topRate,
arithmetic the implementation never performs, so it reported 10.55s against a
15s tolerance and passed while the real behaviour was 17.75s.

TvSeekRateLadder.traverseSeconds now models the ramp the code actually runs,
the test asserts against it, and the documentation states the honest envelope:
~10.6s for a 22-minute episode to ~17.8s for a three-hour film. The property
worth keeping is the SPREAD — under 2x across runtimes, versus 41s vs 338s
before the ceiling was derived — not the absolute number, and that is now what
is asserted. Mutation-checked: changing the ramp cadence fails it.

Also pins the unknown-duration case. Protocol v3 (#200) declares duration
server-side and deliberately refuses a Media3/catalog fallback, so an omitted
duration now reaches the ladder as 0 and lands on MIN_TOP_RATE. That is more
reachable than it was before v3, so it is worth a test.

androidTvApp 1003 -> 1005, all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(tv): the scrubber no longer tops out at 32x

The header still described the fixed ceiling this branch replaced. The rate now
doubles to a runtime-derived ceiling — 256x for a 22-minute episode, 1024x for
a feature — which is the whole point of the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
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