Releases: tsirysndr/zerod
Releases · tsirysndr/zerod
Release list
v0.3.2
[0.3.2] - 2026-06-19
Fixed
- HLS/DASH playback survives transient network loss. Previously a
single fetch failure logged a warning and bumpednext_play_seq,
so a brief outage burned through the segment list in seconds and
silently killed the stream. The player now:- Retries the same seq with exponential backoff (500 ms → 1 s →
2 s → 4 s, capped at 5 s) and transitions toBufferingwhile
reconnecting, flipping back toPlayingon the first success. - Detects the live-edge-rolled-past case: when the requested
seq has fallen off the front of the live window (typical after
a long outage), the fetcher returnsSegFetch::SkipAhead(front)
and the loop resyncs to the current window front instead of
spinning forever on an expired seq. - Logs reconnect attempts (
retry #N in Xms) and a recovery line
(reconnected after N failed attempt(s)) so outages are visible
injournalctlwithout a debug build.
- Retries the same seq with exponential backoff (500 ms → 1 s →
Full Changelog: v0.3.1...v0.3.2
v0.3.1
[0.3.1] - 2026-06-12
Fixed
- HLS playback on macOS no longer clicks/jumps every few seconds.
Four cumulative fixes, in order of audible impact:- Continuous decode across HLS segments.
decode_segmentis now
aStreamDecoderstruct that owns oneBox<dyn Decoder>for the
whole stream. The format reader is still rebuilt per segment, but
the AAC decoder's state (predictor history, SBR/PS continuity,
encoder priming) persists — so the ~50 ms of priming silence /
decoder warm-up no longer reappears at every segment boundary.
This is the model ffplay uses against libavcodec. CpalSinkrewrite. ReplacedMutex<VecDeque<u8>>+Condvar
with anrtrblock-free SPSC ring and moved the consumer half
into the cpal callback. The CoreAudio realtime thread now does
zero locks, allocations, or syscalls per buffer. Resampler upgraded
from nearest-neighbour (which audibly aliased 44.1 → 48 kHz) to
linear interpolation between adjacent source frames.- 1.5 s sink pre-buffer. Resampler waits for ~1.5 s of source
frames before priming so brief producer hiccups can't immediately
underrun. Ring capacity bumped to 512 K samples so the bigger
pre-buffer fits well under the 50 % cap. - Background per-segment prefetch. The playback loop no longer
.awaitsfetcher::prefetch; it'stokio::spawn-ed instead, so
HTTP latency for upcoming segments is hidden from the gap between
writes. Cache miss in the main loop still falls back to a
synchronous fetch, so correctness is preserved.
- Continuous decode across HLS segments.
Full Changelog: v0.2.0...v0.3.1
Full Changelog: v0.2.0...v0.3.1
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
v0.2.0
[0.2.0] - 2026-06-11
Added
- mDNS / zeroconf discovery. The server advertises itself as
_zerod._tcp.local.via a newzerod-discoverycrate (pure-Rust
mdns-sd, no Avahi / Bonjour). The instance name defaults to the
machine hostname and the published TXT records include the daemon
version. zerod discoversubcommand — lists every responder on the LAN
with name, host, port, and advertised version.--name/ZEROD_NAMEflag — when multiple servers respond,
pick one by its mDNS instance name.--discover-timeout-ms/ZEROD_DISCOVER_TIMEOUT_MSflag —
override the default 1500 ms browse window.[mdns]section inzerod.toml—enabled(defaulttrue) and
name(empty → hostname).
Changed
--hostis now optional. Omitting it (andZEROD_HOST) triggers
mDNS discovery and connects to the only responder. Previously it
defaulted tolocalhost.- IPv4 address selection during discovery skips loopback and Docker's
172.17.0.0/16default bridge, sozerodrunning inside Docker or
next to adocker0interface no longer leaks the bridge IP to clients. zerod service installpost-install hint now reflects the
discovery-first UX (zerod system healthinstead of
zerod --host <pi> system health).
Full Changelog: v0.1.0...v0.2.0
v0.1.0
Full Changelog: https://github.com/tsirysndr/zerod/commits/v0.1.0