fix(nests): scope moq-lite publisher to a single track to stop catalog hijack
Listeners stuck on a spinning speaker avatar with no audio (against
both Amethyst-hosted and nostrnests.com-hosted Nests). The trace
captured on a working speaker phone showed every group keyed to
subId=1 / track='catalog.json' even though the broadcaster was
pumping Opus frames:
13:21:27.570 inbound SUBSCRIBE id=1 track='catalog.json'
13:21:27.574 inbound SUBSCRIBE id=0 track='audio/data'
13:21:27.609 openGroup seq=0 keyedOnSubId=1 track='catalog.json'
13:21:27.610 broadcaster: send accepted (subscriber attached)
... every subsequent group keyed=1, every audio frame on the
catalog stream
Root cause: `PublisherStateImpl.openNextGroupLocked` keyed each uni
stream off `inboundSubs.first()` with no track filter. The kdoc
asserted "Inbound subscription set is expected to be small (1 in
nests's listener-per-room model)" — that was true when listeners
only opened the audio sub. Once the listener side started opening a
catalog sub alongside (commits ~Apr 2026), whichever SUBSCRIBE
arrived first (typically the catalog one, by ~4 ms in this trace)
became the routing target for all audio frames. The relay forwarded
those frames to the listener with subscribeId=catalog, the listener
routed them to its catalog handler, RoomSpeakerCatalog.parseOrNull
returned null (it's not JSON), and the audio subscription's frames
channel never received anything — so onSpeakerActivity never fired
and the spinner stayed forever.
Fix: per moq-lite Lite-03, a publisher is responsible for one
(broadcast, track) tuple. Thread `track` through `MoqLiteSession.publish`
and have `PublisherStateImpl.registerInboundSubscription` reject
inbound SUBSCRIBEs whose track doesn't match — those still receive
SUBSCRIBE_OK from the bidi handler (best-effort per Lite-03's
optional-content semantics for catalog) but don't influence the
audio routing. `MoqLiteNestsSpeaker.startBroadcasting` passes
`track = MoqLiteNestsListener.AUDIO_TRACK` (= "audio/data").
Test callsites (MoqLiteSessionTest, NostrnestsProdAudioTransmissionTest,
NostrNestsSustainedSendOutcomesInteropTest) updated to pass the new
required parameter; all use track="audio/data" matching what they
exercise.
The diagnostic logs from the prior commits stay in — they're how we
caught this and they'll catch the next one. They can be stripped in
a follow-up once the fix is confirmed in the field.
https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
This commit is contained in:
+4
-4
@@ -258,7 +258,7 @@ class MoqLiteSessionTest {
|
||||
val (clientSide, serverSide) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey")
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey", track = "audio/data")
|
||||
|
||||
// Relay (serverSide) opens an Announce bidi to us with
|
||||
// AnnouncePlease(prefix="").
|
||||
@@ -283,7 +283,7 @@ class MoqLiteSessionTest {
|
||||
val (clientSide, serverSide) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey")
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey", track = "audio/data")
|
||||
|
||||
// Step 1: relay opens Subscribe bidi.
|
||||
val subBidi = serverSide.openBidiStream()
|
||||
@@ -346,7 +346,7 @@ class MoqLiteSessionTest {
|
||||
val (clientSide, _) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey")
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey", track = "audio/data")
|
||||
// No relay-opened Subscribe bidi → no subscribers → send is
|
||||
// a silent no-op (returns false), matching the listener
|
||||
// semantics where the speaker keeps capturing audio even
|
||||
@@ -363,7 +363,7 @@ class MoqLiteSessionTest {
|
||||
val (clientSide, serverSide) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey")
|
||||
val publisher = session.publish(broadcastSuffix = "speakerPubkey", track = "audio/data")
|
||||
|
||||
// Relay opens an announce bidi.
|
||||
val relayBidi = serverSide.openBidiStream()
|
||||
|
||||
Reference in New Issue
Block a user