Skip to content

fix(android): harden player lifecycle and final writes (PR 108 slice D) - #115

Merged
RXWatcher merged 20 commits into
mainfrom
split/108-d-player-foundation
Jul 27, 2026
Merged

fix(android): harden player lifecycle and final writes (PR 108 slice D)#115
RXWatcher merged 20 commits into
mainfrom
split/108-d-player-foundation

Conversation

@RXWatcher

Copy link
Copy Markdown
Contributor

Stack

Scope

  • isolate mobile/TV playback clocks from high-frequency screen recomposition
  • make exit/teardown non-blocking while retaining identity-captured final position writes
  • preserve player/session ownership across recreation
  • serialize external start ownership against queued teardown
  • reject and close sessions that finish after exit, including cancellation races
  • retain failed final writes for application-scope retry and stop Up Next on exit
  • authenticate exported-service PiP custom actions
  • preserve TV live-clock seeks/duration and account failed load time in retry policy
  • pause/resume episode rollup work with the detail route lifecycle

Transactional subtitle publication, PGS/libass, and letterboxing remain in slice E.

Review corrections

Independent review findings were fixed test-first: stale success/failure publication after exit, late external adoption leaks, valid starts racing queued lazy stops, cancellation between server allocation and adoption, and unwired rollup pause/resume. Final independent verdict: Ready: YES; no remaining Important/Critical findings.

Verification

  • ./scripts/test-check-build-supply-chain.sh
  • ./scripts/check-build-supply-chain.sh
  • ./gradlew -Dorg.gradle.jvmargs="-Xmx4g -Dfile.encoding=UTF-8" testDebugUnitTest :androidApp:assembleRelease :androidTvApp:assembleRelease --max-workers=2
  • Result at code head 2e9ad5f2: BUILD SUCCESSFUL, 334 tasks, 2m50s

Traceability and deferred-commit mapping: docs/superpowers/plans/2026-07-27-pr108-slice-d-player-foundation.md.

RXWatcher and others added 20 commits July 27, 2026 11:20
start() ran its API call outside the lifecycle mutex, and for that whole window
_state is Loading with lastAdoptedSessionId null — so stop()'s ownership guard
found no id to compare against and tore down anyway.

Two consequences. The teardown nulls lastStartParams, and startInternal never
re-set it, so the session that then published Active had none: 404 session
recovery and the final progress flush both silently no-op, losing the user's
resume position on exit. And the newly started session was never stopped,
because the stop that would have owned it had already run — leaving it alive on
the server, counting against the account's concurrent-stream cap until it timed
out. That is the same shape as the orphaned cast sessions that produced the 429
storm.

Publish the start result under the mutex and compare a stop epoch captured
before the API call: if a teardown ran while we were starting, stop the new
session and stay Idle instead of resurrecting a dismissed screen.

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

The 90s window was enforced against an estimate reconstructed from the retry
count, which sums only the delays between attempts. Time spent inside each
failed attempt was invisible, so against a server that accepts connections and
then stalls — 30s socket timeout per try — the real elapsed time ran several
times the window before an error surfaced, leaving a spinner up for minutes.

Measure from the first error of each load task and take whichever bound is
larger. Callers that cannot measure keep the derived behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two findings CodeRabbit raised on PR #97. That PR's code was squashed into main
without them being triaged, so both were live here.

FinalPlaybackPositionWriter drained `pending` before writing and swallowed the
result, so a failed write discarded the user's final position outright — no
retry, no re-queue — and this queue is the durable record that survives a
server-side session reset. Failures now go back on the queue with a backoff,
using putIfAbsent so a newer position submitted meanwhile still wins.

PlayerViewModel.onExit did not cancel the Up Next countdown, so a countdown
still running when the user left could fire and start an episode behind a
dismissed screen — creating exactly the kind of session that outlives the exit
meant to end it.

Also makes the content-reset self-heal timeout injectable. runTest advances
virtual time whenever the scheduler idles, so the fixed 45s value could fire
inside tests that legitimately wait for a settlement, which made
`new content start waits for unresolved replacement settlement` order-dependent.
Tests asserting the wait now pass NEVER_SELF_HEAL; the self-heal test asks for
the real value.

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

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@RXWatcher, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 90a1467d-2351-4758-bfa4-6a802d2b2251

📥 Commits

Reviewing files that changed from the base of the PR and between 6b1d4f8 and 55ba027.

📒 Files selected for processing (30)
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/data/repository/RoomUserItemStateRepository.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/di/PlayerInfraModule.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/pip/SiloPictureInPictureCoordinator.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/FinalPlaybackPositionWriter.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/MediaLoadRetryPolicy.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PipActionCapability.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/PlaybackSessionLifecycle.kt
  • android-shared/src/androidMain/kotlin/org/siloserver/silo/common/player/SiloPlaybackService.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/data/repository/RoomUserItemStateRepositoryTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/FinalPlaybackPositionWriterTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/MediaLoadRetryPolicyTest.kt
  • android-shared/src/androidUnitTest/kotlin/org/siloserver/silo/common/player/PipActionCapabilityTest.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
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/di/AndroidModule.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/detail/ItemDetailScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/detail/ItemDetailViewModel.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/performance/MobilePlayerLifecyclePerformanceSourceTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/detail/EpisodeRollupAccumulatorTest.kt
  • androidApp/src/androidUnitTest/kotlin/org/siloserver/silo/android/ui/screens/player/MobilePlayerPresentationStateTest.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/di/AndroidTvModule.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/TvVideoPlaybackStarter.kt
  • androidTvApp/src/androidUnitTest/kotlin/org/siloserver/silo/tv/ui/screens/player/TvPlayerPresentationStateTest.kt
  • docs/superpowers/plans/2026-07-27-pr108-slice-d-player-foundation.md
  • shared/src/commonMain/kotlin/org/siloserver/silo/repository/port/UserItemStatePort.kt
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split/108-d-player-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Base automatically changed from split/108-c-room-sync to main July 27, 2026 16:38
@RXWatcher
RXWatcher marked this pull request as ready for review July 27, 2026 16:39
@RXWatcher
RXWatcher merged commit f41bda9 into main Jul 27, 2026
2 checks passed
@RXWatcher
RXWatcher deleted the split/108-d-player-foundation branch July 27, 2026 16:39
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