Files
amethyst/nestsClient
Claude 5c32996f93 feat(audio-rooms): M5 MoQ publisher path — ANNOUNCE + inbound SUBSCRIBE + OBJECT emit
Lifts MoqSession from listener-only to bidirectional. A session can now
ANNOUNCE a track namespace, register one TrackPublisher per track name
under it, accept inbound SUBSCRIBEs from the peer, and emit
OBJECT_DATAGRAMs that fan out to every attached subscriber. This is what
the speaker / host path needs in nests — phases M6/M7 layer audio
capture + a NestsSpeaker API on top.

Public API additions:
- `MoqSession.announce(namespace, parameters)` — sends ANNOUNCE, awaits
  ANNOUNCE_OK, returns an `AnnounceHandle`.
- `AnnounceHandle.openTrack(name)` — registers a `TrackPublisher` for a
  track under the announced namespace. Idempotent insert is rejected.
- `TrackPublisher.send(payload)` — encodes one MoQ OBJECT_DATAGRAM and
  emits it to every currently-attached subscriber. Group id is fixed at
  zero per the audio-rooms NIP draft; object ids are monotonic.
- `TrackPublisher.close()` — sends SUBSCRIBE_DONE to attached
  subscribers, removes the track from its parent announce.
- `AnnounceHandle.unannounce()` — sends UNANNOUNCE on the wire, closes
  every registered publisher.
- `ErrorCode.TRACK_DOES_NOT_EXIST` and `SubscribeDoneStatus.{TRACK_ENDED,
  UNSUBSCRIBED}` constants for the SUBSCRIBE_ERROR / SUBSCRIBE_DONE
  values we emit.

Internal additions:
- `pendingAnnounces` keyed by namespace, mirroring the existing
  `pendingSubscribes` pattern.
- `announces` map tracking live publishers per namespace.
- `inboundSubscribers` + `publisherSubscribers` so the control-pump can
  fan an inbound SUBSCRIBE to the right TrackPublisher and so
  TrackPublisher.send can snapshot subscribers without per-OBJECT
  locking.
- Control pump now routes ANNOUNCE_OK / ANNOUNCE_ERROR (publisher-side
  ack) and inbound SUBSCRIBE / UNSUBSCRIBE (we're being asked to send
  OBJECTs, or stop). Unknown-track SUBSCRIBE replies with
  SUBSCRIBE_ERROR(TRACK_DOES_NOT_EXIST) so the peer doesn't hang.
- `close()` propagates session death into every AnnounceHandleImpl /
  TrackPublisherImpl so any in-flight `send` short-circuits cleanly.

All shared-state mutation is funneled through the existing `stateMutex`
(no `synchronized` — that's JVM-only and would break commonMain). The
session-wide `writeMutex` continues to serialize control-stream writes.

Tests (new in MoqSessionTest):
- announce → ANNOUNCE_OK happy path; UNANNOUNCE wire frame on close.
- ANNOUNCE_ERROR surfaces as MoqProtocolException.
- End-to-end: announce + openTrack + peer SUBSCRIBE → SUBSCRIBE_OK +
  three OBJECT_DATAGRAMs round-trip with intact group/object ids.
- send() returns false when no subscribers are attached (no buffering).
- Inbound SUBSCRIBE for unknown track under an announced namespace
  replies SUBSCRIBE_ERROR.

Verified: `:nestsClient:jvmTest` (12 tests, 0 failures) + `:quic:jvmTest`
(green) + `./gradlew spotlessApply` clean.
2026-04-26 03:11:33 +00:00
..