Added 'connected devices' to external lib
CI / check (push) Successful in 1m59s

This commit is contained in:
ab
2026-09-10 17:03:53 +03:00
parent 95bcc9bac4
commit b191c10ba4
10 changed files with 780 additions and 44 deletions
+114
View File
@@ -0,0 +1,114 @@
# Personal playback coordination
`music_dht::playback` owns the personal-device state machine, policy parameters,
snapshot/command envelopes and conflict rules. It has no networking, database,
clock, UI or audio dependency. The existing JSON-lines device-sync transport
continues to carry snapshots and logged commands. Jam is a separate authority
scope; its capability-based protocol is not a personal ownership claim.
## Contract
Each logical device identity must have one coordinator and one selected output
adapter. Independent live installations must not reuse a device identity or
checkpoint. The web hub is a single authority for its user's browser outputs;
running multiple independent hubs for that same identity requires an external
actor/leader arrangement, not two copies of this in-memory adapter.
An owner is selected by an immutable `Claim { counter, issuer, owner }`.
Claims are ordered lexicographically in that order. A locally issued claim
increments the greatest counter observed so far. Persist the winning claim;
never reset it because a peer disappears from an online list. Controllers
gossip the winning claim as well. Concurrent startup, transfer or failover
claims therefore converge to the same winner regardless of delivery order.
Only the winning output may render personal audio. A losing player stops its
audio even if it was playing; it cannot reject a higher claim on that basis.
This is eventual exclusivity, not a quorum lock: partitions can temporarily
produce two outputs. Convergence assumes eventual delivery between trusted
devices, functioning polling/event loops, and no continuing stream of explicit
conflicting user transfers. A disconnected browser cannot be forcibly silenced.
Availability, actual unpaused playback, ownership and network connectivity are
different inputs. Only new reports from the owner's audio endpoint renew its
liveness. Duplicate snapshots and relayed claims do not. Gateway timers must
not turn cached browser state into new output heartbeats. The engine reserves
heartbeat sequence ranges in its checkpoint so a restart cannot roll them back
and every UI tick does not require a database write.
Commands carry `CommandStamp { version, claim, sequence }`. A command can carry
its existing claim ahead of the corresponding announcement. An explicit
handoff must name that claim's owner and originate from its issuer. In a term,
commands are applied in increasing sequence per sender; old, duplicate and
incompatible envelopes are ignored. The command-sender map is bounded to 128
entries per term. Concurrent controllers are serialized by the selected
output, whose snapshots distribute the resulting queue and transport state.
This does not claim exactly-once audio effects across process crashes.
## Adapter obligations
1. Authenticate the peer and trusted group using the existing transport and
operation-log membership checks. Bind snapshot `device_id` to the actual
sender. Names and snapshot wall-clock timestamps do not grant ownership.
2. Restore `Checkpoint` only for its matching group. Construct `Engine` with a
stable device id, local `Config` and a monotonic clock. Do not restore wall
clock liveness deadlines.
3. Feed real output availability and activity. Use `heartbeat` for a local
output or `output_report` with an increasing source-report id for a gateway.
4. Feed announcements, tick the engine, and route explicit transfers through
`transfer`. Ordinary controls only stamp a command in the current term.
5. Persist changed durable state before publishing decisions or executing
commands. Serialize transitions with checkpoint commits and restore the
previous engine if persistence fails. Recheck queued command fences before
their effects reach the output.
6. Apply the resolved owner to the existing UI/audio adapter. Gossip its claim
even while controlling another output. Online lists are presentation only.
Startup has a discovery grace period. If a restored/relayed owner has not yet
reported, keep the startup decision pending until its heartbeat or timeout.
Discovery is necessarily bounded: an entirely unknown peer across a partition
cannot prevent a local startup claim. Once the peers communicate, normal claim
ordering resolves the conflict.
## Configuration
The complete serde `Config` accepts these fields (unknown fields are errors):
| Field | Default | Meaning |
| --- | --- | --- |
| `claim_on_startup` | `true` | One startup intent for this application instance |
| `automatic_failover` | `true` | Claim when the owner stops reporting |
| `take_paused_on_startup` | `true` | Startup may take a paused/idle output |
| `discovery_ms` | `3000` | Initial discovery grace |
| `owner_timeout_ms` | `120000` | Missing fresh output reports before failover |
`Config::passive()` disables application-startup claims and automatic failover.
Furumusic uses this for the always-on server. A real browser startup supplies a
separate one-shot `request_startup`; explicit selection supplies a transfer.
These events are not generated by server polling. Local browser selection uses
the same `Config::should_claim` policy, with automatic cross-federation claims
disabled. Browser presence expiry preserves the current owner and queue.
TUI exposes the configuration as the `playback` section of its existing settings
file and preserves it when saving other settings. The web adapter reads optional
`playback_config_json` on the user's `furumusic__fed_device_identity` row at
session initialization; null selects the passive profile. Audio availability
is a runtime input, not a persisted setting.
## Versioning and rollout
The library-sync JSON ALPN remains `furumi/sync/2`; catalog, pairing and library
data do not require a new transport. Device profiles advertise protocol version
3. Coordination envelopes independently carry version 1. Snapshot coordination
and operation authority are additive optional JSON fields so library-sync
records from older clients still deserialize. Upgraded clients ignore legacy
personal playback snapshots/commands without the coordination envelope.
The exclusivity guarantee applies after all audio clients have upgraded.
The shared generic `Snapshot<S>` and `Command<S>` keep state conversion in the
applications while preventing divergent control message definitions. These
envelopes use JSON, not a backwards-compatible promise for postcard layouts.
Do not change Jam's independent authority by interpreting personal stamps there.
For local integration checks, patch the three frid crates with Cargo's external
`--config` mechanism. Keep released dependency declarations and lockfiles in
the applications; publish frid and then bump client dependencies for rollout.