fix(rtmg/web): reconnect keeps playing the stale audio-source track#278
Open
seanhanca wants to merge 1 commit into
Open
fix(rtmg/web): reconnect keeps playing the stale audio-source track#278seanhanca wants to merge 1 commit into
seanhanca wants to merge 1 commit into
Conversation
…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>
There was a problem hiding this comment.
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
boundFixtureinuseSessionStoreas the client-side source of truth for which fixture the live session is actually bound to. - Records
boundFixtureon 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
boundFixtureis 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.
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.
Bug
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:
Snapshot at connect.
useStartSessionresolves the active fixture once and freezes it intosessionFixture(useStartSession.ts,resolveFixtureForConnect+ theconst sessionFixturesnapshot). 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.Mid-outage track change is dropped. When the user picks a new track,
usePerformanceStore.fixtureupdates anduseFixtureSwap's subscription callsrun(name). Butrun()early-returns whensession.status !== "ready"(useFixtureSwap.ts): during"reconnecting"the swap is silently dropped andlastSwappedTois left untouched. Nothing re-applies it once the socket recovers.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 byusePerformanceStore.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
buildConfigre-readsresolveSourceMode()live — only the fixture name (and its decoded PCM) is snapshotted, which matches the report ("the track that was loaded").Fix plan
boundFixturetouseSessionStore— the single source of truth for the track the live session is actually bound to, set on initial connect and on every reconnect, cleared onreset().useFixtureSwap, reconcile on the"reconnecting"→"ready"edge: if the selected fixture diverged fromboundFixture, re-run the swap exactly once (the existing, contract-respecting swap path).boundFixturein sync after a successful swap.needsFixtureReconcile():selected === bound→ no swap.idle/connecting→ready): never the reconnecting→ready edge → no swap;useStartSessionalready binds the current selection.gen_wire_types.pyregen needed (client-only state).Code change summary
store/useSessionStore.ts: newboundFixturefield +setBoundFixture, cleared onreset().hooks/useStartSession.ts: recordboundFixtureon initial connect and (before status flips to ready) on reconnect.hooks/useFixtureSwap.ts: export pureneedsFixtureReconcile(); subscribe to the session store and reconcile on reconnect→ready; updateboundFixtureafter a swap.tests/unit/fixtureReconnectReconcile.test.ts(decision matrix);tests/unit/sessionStore.test.tsassertsboundFixtureclears 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.ts→ 13 passed.Fails-before evidence: with the source changes reverted but the new test present, all 5
needsFixtureReconcilecases fail (helper absent); they pass with the fix applied.The only failing tests in the suite (
sliceEpoch,float16,replay"refs cache") are a pre-existingMath.f16round is not a functionenvironment 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
qianghanfork); re-opened from a branch on this repo. The fork PR is being closed in favor of this one.