Skip to content
andrew.dunn.dev

Painless Bit-Perfect from Navidrome to a Streamer

We self-host our music on Navidrome. A lot of what gets played in our house lately is audio scraped from short videos and the kids’ shows our children love, kept as playlists in Navidrome and addressable from any device that speaks the Subsonic API. Self-hosting means those playlists do not break when a streaming-service catalog quietly rotates a track out next month.

The companion question for any Subsonic server is the phone client. We have tried Arpeggi and liked it. We have spent the most time with Narjo. The interaction model fits the way we browse, and the maintainer is a single person who answers messages and is willing to work through a feature when a user is willing to test thoroughly. The responsiveness is the part of this we did not expect to find, and is what has made the rest of this post plausible.

What we want next is a different listening pattern in the living room. Today, listening to music without a screen on means going to the record player; juggling that with little kids around is not always practical. The alternatives we have used are an Apple TV playing soma.fm, or an Apple TV plus AirPlay from Narjo. Both work. Both leave the television on, which is the part we are trying to escape. We want the big screen off, and the only display on to be a small streamer’s own screen showing album art, with the option to turn that off too. Beyond AirPlay, the goal is bit-perfect playback at native sample rate. The stretch goal is DSD.

The standard mechanism for the phone-as-remote pattern is UPnP / DLNA: the phone is a control point, the receiving box is the renderer, the phone itself never decodes audio. Narjo speaks UPnP. The piece we needed was a renderer that handles native passthrough including DSD, with a small front-of-box display for album art and a screen-off mode.

UPnP HAND-OFF · THE PHONE NEVER DECODES AUDIOM-SEARCH multicast · ssdp_dump.pydescriptor XML · non-RFC-4122 UDNSetAVTransportURI + Play · stream URLGET /rest/stream?format=rawFLAC or DSD bytes at native rateGetTransportInfo · polls until PLAYINGCONTROL POINTNarjo on the iPhoneRENDEREREversolo DMP-A6SERVERNavidromesends control, never audiodecodes, feeds its own DACserves the untouched file

The phone is only a control point. Once it hands over the stream URL, the Eversolo pulls the raw bytes from Navidrome itself, which is what keeps the path bit-perfect.

The Eversolo DMP-A6 fits that slot on paper. We got one.

It does not show up in Narjo. mconnect Player, a generic UPnP control point, finds it instantly on the same iPhone over the same network. So the wire is fine. The interesting question is what about the Eversolo’s advertisement Narjo is rejecting before it lands on the device list.

With an open-source client this would be a fork-and-read situation. Narjo is closed-source. What we had instead was a responsive maintainer and the recognition that he does not have an Eversolo of his own to test against. The asymmetric move was to send him a packet of evidence: what a control point actually receives from this device on the wire, plus a direct SOAP run that proves the renderer side is sound independent of any iOS code.

That is what narjo-eversolo-upnp is. Python, stdlib only, no install.

  • ssdp_dump.py sends the M-SEARCH multicast, captures responses, groups by USN.
  • descriptor_fetch.py GETs the device descriptor XML and prints a parsed summary.
  • avtransport_proof.py POSTs SOAP SetAVTransportURI and Play, then polls GetTransportInfo and GetPositionInfo until the renderer reports PLAYING.

The README names a working hypothesis. The Eversolo’s UDN is uuid:800a805eef11-dmr: twelve hex characters of MAC-derived identifier with a literal -dmr suffix and no version or variant bits. It is not RFC 4122 conforming. A control point that validates the UDN as a UUID, by passing it to a UUID(...) constructor or a strict 8-4-4-4-12 regex, drops the device before it surfaces. That is the parser-trip candidate.

The whole posture rests on the maintainer being responsive. Closed source plus a willing maintainer plus a willing user to test means evidence is more useful than a bug report.