An audiobook player looks like a solved problem until you point it at a real library. The files are M4B containers written by a dozen encoders that each read the spec differently, they run to hundreds of megabytes, and the one thing a listener actually cares about — where they stopped — is not in the file at all. Four problems fell out of that: getting chapters from files that disagree about how to store them, moving audio into a WebView without stalling it, keeping user data next to files the user is free to move, and giving the whole thing a reason to exist beyond pressing play.
Rust Reads, the WebView Draws
The split is drawn on I/O. Anything that touches a file — tag parsing, cover extraction, MP4 atom traversal, range-serving audio — is Rust, exposed across a single command and one custom URI scheme. Everything above that is Svelte 5 with runes over a set of plain writable stores. Pulling metadata extraction out of the WebView was not an optimisation; it was the difference between reading a tag and buffering hundreds of megabytes into a JavaScript heap that then has to collect it.
Audio moves over a custom protocol rather than the framework asset protocol, because the Web Audio graph behind the equalizer needs a media element source, and that needs CORS headers the asset protocol does not send. Once you are writing the handler anyway you own range handling — which turned out to matter far more than the equalizer did.
Chapters Are a Dialect, Not a Format
An M4B can carry chapters as a QuickTime text track or as a Nero atom buried under the movie header, and a given file may have both, one, or neither, with nothing in it to say which. The reader tries the structured form first and falls back, and both paths needed defences the spec does not mention: a sample-to-chunk table walk, because assuming one sample per chunk silently reports the first chapter of a 179-chapter file and nothing else; a sanity gate that discards a chapter set outright when any timestamp lands past the file real duration, because some re-muxers write a 44-byte stub that parses cleanly into the year 3940; and an allocation ceiling, because the entry count sizing every buffer is a number read out of an untrusted file.
Thirty Round Trips Before the First Second
Large M4Bs commonly carry their index at the end of the file. A WebView opening one issues a long run of sequential range requests near that end before it can play a single second, and the per-request cap decides how many. Raising the open-ended range cap from 2 MB to 16 MB collapsed roughly thirty round trips into a handful and took first-play latency on a 489 MB file from 30–60 seconds down to about ten. Moving the handler off the thread that draws the window stopped those requests serialising behind each other and behind the interface.