Repository navigation
perf: a transcode abandoned by a seek is never cached, so a track seeked on its first play re-transcodes on every play #185
Description
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:
- A consumer that leaves kills the transcode and deletes its partial cache file.
stream_transcodewatchessender.closed(), then callschild.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. - An
offset_msstream is never cached.servecallsstream_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:
- Let an abandoned offset-0 transcode finish when an
offset_msrequest 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. - On an
offset_msrequest 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. - Keep writing the cache after the consumer leaves once the encode is past some fraction of the track, where finishing costs less than redoing.
- Accept it and say so in the API guide, next to the
offset_msparagraph, 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
fix(media): cache the transcode a seek abandons