Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions quest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ grouped into milestones ordered by priority.
## Plan

m0 is everything in flight now: relay hardening and IETF interop ahead of
Seattle, wildcard routing, and audio playout (jitter target and quality
harness). m1 is the next wave across reliability, features, performance, and
Seattle, wildcard routing, broadcast epochs, and audio playout (jitter target
and quality harness). m1 is the next wave across reliability, features, performance, and
planning. m2 holds later features, design studies, and experiments. m3 is
gated on the outside world: hardware, a partner, a consumer, or a provider's
offer. m4 waits on an upstream release.
Expand All @@ -21,7 +21,7 @@ move the blocked quest to the milestone its priority belongs in.

## Required

- [m0: immediate priorities](/quest/m0/README.md) - everything in flight now: relay hardening and IETF interop for Seattle, wildcard routing, and audio playout
- [m0: immediate priorities](/quest/m0/README.md) - everything in flight now: relay hardening and IETF interop for Seattle, wildcard routing, broadcast epochs, and audio playout
- [m1: next wave](/quest/m1/README.md) - reliability, capabilities, performance, and the planning that settles their contracts
- [m2: later work](/quest/m2/README.md) - deferred features, design studies, and experiments
- [m3: deferred](/quest/m3/README.md) - gated on the outside world: hardware, a partner, a consumer, or a provider's offer
Expand Down
18 changes: 14 additions & 4 deletions quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

## Goal

The work in flight now, in three independent tracks. Relay hardening: legal
The work in flight now, in four independent tracks. Relay hardening: legal
moq-transport input never fails a session ahead of Seattle interop on
2026-10-12, every resource a peer can make the relay hold is bounded by what
it sent or by a budget, and no peer input panics the process. Routing: a
service claims the prefix it could serve instead of enumerating broadcasts.
Identity: nothing treats who published a route as what it carries; a path,
with its `@epoch`, is the only content identity, and every first-party
publisher that can restart mints a fresh epoch, so #4741 stalls nobody.
Audio playout: the target is a measured estimate of arrival timing in both
languages, and a browser regression fails a nightly run.

## Plan

The release API gates (#3829..#3878) and the release that followed them are
done. moq.pro tracks this repository as a submodule rather than a release, so
no release quest gates this milestone. The Pronto GPU integration lives in
moq.pro.
done. moq.pro pins this repository's `release` line, so the release gate below
also keeps #4741 from reaching it early.
The Pronto GPU integration lives in moq.pro.

The branch flip ranks first: it is a short cutover that every later PR
targets. Relay hardening: IETF interop leads the rest, since only those
Expand All @@ -29,6 +32,12 @@ work is done on the line branch and waits to land. Serving the relay's
ingested-only view (`origin::Consumer::local()`) to localhost workers belongs
to moq.pro's edge, which embeds moq-relay; it moved there on 2026-09-28.

Identity: the [broadcast epoch](/quest/m0/broadcast-epoch/README.md) line
ranks right after Wildcard and gates the next release (decided 2026-10-03:
#4741 resumes an un-epoched republish into the old broadcast and stalls its
viewers). #4741 can merge to main, but no release ships until first-party
publishers mint epochs.

Audio playout: the jitter target replaces the round-trip guess. The browser
audio quality harness in `test/audio-quality/` has landed; it grades playout
nightly and records the traces it replays. Its native lane is a standalone m1
Expand All @@ -47,6 +56,7 @@ a published `@moq/watch` break.
- [qmux credit](/quest/m0/qmux-credit.md) - qmux returns connection credit for dropped and stopped streams and delivers its close frame, on both lines
- [Shared fronts](/quest/m0/shared-fronts.md) - viewer sessions share a front, so fronts scale with peers, not viewers
- [Wildcard](/quest/m0/wildcard/README.md) - a relay resolves subscriptions against advertised prefixes, a service claims the prefix it could serve and refuses the rest instead of enumerating broadcasts, and the browser player treats a covering claim as availability
- [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - every first-party publisher that can restart mints a fresh `@<uuidv7>` epoch, viewers follow the newest live one, and bare names still resolve on every version
- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - the audio playout target is a measured estimate of arrival timing in both languages, not a round-trip guess

## Related
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,37 @@

## Goal

Reusing a broadcast name is never ambiguous. By default, each publish of
`demo/BBB.hang` goes out as `demo/BBB.hang/@<uuidv7>`, so the relay and viewers
can tell a new broadcast from a continuation. A viewer of the name follows
the newest live epoch as soon as it is announced, instead of waiting ~30s for
the old route's QUIC idle timeout. Moving to a new epoch is a clean boundary
(a fresh catalog and tracks, never spliced), and each run stays addressable
by its full path.
A path, with its `@epoch`, is the only content identity, and no first-party
publisher reuses one for different content. With #4741, any route covering a
path resumes its subscriptions from the first frame the subscriber lacks,
whoever serves it. So epochs are a correctness requirement: a publisher that
restarts its group numbering at 0 under an un-epoched name, while its old
route lingers, is resumed into the old broadcast and stalls viewers until its
sequence catches up.

By default, each publish of `demo/BBB.hang` goes out as
`demo/BBB.hang/@<uuidv7>`, so a restart is a new broadcast. A viewer of the
bare name follows the newest live epoch as soon as it is announced. Moving to
a new epoch is a clean boundary (a fresh catalog and tracks, never resumed
across), and each run stays addressable by its full path.

The epoch rides in the path, so it survives any moq-transport relay, and no
wire message changes. At an epoch-aware relay, a request for a bare name
resolves to its newest live epoch on every protocol version.

Non-goals: pooling redundant publishers that share one epoch, which
[Cluster routing](/quest/m1/cluster-routing/README.md)'s selection and
`--hop` removal own; this line only keeps an epoch a caller supplies. Also out
of scope: trusting the publisher's clock (a far-future epoch
wins until its route goes away).
Non-goals: pooling, which needs nothing here, since every publisher of one
path is already one source; a redundant pair shares an explicit epoch through
[`--hop` removal](/quest/m0/broadcast-epoch/hop-removal.md). Also out of
scope: trusting the publisher's clock (a far-future epoch wins until its
route goes away).

## Plan

Decided:

- This line gates the next release (decided 2026-10-03: #4741 can merge to
main, but without epochs every restarting first-party publisher stalls its
viewers).
- The marker is a child segment `@<uuidv7>`, parsed into
`Option<Epoch>` by [the shared primitive](/doc/concept/moq-lite.md#publisher-epochs). Not a lite-07 flag:
the path is the only carrier.
Expand All @@ -41,6 +50,11 @@ Decided:
bare `foo`, since a route covers its descendants, not its parent. A
bare-name viewer behind one needs a publisher that opts out with the raw
prefix route. Document this rather than promise it works.
- Publishers on the default publish path, such as moq-boy and moq-room,
inherit the epoch from Origin. moq-stats mints its own through
[Stats epochs](/quest/m1/stats-epoch.md), which is outside the release
gate: until it lands, a restarted stats node under a reused name can stall
its viewers the same way.
- Derived output mirrors the epoch it came from
(`.pro/transcode/<pid>/foo.hang/@e`, per the
[wildcard](/quest/m0/wildcard/README.md) line's derived-output layout), so
Expand All @@ -58,9 +72,10 @@ This README owns:

## Required

- [Origin](/quest/m1/broadcast-epoch/origin.md) - moq-net publish mints an epoch, consumers follow the newest live one, and bare requests resolve to it on every version
- [Apps](/quest/m1/broadcast-epoch/apps.md) - moq-cli, the browser publish and watch components, and demo/web publish under epochs and play bare names
- [Gateways](/quest/m1/broadcast-epoch/gateways.md) - RTMP, SRT, and WHIP ingest mint an epoch per incoming connection, so an encoder reconnect is a clean takeover
- [TS restart](/quest/m1/broadcast-epoch/ts-restart.md) - a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch
- [Bindings](/quest/m1/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and inherit the default
- [GStreamer and OBS](/quest/m1/broadcast-epoch/gst-obs.md) - moqsink and the OBS plugin publish each run under a fresh epoch
- [Origin](/quest/m0/broadcast-epoch/origin.md) - moq-net publish mints an epoch, consumers follow the newest live one, and bare requests resolve to it on every version
- [Apps](/quest/m0/broadcast-epoch/apps.md) - moq-cli, the browser publish and watch components, and demo/web publish under epochs and play bare names
- [Gateways](/quest/m0/broadcast-epoch/gateways.md) - RTMP, SRT, and WHIP ingest mint an epoch per incoming connection, so an encoder reconnect is a clean takeover
- [TS restart](/quest/m0/broadcast-epoch/ts-restart.md) - a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch
- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and inherit the default
- [GStreamer and OBS](/quest/m0/broadcast-epoch/gst-obs.md) - moqsink and the OBS plugin publish each run under a fresh epoch
- [Remove `--hop`](/quest/m0/broadcast-epoch/hop-removal.md) - `moq` takes an optional `--epoch` instead of `--hop`, a plain publisher declares a random Hop ID, and the per-session hop stamp and NO_CAPACITY are gone
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
## Goal

`moq-cli` publish and play, `@moq/publish`, `@moq/watch`, and `demo/web` use
the origin default. Each publish run is a new epoch, and watching a bare name
switches to a republish within an RTT rather than after the idle timeout.
The UI and logs show the full epoch path, and a watch link can pin one.
the origin default. Each publish run is a new epoch, so a restart while the
old route lingers is a new broadcast rather than a resume into the old one,
which stalls viewers until the new run's group sequence catches up. Watching
a bare name switches to a republish within an RTT. The UI and logs show the
full epoch path, and a watch link can pin one.

## Plan

Expand All @@ -18,4 +20,4 @@ The UI and logs show the full epoch path, and a watch link can pin one.

## Required

- [Origin](/quest/m1/broadcast-epoch/origin.md) - the publish default and follow logic
- [Origin](/quest/m0/broadcast-epoch/origin.md) - the publish default and follow logic
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ Decided in the 2026-09-30 audit: libmoq is frozen (renamed `rs/moq-c`), so C and

## Required

- [Origin](/quest/m1/broadcast-epoch/origin.md) - the behavior the bindings surface
- [Origin](/quest/m0/broadcast-epoch/origin.md) - the behavior the bindings surface
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

RTMP, SRT, and WHIP ingest publish each incoming connection under a fresh
epoch. An encoder that reconnects to the same stream key becomes a clean
takeover at once, instead of a second publisher racing the stale one until it
times out.
takeover at once. Without it, the reconnect is resumed into the stale
connection's broadcast and stalls viewers until its group sequence catches
up.

## Plan

Expand All @@ -16,8 +17,8 @@ describes ingest paths.

## Required

- [Origin](/quest/m1/broadcast-epoch/origin.md) - the publish default
- [Origin](/quest/m0/broadcast-epoch/origin.md) - the publish default

## Related

- [TS restart](/quest/m1/broadcast-epoch/ts-restart.md) - a signalled restart inside one connection, which this quest's per-connection epoch does not cover
- [TS restart](/quest/m0/broadcast-epoch/ts-restart.md) - a signalled restart inside one connection, which this quest's per-connection epoch does not cover
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ audit: libmoq gets no new API). Update `doc/bin/gstreamer.md` and

## Required

- [Bindings](/quest/m1/broadcast-epoch/bindings.md) - moq-ffi exposes epochs
- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi exposes epochs
- [C++ through moq-ffi](/quest/m1/cpp/README.md) - OBS publishes through the generated C++
54 changes: 54 additions & 0 deletions quest/m0/broadcast-epoch/hop-removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# [M] Redundant publishers share an epoch, not a hop

## Goal

Nothing names a publisher's identity outside its path. `moq` loses `--hop`,
`MOQ_HOP`, and the hidden `--origin` alias; an optional `--epoch` takes their
place for publishing. Omitted, each run gets a fresh epoch (the origin
default); a redundant pair passes the same value. A plain publisher declares
a random Hop ID per process with no flag, as the bindings and js already do,
so a multi-homed publisher still catches its own loops. `--cluster-id` names
a node and no longer falls back to `--hop`. Relays stop stamping an unnamed
publisher's hop chain, and NO_CAPACITY is gone: every refusal is terminal.

## Plan

- Drop the per-session hop stamp for unnamed publishers (decided 2026-10-03:
split horizon rests on `via` and own-hop checks, and the stamp never
matched any relay's id, so it only existed for publisher identity). Delete
`Hops::stamp` (`rs/moq-net/src/model/origin.rs`), the `stamp` fields in
`rs/moq-net/src/{lite,ietf}/subscriber.rs`, `stampHops` in
`js/net/src/hop.ts` and its callers in `js/net/src/{lite,ietf}/subscriber.ts`,
and their tests, including `rs/moq-net/tests/legacy_reconnect.rs`. Keep the
leading 0, so an unnamed chain still ranks anonymous.
- Remove NO_CAPACITY entirely (decided 2026-10-03: the maintainer does not
want a retryable refusal). Every refusal is terminal, and an advertiser
sheds load by withdrawing or re-pricing its route. That covers
`StreamCode.NoCapacity` and its uses in `js/net/src/{error,origin}.ts` and
the reserved-code comments in `rs/moq-net/src/error.rs`.
- Drafts: lite-07 and cluster-02 are in progress, so edit their text and
changelogs in place. In `drafts/draft-lcurley-moq-lite.md`: the stamping
rule, NO_CAPACITY in Error Codes and Resolution, "the identity it stamps",
and "the first entry identifies the endpoint that originated the route". In
`drafts/draft-lcurley-moq-cluster.md`: the unknown-publisher stamping text,
the relay behavior that stamps, NO_CAPACITY and its IANA row, and the
changelog bullets.
- CLI: `rs/moq-cli/src/{args,complete,fetch,ls}.rs`. Relays keep `cluster.id`
as their node id.
- Docs: `doc/bin/cli.md` ("Redundant publishers" uses `--epoch`), the
migration row in `doc/setup/upgrade.md`, the stamping paragraph in
`doc/bin/relay/cluster.md`, and the stamping sentence in
`doc/concept/moq-lite.md`. Search the repo for `--hop`, `MOQ_HOP`, and
`--cluster-id` examples, including demo recipes.

Open: whether anything declares an incumbent dead faster than the QUIC
idle timeout (a failover service that retracts it, or active-active delivery
to the relay). Neither is required to land this.

Public API: removes `--hop`, `MOQ_HOP`, and `--origin`, adds `--epoch`, and
`--cluster-id` stops reading `--hop`; js/net loses `StreamCode.NoCapacity`.
Wire: relays stop stamping, and lite-07 and cluster-02 lose NO_CAPACITY.

## Required

- [Origin](/quest/m0/broadcast-epoch/origin.md) - the default mint `--epoch` falls back to
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ any wire change.
one `@` segment below it. They pick the greatest live epoch, and on a table
change, re-select: move up at once, or fall back when the current one is
retracted. A path that names an epoch pins it and never moves. Each move is
a new broadcast to the caller, never a splice. The #3312 first-hop resume
rule still applies within one epoch path.
a new broadcast to the caller, never a splice. Within one epoch path, any
route resumes a subscription, whoever serves it.
- Bare resolution: a bare request with no route of its own resolves to that
epoch. A takeover ends the bare subscription with a typed reset, never a
silent switch. Choose the code so existing clients resubscribe rather than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ When a TS feed rewinds its time base and signals it
the rest of the same input as a new broadcast under a fresh epoch, in the same
process and connection. This covers an encoder restart or source switch behind
a gateway that keeps its connection up, and a looping playout server, none of
which makes a new connection for [Gateways](/quest/m1/broadcast-epoch/gateways.md)
which makes a new connection for [Gateways](/quest/m0/broadcast-epoch/gateways.md)
to turn into an epoch. An unsignalled rewind stays fatal, as #4543 decided.

## Plan
Expand All @@ -21,7 +21,7 @@ Decided (maintainer, 2026-09-30):

- A rewind is new content, so it is always a new broadcast at a new epoch
path, never a continuation of the old name. Viewers of the bare name follow
it through [Origin](/quest/m1/broadcast-epoch/origin.md).
it through [Origin](/quest/m0/broadcast-epoch/origin.md).
- `decode` stops at the flagged rewind and reports it. The caller finishes the
old broadcast (a clean end, not an abort, so its viewers read to its end),
publishes a new broadcast at a fresh epoch path, and calls
Expand All @@ -45,7 +45,7 @@ and `restart` is new. Wire: none.

## Required

- [Origin](/quest/m1/broadcast-epoch/origin.md) - the fresh epoch path the rest of the feed publishes under
- [Origin](/quest/m0/broadcast-epoch/origin.md) - the fresh epoch path the rest of the feed publishes under

## Closes

Expand Down
Loading
Loading