Skip to content

Resolve client-facing spec gaps - #113

Merged
kahrendt merged 7 commits into
mainfrom
fix/client-facing-1.0
Jul 6, 2026
Merged

Resolve client-facing spec gaps#113
kahrendt merged 7 commits into
mainfrom
fix/client-facing-1.0

Conversation

@maximmaxim345

@maximmaxim345 maximmaxim345 commented Jul 1, 2026

Copy link
Copy Markdown
Member

This PR and one more (the state to available rename in #115) are the last client-breaking changes to the spec. There might be more if we hit issues with the newly added encryption support, but apart from that these two should cover everything I found.

There are also some breaking changes I'm still working out, but those are strictly server-implementation breaking. Clients will not have to adjust anything.

Specifically, this PR addresses the following gaps:

  • client/goodbye: a client switching servers MUST send reason another_server, and clients SHOULD default to shutdown when the device is powering off or otherwise not coming back.
  • Discovery: the path TXT key is now REQUIRED (value still recommends /sendspin).
  • Pairing: the initial static PIN MUST be device-specific (e.g. randomly generated and printed), never a fixed shared default.
  • Custom roles: _-prefixed role names SHOULD carry a vendor-specific prefix to avoid collisions.
  • Player state: persisting volume/muted across reboots is RECOMMENDED, and a server MUST NOT assume they are unchanged after a reconnect.
  • Streaming: codec_header is specified as standard Base64 (the rest of the spec uses base64url), clarifying the existing encoding rather than changing the wire format.
  • external_source: a client whose output is taken by a non-Sendspin activity reports external_source only when it will not yield (the server then cannot stream to it, since stream/start is gated on synchronized); a client that can be interrupted instead stays synchronized so the server can take over at any time.
  • Source role: dropped source@v1_support.supported_formats; a source declares its format per stream in client_stream/start and the server accepts any format.
  • Forward compatibility: clients MUST ignore unrecognized payload fields, and MUST NOT send fields the spec doesn't define for a message (beyond the _-prefixed application-specific role objects a message already permits). Undefined field names stay reserved to the spec, so a client can't squat on a name a later version might claim.
  • stream/request-format: a player MUST only request a format listed in its supported_formats, so a format request can never name something the client cannot decode.

@maximmaxim345
maximmaxim345 force-pushed the fix/client-facing-1.0 branch from 06fd614 to df42e38 Compare July 1, 2026 17:05
Comment thread README.md Outdated
Comment thread TRIAGE.md Outdated
Comment thread TRIAGE.md Outdated
@maximmaxim345
maximmaxim345 force-pushed the fix/client-facing-1.0 branch from df42e38 to 5333d43 Compare July 1, 2026 20:08
@maximmaxim345
maximmaxim345 marked this pull request as ready for review July 1, 2026 20:49
Comment thread README.md Outdated
Comment thread README.md Outdated

@kahrendt kahrendt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good clarifications!

@kahrendt
kahrendt merged commit 92e9145 into main Jul 6, 2026
@kahrendt
kahrendt deleted the fix/client-facing-1.0 branch July 6, 2026 18:52
@maximmaxim345
maximmaxim345 restored the fix/client-facing-1.0 branch July 7, 2026 06:57
@maximmaxim345
maximmaxim345 deleted the fix/client-facing-1.0 branch July 7, 2026 06:58
maximmaxim345 added a commit that referenced this pull request Jul 7, 2026
The remaining client-breaking change after #113: it replaces the
`client/state` `state` enum (`synchronized` | `external_source`) with a
boolean `available`.

- `available: true` - operational and ready to participate; for a player
or source this means its clock has converged. Formerly `synchronized`.
- `available: false` - the client's output is in use by an external
system. Formerly `external_source`.

Pure terminology rename with no behavioral change. The group-leave
behavior it builds on (the `available: false` then `available: true`
flip, and the no-auto-rejoin rule) already landed in #113.
kahrendt pushed a commit that referenced this pull request Jul 13, 2026
The PR with server-implementation-breaking changes I mentioned in #113.
Clients do not have to adjust anything for these; they tighten what a
conforming server must do.

Specifically:
- Role activation: a server MUST NOT activate a role or version the
client did not list in `supported_roles`.
- Forward compatibility: servers (not just clients) MUST ignore
unrecognized `payload` fields, and MUST NOT send fields the spec does
not define.
- `stream/request-format`: a request the server cannot honor is not an
error; the stream continues in a configuration the client supports. For
players, `stream/start` MUST use a format from the client's
`supported_formats`, so the server can never start a stream the client
cannot decode.
- Reconnect backoff: after repeated consecutive Noise-handshake
failures, the server SHOULD NOT keep auto-reconnecting until there is
reason to expect success (operator re-initiates pairing, network
conditions change).
- Artwork late join: after an artwork `stream/start` (initial or
post-reconnect), the server SHOULD immediately send the current image
for each channel whose `source` is not `'none'`, so a client joining
mid-track is not left blank until the next track change.
maximmaxim345 added a commit to Sendspin/aiosendspin that referenced this pull request Jul 14, 2026
A batch of protocol-conformance and correctness fixes. This PR doesn't
include changes to encryption/authentication, that will follow in a
later PR.

## Changes

- **Adopt client/state `available` boolean.** Replace the client state
enum with the spec's `available` boolean end to end: the SDK sends only
`available`, the server tracks a boolean and notifies roles via
`on_availability_changed`. A legacy `state` enum on the wire normalizes
to `available`, with only `external_source` treated as unavailable so a
client reporting an error is not ejected from its group. Spec:
[#115](Sendspin/spec#115),
[#113](Sendspin/spec#113).
- **Stamp `server_transmitted` on stream lifecycle messages.** Add
`server_transmitted` to `stream/start`, `stream/clear`, and
`stream/end`, stamped at actual send time, matching `server/time`. Spec:
[#106](Sendspin/spec#106).
- **Floor send-ahead at `min_buffer`.** Live streams floor at
`min_buffer + static` instead of extending to `required_lead` (which
added latency a realtime queue cannot recover); buffered streams keep
the `min_buffer` floor instead of dropping to `required_lead + static`.
Spec: [#107](Sendspin/spec#107).
- **Document `seek_relative` clamping** as the application's
responsibility. Spec: [#103](Sendspin/spec#103).
- **Track all unimplemented client roles**, not just spec-support
families (controller/metadata/color and wholly-unknown families), still
excluding `_`-prefixed custom roles. Spec:
[#113](Sendspin/spec#113).
- **Make server reconnect per-goodbye-reason and activity-aware.** Retry
a server-initiated connection only on restart or concurrent_attempt; on
a drop with no goodbye, assume restart when the connection was idle or
carried playback, else treat it as a session end. Spec:
[#113](Sendspin/spec#113),
[#116](Sendspin/spec#116).
- **Default the visualizer pitch feature off** (rides reserved binary
type 21; kept as an opt-in extension). Spec:
[#86](Sendspin/spec#86).
- **Mask the beat downbeat bit** unless the role tracks downbeats. Spec:
[#86](Sendspin/spec#86).
- **Remember visualizer request-format when no stream is active**
instead of starting a stream in response (which the spec forbids).
Mirrors player/artwork. Spec:
[#112](Sendspin/spec#112).
- **Note peak-cadence deviation from spec** with a TODO (peak onset
detection currently runs only at the periodic `rate_max` hop). Spec:
[#86](Sendspin/spec#86).
- **Hoist binary-type sets out of the per-frame dispatch path** (reuse
module-level frozensets instead of rebuilding per binary frame).
- **Reset `_stream_started` after the artwork deactivate stream/end**,
matching the other stream roles.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants