snapcast-rs
⚠️ Pre-1.0 — APIs may break on minor version bumps. Until version 1.0, minor releases (e.g. 0.3 → 0.4) may contain breaking changes to the public API. Pin your dependency to a specific minor version if you need stability.
A Rust reimplementation of Snapcast, the excellent multiroom audio system created by Johannes Pohl (badaix). Snapcast synchronizes audio playback across multiple devices with sub-millisecond precision — turning any collection of speakers into a perfectly synced whole-home audio system.
This project exists primarily to serve as a native Rust dependency for SnapDog, a multiroom audio appliance. Rather than shelling out to C++ binaries or bridging through FFI, SnapDog embeds the Snapcast protocol directly as a library — receiving audio, encoding it, distributing it to clients, and controlling playback, all within a single Rust process.
To make this possible, snapcast-rs separates the protocol engine from the application shell. The library crates (snapcast-client, snapcast-server) implement the Snapcast binary protocol, audio encoding/decoding, time synchronization, and mDNS discovery — but own no audio devices, open no HTTP ports, and read no config files. They communicate exclusively through typed Rust channels, making them straightforward to embed in any application.
The binary crates (snapclient-rs, snapserver-rs) are thin wrappers around these libraries. They add the things a standalone application needs: reading audio from pipes and processes, serving the JSON-RPC control API over HTTP and TCP, hosting the Snapweb UI, and outputting audio through platform-native backends via cpal. They are intended as standalone replacements for TCP-based Snapcast audio workflows; WebSocket audio streaming is not implemented yet.
The result is a Snapcast implementation that works as a TCP-audio replacement for common Snapcast deployments and as an embeddable building block for Rust applications that need synchronized multiroom audio.
snapcast-rs is compatible with the original C++ Snapcast over the TCP audio transport when using standard codecs (PCM, FLAC, Opus, Vorbis). However, three optional features break audio compatibility:
| Feature | What it does | C++ behavior |
|---|---|---|
f32lz4 |
32-bit float LZ4 codec | C++ clients reject unknown codec |
custom-protocol |
Application-defined message types (9+) | C++ clients silently ignore |
encryption |
ChaCha20-Poly1305 encrypted f32lz4 | C++ clients reject unknown codec |
If you enable f32lz4 or encryption on the server, C++ clients cannot decode the audio. To prevent them from auto-connecting via mDNS, change the service type:
let config = ServerConfig ;
For full interoperability with C++ clients, use --codec flac or --codec pcm and leave custom-protocol and encryption disabled.
Key Features (Version 0.11+)
- Dynamic Audio Pipeline: The client automatically re-initializes the audio device when the server changes sample rate or channels.
- Integrated Resampling: Automatic fallback to
rubato-based resampling if the local hardware doesn't support the server's native format. - Single Source of Truth Defaults: Ports, schemes, codec names, sample format defaults, mDNS names, bind addresses, and payload limits live in
snapcast-proto. - Bounded Protocol Reads: Client and server reject oversized binary-protocol payloads before allocation.
- Per-Stream Format Ownership: Each server stream owns its codec/sample-format encoder state instead of leaking global defaults into per-stream paths.
- Lossless f32 Decode Path: FLAC and f32lz4 decoders output native f32 samples — no intermediate 16-bit quantization. 24-bit FLAC preserves full resolution end-to-end.
- Configurable Bind Addresses: Library and binary listeners can bind loopback, IPv4, IPv6, or deployment-specific interfaces.
- Systemd Integration: Native
sd-notifysupport on Linux for service readiness and real-time status reporting (volume, codec, format). - Public Audio Monitoring: Public
audio_rxchannel in the library for real-time PCM monitoring and analysis. - End-to-End Testing: Robust integration test suite verifying the entire audio transmission path from server to client.
Architecture
snapcast-rs/
├── snapcast-proto Protocol: binary message serialization
├── snapcast-client Client library: embeddable, f32 audio output
├── snapcast-server Server library: embeddable, f32 audio input
├── snapclient-rs Client binary: cpal audio, software + hardware (ALSA) mixer
├── snapserver-rs Server binary: stream readers, JSON-RPC, HTTP
└── snapcast-tests Integration tests
| Crate | Role | Docs |
|---|---|---|
| snapcast-proto | Binary protocol, message serialization | docs.rs |
| snapcast-client | Client library: embeddable, f32 audio output | docs.rs |
| snapcast-server | Server library: embeddable, f32 audio input | docs.rs |
| snapclient-rs | Client binary: cpal audio, software + hardware (ALSA) mixer | — |
| snapserver-rs | Server binary: stream readers, JSON-RPC, HTTP | — |
| snapcast-tests | Integration tests | — |
Both libraries are pure audio engines — no device I/O, no HTTP, no config files.
Client Library API
use ;
let config = ClientConfig ;
// Create client — returns event receiver and audio output receiver
let = new;
// Run (blocks, reconnects on error)
spawn;
// Monitor decoded audio frames in real-time
spawn;
// Shared state for direct audio device access (used by snapclient-rs)
let stream = clone; // time-synced PCM buffer
let time_provider = clone; // server clock sync
// Events
match event
// Commands
cmd.send.await;
cmd.send.await;
Client Config
ClientConfig
Client Features
| Feature | Default | C dep | Description |
|---|---|---|---|
f32lz4 |
— | none | f32 LZ4 codec (lz4_flex) |
mdns |
✅ | none | mDNS server discovery |
websocket |
— | none | Experimental transport module only; SnapConnection::new rejects ws:// until binary audio WS is implemented |
tls |
— | none | Experimental WSS module only; SnapConnection::new rejects wss:// until binary audio WS is implemented |
resampler |
— | none | Sample rate conversion (rubato) |
custom-protocol |
— | none | Custom binary messages (type 9+) |
encryption |
— | none | ChaCha20-Poly1305 encrypted f32lz4 |
Server Library API
use ;
let config = ServerConfig ;
// Create server — returns event receiver
let = new;
// Add audio streams (each gets its own encoder)
let audio_tx = server.add_stream;
// Per-stream codec override
let zone2_tx = server.add_stream_with_config;
// Run (blocks until Stop)
spawn;
// Typed commands
cmd.send.await;
cmd.send.await;
cmd.send.await;
cmd.send.await;
cmd.send.await;
cmd.send.await;
let = channel;
cmd.send.await;
// Push f32 audio directly
audio_tx.send.await;
// Reactive events
match event
Server Config
ServerConfig
Per-Stream Config
StreamConfig
Server Features
| Feature | Default | C dep | Description |
|---|---|---|---|
f32lz4 |
— | none | f32 LZ4 codec (lz4_flex) |
mdns |
✅ | none | mDNS service advertisement |
flac |
✅ | libFLAC | FLAC encoding |
opus |
— | libopus | Opus encoding |
vorbis |
— | libvorbis | Vorbis encoding |
custom-protocol |
— | none | Custom binary messages (type 9+) |
encryption |
— | none | ChaCha20-Poly1305 encrypted f32lz4 |
Authentication
The server supports streaming client authentication matching the C++ implementation:
use ;
// Config-based auth (users/roles from config file)
let auth = new;
let config = ServerConfig ;
// Or implement AuthValidator for custom auth (database, LDAP, etc.)
Clients send Basic auth in the Hello handshake. The server validates credentials and checks the "Streaming" permission. Unauthenticated or unauthorized clients receive Error(401/403) and are disconnected.
Client Filtering
Filter clients at connection time based on MAC address, hostname, or any Hello field:
use ClientFilter;
use Hello;
/// Only accept clients whose MAC is in the allowlist.
;
let config = ServerConfig ;
Rejected clients are disconnected immediately after Hello with a warning log.
Network Ports
| Port | Protocol | Owner | Purpose |
|---|---|---|---|
| 1704 | TCP | Library | Binary protocol (audio + time sync) |
| 1705 | TCP | Binary | JSON-RPC control |
| 1780 | HTTP/WS | Binary | JSON-RPC + Snapweb UI |
Libraries open only the configured binary protocol listener. JSON-RPC/HTTP are binary-only.
Bind addresses are configurable:
Equivalent config file keys are bind_to_address or bind_address under [tcp-streaming], [tcp-control], and [http].
Codecs
| Codec | Default | C dep | Precision | Latency |
|---|---|---|---|---|
| PCM | ✅ always | none | 16/24/32-bit | zero |
| f32lz4 | optional | none | 32-bit float | zero |
| FLAC | ✅ default | libFLAC | 16/24-bit (decoded to f32) | 24ms (block size) |
| Opus | optional | libopus | 16-bit | 20ms |
| Vorbis | optional | libvorbis | lossy | variable |
f32lz4 path (zero conversion, full precision):
f32 → LZ4 compress → network → LZ4 decompress → f32
Bandwidth Comparison
48 kHz, 16-bit, stereo:
| Codec | Precision | Bandwidth | vs PCM |
|---|---|---|---|
| PCM | 16-bit | 1,536 kbit/s | 100% |
| FLAC | 16-bit | ~700 kbit/s | ~45% |
| f32lz4 | 32-bit float | ~1,800 kbit/s | ~120% |
96 kHz, 24-bit, stereo:
| Codec | Precision | Bandwidth | vs PCM |
|---|---|---|---|
| PCM | 24-bit | 4,608 kbit/s | 100% |
| FLAC | 24-bit | ~2,500 kbit/s | ~55% |
| f32lz4 | 32-bit float | ~3,600 kbit/s | ~78% |
f32lz4 trades bandwidth for precision (32-bit float) and zero conversion latency. On a LAN (100+ Mbit/s) the extra bandwidth is negligible. On WiFi it's still fine.
For bandwidth-constrained networks: use FLAC. For quality + simplicity: f32lz4.
⚠️ f32lz4 is not compatible with the original C++ Snapcast. C++ clients/servers do not recognize this codec. Use
--codec flacor--codec pcmfor interoperability with C++ Snapcast.
Custom Binary Protocol (--features custom-protocol)
⚠️ snapcast-rs only. This feature extends the Snapcast binary protocol with application-defined message types. It is not part of the original C++ Snapcast.
C++ Snapcast safely ignores unknown message types — the factory returns nullptr for any unrecognized type, and the connection logs a warning and continues:
// C++ snapcast: common/message/factory.hpp
switch
This means Rust servers can send custom messages to Rust clients while C++ clients on the same server simply ignore them.
Use Case: Client-Side EQ
A Rust-based multiroom system (e.g. SnapDog) can push per-client EQ settings through the binary protocol — no JSON-RPC, no HTTP, no extra connections:
use CustomMessage;
// Server pushes EQ to a specific client
cmd.send.await;
// Client receives and applies
match event
Message types 0–8 are reserved by the Snapcast protocol. Types 9+ are available for application use. The payload format is opaque — the library passes raw bytes, the application chooses JSON, bincode, protobuf, or any other format.
Encryption (--features encryption)
Optional ChaCha20-Poly1305 authenticated encryption for f32lz4 audio chunks. Pure Rust (RustCrypto), zero C dependencies.
Binary usage
The binaries support f32lz4e as a codec alias — it selects f32lz4 with encryption using a built-in default key:
# Server — just works, default PSK
# Client — just works, default PSK matches
# Custom PSK (must match on both sides)
Library usage
// Server
let config = ServerConfig ;
// Client
let config = ClientConfig ;
- Key derivation: HKDF-SHA256 from PSK + random session salt
- Per-chunk: 12-byte nonce (counter) + 16-byte auth tag = 28 bytes overhead (~0.6%)
- Protects audio content and integrity — metadata (time sync, settings) stays plaintext
- Wrong key: client connects but audio chunks are silently dropped
Documentation
API documentation: snapcast-client · snapcast-server · snapcast-proto
Generate locally: cargo doc --open --no-deps
Building
Native codec features need their system libraries available to the linker:
| Feature | Linux package examples | macOS package examples |
|---|---|---|
flac |
libflac-dev |
flac |
opus |
libopus-dev pkg-config |
opus pkg-config |
vorbis |
libvorbis-dev |
libvorbis |
The CI workflow validates the default build plus custom protocol, encryption, client transport/resampler features, and the Linux native codec feature set with those packages installed.
Usage
# Server
# Client
# Feed audio
Code Quality
#![forbid(unsafe_code)]on protocol crate#![deny(unsafe_code)]on client/server libraries, with narrow module-level FFI exceptions for platform clocks and native FLAC callbacks- Warning-clean
cargo check,cargo test, andcargo clippy -- -D warningsgates - No crate-level
#![allow]blankets and no TODO markers in production code - Shared defaults/constants in
snapcast-protoinstead of duplicated magic strings - Constant-time password comparison (subtle crate)
- Bounded binary-frame payload allocation on both client and server
- Structured tracing logging
Releases
Pre-built binaries for every release:
| Platform | Client | Server | FLAC |
|---|---|---|---|
| Linux x86_64 | ✅ | ✅ | ✅ |
| Linux aarch64 | ✅ | ✅ | ✅ |
| macOS x86_64 | ✅ | ✅ | ✅ |
| macOS aarch64 | ✅ | ✅ | ✅ |
| Windows x86_64 | ✅ | ✅ | — |
Download from GitHub Releases.
Library crates published to crates.io: snapcast-proto, snapcast-client, snapcast-server.
Known Limitations
- No WebSocket audio transport — the server exposes JSON-RPC WebSockets at
/jsonrpc, but binary audio streaming is TCP-only. The CLI rejectsws://andwss://for audio clients until a verified binary audio WebSocket contract is implemented. - Dynamic
Stream.AddStreamis application-owned — the embeddable server can expose and route streams created beforerun(). RuntimeStream.AddStreamreturns an explicit error because the library does not own source readers after startup. - Opus is a native optional feature —
--features opusrequires system Opus discoverable bypkg-configor the native build tools needed byaudiopus_sys.
License
GPL-3.0-only — same as the original Snapcast.