Report static_delay_ms as the spec-conformant wire type - #169
Merged
Conversation
client/state reported the scheduler's static delay directly. That value is a
double over -5000..5000 - fractional from calibration, negative to schedule
later - while the spec types static_delay_ms as an integer 0-5000 and states
negatives are not supported. Three deviations followed from the one conflation,
all outbound; the inbound set_static_delay path was already correct (int?,
clamped 0-5000, gated on advertisement, persisted).
1. Omitted at its default. PlayerStatePayload carried
JsonIgnoreCondition.WhenWritingDefault, and the default is 0 - the common
case - so nearly every player left it out of its initial client/state. The
spec marks it REQUIRED for players, identically to required_lead_time_ms and
min_buffer_ms, which sit beside it and are serialized unconditionally for
exactly that reason. aiosendspin reads an omitted value as "unchanged", which
on the initial message leaves it with no value at all.
2. Serialized as a float. A fractional delay emitted 12.5.
3. Negatives reached the wire. aiosendspin's PlayerStatePayload raises
ValueError("static_delay_ms must be in range 0-5000"), so this failed the
connection rather than being tolerated.
Projected at the wire boundary instead of narrowing the scheduler: IClock-
Synchronizer.StaticDelayMs is unchanged, negative delays still schedule audio
later, and only the report is constrained. ToWireStaticDelayMs clamps, rounds,
guards non-finite (the setter is public and Math.Clamp propagates NaN), and warns
once per value when the projection moves it - the server's group calibration is
then working from a different number than playback is.
PlayerStatePayload.StaticDelayMs and the two ClientStateMessage factories take
int, so the compiler locates every call site that must project. There were
exactly two, which is the point: this codebase's recurring defect is one fact
reported from two places with only one of them updated.
Verified by measuring the serializer, not by reading attributes. Discrimination:
7 of 12 fail with the projection replaced by a plain cast, 10 of 18 with the
required-field fix reverted. Suite 751 green, clean Release build on both TFMs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Answering "are we following the spec on setting delay?" — the inbound
set_static_delaypath is conformant; the outbound reporting ofstatic_delay_mswas not, three ways.What was already right
player@v1_support.supported_commands=["volume","mute"]andclient/state's =["set_static_delay"], which are two different vocabularies in the spec and are correctly kept apart. Inbound commands are ignored unless advertised, clamped to 0–5000, and persisted;ServerCommandMessage.StaticDelayMsis alreadyint?.LoadPersistedStaticDelayruns on every handshake, satisfying "persist across reboots and server reconnections".The three deviations
All outbound, all from one conflation: the client reported the scheduler's value directly. That is a
doubleover −5000…5000 — fractional from calibration, negative to schedule later — while the spec'sstatic_delay_msis an integer in 0–5000 that "does not support" negatives.1. Omitted at its default, and the default is 0.
PlayerStatePayloadcarriedJsonIgnoreCondition.WhenWritingDefault, so nearly every player left it out of its initialclient/state. The spec marks it REQUIRED for players — identically torequired_lead_time_msandmin_buffer_ms, which sit immediately beside it and are serialized unconditionally with a comment saying it is because they are required. aiosendspin's own docstring: "Required on the initial state message; omitted in incremental updates means unchanged" — so on the first message an omission leaves the server with no value rather than 0.2. Serialized as a float. Measured, not inferred:
3. Negatives reached the wire.
MinStaticDelayMs = -5000.0, deliberately, for GroupSync offset semantics. But aiosendspin raises:So unlike 1 and 2 — which mashumaro silently absorbs, truncating
12.5to12— a negative fails the connection.The fix: project, do not narrow
IClockSynchronizer.StaticDelayMsis unchanged. The scheduler genuinely needs negatives and fractions, and a negative delay still schedules audio later. Only the report is constrained, by a singleToWireStaticDelayMsthat clamps, rounds, guards non-finite (the setter is public andMath.ClamppropagatesNaNinto a garbage cast), and warns once per value when the projection moves it — because at that point the server's group calibration is working from a different number than playback is.PlayerStatePayload.StaticDelayMsand bothClientStateMessagefactories now takeint. That is deliberate: it makes the compiler find every site that must project. There were exactly two — and one fact reported from two places with only one updated is this codebase's recurring defect, so having the type system enforce it beats a convention.Verification
dotnet build -c Release --no-incremental: 0 errors, both library TFMs.(int)cast → 7 of 12 fail.double+WhenWritingDefault) → 10 of 18 fail.12.5→13,12.4→12, −200, −5000, 9000,NaN,+∞, with a separate positive control asserting the field is present at 0 — otherwise every zero-expecting case would fail on a missing property, which reads as a different bug.SchedulerKeepsTheConfiguredValue_TheProjectionIsWireOnlypins that the clamp does not write back, and the delta path is pinned separately from the initial-state path rather than assumed to share it.Docs:
MIGRATION-10.0.0.md§8 and the NuGet README's static-delay section.