Skip to content

android: make playback and prefetch recover instead of stall - #11

Merged
ingoau merged 3 commits into
mainfrom
claude/vibrant-cerf-occlu7
Sep 25, 2026
Merged

ingoau merged 3 commits into
mainfrom
claude/vibrant-cerf-occlu7

Conversation

@ingoau

@ingoau ingoau commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Why

The Android app sometimes stopped playing without saying why, and sometimes did not cache the upcoming tracks. A pass over the ExoPlayer bridge, the service lifecycle and the core's playback and prefetch handling found several ways to end up silently stuck or to give up on caching.

What changed

ExoBackend (Android)

  • Paused is reported only when the player no longer means to play (playWhenReady false). A stall with it still set (a rebuffer, a retried load, an error being recovered from) is Buffering. Before, such stalls showed the track as paused and let the service count the player as idle and stop itself mid-recovery.
  • Errors a later prepare() can fix are widened from connection failures/timeouts to 5xx statuses, a stuck player and core stream handles that went away under the player (the core closes idle handles after ten minutes, and ExoPlayer legitimately idles for minutes once its buffer is full).
  • While offline the retry waits instead of spending attempts; the network coming back (NetworkMonitor -> ExoBackend.onConnectivityChanged) retries at once with a fresh schedule.
  • CoreStreamLoadErrorPolicy retries a closed/reaped handle with a fresh open at the same position, which the cache serves.
  • The service's idle stop and task-removed stop never release a player that is still buffering or recovering (ExoBackend.isBusy).

Core

  • An item adopted at a gapless boundary that the backend never confirms with TransitionedToNext (a SetNext still in flight when the player ran out, a preload it dropped) is loaded explicitly after two seconds instead of leaving the session on an item nothing plays.
  • A preloaded follow-up that fails before the transition is forgotten rather than restarting the current item from the top.
  • Prefetch retries failed fetches with a growing backoff (15 s to 5 min), immediately on a network change, and treats a read that left no cache entry as a failure instead of starting it over at once.
  • The stream reader drops reqwest's read timeout, which also ran while ExoPlayer's full buffer left the body unread for minutes (a spurious network error once per load burst on long tracks, and a re-transcode for transcoded streams). It now waits for the server only while a read is actually pending.

Tests

  • Core: cargo test -p hocket-core --features sim (three new actor tests: adoption fallback, prefetch backoff retry, prefetch retry on network return); clippy and fmt clean.
  • Android: :core and :playback unit tests (four new: error classification, stall reporting, busy state, closed-handle retry policy); the real-core end-to-end JVM test against the host build of the core.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnhqP7bbeXKtuNP9dkcrPy


Generated by Claude Code

ExoBackend reports Paused only when the player no longer means to play
(playWhenReady false); a stall while it still does is Buffering, so a
rebuffer, a retried load or an error being recovered from no longer shows
the item paused and no longer lets the service count the player as idle.
Errors a later prepare() can fix are widened from connection failures to
timeouts, 5xx statuses, a stuck player and core stream handles that went
away under the player; while offline the retry waits instead of spending
attempts and the network coming back (NetworkMonitor -> onConnectivityChanged)
retries at once. A handle the core closed is retried by the load-error policy
(a fresh open at the position, served by the cache). The service's idle stop
and task-removed stop never release a player that is still buffering or
recovering.

Core: an item adopted at a gapless boundary that the backend never confirms
with TransitionedToNext (a SetNext still in flight when the player ran out,
a preload it dropped) is loaded explicitly after two seconds instead of
leaving the session on an item nothing plays; a preloaded follow-up that
fails before the transition is forgotten rather than restarting the current
item from the top. Prefetch retries failed fetches with a growing backoff,
at once when the network comes back, and treats a read that left no cache
entry as a failure instead of starting it over immediately. The stream
reader drops reqwest's read timeout, which also ran while ExoPlayer's full
buffer left the body unread for minutes, and waits for the server only while
a read is actually pending.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnhqP7bbeXKtuNP9dkcrPy
@ingoau
ingoau merged commit e46a4fa into main Sep 25, 2026
4 checks passed
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.

2 participants