Edits across all 13 EGGs to remove implementer guesswork. After this an
implementer can build a listener and speaker without reading our source.
README:
- Conventions section: hex casing rule (lowercase, 64 chars, no 0x),
NIP-01 foundations, `a`-tag form, created_at tie-break, JSON / time.
- Joining sequence: numbered end-to-end walkthrough from `kind:30312`
to first audio frame, referencing each EGG.
EGG-01 (room event):
- `relays` tag is one tag with multiple values (not multi-tag).
- `service` URL trailing-slash normalization.
- `private` status: gate is implementation-defined, not "render as open".
- `d`-tag charset locked to [A-Za-z0-9._-] so it interpolates safely
into the moq-auth namespace.
- Relay-discovery rule: publish to `relays` tag ∪ NIP-65 outbox.
- Tie-break on identical created_at via smallest event id.
EGG-02 (auth):
- JWT signing pinned: ES256 over P-256, JWKS at /.well-known/jwks.json,
5-minute relay cache.
- NIP-98 tags pinned: u / method / payload, base64 RFC 4648 standard
(not base64url).
- Error taxonomy: full HTTP status + `error` slug table for /auth, plus
WebTransport CONNECT 200/401/403/404 table.
EGG-03 (audio):
- Pin moq-lite Lite-03 to kixelated/moq-rs `rs/moq-lite/src/lite/`.
- One Opus packet per moq Frame (no container, no timestamp).
- Pubkey hex casing reaffirmed at the suffix / broadcast slots.
- AnnouncePlease prefix="" for speaker discovery.
- Mute = stop publishing (not silence frames, not Announce Ended).
- Mid-stream join: discard pre-skip per RFC 7845.
EGG-04 (presence):
- Heartbeat jitter ±5 s required (anti-thundering-herd).
- "0"/"1" are strings, not booleans.
- Single-room rule via replaceable-event semantics.
EGG-05 (chat): 8 KB suggested, 64 KB hard cap, 3 msg/s render rate.
EGG-06 (reactions): 30s window measured against created_at, drop-on-arrival
if already stale.
EGG-07 (moderation): replay protection — dedupe kicks by id within 120 s.
EGG-08 (scheduling): planned rooms MUST 403 at the auth sidecar.
EGG-10 (theming): bg image caps (1 MB / 4096 px soft, 8 MB / 8192 px hard).
Deferred (per review): EGG-00 (Conventions as a standalone spec), EGG-13
(capability advertisement), EGG-14 (discovery), test vectors corpus.
The "Conventions" section in README covers EGG-00's most urgent content
inline.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
6.0 KiB
EGG-03: Audio plane (moq-lite)
status: draft
requires: EGG-02
category: required
Summary
Audio is carried over moq-lite Lite-03
on top of the WebTransport session opened in EGG-02. Each speaker publishes
one broadcast keyed by their pubkey hex, on a single track named
audio/data, encoded as Opus.
This EGG defines the broadcast/track naming convention and the audio codec
parameters. It does not redefine moq-lite framing — refer to the upstream
spec for the wire layout of Subscribe, Announce, Group, and Frame
control messages.
Normative reference for moq-lite Lite-03
The wire formats for AnnouncePlease, Announce, Subscribe, Group
header, and Frame are defined by the Rust reference implementation at:
https://github.com/kixelated/moq-rs
rs/moq-lite/src/lite/{announce,subscribe,group,setup,session}.rs
Implementers MUST track Lite-03 (NOT the IETF moq-transport draft, which
is incompatible). Cross-version interop tests MUST pin a specific
moq-rs commit hash for reproducibility.
All path / broadcast / track strings on the moq-lite wire are
length-prefixed UTF-8. All integers are RFC 9000 §16 varints unless the
upstream codec says otherwise. Path components on this wire MUST contain
only the characters allowed in a URL path segment per RFC 3986; relays
typically reject / inside a single path component.
Wire format
Speaker → relay (publish)
A speaker MUST send a moq-lite Announce with:
prefix = "" (empty — relative to the JWT's `root` namespace)
suffix = <pubkey hex> (lowercase, 64 chars, see Conventions in README)
status = Active (status byte 0x01 per Lite-03)
hops = 0
After the announce, the speaker opens a unidirectional QUIC stream per group,
prefixed with a moq-lite Group header followed by Frames carrying Opus
payloads.
Each moq-lite Frame carries exactly one Opus packet (one 20 ms frame).
Producers MUST NOT pack multiple Opus packets into a single Frame, MUST NOT
prefix the payload with an Ogg page or any container, and MUST NOT carry an
explicit timestamp inside the Frame — the receive order plus the fixed 20 ms
cadence is the timing contract.
Listener → relay (subscribe)
A listener MUST send a moq-lite Subscribe with:
broadcast = <speaker pubkey hex> (lowercase, 64 chars)
track = "audio/data"
priority = 128 (recommended)
ordered = true
maxLatency = 0 (unlimited)
To learn which broadcasts exist on this session, a listener sends a
moq-lite AnnouncePlease with prefix="" once after the moq-lite Setup
handshake completes. The relay then streams Announce frames for every
active speaker; the listener subscribes to each per the form above.
The relay forwards each subsequent Group and its Frames to the listener
in subscribe-id order.
Codec
| Parameter | Value |
|---|---|
| Codec | Opus (RFC 6716) |
| Sample rate | 48 kHz |
| Channels | 1 (mono) |
| Frame duration | 20 ms (960 samples) |
| Bit-rate target | 32 kbit/s (configurable, no upper cap) |
| VBR | Allowed |
A new Group MUST be opened on every Opus reset (e.g. mute/unmute).
Behavior
- A speaker MUST emit an
Announcewithstatus=Activeimmediately after the WebTransport session reaches a usable state, and emitstatus=Endedwhen stopping the broadcast cleanly. - A speaker MUST cap their broadcast to one concurrent
audio/datatrack per session. Multi-quality stacks are reserved for a future EGG. - Listeners MUST tolerate gaps in the group sequence (frames dropped due to network loss). They MUST NOT request retransmits.
- Listeners SHOULD subscribe with
ordered=true. Out-of-order delivery would re-introduce reorderable jitter the Opus decoder is not equipped to handle. - A speaker MUST encode in mono. Stereo broadcasts are reserved for a future EGG.
- Listeners MUST NOT subscribe to their own broadcast (would create an audio loopback through the relay).
- The relay MUST drop a publishing peer's
Announceand any subsequent stream data if the JWT'sputclaim does not contain<pubkey hex>. - Mute behavior. Muting MUST be implemented as "stop publishing" — the
speaker closes their currently-open Group stream and does NOT open a
new one until unmute. Speakers MUST NOT push silence frames to fake
continuity, and MUST NOT send
Announce status=Endedon mute (mute is a transient state; ending the broadcast is reserved for leaving the stage). The presence event'smutedflag (EGG-04) communicates the intent to listeners that lack the audio plane. - Joining mid-stream. Listeners MUST tolerate joining at any point inside a Group; the first received Frame is independently decodable (Opus is a self-synchronising codec). The first decoded sample MAY contain a few ms of pre-roll noise; receivers SHOULD discard the first decoded packet's pre-skip samples per RFC 7845 §4.2.
Example
A two-speaker room:
A's session: ANNOUNCE prefix="" suffix="speakerA" status=Active
GROUP subscribe-id=…, sequence=0
FRAME <opus payload, 20ms>
FRAME <opus payload, 20ms>
…
Listener's session:
SUBSCRIBE broadcast="speakerA" track="audio/data"
← GROUP sequence=0
← FRAME <opus payload, 20ms>
← …
SUBSCRIBE broadcast="speakerB" track="audio/data"
← GROUP sequence=0
← FRAME <opus payload, 20ms>
← …
Compatibility
EGG-12 adds an OPTIONAL catalog.json track per broadcast carrying codec
metadata. EGG-12 MUST NOT change the audio/data track parameters defined
here.
A future "video plane" EGG MAY introduce additional tracks; their names MUST
NOT collide with audio/data or catalog.json.