api reference
Destinations
Somewhere audio can be sent. A zone is one kind of destination, but this server does not require zones at all — it can run as a DLNA source with a streaming account and nothing else, and a client can play audio itself with no zone configured anywhere.
curl -s http://server:7090/api/v1/destinations
{ "destinations": [
{ "id": "3", "name": "Kitchen", "kind": "zone", "protocol": "sendspin", "available": true },
{ "id": "9000", "name": "This tab", "kind": "local", "protocol": "sendspin", "available": true }
]}
Playback works the same on either: POST /destinations/{id}/play, /pause, /volume and the rest are the same commands as their /zones/{id}/… counterparts, and a destination's id is its zone id.
A local destination is not in GET /zones. A zone is a room in a house — everyone may see that the kitchen is playing. A browser tab is not a room, so it appears in neither the zone list nor the events stream, not even for the browser that registered it. Otherwise every client's room list would grow with every tab anyone opened, and one person's phone would sit beside the speakers, playable by mistake.
A tab does not need it there either: the Sendspin socket it already holds to receive audio pushes title, artist, album, artwork, playback state and progress as server/state. That is the source that cannot be out of step with the sound, and reading the same thing twice from two places is one time too many.
GET /destinations is where a tab finds itself, and it is private: send the clientId you were given at registration, as an X-Sonn-Client-Id header or ?clientId=, and your own destination is listed alongside the configured zones. Send nothing and you get the zones only.
Every zone route still works on a local destination — /zones/{id}/queue, favourites, grouping — because it is a zone underneath. It is only absent from the listings. Drive it through /destinations/{id}/… and you never need to know that.
/destinations exists for the one thing /zones cannot express: registering a client as somewhere audio goes, and telling a zone from a tab (kind). Everything after that is a zone.
Playing audio yourself
A client can be the thing that plays. Register, then connect:
curl -X POST http://server:7090/api/v1/destinations/local -d '{"name":"This tab"}'
{ "id": "9000", "kind": "local", "clientId": "browser-9000",
"streamUrl": "ws://server:7090/sendspin", "protocol": "sendspin", "available": true }
Then open streamUrl and announce clientId in a Sendspin client/hello. Audio arrives as PCM frames on that socket, and POST /destinations/{id}/play starts it. The Sendspin protocol does the rest — format negotiation, clock sync, grouping — and sendspin-js implements the client side for a browser.
streamUrl is the socket itself, ws://…/sendspin — not a base url to append a path to. A library that wants an origin and adds its own path needs the http form of this url, not your page's origin: in development those differ, and pointing it at your own origin dials a port where nothing listens. There is no error when that happens — the connect simply hangs — so bound your connect attempt and treat the timeout as a failure.streamUrl is built from the address your request arrived on, so it is reachable from wherever you are, including behind a proxy. Pass your own clientId back on a later call to reclaim the same registration — that is what a page reload needs, and without it every refresh leaves an orphan behind until it times out. The registration disappears shortly after the socket closes.
DELETE /destinations/local/{id} removes one early. It refuses a configured zone: that is not this route's to delete.
Release a registration you could not use. If registering succeeds but connecting fails, delete it — otherwise the tab that never connected sits in everyone's room list as a speaker that plays nothing until it times out.