This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user