Skip to content

fix(rtmg/web): reconnect keeps playing the stale audio-source track#278

Open
seanhanca wants to merge 1 commit into
mainfrom
fix/source-mode-reconnect-stale-playback
Open

fix(rtmg/web): reconnect keeps playing the stale audio-source track#278
seanhanca wants to merge 1 commit into
mainfrom
fix/source-mode-reconnect-stale-playback

Conversation

@seanhanca

Copy link
Copy Markdown

Bug

When you are in audio source mode, disconnect, then quickly get to the sequencer and reconnect, it will continue playing the track that was loaded while the sequencer UI is showing and playing.

After an abnormal WS close, if the user switches to a different audio source during the auto-reconnect window, the recovered session keeps playing the previously-loaded track even though the UI now shows the new selection.

Root cause analysis

The reconnect path snapshots the fixture at session start and never reconciles it against the user's current selection on recovery:

  1. Snapshot at connect. useStartSession resolves the active fixture once and freezes it into sessionFixture (useStartSession.ts, resolveFixtureForConnect + the const sessionFixture snapshot). The reconnect factory (buildAndConnect) rebuilds every backoff attempt from that snapshot — by design, to avoid re-decoding multi-MB uploads on every retry. Its comment even asserts "the user can't have changed fixtures while a 'Reconnecting…' placard is up", which is exactly the wrong assumption here.

  2. Mid-outage track change is dropped. When the user picks a new track, usePerformanceStore.fixture updates and useFixtureSwap's subscription calls run(name). But run() early-returns when session.status !== "ready" (useFixtureSwap.ts): during "reconnecting" the swap is silently dropped and lastSwappedTo is left untouched. Nothing re-applies it once the socket recovers.

  3. Recovery rebinds the stale track. On success the reconnect factory connects with the snapshot config and calls player.swap(remote.initialBuffer) — i.e. the backend re-encodes and the player swaps to the old track's clean source. The UI (driven by usePerformanceStore.fixture) shows the new track; audio + server session are pinned to the stale one.

Symptom vs. cause: the symptom is stale playback after reconnect; the true cause is that the session's bound fixture (snapshot) and the UI's selected fixture can diverge during an outage, and the reconnect completion never reconciles them.

Note: stem source-mode (full/vocals/instruments) changes during an outage were already safe, because the reconnect's buildConfig re-reads resolveSourceMode() live — only the fixture name (and its decoded PCM) is snapshotted, which matches the report ("the track that was loaded").

Fix plan

  • Add boundFixture to useSessionStore — the single source of truth for the track the live session is actually bound to, set on initial connect and on every reconnect, cleared on reset().
  • In useFixtureSwap, reconcile on the "reconnecting""ready" edge: if the selected fixture diverged from boundFixture, re-run the swap exactly once (the existing, contract-respecting swap path).
  • Keep boundFixture in sync after a successful swap.
  • No regressions: the reconcile is gated to fire only on the reconnect→ready transition with a real divergence. Decision lives in the pure needsFixtureReconcile():
    • Normal reconnect (selection unchanged): selected === bound → no swap.
    • Fresh Play / sequencer-only (idle/connectingready): never the reconnecting→ready edge → no swap; useStartSession already binds the current selection.
    • Audio-source session that didn't change track: no divergence → no swap.
  • No wire-contract / knob registry changes, so no gen_wire_types.py regen needed (client-only state).

Code change summary

  • store/useSessionStore.ts: new boundFixture field + setBoundFixture, cleared on reset().
  • hooks/useStartSession.ts: record boundFixture on initial connect and (before status flips to ready) on reconnect.
  • hooks/useFixtureSwap.ts: export pure needsFixtureReconcile(); subscribe to the session store and reconcile on reconnect→ready; update boundFixture after a swap.
  • Tests: new tests/unit/fixtureReconnectReconcile.test.ts (decision matrix); tests/unit/sessionStore.test.ts asserts boundFixture clears on reset.

Verification

npm run typecheck → clean.

npm run build✓ Compiled successfully, Finished TypeScript, static pages generated.

npx vitest run tests/unit/fixtureReconnectReconcile.test.ts tests/unit/sessionStore.test.ts13 passed.

Fails-before evidence: with the source changes reverted but the new test present, all 5 needsFixtureReconcile cases fail (helper absent); they pass with the fix applied.

The only failing tests in the suite (sliceEpoch, float16, replay "refs cache") are a pre-existing Math.f16round is not a function environment gap in the local Node build — unrelated to this change and failing identically on the base commit.

Made with Cursor


Supersedes #274 (which was opened from the qianghan fork); re-opened from a branch on this repo. The fork PR is being closed in favor of this one.

…ing an outage doesn't keep playing stale

When the WS drops and the user switches to a different audio source before
the auto-reconnect completes, the recovered session kept playing the
previously-loaded track even though the UI showed the new selection.

Root cause: the reconnect path (useStartSession) rebinds the fixture
snapshotted at session start. The mid-outage track change is dropped
because useFixtureSwap.run() bails while status !== "ready", and nothing
re-applies it once the session recovers — so the recovered backend +
AudioPlayer stay bound to the stale track while the perf store's fixture
shows the new pick.

Fix: track the live session's bound fixture in useSessionStore
(boundFixture), set on initial connect and reconnect. useFixtureSwap now
reconciles on the "reconnecting" -> "ready" edge: if the selected fixture
diverged from the bound one, it re-runs the swap exactly once. No effect
on a fresh Play or a clean reconnect where the selection never changed.

Adds needsFixtureReconcile() + a focused unit test pinning the decision
matrix, and asserts boundFixture clears on session reset.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a reconnect edge case in the realtime motion graph web demo where, in audio-source mode, switching tracks during a WS outage could leave the recovered backend/audio player bound to the previously loaded track while the UI shows the new selection.

Changes:

  • Introduces boundFixture in useSessionStore as the client-side source of truth for which fixture the live session is actually bound to.
  • Records boundFixture on initial connect and reconnect, and reconciles on the "reconnecting" → "ready" transition by re-running a fixture swap exactly once when the UI selection diverges.
  • Adds unit tests covering the reconcile decision matrix and ensuring boundFixture is cleared on store reset.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
demos/realtime_motion_graph_web/web/store/useSessionStore.ts Adds boundFixture state + setter; clears it on reset() to keep it per-session.
demos/realtime_motion_graph_web/web/hooks/useStartSession.ts Sets boundFixture during initial session start and before reconnect flips status to "ready".
demos/realtime_motion_graph_web/web/hooks/useFixtureSwap.ts Adds needsFixtureReconcile() and subscribes to session status to reconcile fixture after reconnect; keeps boundFixture in sync after swaps.
demos/realtime_motion_graph_web/web/tests/unit/fixtureReconnectReconcile.test.ts New unit tests for needsFixtureReconcile() decision behavior.
demos/realtime_motion_graph_web/web/tests/unit/sessionStore.test.ts Extends reset test coverage to assert boundFixture is cleared.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants