mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-09-07 15:07:17 +00:00
docs(player): document source selection behaviour and state ordering
The player had no reference covering how a source button behaves or how a selection is confirmed, and both have non-obvious answers learned from real hardware. Records what the speaker actually does: that a READY source is not necessarily selectable, that a bare select against a provider parks it on a stub now-playing while the previous audio carries on, and the four-part signature of that stub. Then the resulting click behaviour per source, the confirmation model (event stream first, bounded readbacks as fallback, definitive versus ambiguous write failures), the revision/epoch ordering the browser relies on, and why source staleness needs two consecutive failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c7319b383c
commit
cf2edd526d
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: "Player: Sources and Selection State"
|
||||
---
|
||||
How the embedded web player (`soundtouch-player`, and the same UI served by
|
||||
`soundtouch-service`) decides what a source button does, and how it decides
|
||||
whether a selection worked.
|
||||
|
||||
Both questions have non-obvious answers, learned from real hardware. This page
|
||||
records what the speaker actually does, so the behaviour is not re-derived or
|
||||
accidentally undone.
|
||||
|
||||
## Not every advertised source can be selected
|
||||
|
||||
A speaker's `/sources` lists what it knows about, each with a `status`. The
|
||||
player renders every `status="READY"` entry as a button. That set is not
|
||||
uniform: some entries are **inputs**, some are **providers**.
|
||||
|
||||
An **input** can be selected on its own. `AUX`, `BLUETOOTH`, a `SPOTIFY`
|
||||
entry with a real `sourceAccount`: `POST /select` with just the source and
|
||||
account is meaningful, and the speaker resumes that input.
|
||||
|
||||
A **provider** cannot. `RADIO_BROWSER`, `TUNEIN` and `LOCAL_INTERNET_RADIO`
|
||||
need a station **ContentItem carrying a `Location`** (see
|
||||
`stations.ResolveContentItem`, which sets `type="stationurl"`). There is
|
||||
nothing for the speaker to resume from the source name alone.
|
||||
|
||||
`STORED_MUSIC` is a third case: one entry per media server, its
|
||||
`sourceAccount` being a server UDN. Selecting it identifies no track or
|
||||
container.
|
||||
|
||||
## What a bare select does to a provider
|
||||
|
||||
The speaker does not refuse. It answers `200`, and parks on a stub
|
||||
now-playing, while whatever was playing before **carries on**:
|
||||
|
||||
```xml
|
||||
<nowPlaying deviceID="..." source="RADIO_BROWSER" sourceAccount="">
|
||||
<ContentItem source="RADIO_BROWSER" type="" location="" isPresetable="false">
|
||||
<itemName>RADIO_BROWSER</itemName>
|
||||
</ContentItem>
|
||||
</nowPlaying>
|
||||
```
|
||||
|
||||
Four things identify the stub: no `playStatus`, empty `type`, empty
|
||||
`location`, and an `itemName` that just echoes the source name.
|
||||
|
||||
The speaker then reports that stub indefinitely. Observed on hardware: the
|
||||
player showed RadioBrowser while Spotify was audible, and a naive readback
|
||||
"confirmed" the selection because the reported source did match the one
|
||||
requested. `LOCAL_INTERNET_RADIO` produces a byte-identical stub.
|
||||
|
||||
This is speaker behaviour, not something the player or the service can fix
|
||||
after the fact. The only remedy is not to issue such a select.
|
||||
|
||||
## What the player does instead
|
||||
|
||||
| Source | Click behaviour | Resumes from Recents |
|
||||
| --- | --- | --- |
|
||||
| `RADIO_BROWSER` | resume newest station, else open RadioBrowser | yes |
|
||||
| `TUNEIN` | resume newest station, else open TuneIn | yes |
|
||||
| `LOCAL_INTERNET_RADIO` | open Play URL | no |
|
||||
| `STORED_MUSIC` | open Library | no |
|
||||
| anything else | `POST /select` as before | n/a |
|
||||
|
||||
Resuming replays the newest Recents entry for that source, using that entry's
|
||||
own ContentItem: the real item the speaker was given, `Location` included.
|
||||
|
||||
**`LOCAL_INTERNET_RADIO` deliberately does not resume.** AfterTouch plays its
|
||||
own one-shot audio through that source: TTS and the notification ding both go
|
||||
out over `/custom/v1/playback/`. Its Recents therefore mix notifications with
|
||||
stations, and on a test speaker the *only* entry was "AfterTouch ding", so
|
||||
resuming played the ding. The announcement path is distinguishable from Play
|
||||
URL's `bmx.BuildOrionLocation`, but it also carries CLI URL playback, and any
|
||||
future audio-injecting feature would have to remember to stay clear of it.
|
||||
Opening Play URL does not depend on classifying what is in Recents.
|
||||
|
||||
`ALEXA` is advertised `READY` too and is left alone, because whether a bare
|
||||
select resumes anything for it has not been verified.
|
||||
|
||||
### The backstop
|
||||
|
||||
The table above only covers sources known to need it, and a source list is
|
||||
whatever the speaker chooses to advertise. So the readback additionally
|
||||
refuses to *confirm* the stub itself, wherever it comes from: a now-playing
|
||||
naming the requested source but with no `Location`, no `PlayStatus`, and an
|
||||
`ItemName` equal to the source is reported as a failure.
|
||||
|
||||
All three conditions are required together. A physical input reports no
|
||||
location and no item name of its own yet is genuinely playing, so any single
|
||||
condition alone would reject real selections.
|
||||
|
||||
## How a selection is confirmed
|
||||
|
||||
`POST /select` returning `200` proves nothing: the speaker can reject a source
|
||||
seconds later, surfacing as a transition to an error source
|
||||
(`INVALID_SOURCE`, `*_ERROR`). So the player posts once, then watches.
|
||||
|
||||
- The **event stream** is the primary watcher. A `nowPlayingUpdated` event
|
||||
reports a late rejection as it happens, and the player turns it into a
|
||||
failure.
|
||||
- **Bounded readbacks** at 2s, 5s and 10s are the fallback for a speaker whose
|
||||
events are not arriving. They stop as soon as a confirmation arrives *and*
|
||||
the readback reports a live event stream, so a confirmed selection normally
|
||||
costs one request rather than three.
|
||||
- Readbacks use `GET /devices/{id}/now-playing`, which refreshes only
|
||||
`/now_playing`. The full device fetch runs a complete status poll: six
|
||||
sequential speaker calls plus `/getGroup` on a stereo-capable model, to
|
||||
answer one question, against a device that may be slow precisely because
|
||||
something is wrong.
|
||||
|
||||
Outcomes are `pending`, `provisional-confirmed`, `final-confirmed`,
|
||||
`unverified` and `failed`, shown in a live region under the source list. A
|
||||
confirmation from a push event is never retracted by a later failed readback.
|
||||
|
||||
### Definitive versus uncertain failures
|
||||
|
||||
A rejected write is not always proof the command never landed:
|
||||
|
||||
- **4xx** is produced before the service contacts the speaker (unknown device,
|
||||
unparseable body, empty source). The command provably never went out, so the
|
||||
failure is reported immediately.
|
||||
- **5xx and transport errors** are ambiguous. `handleSourceControl` reports a
|
||||
failed `Client.SelectSource` through `sendControlResponse`, which maps any
|
||||
speaker-call error to 500, and a request that timed out *after* the speaker
|
||||
already switched looks identical to one it never received. The readbacks
|
||||
keep running and the reason is carried into whatever outcome they reach.
|
||||
|
||||
## Ordering: revisions and epochs
|
||||
|
||||
Status reaches the browser three ways — a full `devices` snapshot, a
|
||||
`status_update` delta, and REST refreshes — with no inherent ordering. Two
|
||||
fields fix that:
|
||||
|
||||
- `revision` advances on every projection, so a frame no newer than what the
|
||||
browser holds is dropped.
|
||||
- `nowPlayingRevision` is the now-playing field's own generation. `revision`
|
||||
alone cannot answer "did now-playing actually change?", because any other
|
||||
field's merge advances it; a selection waiting for confirmation needs
|
||||
exactly that distinction.
|
||||
|
||||
Revisions are per-connection and restart at 0, so they are only comparable
|
||||
within one **`epoch`**, which identifies the connection that produced the
|
||||
status. Without it, a device backed by a fresh connection would publish
|
||||
revisions the browser rejects forever, freezing that device's display until
|
||||
reload. Epochs are seeded from the wall clock and forced strictly increasing,
|
||||
so they keep rising across a service restart, and are in milliseconds because
|
||||
the browser compares them as JSON numbers.
|
||||
|
||||
## Source inventory staleness
|
||||
|
||||
`sourcesStale` marks an inventory the speaker has stopped confirming; the
|
||||
player keeps showing it but disables the buttons.
|
||||
|
||||
It is set after **two consecutive** failed `/sources` reads, not one. A single
|
||||
dropped read is not evidence the list is wrong, and marking it stale
|
||||
immediately disabled every source button on a transient hiccup. This mirrors
|
||||
`offlineFailureThreshold`, which debounces connectivity the same way. A
|
||||
successful read clears the marker and resets the count.
|
||||
|
||||
Successful reads remain ordered by generation, so an older one cannot
|
||||
overwrite a newer one. Failures are not ordered: a failure carries no
|
||||
inventory, so spending the generation on it would let a failed read discard a
|
||||
concurrent successful one.
|
||||
|
||||
## Related
|
||||
|
||||
- [Source Selection Guide](SOURCE-SELECTION.md) — the `/select` endpoint and
|
||||
the client library
|
||||
- [WebSocket Events](WEBSOCKET-EVENTS.md) — the event stream the confirmation
|
||||
relies on
|
||||
- [Radio Browser](radio-browser.md) — the RadioBrowser provider
|
||||
Reference in New Issue
Block a user