# Snapcast
A pure-Elixir **Snapcast server**: speak [Snapcast](https://github.com/badaix/snapcast)'s
binary protocol directly to `snapclient`s, owning the audio clock and timestamping every
chunk — so there is **no external snapserver** and no ffmpeg/snapserver pacing to fight.
The server stamps each `WireChunk` with the server-clock time at which it should play, and
each client plays it `bufferMs` later on its sync-corrected clock. Because the server (not
arrival order) assigns timestamps, there is no producer/consumer drift: the only requirement
is to send a chunk before its play deadline, and the `bufferMs` lead absorbs all jitter.
`ffmpeg` is required on the `PATH` (or configured) — it is used purely as a decoder to turn
sources into raw PCM.
## Install
```elixir
def deps do
[{:snapcast, "~> 0.1.1"}]
end
```
## Configure
```elixir
config :snapcast,
enabled: true,
port: 1704,
bind_ip: {0, 0, 0, 0},
# fixed PCM output format: {sample_rate, bits_per_sample, channels}
format: {48_000, 16, 2},
# optional: receive lifecycle events
listener: MyApp.SnapcastListener
```
All settings are optional; see `Snapcast` for the full list and defaults
(`format`, `chunk_ms`, `buffer_ms`, mDNS advertising, a supervised local
`snapclient`, the `ffmpeg` path, …).
Snapcast decodes every source to the configured fixed PCM output format. To send
24-bit/96 kHz stereo PCM to clients, configure:
```elixir
config :snapcast,
format: {96_000, 24, 2}
```
Supported PCM bit depths are 16, 24, and 32 bits. The default remains
48 kHz/16-bit stereo for broad snapclient compatibility; use a higher fixed
format only when the target clients and output chain support it.
## Supervise
```elixir
children = [
# ... your other children ...
] ++ Snapcast.children()
Supervisor.start_link(children, strategy: :one_for_one)
```
`Snapcast.children/0` returns the server subtree when `enabled: true`, otherwise `[]`.
## Play
```elixir
# Stream a file/URL to one or more connected clients (by their snapclient host id)
Snapcast.play("/music/track.flac", ["kitchen", "office"], position_ms: 0)
Snapcast.pause()
Snapcast.resume()
Snapcast.seek(30_000)
Snapcast.set_volume("kitchen", 40)
Snapcast.stop_playback()
Snapcast.clients()
#=> [%{pid: #PID<...>, client_id: "kitchen", name: "Kitchen"}, ...]
```
A `source` may be a binary path/URL or a **0-arity function** returning one — the
function is called when the stream (re)starts, which is handy for short-lived signed
URLs that must be fetched fresh on each play/seek.
## Lifecycle events
Implement `Snapcast.Listener` to be told when clients connect/disconnect and how
playback is progressing:
```elixir
defmodule MyApp.SnapcastListener do
@behaviour Snapcast.Listener
@impl true
def clients_changed, do: MyApp.broadcast_endpoints()
@impl true
def progress(endpoint, position_ms), do: MyApp.Playback.progress(endpoint, position_ms)
@impl true
def ended(endpoint), do: MyApp.Playback.ended(endpoint)
end
```
The `endpoint` term is whatever you passed as `:endpoint` to `Snapcast.play/3` — it is
opaque to the server and echoed back unchanged.
## License
GPL-3.0-or-later. See [LICENSE](LICENSE).