Skip to content

perf: a transcode abandoned by a seek is never cached, so a track seeked on its first play re-transcodes on every play #185

Closed

Description

@InstaZDLL

A client that seeks inside a transcoded track during its first play never gets that transcode cached. Every later play of the track is a live transcode again: ffmpeg runs again, there are no byte ranges again, so the client has to seek through offset_ms again — which abandons the stream again. The track never leaves that loop.

Deduced from the code on main (a8b800a), not reproduced.

Why it happens

Two behaviours in src/media.rs, each reasonable on its own:

  1. A consumer that leaves kills the transcode and deletes its partial cache file. stream_transcode watches sender.closed(), then calls child.kill() and removes the .part-* file. The comment gives the reason: an abandoned transcode would otherwise hold a process and a concurrency slot for as long as the encode takes.
  2. An offset_ms stream is never cached. serve calls stream_transcode(…, query.offset_ms, None) for any non-zero offset — correctly, since it is not the whole track.

Seeking in a live transcode goes through (2), and is the very act that triggers (1): the client drops the offset-0 stream to request the offset one. The only stream that could have filled the cache is the one the seek kills, and the one replacing it is not allowed to. The cache is filled only by a play that runs from the start of a track to its end without touching the seek bar.

Who hits it today

Any OpenSubsonic client, not only a future one. The server announces transcodeOffset (src/subsonic/protocol.rs:188), and src/subsonic/media.rs maps timeOffset onto offset_ms.

Resonus, for one, seeks through timeOffset whenever a server stream has no length, and counts on the second play being natively seekable. Its own comment: "Second time round it seeks fine because Navidrome serves the cached transcode, with length." Against WaveFlow, a track seeked on its first play has no such second time.

The Android client is about to be in the same position: transcoding profiles arrive in InstaZDLL/waveflow-android#51, and seeking inside a live transcode will have to go through offset_ms for the reason above.

What would help

Not prescriptive. The cost that (1) protects against is real, and a client that skips a track after ten seconds should not keep an encoder busy. Some directions, with their trade-offs:

  1. Let an abandoned offset-0 transcode finish when an offset_ms request for the same track, format and bitrate arrives from the same user shortly after. That is a seek, not a skip, and the requests alone tell the two apart. It holds a slot for the rest of the encode, but only for a track the user is demonstrably still listening to.
  2. On an offset_ms request with no cache, fill the cache in the background with an offset-0 transcode at lower priority than live streams — for instance only when a global slot is free, and never against the per-user limit. The next play is then natively seekable.
  3. Keep writing the cache after the consumer leaves once the encode is past some fraction of the track, where finishing costs less than redoing.
  4. Accept it and say so in the API guide, next to the offset_ms paragraph, so a client knows that seeking on a first play gives up the cache.

Encodes typically run far faster than real time, so (1) and (2) may hold the extra slot for seconds per track rather than for its duration. Worth measuring before choosing.

Adjacent, not a bug

transcoding_available is hard-coded to true in MediaService::initialize. That looks wrong until one reads that startup aborts without ffmpeg (src/lib.rs:539), so GET /api/v2/transcode/status cannot report available: false from this server. Fine as designed. Mentioned because a client reading the contract will build UI for a false that never comes — the Android client just did.

Activity

  1. self-assigned this
    on Sep 11, 2026
  2. added a commit that references this issue on Sep 15, 2026
    03f5a06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

scope: streamingStreaming, transcoding, FFmpegtype: perfPerformance improvement

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions