bd12d5ef72
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
156 lines
6.0 KiB
Markdown
156 lines
6.0 KiB
Markdown
# EGG-03: Audio plane (moq-lite)
|
|
|
|
`status: draft`
|
|
`requires: EGG-02`
|
|
`category: required`
|
|
|
|
## Summary
|
|
|
|
Audio is carried over [moq-lite](https://github.com/kixelated/moq) **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 `Frame`s 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 `Frame`s 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
|
|
|
|
1. A speaker MUST emit an `Announce` with `status=Active` immediately after
|
|
the WebTransport session reaches a usable state, and emit `status=Ended`
|
|
when stopping the broadcast cleanly.
|
|
2. A speaker MUST cap their broadcast to one concurrent `audio/data` track
|
|
per session. Multi-quality stacks are reserved for a future EGG.
|
|
3. Listeners MUST tolerate gaps in the group sequence (frames dropped due to
|
|
network loss). They MUST NOT request retransmits.
|
|
4. Listeners SHOULD subscribe with `ordered=true`. Out-of-order delivery
|
|
would re-introduce reorderable jitter the Opus decoder is not equipped to
|
|
handle.
|
|
5. A speaker MUST encode in mono. Stereo broadcasts are reserved for a future
|
|
EGG.
|
|
6. Listeners MUST NOT subscribe to their own broadcast (would create an audio
|
|
loopback through the relay).
|
|
7. The relay MUST drop a publishing peer's `Announce` and any subsequent
|
|
stream data if the JWT's `put` claim does not contain `<pubkey hex>`.
|
|
8. **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=Ended` on mute (mute is
|
|
a transient state; ending the broadcast is reserved for leaving the
|
|
stage). The presence event's `muted` flag (EGG-04) communicates the
|
|
intent to listeners that lack the audio plane.
|
|
9. **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`.
|