docs(audio-rooms): full moq-lite wire spec + IETF gap call-outs

Background research turned up the complete moq-lite (Lite-03) wire
format from kixelated/moq-rs and @moq/lite v0.1.7. Folded the spec
into nestsClient/plans/2026-04-26-moq-lite-gap.md as a phase-5
implementation plan:

  - ALPN ("moq-lite-03"); no SETUP/control message in Lite-03 (the WT
    handshake IS the handshake)
  - Per-request bidi streams keyed by ControlType varint (Announce=1,
    Subscribe=2, Fetch=3, Probe=4)
  - AnnouncePlease(prefix) / Announce(status, suffix, hops) shape
  - Subscribe with priority (raw u8), ordered, maxLatency (ms),
    startGroup/endGroup (off-by-one None-encoded), reply Ok/Drop
  - Group = uni stream with (DataType=0, subscribeId, sequence)
    header followed by varint-length frames until QUIC FIN
  - No datagrams; no per-frame envelope beyond size
  - Mandatory path normalisation; FIN-as-unsubscribe; RESET_STREAM
    for errors

Phase-5a..e implementation plan included (~1 week scope).

Doc + KDoc updates so the IETF MoQ-transport code is correctly
labelled and the moq-lite gap is discoverable from every entry point:

  - .claude/CLAUDE.md project description and architecture diagram
  - nestsClient/plans/2026-04-26-audio-rooms-completion.md status block
  - MoqSession.kt, MoqMessage.kt, MoqObject.kt, MoqCodec.kt KDoc — flag
    these as "IETF draft-ietf-moq-transport-17, NOT moq-lite", with
    pointers to the gap doc
  - NestsConnect.kt — note that step 3 of the listener handshake
    (SETUP) does NOT match nostrnests's relay framing
