A live internet radio server written in Rust with nothing but the standard
library — no tokio, no MP3 crates, just std::net, std::thread, and a
frame parser written against the MPEG spec.
I built this to understand what actually happens inside a streaming service, instead of importing a library and calling it done.
Each folder of MP3s becomes a station, broadcast live: one thread per station reads the files frame by frame and paces itself to real time, so everyone tuned in hears the same moment of the same song. Tuning in is a one-line TCP handshake; the audio itself arrives as sequence-numbered UDP datagrams, and reordering them is the client's job.
The interesting parts:
src/mp3.rs— MPEG frame header parsing straight from the bytes: sync words, ID3 tags, resyncing after garbage.src/server.rs— the fan-out with plain threads, a mutex, and a UDP socket. No async runtime.src/bin/listen.rs— the reorder buffer, with tests for every way packets can misbehave.
No transcoding: whatever's in a station folder has to already be valid MP3.
One folder per station under the root directory (default stations/):
stations/
classical/
debussy.mp3
tchaikovsky.mp3
chiptune/
track1.mp3
Folder name = station name. Files play in order and loop. At least one station folder is required.
cargo run
Binds 0.0.0.0:7878 on both TCP and UDP. Configuration:
| Variable | Default | What it does |
|---|---|---|
RADIO_ADDR |
0.0.0.0:7878 |
address to bind (tcp + udp) |
STATIONS_DIR |
stations |
root folder of station subfolders |
To check a station without a real client:
cargo run --bin listen classical | mpg123 -
docker build -t radiogram .
docker run -d -p 7878:7878/tcp -p 7878:7878/udp \
-v /path/to/your/stations:/app/stations radiogram
Station folders are mounted at runtime, not baked into the image.
Browsers can't speak the protocol directly, so example/go-client/ is a
small Go bridge that re-serves stations as plain HTTP audio, with a web
player and a live stats page.
cd example/go-client
go run . -radio 127.0.0.1:7878
Then open http://localhost:8090.
One line over TCP to tune in, audio over UDP. The Go bridge and the Rust
listen client are both reference implementations.
List stations — send LIST\n, get one name per line, connection
closes:
classical
chiptune
noise
Tune in — send the station name as a line:
classical\n
Reply is OK classical 7\n (the number is your token) or
ERR unknown station 'classical'\n. Keep the connection open — it never
carries audio, it just tells the server you're still listening. Closing
it tunes you out.
Hello over UDP — send the token as ASCII in a single datagram to the same host and port, from the socket you want audio on. This is how the server learns your address and ties it to your registration.
Receive — audio then arrives as:
[4-byte big-endian sequence number][one MP3 frame]
One frame per packet, small enough to avoid fragmentation. The sequence number is shared by every listener of a station.
Reorder — packets that arrive early wait in a small buffer until the
gap before them fills in. If it hasn't after ~8 out-of-order packets, the
missing one is presumed lost and skipped; if it shows up later anyway,
it's dropped. See ReorderBuffer in
src/bin/listen.rs or the Go version in
example/go-client/main.go.