Skip to content

v0.3.0

Choose a tag to compare

@tsirysndr tsirysndr released this 11 Jun 20:14

[0.3.0] - 2026-06-11

Added

  • Server-streaming EventsService.Subscribe — a new zerod-events
    leaf crate owns an in-process broadcast channel that every
    subsystem publishes into. Subscribers filter by stable kind label
    (exact match like stream.state or bt.* wildcard). Slow
    subscribers see a synthetic LaggedEvent rather 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-snapcast crate — hand-rolled JSON-RPC
    2.0 client over TCP port 1705 with a single long-lived connection,
    exponential-backoff reconnect, per-request oneshot correlation,
    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 when forward_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 librespot subprocess
    (crates/stream/src/sources/librespot.rs). Spawns librespot --backend pipe --format S16 and pipes its 44.1 kHz / 2ch S16LE
    stdout through the existing AudioSink so per-stream gain Just
    Works. kill_on_drop on the Child makes Player::cancel() reap
    it cleanly.
  • StreamService.SpotifyStart / SpotifyStop RPCs plus a new
    PlaybackSource enum (Hls / Dash / Spotify) on
    StatusResponse so 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
    returns FAILED_PRECONDITION until you flip it on.
  • A2DP sink mode (Pi-as-Bluetooth-speaker). A new
    crates/bluetooth/src/agent.rs registers a BlueZ pairing agent at
    server boot. RequestConfirmation publishes a
    BluetoothPairingRequest event and either auto-accepts (kiosk
    mode) or parks on a per-address oneshot waiting for
    BluetoothService.RespondPairing; a 30 s timeout falls back to
    rejection so stuck prompts can't leak the channel.
    AuthorizeService accepts the A2DP Source UUID
    (0000110a-…) and emits BluetoothA2dpConnected.
  • SetDiscoverable, RespondPairing, A2dpEnable, A2dpDisable
    RPCs.
    A2dpEnable preflights bluealsa-aplay.service via the
    existing systemd allowlist and returns a helpful "install
    bluez-alsa-utils" message if the unit is missing.
  • zerod a2dp {enable, disable} and zerod 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_unit is auto-appended to the systemd allowlist
    when enabled = true so users don't have to remember to list it
    under [systemd].units.

Changed

  • Stream subsystem now emits state-transition events
    StreamStateChanged from Player::set_state (covers Stopped /
    Buffering / Playing / Paused / Errored), StreamVolumeChanged on
    per-stream gain updates.
  • Bluetooth, systemd, and volume subsystems also emit events on
    successful operations: BluetoothDeviceChanged after pair /
    connect / disconnect / remove, SystemdUnitState after every
    successful zbus verb (re-reads status once), VolumeChanged after
    ALSA mixer set / mute.
  • Player gains a source: AtomicU8 field exposed via
    StatusResponse.source. The HLS / DASH branch sets it from
    ManifestKind; the Spotify branch sets Spotify.
  • bluer pinned to =0.17.4 exact — 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 librespot installed on the device
    (apt install librespot on Debian / Ubuntu / Raspberry Pi OS).
    librespot itself is not bundled — zerod only supervises the
    subprocess.
  • A2DP audio routing leans on bluealsa-aplay from
    bluez-alsa-utils. zerod registers 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