This commit is contained in:
Claude
2026-04-26 16:39:44 +00:00
parent 1887bd1fa7
commit 7f48e52541
8 changed files with 305 additions and 95 deletions
@@ -43,6 +43,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
* bearer token.
* 3. Run the MoQ SETUP handshake.
*
* Note: step 3 runs the IETF `draft-ietf-moq-transport-17` SETUP, which is
* NOT what nostrnests's relay (moq-lite) expects. Steps 1 and 2 are
* already on the moq-rs wire shape (path = `/<namespace>`, JWT in
* `?jwt=`). The MoQ framing layer needs a moq-lite codec swap before
* end-to-end interop works — see
* `nestsClient/plans/2026-04-26-moq-lite-gap.md`.
*
* The returned [NestsListener] is in state [NestsListenerState.Connected];
* if any step fails, the listener is returned in
* [NestsListenerState.Failed] with the underlying cause attached and the
@@ -24,7 +24,8 @@ import com.vitorpamplona.nestsclient.moq.MoqCodec.encode
import com.vitorpamplona.quic.Varint
/**
* Encode/decode MoQ control-stream messages per draft-ietf-moq-transport.
* Encode/decode IETF `draft-ietf-moq-transport-17` control-stream
* messages.
*
* Wire format for every control message:
*
@@ -32,6 +33,10 @@ import com.vitorpamplona.quic.Varint
*
* The payload layout is per-message. Payload encoders here produce only the
* payload; [encode] wraps it with type+length for on-the-wire use.
*
* Wire-protocol scope: IETF MoQ-transport, NOT moq-lite. nostrnests's
* reference relay uses moq-lite — see
* `nestsClient/plans/2026-04-26-moq-lite-gap.md`.
*/
object MoqCodec {
/**
@@ -21,17 +21,19 @@
package com.vitorpamplona.nestsclient.moq
/**
* Subset of the MoQ-transport control-plane messages needed for a listener
* talking to a nests server. Per draft-ietf-moq-transport, control messages
* on the control stream share the wire layout:
* Subset of the **IETF `draft-ietf-moq-transport-17`** control-plane
* messages. Wire layout per the IETF draft: control messages on the
* single bidi share
*
* message_type (varint) | message_length (varint) | payload...
*
* Listener-side messages (CLIENT/SERVER_SETUP, SUBSCRIBE, SUBSCRIBE_OK /
* SUBSCRIBE_ERROR, UNSUBSCRIBE) are implemented. Publisher-side messages
* (ANNOUNCE / ANNOUNCE_OK / SUBSCRIBE-receiving / SUBSCRIBE_DONE) are not
* yet implemented; see `nestsClient/plans/2026-04-26-audio-rooms-completion.md`
* (Phase M5).
* Wire-protocol scope: this is the IETF MoQ-transport flavour. The
* nostrnests reference relay speaks **moq-lite** (kixelated's variant)
* which uses a wire-incompatible shape — no SETUP message, ControlType
* discriminator on each fresh bidi stream, single-string broadcast +
* track names rather than tuples. See
* `nestsClient/plans/2026-04-26-moq-lite-gap.md` for the full spec and
* the planned moq-lite parallel codec.
*/
sealed class MoqMessage {
abstract val type: MoqMessageType
@@ -21,20 +21,25 @@
package com.vitorpamplona.nestsclient.moq
/**
* A single MoQ object — a protocol-level unit of publisher-produced data
* (one Opus frame for nests audio) identified by its (track, group, object)
* coordinates and carrying application bytes.
* A single object in **IETF `draft-ietf-moq-transport-17`** — a
* publisher-produced unit identified by `(track_alias, group_id,
* object_id)` coordinates with a `publisher_priority` and a `status`
* varint, carrying app bytes.
*
* MoQ objects are delivered in three ways per draft-ietf-moq-transport:
* Per the IETF draft, objects are delivered three ways:
*
* 1. OBJECT_DATAGRAM — one object per QUIC datagram. Lowest latency, no
* retransmits. Used by nests for real-time audio.
* retransmits.
* 2. STREAM_HEADER_SUBGROUP — multiple objects per uni stream, reliable.
* 3. FETCH_HEADER — historical objects over a bidi stream.
*
* Today the listener path implements only (1) — OBJECT_DATAGRAM — which is
* what nests uses for live audio. (2) and (3) are reserved for future
* stream-delivered media; see the audio-rooms completion plan.
* This shape is **not** what nostrnests's relay (moq-lite) uses on the
* wire. moq-lite delivers media as *frames within groups within uni
* streams*: one uni stream per group, each with `(subscribe_id, group_seq)`
* header and then a sequence of `(size_varint, payload)` frames; no
* datagram path, no `track_alias`, no `publisher_priority`/`status`
* envelope. See `nestsClient/plans/2026-04-26-moq-lite-gap.md` §5 for
* the moq-lite frame layout.
*/
data class MoqObject(
val trackAlias: Long,
@@ -56,7 +56,17 @@ object MoqVersion {
}
/**
* Session wrapper over a [WebTransportSession] that speaks MoQ-transport.
* Session wrapper over a [WebTransportSession] that speaks IETF
* `draft-ietf-moq-transport-17` (CLIENT_SETUP / SERVER_SETUP, namespace
* tuples, OBJECT_DATAGRAM with `track_alias`).
*
* **Wire-protocol scope:** this is the IETF MoQ-transport flavour, NOT
* kixelated's moq-lite. The two are wire-incompatible (single-string
* paths vs. tuple namespaces, different ANNOUNCE/SUBSCRIBE shape).
* nostrnests's reference relay (`kixelated/moq-rs`) speaks moq-lite, so
* this session cannot exchange a single MoQ message with the real
* nostrnests stack. See `nestsClient/plans/2026-04-26-moq-lite-gap.md`
* for the gap and the planned moq-lite parallel codec.
*
* Lifecycle:
* 1. [client] / [server] attaches to a transport. No traffic yet.