WaveFlow exposes the active profile's library on the LAN as a urn:schemas-upnp-org:device:MediaServer:1. DLNA-compatible receivers (Yamaha MusicCast, Sonos S2, Kodi, BubbleUPnP, VLC, β¦) discover and stream the collection without any per-receiver pairing.
The integration ships disabled by default β enable it from Settings β Integrations β DLNA / UPnP Server.
A single dedicated worker thread (dlna-worker) owns a tokio runtime and the running tasks. Same pattern as media_controls and discord_presence: a sync DlnaServer handle on AppState ferries Cmd::{Start, Stop, Status} over a crossbeam channel so the rest of the app keeps a sync API.
AppState.dlna ββΊ Cmd channel ββΊ dlna-worker
βββΊ axum HTTP server (port N)
β /description.xml
β /service/{ContentDirectory,ConnectionManager}.xml
β /control/ContentDirectory (SOAP)
β /control/ConnectionManager (SOAP, stub)
β /stream/<track_id> (Range)
β /art/<hash.ext>
β /healthz
βββΊ SSDP announcer + responder (239.255.255.250:1900)Persisted in the global app_setting table because the server is process-wide, not per-profile. Switching profiles re-binds the same listener to whatever the new profile points at.
| Key | Default | Note |
|---|---|---|
dlna.enabled |
0 |
Opt-in. Auto-started at boot when set. |
dlna.server_name |
WaveFlow |
Friendly name shown in controllers. |
dlna.port |
0 |
0 lets the OS pick a free port; the SSDP LOCATION carries the actual port. Pin a value if your firewall is configured for it. |
Object IDs are string prefixes, routed by cds.rs:
0 Root container
ββ 0/artists All artists (paginated)
β ββ 0/artists/<id> Albums for that artist (containers)
ββ 0/albums All albums (paginated)
ββ 0/albums/<id> Tracks for that album (items)
0/track/<id> Single-track BrowseMetadata payloadPagination via StartingIndex + RequestedCount β SQL LIMIT/OFFSET, capped at MAX_PAGE_SIZE = 500. RequestedCount = 0 (the spec's "all") folds to the same cap so a misbehaving controller can't pull 50k tracks into one DIDL document.
Each track DIDL item carries:
dc:title,dc:creator,upnp:artist,upnp:albumupnp:class = object.item.audioItem.musicTrackupnp:albumArtURIpointing at/art/<blake3>.<ext>(probes both the per-profileartwork/dir and the sharedmetadata_artwork/dir)<res protocolInfo="http-get:*:<mime>:DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000β¦">plusduration(H:MM:SS.000, padded for Sonos S2),size,bitrate(in DLNA bytes/s),sampleFrequency,nrAudioChannels.
The transferMode.dlna.org: Streaming and contentFeatures.dlna.org headers on /stream/<id> are mandatory for DLNA controllers to expose a scrubber.
ssdp.rs joins 239.255.255.250:1900 via socket2 (so we get SO_REUSEADDR on Windows + SO_REUSEPORT on unix and coexist with other UPnP services).
- Periodic NOTIFY ssdp:alive β one batch every
CACHE_MAX_AGE/4β 7 minutes, advertisingupnp:rootdevice, the device UUID,MediaServer:1,ContentDirectory:1,ConnectionManager:1. - M-SEARCH responder β unicast HTTP/1.1 200 OK to controllers that probe with
ST:matching one of our targets (orssdp:all).
The device UUID is Uuid::new_v5(NAMESPACE, server_name) so controllers see the same uuid: URN across launches even when the LAN IP changes β no on-disk persistence needed.
/stream/<track_id> parses the Range: header, replies with 206 Partial Content + Content-Range, and pipes the file through tokio_util::io::ReaderStream::with_capacity(64 KiB). take(length) caps the reader so we never overshoot the window even if the controller closes early.
Failures inside the worker are surfaced through DlnaStatus.last_error so the Settings UI can display them inline:
- Bind failure (port in use, no privilege) β
"bind 0.0.0.0:1234: β¦"and the server stays in the stopped state. - SSDP socket failure β HTTP keeps serving for controllers that already know the LOCATION; only auto-discovery breaks. Surfaced as
"SSDP: β¦".
- No
Searchaction βGetSearchCapabilitiesreturns an empty string so controllers fall back to Browse. - No event subscriptions (
SUBSCRIBE/NOTIFYfrom the event sub URLs). Controllers that need them will retry; nothing breaks. - DSD (
.dsf/.dff) doesn't appear in the audio MIME mapping β would need its ownaudio/x-dsdadvertisement once the playback path lands. - The bind is
0.0.0.0. If you're on a public Wi-Fi network, disable the toggle β there's no auth.