13 KiB
bluos-upnp-bridge
Play your Subsonic / DLNA music on Bluesound (BluOS) players – controlled from Symfonium or any other UPnP app
🏰 Website · 📰 Community · 🐘 Mastodon · 💬 Support
Why?
BluOS players (Bluesound, NAD, …) can't be extended with your own music services, and BluOS deliberately does not act as a UPnP/DLNA renderer. If your music lives in Subsonic, Airsonic, Navidrome, gonic or any other DLNA-capable library, the only "native" ways are SMB shares (no playlists, no favourites) or AirPlay (16 bit, phone has to stay in charge).
bluos-upnp-bridge makes a BluOS player show up as a regular UPnP/DLNA and OpenHome renderer on your network. Pick it in Symfonium (or BubbleUPnP, Home Assistant, …), browse your own library with your playlists and favourites – and the Bluesound plays the original file directly from your server. No transcoding, no AirPlay, no extra app.
Symfonium (phone) ── UPnP: "play URL X" ──▶ bluos-upnp-bridge ── BluOS API :11000 ──▶ Bluesound player
│ │
└── rewrites Subsonic credentials └── fetches the stream
to a stream-only service account directly from Subsonic
Features
- 🎵 UPnP/DLNA MediaRenderer (AVTransport, RenderingControl, ConnectionManager, GENA events, SSDP discovery)
- 📜 OpenHome renderer (Product, Playlist, Time, Info, Volume): your app hands over the whole queue and the bridge plays it on its own – the phone may sleep or leave the house
- 🔐 Credential rewriting for Subsonic: the URLs your app hands over contain your login. The bridge replaces them with a dedicated, stream-only service account (token auth) – your password never reaches the player. URLs with credentials that can't be replaced are refused instead of being passed on.
- 🎚️ Play, pause, stop, next track (queued via
SetNextAVTransportURI, short gap between tracks), volume and mute - ⏩ Seeking through a built-in stream proxy (Content-Length + byte ranges)
- 🔗 Gapless playback (experimental) for MP3 and FLAC – live albums, DJ mixes and audio plays run seamlessly from track to track; mixed formats are converted with ffmpeg
- 🖼️ Title and cover art in the BluOS app – updated per track, also in gapless mode
- 🌗 Soft fades on pause, stop and track changes
- 📈 Monitoring endpoints for Uptime Kuma, PRTG and Prometheus
- 🔁 Handles BluOS quirks: falls back to its own queue at the end of a stream, "repeat one" loops URLs,
download.viewis refused by BluOS (rewritten tostream.view) - 📊 Web UI (English / German): live status page (now playing, player state, event log) and a settings page behind basic auth
- 🐳 Python standard library only, multi-arch Docker image (amd64, arm64)
Requirements
- A BluOS player reachable on TCP 11000 from the bridge
- Docker host in the same network as your control point, or an SSDP relay (UDP 1900,
239.255.255.250) between the VLANs – see Networking - For Subsonic: a local (non-LDAP) user that is only allowed to stream (see below)
Quick start
git clone https://git.techniverse.net/scriptos/bluos-upnp-bridge.git
cd bluos-upnp-bridge
cp .env.example .env
nano .env # player IP, bridge IP, Subsonic service account, web password
mkdir -p data && sudo chown 10001 data
docker compose up -d # pulls git.techniverse.net/scriptos/bluos-upnp-bridge:latest
docker compose logs -f
Prefer building yourself? Replace image: with build: . in docker-compose.yaml and run
docker compose up -d --build.
Open http://<bridge-ip>:49494/ for the status page. Your player now appears in Symfonium as
" (BluOS Bridge)".
Subsonic service account
Create a local user in your Subsonic server (e.g. bluos_stream) with only the stream and cover art
roles. Why a separate account?
- The stream URL is stored on the player and visible in the BluOS app – it must not contain your own password.
- Subsonic only supports token authentication for local users. LDAP users would need the password in the URL.
Airsonic Pulse, Navidrome & other OpenSubsonic servers
Set SERVER_TYPE=opensubsonic. Preferably create an API key for the service account (Airsonic Pulse:
Personal settings → API keys) and put it into SUBSONIC_API_KEY. Without a key the bridge uses password
authentication – Airsonic Pulse stores passwords hashed, so token auth isn't possible there. The password only
travels between bridge and server: streams and covers reach the player through the bridge, so PROXY_STREAMS
must stay enabled in this mode.
If your app talks to the server via a public domain while the bridge uses the internal address, add the domain
to SUBSONIC_ALIASES so the bridge recognises the app's URLs.
Configuration
All settings come from environment variables (.env). Settings saved in the web UI are stored in
data/settings.json and take precedence.
| Variable | Default | Description |
|---|---|---|
BLUOS_HOST |
– | IP or hostname of the BluOS player (required) |
BLUOS_PORT |
11000 |
BluOS API port |
ADVERTISE_IP |
– | IP of the Docker host, announced via SSDP (required) |
HTTP_PORT |
49494 |
Port for UPnP and the web UI |
FRIENDLY_NAME |
player name + (BluOS Bridge) |
Name shown in your UPnP app |
SERVER_TYPE |
subsonic |
subsonic (classic Subsonic, token auth) or opensubsonic (Airsonic Pulse, Navidrome, … – API key, without key password auth) |
SUBSONIC_URL |
– | Music server as reached from the bridge, e.g. http://192.168.1.20:4040 – enables credential rewriting |
SUBSONIC_ALIASES |
– | Comma-separated further hostnames your app uses for the same server (e.g. its public domain) |
SUBSONIC_USER |
– | Stream-only service account |
SUBSONIC_PASSWORD |
– | Its password |
SUBSONIC_API_KEY |
– | API key of the service account (opensubsonic only, preferred) |
CLIENT_NAME |
bluos-upnp-bridge |
Client name shown by the server (e.g. user@BluOS Player); {player} = BluOS player name |
PROXY_STREAMS |
true |
Serve Subsonic streams through the bridge (Content-Length + byte ranges → seeking, clean track end) |
GAPLESS |
false |
Experimental gapless playback (needs PROXY_STREAMS), see Gapless playback |
LOSSLESS_ONLY |
true |
Gapless: convert follow-up tracks only if no quality is lost |
FADE_MS |
500 |
Fade in/out on pause, stop and track change in milliseconds, 0 = off |
OPENHOME |
true |
Offer the OpenHome services (restart required) |
UI_LANG |
en |
Language of the web UI: en or de |
REPEAT_OFF |
true |
Switch BluOS repeat off when the bridge starts playback (your app handles repeat) |
WEB_USER / WEB_PASSWORD |
admin / – |
Basic auth for /settings. Settings are disabled without a password |
SSDP_INTERVAL |
60 |
Seconds between SSDP announcements |
LOG_LEVEL |
INFO |
DEBUG, INFO or WARNING |
DATA_DIR |
/data |
Where settings.json is stored |
Web UI
| URL | Auth | Content |
|---|---|---|
/ |
– | Now playing, player and bridge status, live event log |
/settings |
Basic auth | Change settings, test player and Subsonic login, pause/resume, restart the bridge |
/api/status, /api/log |
– | JSON used by the status page |
/api/health |
– | Health JSON – HTTP 200 if bridge, player and music server are fine, otherwise 503 |
/api/prtg |
– | PRTG "HTTP Data Advanced" sensor format (bridge, player, server, latency, playing, uptime) |
/metrics |
– | Prometheus metrics |
The status page never shows credentials or player URLs; the cover is proxied through the bridge. Basic auth runs over plain HTTP – put a reverse proxy with TLS in front if your network isn't trusted.
Uptime Kuma: monitor type HTTP(s) on http://<bridge-ip>:49494/api/health (accepted status code 200).
PRTG: sensor HTTP Data Advanced with URL http://<bridge-ip>:49494/api/prtg.
OpenHome
Apps with OpenHome support (e.g. BubbleUPnP, Linn Kazoo) can send the complete play queue to the bridge instead of one track at a time. The bridge then plays the queue by itself, keeps the BluOS player fed with the current and the following track (gapless mode included) and reports position, track and volume back to every connected app. Your phone can go to sleep – the music keeps playing.
Supported services: Product (single "Playlist" source), Playlist (incl. repeat), Time, Info, Volume. Shuffle is reported but left to the app (it sends the queue in shuffled order).
Note: Symfonium does not support OpenHome – it always uses the UPnP/DLNA path (current + next track), which works fine, but the app stays in charge of the queue.
Gapless playback
BluOS restarts its audio pipeline for every URL, so normally there is a short gap between tracks. With
GAPLESS=true (or the switch on the settings page) the bridge serves one continuous stream instead and
appends the next track your app announces (SetNextAVTransportURI) about 20 seconds before it is due – queue
changes in the app until then are respected:
| Format | How it's stitched |
|---|---|
| MP3 | ID3v2/ID3v1 tags and the Xing/Info frame of follow-up tracks are dropped, MPEG frames are concatenated. A few milliseconds of encoder padding remain. |
| FLAC | The first track's STREAMINFO is marked open-ended (total samples / MD5 unknown), follow-up tracks are appended without their metadata blocks. Requires identical sample rate, channels and bit depth. |
The bridge maps BluOS' playback position to the audible track and reports every track change to your app.
The BluOS app shows the current track in its second line and its cover (ICY metadata). Seeking restarts the
stream at the new position (short pause). The stream takes the format of its first track. Follow-up tracks in
another format are converted with ffmpeg – by default only if no quality is lost (LOSSLESS_ONLY=true):
| Stream | Follow-up track | Result |
|---|---|---|
| FLAC | MP3 | converted to FLAC (just re-packed, no further loss) – gapless |
| FLAC | FLAC with higher sample rate / bit depth | not converted – normal track change, original quality |
| MP3 | FLAC | not converted – normal track change, FLAC in original quality |
With LOSSLESS_ONLY=false every follow-up track is converted (FLAC → MP3 with 320 kbit/s, resampling).
Albums ripped in one format are never converted.
BluOS drops paused "radio" streams after about two minutes and reconnects on resume – the bridge then continues exactly where playback was paused.
Networking
- Host network is required (
network_mode: host): SSDP discovery needs multicast on the LAN interface. - VLANs: if your phone and the bridge live in different networks, relay SSDP (UDP 1900,
239.255.255.250) between them. On OPNsense/pfSense theudpbroadcastrelayplugin does the job. mDNS repeaters are not enough. - Firewall: bridge → player TCP 11000, control point → bridge TCP 49494, player → bridge TCP 49494 (stream proxy), bridge → your music server (HTTP/HTTPS).
- ⚠️ Docker subnets: if a Docker network on the host overlaps the player's subnet (e.g. a default
172.20.0.0/16bridge network and the player in172.20.x.x), the bridge getsNo route to host. Check withip route get <player-ip>.
Known limitations
- Track transitions: without
GAPLESSthere is a short gap between tracks. - Seeking needs the stream proxy (
PROXY_STREAMS=true, default) – without it BluOS can't seek Subsonic streams. - Metadata: BluOS only accepts a single title line and a cover image for URL playback, so the bridge shows
Title – Artist. Album is not shown in the BluOS app. - Image size: ffmpeg (for gapless format conversion) makes the image ~550 MB.
- One player per instance: run one container per player (different
HTTP_PORT). Groups created in the BluOS app work: if the controlled player is the group master, everything plays in sync.
Tested with
| Component | Version |
|---|---|
| Bluesound PULSE MINI 2i | BluOS 4.16.6 |
| Symfonium (Android) | current Play Store version |
| Subsonic | 6.1.6 |
| Airsonic Pulse | 13.2.1 |
Other UPnP control points and Subsonic-compatible servers should work as well – feedback is welcome.
Disclaimer
This project is not affiliated with, endorsed or supported by Lenbrook (Bluesound, NAD, BluOS). It uses the local BluOS HTTP API as available on the player. All trademarks belong to their respective owners.
© Patrick Asmus · Techniverse Network · License