Define scheduled updates for metadata, color, and artwork - #135
Conversation
kahrendt
left a comment
There was a problem hiding this comment.
Should we specify a suggestion in the spec on how much lead time the server should future data? Doing it early on means potentially more network traffic around the artwork role; i.e., pausing needs to clear the pending buffer and then it would get resent as soon as its resumed. If it only sends it a small time in advance this window gets narrower.
My Music Assistant implementation/prototype sends it around 30s in advance, would limiting it to maybe 20s or 15s (with a SHOULD) be fine with you? Or keep it 30s and add a note in the Spec? It's not explicitly mentioned anywhere, but a nice feature this can allow is a "coming up" display, already showing the next track with a countdown. For reference, Roon has something similar in their player: |
|
For now I set the limit at 20 seconds, but I can still change it if you think that isn't optimal. Also explicitly mentioned how clients might use the data to encourage servers to actually send it in advance. |
|
Found a gap in this PR while I was thinking about #128. beebf5f now discards the pending image if |
This comment was marked as outdated.
This comment was marked as outdated.
`client/state`, `server/state` and `group/update` each defined their own merge rules, and the three disagreed on whether merging was required, whether `null` cleared a field, how deep the merge went, and which message had to be complete (#149). Rather than reconcile them into one rule, drop merging. ## Changes: - All three messages carry full state on every message. - For `client/state` and `server/state` the role object is the unit: an included object carries that role's full state, and an omitted one leaves that role's state unchanged. Differential updates survive at role granularity, which is what the optional role objects were always for. - Drop the `| null` variant from leaf fields in the `metadata` and `color` objects. A role object may still be `null` in `server/state`, which is how the server ends a role's state when it drops the role from `active_roles`, and it was never a delta mechanism. - `group/update`'s `playback_state`, `group_id` and `group_name` are no longer optional. What this costs is bandwidth on metadata text, which is sent on state change rather than continuously; anything large already travels as a binary message. What it saves is the reconciliation layer on the receiving side, which has been a steady source of bugs and runs to a few hundred lines in a client that gets it right. ## Fields whose absence carried a second meaning Dropping the merge silently repurposes every optional field that used to mean "unchanged". Each one is now stated where the reader meets the field, rather than in a rule they have to remember: - `metadata.progress`: omitting it clears the client's position, so the server includes it in every `metadata` state that has a position to report. Track position is calculated from the current metadata state rather than from the last message that happened to include `progress`, which only made sense while absent meant unchanged. A resent `progress` is measured at the new `timestamp` rather than copied forward under it, which the merge rules used to make moot by never resending it at all. - The player state object's `supported_commands`: absence now says the player accepts no commands, which is what `[]` already says. The `?` goes rather than the redundant encoding staying around to silently revoke `set_output_delay`. Both other `supported_commands` in the spec are already non-optional. - Role objects themselves, covered by the omitted-object rule above. ## Follow-on cleanups - The player role's "Delta updates" paragraph is gone. - `REQUIRED for players` on the three non-optional fields in the player state object goes with it. It was an anchor for that paragraph, `?` already marks optionality (see the note under Communication), and the object is player-only. - The `available: false` handling no longer describes sending a `group/update` carrying only `playback_state`. This conflicts with #135 on `messaging.md`, `roles/metadata/v1.md` and `roles/color/v1.md`, in that PR's favour: with no merging, its rule that the first message after a scheduled one must repeat the scheduled message's fields has nothing left to do. Out of scope: the `management/set-pairing-config` patch semantics in `management.md`, which is a configuration API rather than state streaming. Closes #149 Refs #101 ## Breaking changes Both directions break, not just the server. Servers must send full `server/state` and `group/update` messages instead of deltas. A client that still merges keeps stale leaves, because clearing a leaf is now omission rather than an explicit `null`. Clients must send full `client/state` messages, and a player must now always carry `supported_commands`. A client that still sends deltas has them read as full state, so every field it omits is dropped rather than retained. Neither side errors. --------- Co-authored-by: Maxim Raznatovski <nda.mr43@gmail.com>
beebf5f to
3fe35bd
Compare
An artwork image larger than one Noise frame (65518 bytes of payload) currently relies on transport fragmentation, which admits no interleaving: the sender must finish the fragmented message before any other frame, so the whole image is an indivisible burst that audio chunks cannot preempt. A player holding only `min_buffer_ms` of audio can underrun behind it; on a live stream that truncates audio. With BMP gone (#168) the worst case shrank, but photographic JPEG still exceeds one frame beyond roughly 500x500 (the spec's own `stream/start` example, 800x800 JPEG, is 3-5 frames), and PNG exceeds it at almost any artwork size. ## Changes: - An image is transferred as a fixed 14-byte announce message carrying the timestamp and the image's `total_size` (uint32), followed by parts carrying only image data, all on the channel's message type; a flags byte after the type byte distinguishes the message kinds, following the shape #172 gave fragmentation - The announce carries no image data, so a client can allocate its image buffer before any image bytes arrive and write each part directly into it; the transfer completes when the received data reaches `total_size` - Every artwork message is capped at 65518 bytes after the type byte so it never needs transport fragmentation; any other messages MAY be sent between the messages of a transfer, and transfers on different channels are independent - The pending image is now the most recently announced image, from its announce until it becomes current: an announce discards any held pending image, partly received or complete, and the pending image becomes current once its transfer completes and its timestamp is reached - A rejected announce (no active stream, or client not available) still starts its transfer, so its parts are rejected rather than treated as malformed - The clear message becomes an announce with `total_size` 0, which completes immediately with no parts - A two-byte cancel message (flags bit 1) discards the channel's pending image, partly received or complete, leaving the current image showing; it replaces resend-the-current-image as the cancel recipe in the scheduled-artwork rules, which under chunking would cost a full image retransfer - Malformed sequences (a message under 2 bytes or over the frame cap, an announce whose length is not 14 bytes, a cancel longer than 2 bytes, a part with no transfer in flight, data extending past `total_size`, a nonzero reserved flag bit, both flag bits set) close the connection, matching fragmentation The pending-image redefinition keeps the newest-wins rule from #135 applying at the announce, as it did at message arrival before chunking, and means a client needs at most two image buffers per channel, receiving the transfer directly into the pending one. Every image costs one extra small message, including images that fit in a single frame; the uniform format was judged worth ~31 bytes of ciphertext per image to give clients flexibility to transfer directly into a proper sized buffer. There is no explicit last-part flag; `total_size` already determines completion, and a redundant end marker would add mismatch states to define. Every artwork message changes shape: the flags byte shifts the timestamp, the header and image data travel in separate messages, and the empty clear message becomes a bare announce with `total_size` 0. Closes #134 ## Breaking changes: The wire format for artwork is completely changed. All servers and clients will have to re-implement to handle: - the announce binary message - the artwork specific split chunks versus using the protocol's generalized noise split - handling the pending slot; i.e., servers should now send a cancel message instead of re-sending the current image --------- Co-authored-by: Maxim Raznatovski <nda.mr43@gmail.com>

Define what a future timestamp on
metadata,color, and artwork messages means: the update is scheduled and takes effect at that time, with one shared current-plus-pending model where the newest message always wins the pending slot.Changes
The
metadataandcolorstate timestamps left the future-timestamp case undefined, and the artwork binary rule ("latest wins") could be read as either arrival order or timestamp order, so client behavior diverged and a server could not safely send an update ahead of time (e.g. the next track's metadata timed to the audible track change, which lags the server's own transition by the buffered audio).All three roles now define the same model. Clients keep their current state plus at most one pending update: a future-timestamped message becomes the pending update, replacing any held one, and applies when its time is reached; any other message applies immediately and discards the held one. Clients never compare timestamps. Around it,
server/statedefers to the role sections instead of implying apply-on-receipt, anullrole object discards any pending update, omitting a role object leaves a pending update in place, the first state message for a role must carry a past-or-present timestamp, progress extrapolation reads only from the current state and never from a pending update, an empty artwork message is a scheduled clear, andstream/enddiscards pending images. There is no wire change and no role version bump; this pins down what v1 left undefined.This solves the problem without needing
stream/clear.stream/clearwould not properly solve this problem, since:metadataandcolordon't use binary streams.stream/clearwould clear all pending artwork channels, this would not look great in practice, causing disappearing images.What #175 removed
Rebased onto main now that #175 has landed. Dropping delta merging retired the rule this PR previously needed: that the first message sent after a scheduled update repeat every field the scheduled message carried. With full state on every message, a client that applied the pending update and one that discarded it both land in the next message's state, so there is nothing left to reconcile. The cancel recipe survives in simplified form and now reads the same way in all three roles.
#175 also anchors
track_progressto the metadatatimestamp, which is what makes a scheduledprogresswell defined: it describes the position at the moment the update takes effect.Changed after review
The lead-time bound now measures from the timestamp rather than from the moment of scheduling: "SHOULD NOT send a scheduled update more than 20 seconds before its
timestamp". That is a different rule from the original "SHOULD NOT schedule an update more than 20 seconds ahead", and the better one, since it bounds how long a client holds a pending update and therefore how much a pause discards.Breaking changes: server
In practice none: AFAIK no server sends timestamps in advance, and timestamping every update with the current time remains fully conformant with no obligations. A server that schedules ahead cancels by resending the current state with a past or present timestamp; artwork needs nothing special since every message is a complete image.
Breaking changes: client
Clients that apply
metadata/coloron receipt render scheduled updates too early once servers start scheduling; they need the single pending slot: hold a future-timestamped message until its time, let any newer message replace it, no timestamp comparison anywhere. Artwork clients that queue multiple timestamped images or block on them must reduce to the same single pending image per channel and discard pending images onstream/end.