- Follow-up tracks are sent to the player only 20 s before they start, so queue changes in the app still take effect; emptied queue removes them - Playlist end: pause before closing the stream and refuse reconnects to finished sessions (first track briefly replayed before) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 MediaRenderer 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)
- 🔐 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
- 🖼️ Title and cover art in the BluOS app
- 🔁 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: live status page (now playing, player state, event log) and a settings page behind basic auth
- 🐳 Single Python file, standard library only, tiny Docker image
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 --build
docker compose logs -f
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 |
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 |
The status page never shows credentials or player URLs; the cover is proxied through the bridge. The UI texts are currently German. Basic auth runs over plain HTTP – put a reverse proxy with TLS in front if your network isn't trusted.
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 (ICY metadata); the cover stays at the first track. Seeking restarts the stream at the new position (short pause). If two tracks can't be stitched (e.g. MP3 → FLAC), the bridge falls back to a normal track change.
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. - 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 |
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