Skip to content

Stream sync correction: make rules #1 and #3 prescriptive, decide on rule #2 #88

Description

@balloob

This issue comes out of a cross-SDK conformance audit comparing every Sendspin client/server implementation (aiosendspin, sendspin-cli, sendspin-cpp, sendspin-dotnet, sendspin-go, sendspin-js, sendspin-jvm, sendspin-rs, SendspinKit) against the spec. sendspin-cpp is treated as the reference implementation throughout — it most closely matches the spec's language and is the only SDK that runs on the constrained-embedded target the spec was originally written for.

The spec's Playback Synchronization section names three rules
but is loose about how to implement them. Across nine SDKs we found wide
divergence — 200× variation in late-chunk drop thresholds, two distinct
"drift correction" clusters, and zero implementations of the spec's
underrun handshake. Recommendations below are anchored on sendspin-cpp,
which is the only SDK that stays inside the spec's "sample insertion /
deletion with interpolation" vocabulary and has hard-coded thresholds we
can lift wholesale.

1. Make rule #1 (drift correction) prescriptive

Add to Playback Synchronization:

Clients SHOULD use a two-tier correction strategy: a soft tier that
inserts or drops a single sample/frame using neighbour-averaging
interpolation, and a hard tier that inserts silence (when ahead of
the timeline) or skips frames (when behind).

Clients SHOULD engage the soft tier when |sync error| ≥ 100 µs and
the hard tier when |sync error| ≥ 5 ms. Clients SHOULD reanchor
(clear buffers and restart from the next chunk's timestamp) when
|sync error| ≥ 500 ms.

Clients MAY substitute a resampler or playback-rate adjustment for the
soft tier provided the resulting end-to-end sync error stays within
the same envelope.

These are the constants from sendspin-cpp/src/sync_task.cpp:36-39:
SOFT_SYNC_THRESHOLD_US = 100, HARD_SYNC_THRESHOLD_US = 5000,
HARD_SYNC_SETTLE_THRESHOLD_US = 500.

2. Make rule #3 (late chunks) prescriptive

Add:

Clients SHOULD drop any audio chunk whose scheduled play time is more
than 5 ms before the end of currently buffered/playing audio.
Clients MAY trim the stale prefix of a chunk whose tail is still in
the future.

This matches sendspin-cpp (5 ms) and sendspin-cli (expected-next-timestamp
trim). Today thresholds range from 5 ms (cpp, cli) to 50 ms (go, SendspinKit)
to 1 s (jvm).

3. Decide on rule #2 (underrun handshake)

This is the most material non-conformance finding from the audit: no
implementation sends the spec's state: 'error' → mute → buffer →
state: 'synchronized' handshake on buffer underrun.
Several SDKs
(sendspin-js, sendspin-rs, SendspinKit) even define the error
state in their types but never emit it.

The practical consequence: a Sendspin server today has no way to learn
that a player has lost sync. Every client looks permanently synchronized
even when playing silence or stale audio.

Two options, pick one:

  • (a) Keep the rule. Mandate the handshake, and we'll need every
    SDK to fix it (issues already filed against each SDK).
  • (b) Drop the rule. Acknowledge that the de-facto SDK pattern is
    "silence on underrun, recover silently" and remove the
    error/synchronized requirement from the spec.

Source audit doc: docs/stream-sync-correction.md
Full audit branch: claude/stream-sync-correction-sdks-AWoNC
Per-SDK digest: sdk-issues digest

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions