Skip to content

Build a Streaming Box

vicrodh edited this page Jul 24, 2026 · 3 revisions

Build a Streaming Box (Step-by-Step)

This is the guided version of the Headless Daemon manual: one worked example, from a bare board to a silent always-on streamer wired to your DAC that you drive from the Qobuz app on your phone, from any terminal on your LAN, or from your own scripts.

The example rig is a Raspberry Pi 4 with a HiFiBerry DAC+ HAT — but the Pi is the example, not the target. Everything below works identically on any always-on Linux box: an old laptop in a closet, a NUC behind the TV, an LXC container, a fanless mini PC. Wherever a step is Pi-specific it says so; everywhere else, substitute your own hostname and DAC.

Time budget: about 30 minutes, all of it over SSH. No screen, no keyboard, no desktop environment on the box.

The example rig

Piece Example Substitute with
Box Raspberry Pi 4B (4 GB), Raspberry Pi OS Bookworm any Linux box/container that's always on
DAC HiFiBerry DAC+ (I2S HAT) any USB DAC — often the simpler option
Network Ethernet Wi-Fi works; wired is happier for hi-res
Access SSH from your desktop that's it

Released qbzd binaries are built against glibc 2.35, so they run unmodified on Raspberry Pi OS / Debian Bookworm and similarly-aged distros — no toolchain, no compiling on the Pi.

HiFiBerry-style HAT only: enable the overlay and disable onboard audio in /boot/firmware/config.txt (dtoverlay=hifiberry-dacplus, dtparam=audio=off), then reboot — see HiFiBerry's own docs for your exact board. A USB DAC needs none of this; plug it in and it enumerates.

Step 1 — Get qbzd onto the box

Grab the aarch64 tarball (for a Pi; x86_64 for a PC) from the latest release, copy it over, and install:

scp qbzd-*.tar.gz pi@streambox:
ssh pi@streambox
tar xf qbzd-*.tar.gz && cd qbzd-*
sudo install -Dm755 qbzd /usr/bin/qbzd
sudo install -Dm644 qbzd.service /usr/lib/systemd/user/qbzd.service
systemctl --user daemon-reload
qbzd version

Not on systemd? qbzd service prints a ready-to-install OpenRC/runit/systemd file with the install steps as comments — see Install on the manual page.

Step 2 — Log in

Two ways. Pick one.

Fresh login (nothing configured anywhere yet):

qbzd login

Login is browser-based OAuth — the printed URL automatically uses the box's LAN address, so paste it into the browser on your phone or desktop, approve, done. No password ever touches the terminal. (Stuck? qbzd login --paste and the other fallbacks are on the manual page.)

Bring your desktop settings instead — if you already have quality preferences, gapless, and a tuned setup in desktop QBZ, hand them off rather than re-doing them in a TUI:

# on the desktop box
qbzd settings export --from desktop --include-auth
scp qbz-settings-*.qbzb pi@streambox:

# on the box
qbzd settings import qbz-settings-*.qbzb --include-auth
shred -u qbz-settings-*.qbzb        # then delete the desktop copy too

--include-auth must be passed on both sides or credentials are deliberately skipped. The bundle file is your logged-in session — treat it like a password: move it over SSH only, delete it at both ends once imported.

The one gotcha: the bundle carries the source machine's audio config and Connect name — your desktop's PipeWire device and its QBZ (desktop) identity. Always redo Step 3 and Step 5's naming after an import, or the box will collide with your desktop's identity in the Qobuz app.

Step 3 — Point it at the DAC

First, ask ALSA what your DAC is called:

aplay -L | grep -A1 ^hw:

On the example Pi the HiFiBerry shows up as hw:CARD=sndrpihifiberry,DEV=0. Use that CARD= name form, not hw:0,0 — card numbers can reshuffle between boots; card names don't.

Now configure — either interactively:

qbzd setup        # → Audio → Backend: ALSA → Output device: pick the DAC

qbzd setup TUI, Audio screen

…or in three non-interactive lines (handy for scripting a box build):

qbzd settings set audio.backend alsa
qbzd settings set audio.device "hw:CARD=sndrpihifiberry,DEV=0"
qbzd settings set audio.alsa_plugin hw

hw means direct hardware access: no mixer, no resampler, nothing between the decoder and the DAC. That's the bit-perfect path — a 96 kHz/24-bit track leaves the box at 96 kHz/24-bit, and if your DAC has a display you'll see the rate switch per track. If PipeWire happens to be installed (Raspberry Pi OS ships it), it doesn't matter: with the ALSA backend the daemon opens the device directly and PipeWire is simply bypassed.

Step 4 — Make it survive reboots

systemctl --user enable --now qbzd
sudo loginctl enable-linger $USER

Linger is the step people skip and then wonder why the box vanished. Without it, your user services die the moment your SSH session closes. qbzd status warns you if it's off.

Then the one command to confirm everything at once:

