-
Notifications
You must be signed in to change notification settings - Fork 17
Architecture
How TinyIce is put together internally. This is the orientation for anyone reading the source or planning a change; pair it with Developing.
main.go Entry point: flags, config, signals, subcommands.
config/ tinyice.json schema, defaults, atomic save.
relay/ Audio/video core (the engine).
server/ HTTP/TCP layer, auth, handlers, sockets.
server/frontend/ Preact + Vite admin SPA and player (embedded into the binary).
monitoring/ Grafana dashboard + Prometheus scrape config.
packaging/ nFPM spec + systemd unit for .deb/.rpm.
The released binary is fully self-contained: pure-Go decoders/encoders, a
CGO-free SQLite driver (github.com/glebarez/sqlite), and the entire frontend
embedded via go:embed. It cross-compiles to Linux, macOS, Windows, and FreeBSD
with no C toolchain.
The heart of TinyIce is the relay (relay/), a pub/sub engine. Every mount
is a Stream backed by a single fixed-size circular buffer, not per-listener
queues.
- A source writes chunks into the buffer once.
- Each listener holds an offset into that shared buffer and is woken by a signal channel when new data arrives; it pumps bytes straight from shared memory to its socket.
- New listeners subscribe at an offset rewound by the mount's burst size, so they get an "instant start" burst from buffer history.
This is why per-listener overhead is small and the real ceiling is network bandwidth, not RAM (see Deployment). Ingestion is O(1); signalling N listeners is O(N) and cheap in Go.
Streaming servers live or die on their locking. TinyIce's rules, learned partly the hard way (the changelog documents the incidents):
-
Two-tier locks: a relay-wide
RWMutexguards the map of streams; eachStreamhas its ownRWMutexfor its metadata and listener set. Bandwidth counters aresync/atomic. -
Never hold a lock across blocking I/O.
SetCurrentSongcaptures the diff under the lock then runs the SQLite history write outside it;Broadcastreleases the stream write lock before fanning out listener signal channels (the former full-lock fan-out was the dominant contention vector). -
No recursive RLock. Go's writer-preferring
RWMutexdeadlocks if a writer queues between a re-entrant pair — fixed inSavePlaylist/GetSongTitle. -
Don't close a channel a sender might still use.
Unsubscribeno longer closes the signal channel; arecover()in the fan-out is the backstop. -
Bounded, always-advancing parsers. The Ogg page-boundary scanner advances
≥1 byte per iteration (a
i += n - 3that could stall held a read lock and hung the whole relay); the TS demuxer resyncs byte-by-byte. - Deadlines on every hijacked read so a silent peer can't pin a goroutine + FD + mount forever.
| Package / file | Responsibility |
|---|---|
relay/relay.go, stream.go, buffer.go
|
Stream lifecycle, circular buffer, broadcast/subscribe. |
relay/streamer.go, transcode.go
|
AutoDJ playback loop and native MP3/Opus encoding. |
relay/webrtc.go |
pion/webrtc source ingest and WHEP egress, Ogg muxing. |
relay/client.go |
Pulling upstream Icecast relays. |
relay/mpd.go |
Minimal MPD control protocol per AutoDJ. |
relay/history.go |
GORM models on pure-Go SQLite (history.db). |
server/server.go |
Routing, startup, hot-swap. |
server/auth.go |
Sessions, roles, IP security, lockouts. |
server/listener.go |
Listener lifecycle + the internal metrics/pprof server. |
server/handlers_*.go |
Admin, API, player, HLS handlers. |
server/socket_*.go |
OS-specific SO_REUSEPORT syscalls. |
Listener (Icecast): handleListener accepts the request → Stream.Subscribe
returns a buffer offset rewound by burst_size → the loop reads from the
circular buffer and waits on a signal channel for new data.
AutoDJ: the streamer selects a file → decode to PCM → transcode to MP3/Opus
chunks → write to the stream buffer → Broadcast wakes listeners. Playback is
paced to real time.
WebRTC source (Go Live): browser getUserMedia → Opus over a WebRTC track →
muxed to Ogg pages → written into the stream buffer like any other source.
Video (RTMP/SRT → HLS): H.264 + AAC/MP3 is demuxed, the HLS muxer emits one
PES per frame with real 90 kHz PTS+DTS, ADTS audio, PCR per PES, and keyframe-
aligned segments at /<mount>/playlist.m3u8.
Sources, transcoders, and relays share a common pipeline/stream abstraction so
stats (BytesIn/BytesOut via atomics), health, and lifecycle behave
uniformly regardless of where the bytes came from. Transcoded outputs are tied
to a configured encoder that re-attaches on source resume, which is why they're
exempt from silent-mount reaping (Transcoding).
- No in-process self-updater. Removed deliberately; distributions own the binary (Installation).
- No shared-state clustering. Relay chaining works (node pulls from node), but multiple nodes don't share listener/stream state. Scale a single node vertically and to its bandwidth ceiling, or chain edges.
- Multi-tenant is schema-level groundwork, not a finished feature.
Next: Developing · Deployment · Command Line and Signals
Repository · Releases · Issues · Security policy · Apache-2.0
Getting started
Streaming
Integrations
Operations
Internals
Help