v0.3.0
[0.3.0] - 2026-06-11
Added
- Server-streaming
EventsService.Subscribe— a newzerod-events
leaf crate owns an in-processbroadcastchannel that every
subsystem publishes into. Subscribers filter by stable kind label
(exact match likestream.stateorbt.*wildcard). Slow
subscribers see a syntheticLaggedEventrather than a closed
stream. The reserved variants (SnapcastClientChanged,
LibrespotStateChanged, A2DP connect/disconnect) ship now so later
features don't need a wire bump. zerod events tail [--filter …]subcommand — streams events as
one JSON line per event.SnapcastService+zerod-snapcastcrate — hand-rolled JSON-RPC
2.0 client over TCP port 1705 with a single long-lived connection,
exponential-backoff reconnect, per-requestoneshotcorrelation,
and fail-fast on calls made while disconnected. MVP verbs:
GetServerStatus,ListClients,ListSnapStreams,
SetClientVolume/Latency/Name,SetGroupStream/Mute/Clients. Push
notifications (Client.OnVolumeChanged, …) are forwarded onto the
event bus whenforward_notifications = true.zerod snapcast {status, clients, streams, volume, latency, name, group-stream, group-mute, group-clients}subcommands.[snapcast]config section (enabled/host/port/
forward_notifications).- Spotify Connect source via a
librespotsubprocess
(crates/stream/src/sources/librespot.rs). Spawnslibrespot --backend pipe --format S16and pipes its 44.1 kHz / 2ch S16LE
stdout through the existingAudioSinkso per-stream gain Just
Works.kill_on_dropon theChildmakesPlayer::cancel()reap
it cleanly. StreamService.SpotifyStart/SpotifyStopRPCs plus a new
PlaybackSourceenum (Hls/Dash/Spotify) on
StatusResponseso clients can tell what's playing.zerod stream spotify {start, stop}subcommands.[librespot]config section (enabled/binary/name/
bitrate/cache_path). Disabled by default —SpotifyStart
returnsFAILED_PRECONDITIONuntil you flip it on.- A2DP sink mode (Pi-as-Bluetooth-speaker). A new
crates/bluetooth/src/agent.rsregisters a BlueZ pairing agent at
server boot.RequestConfirmationpublishes a
BluetoothPairingRequestevent and either auto-accepts (kiosk
mode) or parks on a per-addressoneshotwaiting for
BluetoothService.RespondPairing; a 30 s timeout falls back to
rejection so stuck prompts can't leak the channel.
AuthorizeServiceaccepts the A2DP Source UUID
(0000110a-…) and emitsBluetoothA2dpConnected. SetDiscoverable,RespondPairing,A2dpEnable,A2dpDisable
RPCs.A2dpEnablepreflightsbluealsa-aplay.servicevia the
existing systemd allowlist and returns a helpful "install
bluez-alsa-utils" message if the unit is missing.zerod a2dp {enable, disable}andzerod bluetooth {discoverable, respond-pairing}subcommands.[bluetooth.a2dp]config section (enabled/
bluealsa_aplay_unit/auto_accept_pairings/adapter_alias/
discoverable_on_boot/discoverable_timeout_secs). The
bluealsa_aplay_unitis auto-appended to the systemd allowlist
whenenabled = trueso users don't have to remember to list it
under[systemd].units.
Changed
- Stream subsystem now emits state-transition events —
StreamStateChangedfromPlayer::set_state(covers Stopped /
Buffering / Playing / Paused / Errored),StreamVolumeChangedon
per-stream gain updates. - Bluetooth, systemd, and volume subsystems also emit events on
successful operations:BluetoothDeviceChangedafter pair /
connect / disconnect / remove,SystemdUnitStateafter every
successful zbus verb (re-reads status once),VolumeChangedafter
ALSA mixer set / mute. Playergains asource: AtomicU8field exposed via
StatusResponse.source. The HLS / DASH branch sets it from
ManifestKind; the Spotify branch setsSpotify.bluerpinned to=0.17.4exact — the agent callback shape varies
across minor versions and silent rewires of the pairing flow are
worse than a build break.
Notes
- Spotify Connect requires
librespotinstalled on the device
(apt install librespoton Debian / Ubuntu / Raspberry Pi OS).
librespotitself is not bundled —zerodonly supervises the
subprocess. - A2DP audio routing leans on
bluealsa-aplayfrom
bluez-alsa-utils.zerodregisters the BlueZ pairing agent and
flips the adapter; the actual SBC / AAC decode happens in
bluealsa-aplay. - A2DP pair flows that need a PIN code or passkey input fall through
to BlueZ's default rejection — legacy headphones using pre-2.1
pairing won't work yet.
Full Changelog: v0.2.0...v0.3.0
Full Changelog: v0.2.0...v0.3.0