qbzd status

One line each for auth, audio device, playback, Qobuz Connect, and network. Anything wrong names itself here.

Step 5 — Play something

Give the box its public name first (this is what shows in the Qobuz app):

qbzd qconnect name "Living Room"

From your phone — open the Qobuz app, open the Connect device picker, choose Living Room. Playback starts on the box's DAC; the phone is just the remote. This is the everyday mode: nothing to install, everyone in the house already knows how to use it.

From any terminal on your LAN — the CLI is a full remote control, not just play/pause. Point it at the box once:

export QBZD_HOST=streambox:8182     # or an IP; put it in your shell rc

Then the basic loop is search → play:

qbzd search "dark side of the moon" --type album
qbzd play album:<ID>                # an id from the search results…
qbzd play https://open.qobuz.com/album/…   # …or just paste a share URL
qbzd now
qbzd volume 35

qbzd play swallows anything — track:/album:/artist:/playlist: ids, Qobuz share URLs, even Deezer links (resolved by metadata match). Bare qbzd play resumes.

Beyond that, the whole catalog is there:

qbzd radio artist:<ID>              # endless radio from a seed artist/track/album
qbzd discover new-releases          # the Discover rails, in your terminal
qbzd artist <ID> --top              # an artist's top tracks, with ids
qbzd queue list                     # what's coming up
qbzd queue add <TRACK_ID> --next    # cut the line

Every list verb takes --ids to print bare ids for piping. Two verbs read ids from stdin — playlist add <ID> - and suggest --seed - — so you can chain them:

# dump an album's tracks into one of your playlists
qbzd album <ID> --ids | qbzd playlist add <PLAYLIST_ID> -

# queue up suggestions seeded from what's playing right now
qbzd suggest --ids | head -5 | while read id; do qbzd queue add "$id"; done

The full verb tables are in the CLI Cheat Sheet.

Step 6 — Automate it

This is where a headless box earns its keep. Three building blocks: the CLI (scriptable, frozen exit codes), qbzd watch (push events, no polling), and the plain HTTP API on :8182.

Wake up to music — a cron job on the box:

30 6 * * 1-5  /usr/bin/qbzd volume 20 && /usr/bin/qbzd play playlist:<ID>
0  8 * * 1-5  /usr/bin/qbzd stop

(On the box itself no QBZD_HOST is needed; from another machine, set it in the crontab line.)

React to what's playingqbzd watch streams daemon events as newline-delimited JSON. A desktop notification for every track change, from your desktop, pointed at the box:

QBZD_HOST=streambox:8182 qbzd watch \
  | jq --unbuffered -r 'select(.type=="TrackStarted") | .data.track.title' \
  | while read -r title; do notify-send "Living Room" "$title"; done

The same stream feeds a status-bar module, an LED panel, an OBS overlay — anything that would otherwise poll.

Love the current track from anywhere:

qbzd fav add track --current

Bind that to a hotkey on your desktop and you can favorite what's playing in the living room without leaving your editor.

No CLI at all — plain HTTP. Everything above is curl-able, which is all a home-automation system needs:

curl -X POST http://streambox:8182/api/playback/toggle
curl http://streambox:8182/api/now-playing

As a Home Assistant rest_command:

rest_command:
  living_room_toggle:
    url: "http://streambox:8182/api/playback/toggle"
    method: POST

GET /api/now-playing returns {"playback": …, "track": …} (track is null when idle) for a REST sensor, and GET /api/artwork/current is a redirect to the live cover art — point an <img> straight at it. On a Pi, the classic move is a GPIO button whose handler is exactly that one curl line: a physical play/pause button on the shelf.

Health check — exit codes are frozen across every verb, so monitoring needs no parsing:

qbzd ping || echo "streambox is down" | mail -s "qbz alert" you@example.com

3 = daemon unreachable, 4 = needs re-login, 5 = audio device trouble — the full table is on the manual page.

Keep it on the LAN

The control port is deliberately LAN-first, like Sonos or MPD: anyone on your network can drive playback, nobody can read your credentials (they never cross the API). An always-on origin shield blocks browser-based abuse, and you can opt into a bearer token or bind to 127.0.0.1 if your LAN isn't fully trusted. It is plain HTTP — never port-forward it to the internet. Details: Network & Security Posture.

When it doesn't sing

  • Silent, or anything weird → qbzd status, always first. It names the failing subsystem and the fix.
  • Box disappears from the Qobuz app after you log out → linger is off (Step 4).
  • DAC not in the device list → re-plug, then r (refresh) on the Audio screen of qbzd setup; check aplay -L sees it at all.
  • Two boxes fighting over one identity → you imported a bundle and skipped the naming gotcha in Step 2 — give each node its own qbzd qconnect name.

More on the manual page's Troubleshooting section.


See also: Headless Daemon · Audio Configuration · HiFi Wizard · Casting and Remote Control

Clone this wiki locally