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
5 changes: 5 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ announce churn on today's hop-list mesh, shipping with the next cut from
route layer ships; and stats groups as
tracks, so per-group stats stop costing cluster announces.

Added 2026-10-23 (maintainer, from a linger audit): broadcasts lose their
finished state and retention moves to moq-mux, a break in every binding, so
it lands before the cut.

## Required

- [Epoch correctness](/quest/m0/broadcast-epoch/README.md) - publisher identity, duplicate suppression, and catalog pinning gate the breaking cut
Expand Down Expand Up @@ -107,6 +111,7 @@ tracks, so per-group stats stop costing cluster announces.
- [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it
- [Moxygen minimal interop](/quest/m1/smoke-moxygen.md) - waiting on Moxygen to send a registered filter type, then the smoke lane stops hiding its failure
- [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause
- [Broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md) - broadcasts are online or offline with no finished state in moq-net, finished-broadcast retention is a fixed moq-mux policy, and each linger timer has one meaning
- [JS interop tone](/quest/m1/interop-js-tone.md) - the `js ->` cells pass the audio check on every draft again, after the 2026-10-23 regression
- [A smaller, safer API than release](/quest/m1/api-audit/README.md) - every surface added since release has a consumer or is gone, overlapping operations are collapsed, names mirror across Rust, JS, and the bindings, and every break has its MIGRATE entry
- [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface
Expand Down
4 changes: 2 additions & 2 deletions quest/m1/api-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ Rejected in planning:
`close` collides with `AutoCloseable`, and no single verb fits every
language's reserved names.
- A follow policy enum folding `linger` and `stitch`: one name per meaning
([CLI flags](/quest/m1/api-audit/cli-flags.md)) fixes the confusion more
cheaply.
([TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md)
renames the export wait `--follow`) fixes the confusion more cheaply.

## Required

Expand Down
10 changes: 5 additions & 5 deletions quest/m1/api-audit/cli-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ Decided 2026-10-22 (API audit):
raw TS multiplex on `import|export ts`. Rename the fmp4/hls one to
`--container cmaf` or another distinct name, consistent with
[container formats](/quest/m1/api-audit/container-format.md).
- `linger`: publishers already linger on a clean end in rtmp, srt, rtc, and
the CLI. Drop moqsink's new boolean `linger` property and `<moq-publish>`'s
`linger` attribute so every publisher lingers; NULL and `close` still end
at once. `linger` then means only the subscriber-side wait for a
replacement (`moq export --linger`).
- `linger`: drop moqsink's new boolean `linger` property and
`<moq-publish>`'s `linger` attribute, keeping today's close at the end.
Revised 2026-10-23 (maintainer): don't make every publisher finish through
moq-net's `Finish`; [broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md)
deletes it and owns completion and retention.
- gst: moqsrc's `versions` (comma list) versus the CLI's repeatable
`--connect-version`; mirror one name, give moqsink the same, and give both
elements the same connection properties (moqsrc lacks
Expand Down
7 changes: 5 additions & 2 deletions quest/m1/api-audit/migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ Decided 2026-10-22 (API audit). Known gaps, verify each against
(`Shutdown(ctx) error`, and `Client.Close()` now blocks while draining),
`kt/moq`, `dart/moq` (`Moq.close()` returns `Future<void>`).
- Bindings: merge the contradictory `finish` entries ("removed (#4152)" and
"the new `finish` lets go (#6157)") into one: `finish()` now returns a
linger handle; call `close()` for the old behavior.
"the new `finish` lets go (#6157)") into one that names the final API:
once [stateless broadcasts](/quest/m1/broadcast-lifecycle/stateless.md)
lands, the broadcast `finish()` is gone, so a publisher finishes its tracks
and closes the broadcast. If this lands first, leave the entry for that
quest to rewrite rather than document the linger handle.
- Bindings: `rs/moq-ffi/MIGRATE.md` says a raw track is timed by default,
but `MoqTrackInfo.timescale` defaults to `None` (untimed) while release
defaulted to microseconds; the net change is timed to untimed by default.
Expand Down
59 changes: 59 additions & 0 deletions quest/m1/broadcast-lifecycle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Broadcasts are online or offline, and retention is app policy

## Goal

A moq-net broadcast has no lifecycle state of its own: it is online while
announced and offline once closed. The app marks the end of its content with
a track FIN (hang: finish the catalog). Keeping a finished broadcast up for
late readers is a fixed-duration app policy in moq-mux, not a deadline moq-net
derives from track max-age. Every other timer called "linger" has one meaning
per name.

## Plan

Decided with the maintainer on 2026-10-23, from an audit of every linger
timer on main at 1582fdc07d:

- `broadcast::Producer::finish` exists only in moq-net. Nothing reaches the
wire: ANNOUNCE active/ended already says online/offline, and a track FIN
already says a track's content ended. A BROADCAST wire message was
considered and rejected; it adds no behavior over those two and would need a
moq-transport mapping.
- The finished linger was inconsistent and partly accidental:
- Its duration is the max of track max-ages, which describe how long a
superseded group stays fresh and never the newest one. The JS publish
catalog declares `Number.MAX_SAFE_INTEGER` max-age, so every browser
publisher lingered the full 30s; Rust catalogs declare none.
- It counted from different events: Rust from the first poll of `wait`,
JS from the `wait` call, UniFFI from `finish`, and C from the caller's
own clock.
- Only Rust ended it when a newer epoch appeared at the path.
- Closing a broadcast already leaves tracks handed out playing to their end,
and session drain delivers their tail to attached subscribers. Retention
only serves readers that arrive after the end.
- A retaining publisher ignores a newer instance at its path: if the newer
one leaves, the old one is announced again, and readers see its finished
catalog and stop. Simpler than watching for supersession, and acceptable
under online/offline.
- The other timers stay. Each is a cache or debounce with its own name and
owner: the Rust idle track copy (30s), discovery (2s), the JS connection
pool (2s), and the relay's `[stats] linger` (5m). They are not cross-language
lifecycle guarantees.
- After this epic, user-facing "linger" means only publisher retention
(`moq` publishing subcommands, moqsink) and the relay's stats setting. The
export-side wait becomes `--follow`.
- Docs change inline in each quest; no separate lifecycle page.

## Required

- [Publisher retention in moq-mux](/quest/m1/broadcast-lifecycle/retention.md) - moq-cli, the gateways, and moqsink keep a finished broadcast up for a fixed `--linger` (default 30s) through a moq-mux helper instead of moq-net's `Finish`
- [Stateless broadcasts](/quest/m1/broadcast-lifecycle/stateless.md) - `Finish`, its linger, and finished-only supersession are gone from moq-net, `@moq/net`, moq-ffi, moq-c, and every binding
- [TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md) - `export ts|srt --linger` becomes `--follow <duration>`, a replacement's catalog has no separate deadline, and a track failure is classified by its cause instead of a 1s grace
- [SRT tells a clean end from a failure](/quest/m1/broadcast-lifecycle/srt-end.md) - an SRT ingest whose peer goes silent aborts instead of finishing as if it had shut down
- [Timer cleanups](/quest/m1/broadcast-lifecycle/timers.md) - `ConnectionProps.linger` is gone and stale linger references are fixed

## Related

- [moq-transport finish interop](/quest/m2/ietf-finish.md) - the interop `finish` case, whose late readers rely on retention
- [CLI and gst flags](/quest/m1/api-audit/cli-flags.md) - removes the opt-in `linger` booleans, leaving completion to this epic
- [Publication owner](/quest/m1/api-audit/announce-owner.md) - adds `origin::Publication`; it must not forward a `finish` this epic deletes
42 changes: 42 additions & 0 deletions quest/m1/broadcast-lifecycle/retention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# [M] Publisher retention in moq-mux

## Goal

moq-cli imports (stdin, HLS), the RTMP, SRT, RTC, and HLS gateways, and
moqsink keep a cleanly ended broadcast announced and serving its finished
tracks for a fixed duration counted from the end, then close it. They no
longer use moq-net's `broadcast::Finish`. The duration is `--linger
<duration>` on moq-cli's publishing subcommands and a `linger` duration
property on moqsink, defaulting to 30s; 0 closes at once. An interrupt
(Ctrl-C, moqsink NULL) ends it at once.

## Plan

Decided with the maintainer on 2026-10-23:

- One small moq-mux helper owns it, shared by moq-cli, `moq-rtmp`,
`moq-srt`, `moq-rtc`, and moqsink. It finishes the catalog and media
tracks, holds the broadcast and track producers, and closes the broadcast
when the deadline passes. moq-net gains nothing.
- The deadline is fixed when the content ends and does not depend on track
max-age or on when anything is polled. 30s matches today's effective
behavior (media max-age) and the relay's idle-copy window.
- It ignores a newer instance announced at the path; see the epic.
- `<moq-publish>` and the FFI/C bindings do not use it: they finish their
catalog and close, and a binding app owns its own process lifetime.
- moq-cli RTSP closes at once today (`rs/moq-cli/src/rtsp.rs`); give it the
helper too, or say why not.
- Keep each gateway's existing split between a clean end and a failure
(RTMP unpublish vs disconnect, WHIP DELETE vs a vanished peer); only a
clean end retains. SRT's split is
[its own quest](/quest/m1/broadcast-lifecycle/srt-end.md).
- Test the helper with mocked time: a late subscriber gets the whole tail
before the deadline, and the path is unroutable after it.
- Docs: `doc/bin/cli.md`, `doc/bin/gstreamer.md`, the gateway pages under
`doc/bin/`.

Public API: moq-mux gains the helper. moq-cli gains `--linger` on publishing
subcommands; `moq export --linger` keeps its meaning until
[TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md)
renames it. moqsink's `linger` returns as a duration (the boolean never
shipped in a release, so no MIGRATE entry). Wire: none.
22 changes: 22 additions & 0 deletions quest/m1/broadcast-lifecycle/srt-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# [S] SRT tells a clean end from a failure

## Goal

An SRT ingest whose peer stops sending without a shutdown handshake ends its
broadcast with an error, while a peer that shuts down cleanly finishes it.
Today both finish (`rs/moq-srt/src/server.rs`, the `Ok(())` arm after the
socket stream ends), because srt-tokio ends the stream the same way for both.

## Plan

Decided with the maintainer on 2026-10-23: fix it at the source.

- Find how srt-tokio reports a shutdown handshake versus a peer timeout. If
it exposes the difference, use it in both the server and the `Client`
pull (`serve_publish`).
- If it does not, an upstream srt-tokio change is needed: it is outside the
moq-dev and kixelated orgs, so ask before posting there.
- Test both ends with a mocked clock: a silent peer aborts, a clean shutdown
finishes.

Public API: none. Wire: none.
43 changes: 43 additions & 0 deletions quest/m1/broadcast-lifecycle/stateless.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# [L] Stateless broadcasts

## Goal

moq-net and `@moq/net` broadcasts have no finished state. `Producer::finish`,
`Finish`, `Finish::linger`, `Finish::wait`, and the origin's finished-only
supersession (`Consumer::supersede`, `routes.superseded`, `Announcer::superseded`,
`BroadcastState.finished`/`superseded`) are deleted, along with moq-ffi's
`MoqBroadcastFinish` and moq-c's `moq_publish_finish` and
`moq_publish_finish_close`, and their wrappers in every binding. A publisher
Comment thread
kixelated marked this conversation as resolved.
ends its content by finishing its tracks (hang: the catalog) and goes offline
by closing the broadcast.

## Plan

Decided with the maintainer on 2026-10-23; see the epic for why.

- Keep the epoch routing that lets a restarted publisher win
(#6452); only supersession of a finished broadcast goes.
- Prove the property this relies on before deleting: a relay keeps
delivering a finished track to its existing downstream subscribers after
the upstream publisher's session closes, on lite and moq-transport. Fix
it at the source if it does not.
- The interop `finish` case must keep passing on lite. Each interop
publisher (Rust through the moq-mux helper; JS, C, C++ and the bindings by
holding their finished broadcast open) keeps its broadcast up long enough
for the late readers.
- `js/publish` drops its `Finish` bookkeeping
(`js/publish/src/broadcast.ts`, the catalog's `Number.MAX_SAFE_INTEGER`
max-age only if nothing else needs it). `js/net` drops `hooks.linger` and
its `CACHE_WINDOW` use there.
- Update `doc/concept` with one paragraph on the model (online = announced,
end of content = track FIN, retention = app policy), plus `doc/lib/*` and
every affected package's `MIGRATE.md`.
- Coordinate with [publication owner](/quest/m1/api-audit/announce-owner.md),
which touches the same handles.

Public API: breaking in moq-net, `@moq/net`, moq-ffi, moq-c, and every
binding. Wire: none.

## Required

- [Publisher retention in moq-mux](/quest/m1/broadcast-lifecycle/retention.md) - Rust publishers stop using `Finish` first, so none loses its tail in between
24 changes: 24 additions & 0 deletions quest/m1/broadcast-lifecycle/timers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# [XS] Timer cleanups

## Goal

`@moq/net`'s `ConnectionProps.linger` is deleted and the pool always uses its
fixed 2s idle timer. Comments that cite timers which no longer exist are
fixed.

## Plan

Decided with the maintainer on 2026-10-23:

- `ConnectionProps.linger` (`js/net/src/connection/pool.ts`) has no caller
outside the pool, and the pool keeps the largest value any handle ever
asked for, even after that handle leaves. Delete the prop instead of
defining a merge rule.
- `rs/moq-video/src/decode/consumer.rs` cites `TRACK_IDLE_LINGER`, which is
now `moq_net::model::track::IDLE_LINGER` (crate-private).
- The OBS "Give up after" help text (`cpp/obs/src/moq-settings.cpp`) says the
broadcast "lingers for viewers across the gap", but the setting is a
reconnect backoff timeout. Say what it does.

Public API: `@moq/net` breaking (`ConnectionProps.linger` removed). Wire:
none.
36 changes: 36 additions & 0 deletions quest/m1/broadcast-lifecycle/ts-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# [M] TS export follows by cause

## Goal

`moq export ts --linger` and `moq export srt --linger` become `--follow
<duration>`: how long to wait for the publisher, or with `--stitch` a
replacement, to come back. The default 0 means don't follow. The Rust
`container::ts::Follower::with_linger` and moq-srt's `Subscribe::with_linger`
and `Client::with_linger` become `with_follow`. A replacement's first catalog
is awaited until it arrives or the replacement withdraws, with no deadline of
its own. A track failure is classified by its cause, not by waiting
`CLOSE_GRACE` (1s) to see whether the announcement also goes.

## Plan

Decided with the maintainer on 2026-10-23:

- One knob, one meaning. Today `--linger` also bounds the stitch catalog
fetch (`rs/moq-mux/src/container/ts/follower.rs`, `Awaiting { deadline }`),
so `--stitch --linger 0` fails on a cold remote replacement. The
replacement's own lifetime is the bound instead.
- `CLOSE_GRACE` exists because the track error and the announcement removal
arrive on different streams. Find what tells "the publisher left" from "this
export failed" in the track's own end: the abort cause a relay gives a copy
whose upstream went away. If no cause distinguishes them today, add one at
the source. A wire error code change updates the drafts.
- The hidden `--linger` on the other container exports
(`rs/moq-cli/src/args.rs`) keeps refusing; point its message at `--follow`
only where following applies.
- Coordinate the name with [readers follow restarts](/quest/m1/sink-restart-follow.md),
which owns `moq_mux::follow::Instances`.
- Docs: `doc/bin/cli.md`, `rs/moq-cli/MIGRATE.md`, `rs/moq-mux` and
`rs/moq-srt` MIGRATE entries.

Public API: moq-mux and moq-srt renames (breaking); moq-cli flag rename.
Wire: none unless the failure cause needs a new code.
4 changes: 3 additions & 1 deletion quest/m1/sink-restart-follow.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ Audit and fix the rest the same way:
(`EXT-X-DISCONTINUITY`); a failed timeline answers its playlists with a no-store 5xx.
- `export rtmp`, `export srt`, `export rtc`, both `--listen` (per-viewer
streams in `moq-rtmp`, `moq-srt`, `moq-rtc`) and `--connect` (one push).
SRT already rides the TS follower (`--linger`, `--stitch`); confirm its
SRT already rides the TS follower (`--linger`, becoming `--follow` in
[TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md),
and `--stitch`); confirm its
listen side does too. FLV and WebRTC need a decision.
- `transcode`: its source broadcast; following means restarting the ladder
on the new instance.
Expand Down
3 changes: 2 additions & 1 deletion quest/m2/ietf-finish.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
## Goal

The interop `finish` case passes on every even moq-transport draft, not only
moq-lite: each reader that joins a finished broadcast during its linger gets
moq-lite: each reader that joins a finished broadcast while its publisher retains it
([broadcast lifecycle](/quest/m1/broadcast-lifecycle/README.md)) gets
the whole ten-frame tail, natural media and catalog EOF, the exact broadcast's
withdrawal, and the publisher's later health broadcast through the same
session. Until then `interop.sh` records those cells as skipped; delete that
Expand Down
Loading