Skip to content

Playback fails to start: race condition + cache file corruption in ServerPlaybackRoutes #3089

Description

@jablonowskit

Current Behavior

Tracks intermittently fail to play (Windows debug and Android APK), with
media_kit/MPV logging:

[MediaKitError]
Failed to open http://localhost:PORT/stream/<trackId>.

and the local proxy server logging:

Bad state: No element
#0  ListBase.firstWhere
#1  ServerPlaybackRoutes._getSourcedTrack (playback.dart)
#2  ServerPlaybackRoutes.getStreamTrackId (playback.dart)

Root cause 1 — race condition in _getSourcedTrack

_getSourcedTrack looked up the requested media in
audioPlayer.playlist.medias by matching request.requestedUri:

final media = audioPlayer.playlist.medias
    .firstWhere((e) => e.uri == request.requestedUri.toString());

MPV issues the HTTP request to the local proxy before the Dart-side
player state (audioPlayer.playlist.medias, from media_kit) has
synchronized with the native player state. In that window medias does
not yet contain the requested URI, so firstWhere throws
Bad state: No element → the server returns 500 → MPV reports
"Failed to open". This is why the issue sometimes "resolved itself" after
a moment (once Dart-side state caught up) without an app restart.

Not platform-specific — server.dart, audio_player.dart, and
playback.dart's _getSourcedTrack have no per-platform branching on
this path; Android hits the race more often due to slower startup
(wider timing window).

Root cause 2 — cache file corruption under concurrent range requests

Separately, after fixing root cause 1, caching threw:

PathAccessException: Cannot rename file to '...track....m4a',
path = '...m4a.part'
(OS Error: The process cannot access the file because it is being used
by another process, errno = 32)

MPV opens several concurrent range requests for the same track
(buffering, seeking, reconnects can produce 2-3 simultaneous requests for
one track). Each request opened its own openWrite sink to the same
.part file in writeOnlyAppend mode. When one request finished
(onDone) and called rename, another still held the file open, so
Windows blocks the rename (errno 32). Concurrent sinks also appended to
each other, corrupting the cache.

Steps to reproduce

  1. Launch Spotube (Windows debug build or Android APK).
  2. Play a track from a playlist/queue.
  3. Observe intermittent Bad state: No element in server logs / "Failed
    to open" in MPV logs, and (with music caching enabled) intermittent
    PathAccessException on cache rename.

Suggested fix

  • _getSourcedTrack: stop depending on audioPlayer.playlist.medias
    entirely — resolve the track directly from playlist.tracks (Riverpod
    state) by trackId, using firstWhereOrNull + null check instead of
    a throwing firstWhere.
  • Cache writer: guard with a Set<String> _cachingInProgress so only one
    request caches a given track at a time (other concurrent range requests
    stream through without writing to cache); only cache full requests
    (contentRange.start == 0); use FileMode.writeOnly (truncate) instead
    of writeOnlyAppend; wrap onDone in try/catch/finally so the
    in-progress marker and incomplete .part file are always cleaned up.

Operating System

Windows, Android

Spotube version

v5.1.2+45 (current master)

Self grab

  • I'm ready to work on this issue!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions