From 01ceb4cabf4da25850f32af98cb4ce447bf8d54a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 12:42:11 -0700 Subject: [PATCH 1/3] chore(quest): audit cleanup (wip) --- quest/README.md | 6 +- quest/m0/README.md | 107 +++--------- quest/m0/audio-quality-harness/README.md | 25 +-- quest/m0/audio-quality-harness/browser.md | 20 ++- quest/m0/bbb-sd.md | 39 ----- quest/m0/plan-av-clock.md | 27 ++- quest/m0/release.md | 89 ---------- quest/m0/wildcard/README.md | 29 ++-- quest/m0/wildcard/demand.md | 49 ------ quest/m0/wildcard/resolve.md | 105 ------------ ...l-clock-latency-target-for-synchronized.md | 29 ++-- ...bandwidth-grant-in-moq-audio-instead-of.md | 5 + ...r-a-synchronous-decode-so-the-publisher.md | 4 +- ...otation-is-not-atomic-across-thread-per.md | 4 +- ...s-dropping-one-split-server-resizes-the.md | 5 +- ...ic-tracks-and-preserve-sequences-across.md | 24 ++- ...coder-captures-the-rewind-generation-at.md | 13 ++ quest/m1/3489-ts-import-stream-liveness.md | 6 +- quest/m1/announce-live-apps.md | 12 +- quest/m1/announce-live-bindings.md | 33 ---- quest/m1/archive/README.md | 11 +- quest/m1/audio-codecs/encode-backend.md | 6 + .../native.md => m1/audio-quality-native.md} | 6 +- quest/m1/audio-warmup.md | 6 +- quest/m1/auth/README.md | 2 + .../auth/expired-error.md} | 7 +- quest/m1/bbr-ack-cleanup.md | 10 +- quest/m1/bbr-idle-burst.md | 31 ---- quest/m1/bench-ci.md | 6 +- quest/m1/binding-surface.md | 8 +- quest/m1/broadcast-remove.md | 21 --- quest/m1/browser-benchmarks.md | 25 +-- quest/m1/c/README.md | 17 +- quest/m1/c/package.md | 4 +- quest/m1/c/retire.md | 8 +- quest/m1/cache-expiry-growth.md | 7 +- quest/m1/cache-wall-eviction.md | 6 + quest/m1/captions-msf.md | 14 +- quest/m1/capture-control.md | 19 ++- quest/m1/cli-given-flags.md | 3 +- quest/m1/cli-inspect/README.md | 24 --- quest/m1/cli-inspect/caught-up.md | 41 ----- quest/m1/cli-inspect/ls.md | 29 ---- quest/m1/close-codes.md | 4 + quest/m1/cluster-routing.md | 12 +- quest/m1/cpp/cancel.md | 44 ++--- quest/m1/data-capture-bindings.md | 5 +- quest/m1/decoded-frames.md | 47 ------ quest/m1/doc-samples-go-dart.md | 13 +- quest/m1/drain/README.md | 2 +- quest/m1/dropped-sources.md | 2 +- quest/m1/epoch.md | 2 +- quest/m1/export-linger.md | 4 - quest/m1/ffi-shape/README.md | 22 ++- quest/m1/ffi-shape/codec.md | 13 +- quest/m1/ffi-shape/json.md | 23 +-- quest/m1/ffi-shape/media.md | 4 +- quest/m1/ffi-shape/net.md | 4 +- quest/m1/flate-binary.md | 50 ++++++ quest/m1/frame-slot-charge.md | 53 ++++++ quest/m1/gateway-live-clock.md | 32 ---- quest/m1/gpu-ci.md | 20 ++- quest/m1/gpu-pool-reservation.md | 33 ---- quest/m1/ietf-announce-count.md | 24 --- quest/m1/ietf-max-age.md | 47 ------ quest/m1/ietf-uni-stream-types.md | 23 ++- quest/m1/iroh-lite-wip.md | 6 +- quest/m1/js-announce-caught-up.md | 20 --- quest/m1/js-ietf-datagram.md | 2 +- quest/m1/js-subscribe-abandonment.md | 14 +- quest/m1/keyframe-trigger.md | 37 ---- quest/m1/kt-jvm-exit.md | 4 - quest/m1/ladder/README.md | 10 +- quest/m1/lite-count-settle.md | 28 ---- quest/m1/merge-queue.md | 11 +- quest/m1/moq-c.md | 33 ---- quest/m1/mux-wasm-target.md | 26 --- quest/m1/obs-moq-video/README.md | 10 +- quest/m1/obs-moq-video/audio-playback.md | 16 +- quest/m1/obs-moq-video/linux-bundle.md | 7 +- quest/m1/obs-moq-video/rate-control.md | 9 +- quest/m1/obs-moq-video/source.md | 12 +- quest/m1/open-gop-leading-pictures.md | 16 +- quest/m1/opus-conceal.md | 4 + quest/m1/p2p/README.md | 17 +- quest/m1/p2p/cost-scopes.md | 3 +- quest/m1/p2p/transit.md | 4 - quest/m1/p2p/watch.md | 3 +- ...relay-cpu-is-vdso-clock-reads-the-drive.md | 5 +- ...use-sendmsg-zc-for-large-udp-gso-trains.md | 8 +- ...ter-tx-pool-buffers-for-zero-copy-sends.md | 9 + quest/m1/perf/README.md | 6 +- quest/m1/perf/egress-keepalive.md | 2 +- quest/m1/perf/egress-requeue.md | 4 - quest/m1/perf/uring-one-enter.md | 2 +- quest/m1/perf/uring-quiescence.md | 3 +- quest/m1/performance-comparisons.md | 2 +- quest/m1/performance-profiles.md | 3 +- quest/m1/pop-skipping/README.md | 158 ------------------ quest/m1/pop-skipping/peer-reconfigure.md | 70 -------- quest/m1/pop-skipping/rank.md | 91 ---------- quest/m1/pop-skipping/warm-advertise.md | 51 ------ quest/m1/processor/README.md | 19 ++- quest/m1/processor/advertise-auth.md | 34 ++-- quest/m1/processor/grant-lease.md | 12 +- quest/m1/processor/media-contract.md | 15 +- quest/m1/publish-codec-string.md | 8 +- quest/m1/qos/README.md | 6 + quest/m1/qos/final-lag-sample.md | 7 + quest/m1/qos/stats/README.md | 7 +- quest/m1/qos/stats/encoder-feedback.md | 8 +- quest/m1/qos/stats/js.md | 4 - quest/m1/qos/stats/rust.md | 4 - quest/m1/quic/README.md | 37 ++-- quest/m1/quic/bbr-ack-sampling.md | 35 ---- quest/m1/quic/bbr-app-limited-edges.md | 9 +- quest/m1/quic/bbr-app-limited.md | 59 +++---- quest/m1/quic/bbr-loss-parity.md | 1 - quest/m1/quic/bbr-loss-undo.md | 29 ---- quest/m1/quic/bbr-packet-identity.md | 32 ---- quest/m1/quic/bbr-probe-feedback.md | 28 ---- quest/m1/quic/bbr-probe-rtt.md | 30 ---- quest/m1/quic/bbr-release.md | 36 ---- quest/m1/quic/bbr-startup-pacing.md | 33 ---- quest/m1/quic/deadline.md | 7 +- quest/m1/quic/peer-limits.md | 5 +- quest/m1/quic/release.md | 2 - quest/m1/quic/upstream.md | 33 ++-- quest/m1/raw-stream-codes.md | 5 +- quest/m1/relay-memory.md | 15 +- quest/m1/remove-live.md | 49 ++++++ quest/m1/route-cost.md | 27 --- quest/m1/rs2ts/remove-wasm.md | 3 +- quest/m1/session-death.md | 5 + quest/m1/signal-race.md | 30 ---- quest/m1/signed-priority.md | 21 ++- quest/m1/track-demand.md | 4 - quest/m1/track-tail-hardening.md | 6 +- quest/m1/track-tail-interop.md | 19 +++ quest/m1/transport-upgrade/README.md | 4 +- quest/m1/transport-upgrade/js.md | 11 +- quest/m1/ts-export-byte-schedule.md | 4 - quest/m1/ts-import-shared-shift.md | 15 +- quest/m1/video-keyframe-flag.md | 24 --- quest/m1/watch-audio-time-stretch.md | 5 +- ...ing-requirements-broadcast-contribution.md | 111 ++++-------- ...ipewire-dma-bufs-safely-into-the-vulkan.md | 91 +++------- quest/m2/703-experimental-webgpu-renderer.md | 26 --- quest/m2/823-svc-support.md | 22 --- quest/m2/aac-encode-refusal.md | 22 --- quest/m2/capture-clock-source.md | 20 --- quest/m2/cat/README.md | 5 +- quest/m2/cat/verify.md | 14 +- quest/m2/cpp-conan.md | 9 +- quest/m2/cpp-vcpkg.md | 5 +- quest/m2/flate/README.md | 27 +-- quest/m2/flate/bindings.md | 72 +++----- quest/m2/flate/track.md | 55 ------ quest/m2/gop-overhead.md | 5 - quest/m2/intra-refresh/README.md | 2 +- quest/m2/intra-refresh/bindings.md | 30 ++-- quest/m2/intra-refresh/consumer-warmup.md | 30 ++-- quest/m2/js-discontinuity.md | 35 ++-- quest/m2/latency-ledger.md | 9 +- quest/m2/mobile-ownership.md | 5 - quest/m2/multipath-spike.md | 7 +- quest/m2/obs-wave-layout.md | 17 -- quest/m2/pipewire-camera-planes.md | 4 +- quest/m2/quic-bbr-google.md | 6 +- ...p-limited.md => quic-bbr-natural-drain.md} | 4 - quest/m2/quic-probe.md | 6 +- quest/m2/redundant-ingest.md | 24 ++- quest/m2/routing-cost-domains.md | 21 +-- quest/m2/teleop/README.md | 18 +- quest/m2/teleop/browser-package.md | 6 +- quest/m2/teleop/docs.md | 6 +- quest/m2/teleop/mavlink.md | 21 ++- quest/m2/teleop/robot.md | 43 ++--- quest/m2/unreal.md | 1 - quest/m3/README.md | 5 +- quest/m3/dpdk.md | 7 +- quest/m3/libmoq-cmake-lib.md | 29 ---- quest/m3/libmoq-fetch.md | 20 --- quest/m3/libmoq-hidden.md | 16 -- quest/m3/libmoq-shutdown.md | 54 ------ quest/m3/suffix-announce.md | 38 +++++ quest/m3/upstream-forks.md | 4 + quest/m3/video-embedded.md | 5 + quest/m3/video-hardware.md | 6 + quest/m4/msfts-convergence.md | 4 - 190 files changed, 1144 insertions(+), 2701 deletions(-) delete mode 100644 quest/m0/bbb-sd.md delete mode 100644 quest/m0/release.md delete mode 100644 quest/m0/wildcard/demand.md delete mode 100644 quest/m0/wildcard/resolve.md delete mode 100644 quest/m1/announce-live-bindings.md rename quest/{m0/audio-quality-harness/native.md => m1/audio-quality-native.md} (91%) rename quest/{m2/auth-expired-error.md => m1/auth/expired-error.md} (61%) delete mode 100644 quest/m1/bbr-idle-burst.md delete mode 100644 quest/m1/broadcast-remove.md delete mode 100644 quest/m1/cli-inspect/README.md delete mode 100644 quest/m1/cli-inspect/caught-up.md delete mode 100644 quest/m1/cli-inspect/ls.md delete mode 100644 quest/m1/decoded-frames.md create mode 100644 quest/m1/flate-binary.md create mode 100644 quest/m1/frame-slot-charge.md delete mode 100644 quest/m1/gateway-live-clock.md delete mode 100644 quest/m1/gpu-pool-reservation.md delete mode 100644 quest/m1/ietf-announce-count.md delete mode 100644 quest/m1/ietf-max-age.md delete mode 100644 quest/m1/js-announce-caught-up.md delete mode 100644 quest/m1/keyframe-trigger.md delete mode 100644 quest/m1/lite-count-settle.md delete mode 100644 quest/m1/moq-c.md delete mode 100644 quest/m1/mux-wasm-target.md delete mode 100644 quest/m1/pop-skipping/README.md delete mode 100644 quest/m1/pop-skipping/peer-reconfigure.md delete mode 100644 quest/m1/pop-skipping/rank.md delete mode 100644 quest/m1/pop-skipping/warm-advertise.md delete mode 100644 quest/m1/quic/bbr-ack-sampling.md delete mode 100644 quest/m1/quic/bbr-loss-undo.md delete mode 100644 quest/m1/quic/bbr-packet-identity.md delete mode 100644 quest/m1/quic/bbr-probe-feedback.md delete mode 100644 quest/m1/quic/bbr-probe-rtt.md delete mode 100644 quest/m1/quic/bbr-release.md delete mode 100644 quest/m1/quic/bbr-startup-pacing.md create mode 100644 quest/m1/remove-live.md delete mode 100644 quest/m1/route-cost.md delete mode 100644 quest/m1/signal-race.md delete mode 100644 quest/m1/video-keyframe-flag.md delete mode 100644 quest/m2/703-experimental-webgpu-renderer.md delete mode 100644 quest/m2/823-svc-support.md delete mode 100644 quest/m2/aac-encode-refusal.md delete mode 100644 quest/m2/capture-clock-source.md delete mode 100644 quest/m2/flate/track.md delete mode 100644 quest/m2/obs-wave-layout.md rename quest/m2/{quic-bbr-app-limited.md => quic-bbr-natural-drain.md} (95%) delete mode 100644 quest/m3/libmoq-cmake-lib.md delete mode 100644 quest/m3/libmoq-fetch.md delete mode 100644 quest/m3/libmoq-hidden.md delete mode 100644 quest/m3/libmoq-shutdown.md create mode 100644 quest/m3/suffix-announce.md diff --git a/quest/README.md b/quest/README.md index 3f2ded513b..3a687ff8d5 100644 --- a/quest/README.md +++ b/quest/README.md @@ -7,8 +7,8 @@ grouped into milestones ordered by priority. ## Plan -m0 is everything in flight now: the release API gates, the release itself, and -the reusable Pronto GPU path. m1 is the next wave across reliability, features, +m0 is everything in flight now: announce and wildcard routing, the local +origin, and audio playout (jitter target, quality harness, A/V clock). m1 is the next wave across reliability, features, performance, and planning. m2 holds later features, design studies, and experiments. m3 is deferred: work whose first step is outside this repository. m4 waits on an upstream release or external dependency to ship. Priority is @@ -17,7 +17,7 @@ under the repository rules. ## Required -- [m0: immediate priorities](/quest/m0/README.md) - everything in flight now: the release API gates, the release, and the Pronto GPU path +- [m0: immediate priorities](/quest/m0/README.md) - everything in flight now: announce and wildcard routing, the local origin, 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 nobody has, a partner, or a provider's offer diff --git a/quest/m0/README.md b/quest/m0/README.md index e621b682fb..9aa54fa283 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -2,106 +2,41 @@ ## Goal -Settle the public contracts of `moq-archive`, `moq-e2ee`, `moq-sock`, -`moq-uring`, `moq-audio`, `moq-video`, `moq-transcode`, and `moq-nvenc` -before the imminent release, while supplying the reusable GPU media support -needed to remove raw-pixel CPU transfers from the Pronto CARLA demo. These are -independent immediate tracks rather than mutual prerequisites. Then cut the -release moq.pro adopts from the merged tree. +The work in flight now, in two independent tracks. Routing: a publisher stops +sending announce updates the wire cannot tell apart, a service claims the +prefix it could serve instead of enumerating broadcasts, and localhost workers +read only what their relay ingested. Audio playout: the target is a measured +estimate of arrival timing in both languages, a browser regression fails a +nightly run, and the audio playhead becomes the clock video follows. ## Plan -dev landed on main as #3793 on 2026-09-20; -[Release](/quest/m0/release.md) names what gates the release that follows. -Published API or wire breaks still land on dev; the quest's Plan says so. +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. -The archive, E2EE, and uring crates are 0.0.x; socket, audio, video, -transcode, and nvenc are 0.1.x so dependents can take compatible patches. Their -API quests gate the release; a published break to a 0.1.x crate targets dev. -Inspect transitive public exposure before changing a shared symbol: `moq-tokio` -publicly re-exports `moq-sock`'s bind module. Keep that re-export and its -current names. +Routing: announce-update dedupe is a wire-compatible fix on every version. The +wildcard line is prefix-only on the wire; its resolve and demand work is done +on the line branch and waits to land. Local origin serves the relay's +ingested-only view on the internal listener. -Keep the useful boundaries: archive owns storage and codecs, E2EE owns -protection rather than catalogs, sock owns runtime-neutral sockets, and uring -owns the local worker and its I/O. Prefer standard Rust ranges to a new public -range type. Keep uring's root `Config` reachable, since worker is private; -renaming `TxBuf` or moving bind names does not improve an ownership contract. +Audio playout: the jitter target replaces the round-trip guess. The harness's +browser lane grades it nightly and records the traces it replays; the native +lane is a standalone m1 quest, since nothing here waits on it. The A/V clock +builds on the jitter target's per-track spread. -Their package boundaries are explicit: - -- `moq-archive` replaces reversed integer-pair bounds with validated finite - `RangeInclusive` values, unifies streaming and paginated listing under - archive-owned query/entry types, and hides path helpers that are not consumer - APIs. Persisted object paths and bytes remain unchanged. -- `moq-e2ee` replaces raw/profile-global construction with application-owned - secrets and epoch-scoped `Credential`, `Generation`, `Epoch`, and track/group - handles. Raw crypto, catalog policy, retransmission internals, and the global - `Publication` registry leave the public surface. -- `moq-sock` makes incomplete reuseport groups unservable and retains every - member socket for the served group's lifetime. -- `moq-uring` derives worker and steering identity from owned sockets and - connections instead of independently supplied handles or shard values. -- Published `moq-tokio` keeps its worker signatures and `bind` re-export while - adapting internal plumbing. Its root names do not move. - -The media crates are 0.1.x too, so a published break to them targets dev. -Adapt callers in other packages without breaking their published APIs, C -layouts, or wire formats. Do not bump versions as part of these quests. - -Their package boundaries are explicit: - -- `moq-audio` owns the PCM/layout and codec configuration split, decoder entry - point, publication authority, and extensible audio frame and packet - construction. -- `moq-video` owns frame conversion and construction, decoder output policy, - synchronous codec thread confinement, capture timestamps and rational rates, - extensible group configuration and `cut` naming, and its feature defaults. -- `moq-transcode` adopts the video rate, group, output, and feature contracts in - its public configuration and observations without adding another media model. -- `moq-nvenc` narrows its safe facade around owned resources and completion, - while loading and incompatibility become fallible public errors. -- Published `moq-mux` gains only the additive shared `rate` namespace. Published - `moq-ffi`, `libmoq`, and language-binding signatures, layouts, and sentinel - behavior remain unchanged while their internals adapt. - -The agreed media direction is small, honest APIs: typed PCM layouts, rational -video rates, extensible GOP and frame records, explicit ownership, and no knobs -that claim behavior they do not provide. Synchronous codecs remain public and -thread-confined; async sinks own codec execution. Native versus CPU output is a -choice, not a promise that every native backend yields a GPU surface. OpenH264 -becomes optional but stays enabled by default. Rendering becomes opt-in; -inexpensive native codec defaults remain. - -The Pronto GPU quests remain an independent deliverable within m0. They define -portable Vulkan/CUDA ownership, safe partial NVENC initialization, and a strict -GPU conversion path without changing either API audit. Product integration and -installation live in moq.pro. - -Each implementation updates its existing README, examples, and affected docs -inline and adds regression coverage to normal or nightly CI. Feature checks -exercise each media crate independently, since workspace feature unification -hides missing gates. Cross-platform compilation and hardware execution are -separate evidence. The audits were source-based, not a cryptographic review, -fresh compilation, benchmark, or Linux runtime validation. - -API-preserving implementation, codec additions, allocation work, and hardware -proof remain in the existing backlog. Keep audio's integrated packetizing -Producer and transcode's validated Ladder and coalescing active cursor. Keep -one video Frame/Surface hierarchy and its deliberate native/wgpu type interop; -do not add another media abstraction or a renderer crate during stabilization. +Published API or wire breaks still land on dev; each quest's Plan says so. ## Required -- [Release](/quest/m0/release.md) - the release moq.pro adopts: binding docs, an upgrade page, and a staging soak gate it rather than the merge - [Skip unchanged announce updates](/quest/m0/announce-update-dedupe.md) - a publisher sends an announce update only when the wire route changed - [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 - [Local origin](/quest/m0/local-origin.md) - localhost workers read only the broadcasts their relay ingested, from the internal listener -- [Audio quality harness](/quest/m0/audio-quality-harness/README.md) - a playout latency regression fails a run instead of arriving as a bug report, and its recorder supplies the jitter target's replay traces +- [Audio quality harness](/quest/m0/audio-quality-harness/README.md) - a browser playout latency regression fails a nightly run instead of arriving as a bug report, and its recorder supplies the jitter target's replay traces - [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 - [A/V clock](/quest/m0/plan-av-clock.md) - the audio playhead drives Sync.reference while audio plays, through per-track sync handles -- [SD rendition for bbb](/quest/m0/bbb-sd.md) - `just pub bbb` publishes a pre-encoded 360p rung beside the 720p source, for localhost demos and moq.pro's fleet demo ## Related -- [Pronto GPU integration](https://github.com/moq-dev/moq.pro/tree/main/quest/main/pronto/gpu) - CARLA bridge, release adoption and desktop installation +- [Pronto GPU integration](https://github.com/moq-dev/moq.pro/tree/main/quest/m0/pronto/gpu) - CARLA bridge, release adoption and desktop installation diff --git a/quest/m0/audio-quality-harness/README.md b/quest/m0/audio-quality-harness/README.md index 0daecf0d58..457844c109 100644 --- a/quest/m0/audio-quality-harness/README.md +++ b/quest/m0/audio-quality-harness/README.md @@ -6,8 +6,10 @@ A regression in audio playout latency fails a run instead of arriving as a bug report. The harness plays a broadcast over an impaired path, counts what the listener would actually have heard (underruns, short quanta, discarded samples, skip-aheads) and what each stage of the pipeline contributed to the delay, and -grades the result against a checked-in budget. It runs in the browser and -natively, on the same jitter profiles, reporting the same numbers. +grades the result against a checked-in budget. This line is the browser; +the native lane is [Native audio +quality](/quest/m1/audio-quality-native.md), on the same jitter profiles and +reporting the same numbers. Boundaries: audio only. The stage breakdown is defined generically so video can adopt it later, but no video assertion ships here. No perceptual scoring: the @@ -15,16 +17,17 @@ grade is glitches and latency, not an opinion about how it sounds. ## Plan -Two quests, browser first, because that is where the traces and the reported -bug both are. The native lane follows against the same budgets and the same -metric schema, so the two implementations can be compared rather than merely -both passing. +Browser only, because that is where the traces and the reported bug both +are. The native lane moved to m1 as a standalone quest (decided in the +2026-09-28 quest audit): nothing in m0 waits on it, and it follows against the +same budgets and metric schema so the two implementations can be compared +rather than merely both passing. The starting point is not a blank page. The reporter on #3477 already built a -working browser harness on their fork (`fperex/moq`, branch `debug/rt-audio`): -a CDP driver, a beacon sink, a trace analyzer, a ring replay, a five-scenario -`bench.sh`, and a `compare.mjs` that prints before-and-after tables. Upstream -that rather than reinventing it. The raw traces it shipped with are gone, so +working browser lane on their fork (`fperex/moq`, branch +`debug-findings-solution`, under `test/audio-quality/`): a Playwright-driven +matrix over `moq-shaper`, a budget file graded under `--enforce`, a nightly +job, and a replay runtime. Upstream that rather than reinventing it. The raw traces it shipped with are gone, so this harness records fresh ones, and the [jitter target's watch quest](/quest/m0/audio-jitter-target/watch.md) replays them (decided with the maintainer during the merged-PR audit). @@ -47,10 +50,10 @@ each of those dimensions moves the expected floor. ## Required - [Browser](/quest/m0/audio-quality-harness/browser.md) - upstream the fork's harness, grade it against a budget, run it nightly -- [Native](/quest/m0/audio-quality-harness/native.md) - the same profiles and budgets through `moq play` on a dummy device ## Related +- [Native audio quality](/quest/m1/audio-quality-native.md) - the same profiles and budgets through `moq play` on a dummy device - [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - the estimator this exists to keep honest - [Latency ledger](/quest/m2/latency-ledger.md) - promotes this harness's probes into a public API - [Time stretch](/quest/m1/watch-audio-time-stretch.md) - graded by this harness once it lands diff --git a/quest/m0/audio-quality-harness/browser.md b/quest/m0/audio-quality-harness/browser.md index 887f864544..a47aed7969 100644 --- a/quest/m0/audio-quality-harness/browser.md +++ b/quest/m0/audio-quality-harness/browser.md @@ -13,16 +13,18 @@ production path is the one without cross-origin isolation. ## Plan -Upstream the reporter's harness from `fperex/moq` branch `debug/rt-audio` -rather than rebuilding it, keeping the attribution. It already has the CDP -driver, the beacon sink, the analyzer, the ring replay, `bench.sh`'s five -scenarios, and `compare.mjs`. What it does not have is a home in `test/`, a -budget, or a schedule. +Upstream the reporter's lane from `fperex/moq` branch +`debug-findings-solution` rather than rebuilding it, keeping the attribution. +It already has a built `test/audio-quality/` lane: `just test audio-quality` +over `moq-shaper`, a Playwright-driven Chromium matrix, a `budgets.json` +graded by `grade.ts` under `--enforce`, a nightly job that keeps a failure's +run directory, and `chromium`, `safari`, and `replay` runtimes. It also +carries player, estimator, and shaper changes (checked 2026-09-28: 222 commits +ahead of and 148 behind `main`), so land the lane on its own and hold its +schema to the contract below rather than taking the branch wholesale. -- Land the driver and analyzer under `test/`, alongside the existing `interop` - and `drill` lanes, wired into the `justfile` the way they are. Playwright is - already in the tree for the harness quests, so prefer it over a bespoke CDP - driver if the switch is cheap; if it is not, say so and keep CDP. +- Land the lane under `test/`, alongside the existing `interop` and `drill` + lanes, wired into the `justfile` the way they are. - Keep the instrumentation ad-hoc for now. The probes stay a debug surface, not public API; promoting them is [Latency ledger](/quest/m2/latency-ledger.md), which nothing here waits on. diff --git a/quest/m0/bbb-sd.md b/quest/m0/bbb-sd.md deleted file mode 100644 index 287e5455ed..0000000000 --- a/quest/m0/bbb-sd.md +++ /dev/null @@ -1,39 +0,0 @@ -# [S] SD rendition for the bbb demo - -## Goal - -`just pub bbb` publishes `bbb.hang` with two video renditions, the 720p -source and a 360p ~600 kbps rung, and a player watching it through `just dev` -(including moq.dev's pages on localhost) switches between them on bandwidth -and viewport size. The SD rung is pre-encoded, so publishing costs no encode -CPU. moq.pro's always-on demo consumes the same asset. - -## Plan - -- Encode `bbb-sd.mp4`: video only, H.264 360p ~600 kbps, from `bbb.mp4` with - identical frame count and timestamps, and keyframes forced at the source's - keyframe times so both renditions switch on the same boundaries. Fragment it - like the other assets. `just pub encode-bbb-sd` now reproduces the uploaded - asset; `bbb.mp4` stays unchanged. The source has 14,313 visible frames and - two duplicate packets marked discard. Preserve the visible frames, and - extend the final SD sample duration to match the source audio's loop period: - otherwise independent inputs drift by about 39 ms per loop. -- `bbb` downloads both and feeds them as two `-stream_loop -1 -re` inputs to - one ffmpeg with `-map 0 -map 1:v -c copy` into `import ts`, which turns each - video PID into its own rendition. WHEP and non-multitrack RTMP serve the - 720p one, the largest, whatever the mapping order. -- Verified: both catalog renditions have a `bitrate`, and - `just pub check-bbb` compares all visible timestamps and keyframes across - three loops. Nightly CI runs this check against the hosted assets. -- Chromium with `just dev` switches down and back up on viewport changes and - explicit bitrate caps. A local UDP proxy capped at 1.3 Mbps also produced - rendered 720p -> 360p -> 720p transitions, with reported receive estimates - of about 1.63 Mbps while capped and 23.3 Mbps after recovery. -- Remaining: investigate an earlier throttle run that kept HD selected and - skipped groups for the 45-second observation window. The cause is not yet - established; the successful instrumented run does not explain it. Verify - moq.dev's localhost pages and the related moq.pro fleet integration. - -## Related - -- [moq.pro demo simulcast](https://github.com/moq-dev/moq.pro/blob/main/quest/m0/demo-simulcast.md) - the always-on fleet demo switches to this asset diff --git a/quest/m0/plan-av-clock.md b/quest/m0/plan-av-clock.md index 22f14a12c6..c82e3ff855 100644 --- a/quest/m0/plan-av-clock.md +++ b/quest/m0/plan-av-clock.md @@ -14,13 +14,14 @@ the next re-anchor. Settled: per-track handles, and this quest lands them. `sync.track("audio")` and `sync.track("video")` each report their advertised delay and measured spread, and one is nominated as the clock source. `SyncInput` -(`js/watch/src/sync.ts:21-46`, today `delay`, `buffer`, `probe`, `audio`, -`video`) breaks once, and a third track joins without another pair of inputs. -The measured spread per track comes from -[Watch](/quest/m0/audio-jitter-target/watch.md); its branch carries flat -`audioSpread` and `videoSpread` inputs in place of `probe`, which this quest -folds into the handles. `SyncInput` is a published `@moq/watch` shape, so -the break lands on dev. +(`js/watch/src/sync.ts`, today `delay`, `buffer`, and `probe`) breaks once, +and a third track joins without another pair of inputs; `Sync.register` +already keeps one jitter entry per track and grows into the handles. The +measured spread per track comes from the [Audio jitter +target](/quest/m0/audio-jitter-target/README.md) line, whose watch branch +replaces `probe` with per-track spread inputs that this quest folds into the +handles. `SyncInput` is a published `@moq/watch` shape, so the break lands on +dev. Recommendations for the implementation: @@ -29,16 +30,15 @@ Recommendations for the implementation: postMessage path (`js/watch/src/audio/ring-buffer.ts`) the worklet posts an estimate and the main thread extrapolates between posts. - Video reads a locally extrapolated clock, re-synced once per audio quantum, - so the per-frame `sync.wait()` (`js/watch/src/video/decoder.ts:332`) never + so the per-frame `sync.wait()` (`js/watch/src/video/decoder.ts`) never crosses a thread. - Transitions. On mute or audio track end the reference falls back to the wall clock at the last audio-derived value, so video does not jump. A ring re-stall reads as the playhead pausing, and the reference pauses with it. -- Reset coupling stays: `` already flushes the ring alongside - `sync.reset()` (`js/watch/src/element.ts:301`, `:620-621`). +- Reset coupling stays: `Player.reset()` (`js/watch/src/player.ts`) already + flushes the audio ring alongside `sync.reset()`. - The text renderer is the third track: it reads `sync.now()` - (`js/watch/src/text/renderer.ts:261`) to drive the cue clock and prune cues - at `:263-265`. + (`js/watch/src/text/renderer.ts`) to drive the cue clock and prune cues. - Close the player gaps [#4170](https://github.com/moq-dev/moq/pull/4170) left, since the handles own them. `Sync.received` only ever lowers its reference, so after the earliest subscribed track leaves, playback stays @@ -51,10 +51,9 @@ Recommendations for the implementation: ## Required -- [Watch](/quest/m0/audio-jitter-target/watch.md) - lands the per-track spread inputs this shape carries +- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - the estimator this sits on, and the per-track spread inputs this shape carries ## Related -- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - the estimator this sits on - [Time stretch](/quest/m1/watch-audio-time-stretch.md) - stretching needs a clock to converge toward - [Watch worker](/quest/m1/watch-worker.md) - moves `Sync` into a worker afterwards; keep the handles free of main-thread assumptions diff --git a/quest/m0/release.md b/quest/m0/release.md deleted file mode 100644 index 81072cc0a6..0000000000 --- a/quest/m0/release.md +++ /dev/null @@ -1,89 +0,0 @@ -# [M] Cut the release moq.pro adopts - -## Goal - -The first release from the merged tree is the one moq.pro pins: every -binding's docs match the surface it exposes, an upgrade page walks a -consumer from the last main release to this one, and the merged relay has -run on staging long enough that the origin and HLS rewrites are trusted. -The binding restructure in [FFI shape](/quest/m1/ffi-shape/README.md) -follows this release rather than riding it. - -## Plan - -Write `doc/setup/upgrade.md`, one section per package group, each break -with its PR and the replacement call: - -- net: announcements are prefix routes (#3225); serving folds into - `origin::Producer::dynamic` and broadcasts announce themselves (#3400, - #3581); `Reload`/`Shared` collapse into one `Connection` (#3614, #3636); - `writeDatagram` is `insertDatagram` (#3666); reader group and frame limits - are explicit (#3647); groups expire on timestamps alone; an oversized group - aborts with GROUP_TOO_LARGE instead of shedding its head (#3585); the - `Latency` type became `max_age` and delivery order became the `Ordered` - handle (#2688, #2955); the four moq-lite stream codes sent from the - reserved range moved to 0x36-0x39 in the draft's own range; subscriptions resume - across routes sharing a first hop (#3312); the send estimate is split among - JS publishers (#3616); @moq/net and @moq/pattern mirror Rust (`consume`, `Time.Milli`, - one `readFrame()`, `InvalidPattern`); the announce and request names; and - moq-tokio's names sit under their modules (`connection::Goaway`, `cli::Duration`, - `transport::Session`, `watch::Files`, `resolve()`; #3745). -- hang and json: the catalog `timeline` is `archive` (#3612); `json` and - `binary` catalog sections (#3109) take one options object (#3640); Rust - `modify()` is fallible and a failed dropped edit aborts the track (#3644), - paired with JS `update()`/`mutate()` and the Rust `mutate()`; an fMP4 export - fragment is a group (#3573). -- watch and play: `latency` splits into `delay` and `buffer` and - `--latency-max` is renamed (#3396); `moq play` has a real playout clock with - `--delay` (#3528); Firefox hardware encoding and screen-source scaling - (#3535). -- relay and CLI: embedders own listeners and workers (#3638); the cluster - origin is constructed once (#3582); LAN discovery is partitioned by - application (#3621) and meshes CLI and relay peers (#3648); config merges - with provenance (#3587); auth is one contract, a `Request` in and a - `Grant` with a lease out, and `--auth-api-mode` is gone (#3688); the CLI parses - with usage-rs and refuses the flags it dropped (#3030); moq-native is - moq-tokio (#2896). Released spellings refuse rather than warn or silently - alias: `--cluster-linger` is gone; `--cluster-connect` needs a full URL; - TOML `connect`/`failover_delay`/`listen`/`disable_verify`/`[server]`/`[client]` - name `url`/`race`/`bind`/`insecure`/`[listen]`/`[connect]`; CLI `--origin`/ - `--name`/`--latency-max` and `publish`/`subscribe` name `--hop`/`--broadcast`/ - `--max-age` and `import`/`export`. Unused `#[deprecated]` items are gone. - JS `announced()` always drops reflected announces (`ignoreSelf` is gone); - an `oct` JWK without `kty` is refused. The gstmoq properties - `estimated-send-bitrate`/`estimated-recv-bitrate` are `estimated-*-rate` - with no alias, a runtime failure for a `gst-launch` line. -- bindings: the Go module is `moq.dev/moq` with `context.Context` on every - blocking call (#2957); `MoqAudioCodec` is an `opus()` object (#3671); the - configuration setters are fallible (#3642); durations are microseconds and - the rate estimates are `estimated_*` (#3744); `MoqVideoDecoderOutput.format` - picks I420 or RGBA decode output, additively. - -Release-notes outline, the additions worth leading with, in order of value to -a consumer: prefix routes and wildcard Pattern events (#3225, #3649); -self-announcing broadcasts and `dynamic` handles (#3400, #3581); the archive -catalog and store (#3612); the delay/buffer split and playout clock (#3396, -#3528); GROUP_TOO_LARGE (#3585); explicit reader limits and timestamp-only -expiry (#3647); publish robustness (Firefox hardware encoding, file demux, -`stalled` on lagging renditions #3630, stream resets at boundaries #3580); -relay embedding and the LAN mesh (#3638, #3648, #3621, #3587); one auth -contract with leases (#3688, #3739); data tracks and captions (#3109, #3640); one `Connection` with URL -replacement (#3614, #3636); first-hop resume and the shared send estimate -(#3312, #3616). The m0 release API quests settle archive, sock, and uring -before release without making the independent Pronto or media tracks a release -prerequisite. `moq-e2ee` ships the `moq-e2ee-00` epoch profile; the -[E2EE](/quest/m1/e2ee/README.md) questline owns its twin and interop. - -The soak bullet below is cleared by hand: the merged relay serves moq.pro -staging with `/metrics` watched and a fresh viewer joining a days-old -`moq import ts` broadcast over HLS at the end; the bounded `moq_json::window` -timeline (#3240) is what makes that hold, and only a long run proves it. dev -landed on main as #3793; before cutting, run `just check --all` and -`just test interop --all` on the release revision and record it. Then cut the release under the existing release-plz and npm workflows; this -quest bumps no versions itself. - -Public API: none beyond the required quests. Wire: none. - -## Required - -- The merged relay has soaked on moq.pro staging and the maintainer has signed it off diff --git a/quest/m0/wildcard/README.md b/quest/m0/wildcard/README.md index 0feb062b21..6bc8b6153b 100644 --- a/quest/m0/wildcard/README.md +++ b/quest/m0/wildcard/README.md @@ -1,4 +1,4 @@ -# Wildcard advertisements +# [L] Wildcard advertisements ## Goal @@ -38,7 +38,10 @@ widest prefix that covers it (`**` is the root) and the request is the authority, so the advertise half of this questline is re-scoped to prefix claims resolved against pattern interest. The three workloads above still hold: the transcoder claims the root and refuses what it will not serve. -Resolve and Demand are additive and land on main. +Resolve and Demand are additive and land on main. Both are done on the line +branch (#4050 re-resolves on a refusal; 9d059b1b9 lists and demands covered +renditions in the browser player), so main no longer lists them; the line +branch moves to `quest/m0/wildcard/README` to match this path. ### What already exists, and what does not @@ -64,7 +67,7 @@ the requester's excluded hop, ordered by `route_order` (`rs/moq-net/src/model/origin.rs:633`), served on demand by the session that announced it and cached per prefix in `ServeState.served` (`:764`). That is the split-horizon-safe lookup the old `origin::Dynamic` could not provide, and it is what -[Resolve](/quest/m0/wildcard/resolve.md) now extends rather than replaces. +resolve extends rather than replaces. Request resolution, by contrast, is still prefix-only (`best_server` in `rs/moq-net/src/model/origin.rs`). The pattern matcher itself exists: @@ -123,15 +126,13 @@ field. The seed still has a floor, because standby and running claims of equal specificity do meet: a standby concrete claim (`with_cost(1000)` is the existing per-broadcast convention) shares a tier with a running publisher's - concrete announcement and with warm-advertise's exact-path warm routes. The + concrete announcement. The floor MUST exceed the deployment's enforced maximum charged-link count times its enforced maximum link cost (32 links at cost at most 5 gives a bound of 160, with producing origins seeded at 0), or a nearby standby outranks a distant running copy and the mesh starts a second encode of a stream it is already serving. That floor replaces the ad-hoc standby bias the moq.pro (downstream) transcode worker - carries today, and it is the same stride discipline - [pop-skipping](/quest/m1/pop-skipping/README.md) states for provider - economics. + carries today. - **One cost varint, not the pair.** `Cost` is `{ warm, cold }` because a relay that is carrying a broadcast discounts the warm half. A wildcard carries nothing and can never be warm, so the two halves are provably equal and the @@ -154,7 +155,7 @@ field. composer waiting for an announcement that only demand would produce. The browser player currently enforces the opposite (`js/watch`'s `#isPathAnnounced` hides a catalog rendition with no exact-path - announcement); [Demand](/quest/m0/wildcard/demand.md) makes a covering wildcard count as + announcement); demand makes a covering wildcard count as availability there. - **Refusal is a typed stream reset, with no negative cache.** An advertiser resets a subscribe it will not serve, and the reset carries which KIND of @@ -256,21 +257,13 @@ no generation, so a client that must distinguish recording generations reads the catalog's archive entry ([archive](/quest/m1/archive/README.md)) rather than announce state. -## Required - -- [Resolve](/quest/m0/wildcard/resolve.md) - a relay resolves a subscribe or - FETCH for an unannounced path against the best matching wildcard -- [Demand](/quest/m0/wildcard/demand.md) - the browser player subscribes to a - catalog-referenced broadcast a wildcard covers, breaking the lazy-rendition - deadlock - ## Related - [path-patterns](/quest/m1/path-patterns.md) - owns the pattern dialect and the shared matcher advertisements reuse - [archive](/quest/m1/archive/README.md) - an archive advertises the catch-all pattern, and its catalog names the generations a wildcard cannot -- [pop-skipping](/quest/m1/pop-skipping/README.md) - it owns the route cost and - the rank hash this reuses +- [Cluster routing](/quest/m1/cluster-routing.md) - origin selection by cost + with an HRW tie-break, built on this line's specificity - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - derived output moves under the source's `@` segment, which the suffix patterns still match diff --git a/quest/m0/wildcard/demand.md b/quest/m0/wildcard/demand.md deleted file mode 100644 index 80be0b35ed..0000000000 --- a/quest/m0/wildcard/demand.md +++ /dev/null @@ -1,49 +0,0 @@ -# [S] Demand - -## Goal - -The browser player subscribes to a catalog-referenced broadcast a wildcard -covers, instead of hiding renditions that are not announced. Without this, a -lazily-produced rendition is a deadlock: the encoder starts on demand, and the -player never demands what it hides. - -## Plan - -The gate is JS-only, and it is already prefix-aware. -[moq#3225](https://github.com/moq-dev/moq/pull/3225) made `#isPathAnnounced` -(`js/watch/src/broadcast.ts:216`) hold the set of announced prefixes and accept -any that covers the path, so a route at `room/` already makes -`room/alice/cam.hang` selectable without naming it. What it cannot do is match -a pattern, since it tests with `Path.hasPrefix` (`:223`). - -So the remaining work is narrow: teach the JS client the wildcard -advertisement (`js/net/src/announced.ts` and `js/net/src/lite/announce.ts`, -mirroring the moq-net wildcard advertisement) -and make the covering test use `Path.Pattern` (`js/net/src/path.ts:526`) -rather than prefix containment. -Withdrawal of the last covering wildcard hides the rendition again, the same -reactive shape announcements have today. - -Do not simply delete the gate. It exists so the player does not subscribe to -absent broadcasts and so renditions appear and disappear reactively with -announcements. The Rust side needs nothing here: `moq-mux::Source` resolves -references through `request_broadcast`, which -[resolve](/quest/m0/wildcard/resolve.md) teaches to consult patterns. - -Two existing soft spots to not reintroduce: the first evaluation runs before -the announcement stream has populated, briefly hiding cross-broadcast -renditions on startup; and a token without announce visibility over the -sibling's path hides it permanently even though a direct subscribe would work. -A covering wildcard fixes the second only if patterns are forwarded under the -subscriber's scope, which advertise's rebasing rule guarantees. - -Tests: a rendition whose broadcast is covered only by a wildcard is listed and -playable, subscribing it is what starts production (the subscribe arrives -before any announcement), the rendition disappears when the last covering -wildcard is withdrawn, and a concrete announcement arriving later changes -nothing visibly. - -## Required - -- [Resolve](/quest/m0/wildcard/resolve.md) - recognizing the wildcard is useless - until the relay routes the resulting subscribe through it diff --git a/quest/m0/wildcard/resolve.md b/quest/m0/wildcard/resolve.md deleted file mode 100644 index c582df54ad..0000000000 --- a/quest/m0/wildcard/resolve.md +++ /dev/null @@ -1,105 +0,0 @@ -# [L] Resolve - -## Goal - -A relay resolves a subscribe or FETCH for an unannounced path against the best -matching wildcard. - -## Plan - -Specificity before cost is an explicit routing policy: a catch-all must not -silently take over a path still claimed by a concrete service, even when that -service refuses the request. Keep the draft and regressions aligned with it. - -[moq#3225](https://github.com/moq-dev/moq/pull/3225) built the table this quest -needs. `Consumer::request_broadcast` resolves a local broadcast first, then -`best_server`, which filters routes to those covering the path, drops any whose -hop chain contains the requester's excluded hop, keeps the longest covering -prefix, and orders the survivors by `route_order` -(`rs/moq-net/src/model/origin.rs:633`). The winning session serves the request -on demand, and `ServeState.served` (`:764`) caches the result per path so -repeat requests share one upstream subscription. A route never passes through -`origin::Dynamic`'s shared FIFO, so requester identity and the hop chain are -both available to selection. - -So this quest extends a working table rather than standing one up: teach the -route entries to hold a pattern instead of only a literal prefix, and teach -selection the tiering and pooling below. Keep the exclusion filter where it is, -applied before selection, so an out-of-band request can never be served back -through the peer that made it. - -Among the survivors, only the tier selected by the matcher's shared structural -specificity is consulted, with equal-specificity patterns forming one pool. A -refusal from that tier never falls through to a less -specific one, so `**/transcode.pro` shadows the archive's `**` for -every transcode path, matched or refused. Selection within the tier is lowest -accumulated cost first, then a hash of the REQUESTED path against each -advertiser's origin id. Keying on the request rather than the pattern is the -whole point: hashing the pattern would hand one advertiser every path matching -it. A concrete announcement is maximally specific and shadows every wildcard -regardless of cost. A terminal refusal from that concrete claim never falls -through to a wildcard; it shadows until the claim is withdrawn. - -Both lookup kinds route through this table: subscribe via `recv_subscribe`'s -existing fallback, and FETCH the same way, since the archive's whole use case -is serving stored groups to FETCH for paths nothing announces. A FETCH selects -the same advertiser through the same hash and completes without installing a -route. - -A served path is not announced downstream, so a wildcard never manufactures -announcements. But a resolved upstream SUBSCRIPTION must be installed as a -ROUTE on the path's origin node, seeded with the wildcard's accumulated cost, -not parked in the request-level `served` cache alone. Preserve the route's -wildcard provenance and specificity: installing an exact-path node must not -promote it into a concrete announcement. A concrete announcement arriving -later lands on the same node and wins by specificity, and the front's -ordinary route change moves consumers at a group boundary. A -cache-only answer would strand every bound consumer on the wildcard -subscription with nothing able to migrate or stop it. When the concrete claim -is a DIFFERENT publisher, its consumers end and resubscribe rather than -splicing, per the first-hop resume rule ([moq#3312](https://github.com/moq-dev/moq/pull/3312)); this quest's -obligation is the route install that makes selection between them possible. Repeat requests for one path -share that one route, keyed to survive the exclusion filter rather than -handing one peer's answer to another. - -An upstream reset is a refusal. A capacity code re-resolves once against the -routing table, with the refusing advertiser excluded from that attempt. The -exclusion is what makes the retry safe: the reset and the advertiser's -retraction travel independently, so the table may not have learned yet, and -re-resolution may equally find no other advertiser and return unroutable. That -is a correct outcome, not a case to handle away. Every other code, and any -unrecognized one, is terminal and propagates. Hold no state either way. - -Tests, at the process level with real sessions rather than an in-process stand-in: - -- One path always selects the same advertiser, and a fixed set of many paths - spreads across advertisers rather than piling onto one. Do not assert that two - particular paths differ: a correct hash may legitimately rank the same - advertiser first for both, so that assertion fails valid implementations. -- A request arriving from a peer that appears in the cheapest wildcard's hop - list selects a clean alternative, or fails unroutable, and never opens a - cyclic subscription. -- Two requesters with different excluded origins do not receive each other's - resolved broadcast. -- A concrete announcement from the SAME publisher takes over from the wildcard - route at a group boundary, without announcement churn; a concrete claim from a - DIFFERENT publisher ends the wildcard-served subscription instead of splicing - into it. -- A high-cost concrete claim shadows a cheaper wildcard, including after - that wildcard has served the path; a terminal concrete refusal does not - fall through while the concrete claim remains advertised. -- A FETCH for an unannounced archived path resolves through the catch-all the - same way a subscribe does. -- A capacity refusal re-resolves onto another advertiser exactly once, and a - second capacity refusal is terminal rather than looping. -- A retraction racing an in-flight request (the request arrives after the - advertiser filled its last slot) ends with the requester served by another - advertiser, or unroutable, but never hung and never looping. -- A permanent refusal does not re-resolve, so a request for a path nobody serves - costs exactly one round trip. -- A path matched by both a suffix pattern and the catch-all resolves against - the suffix pattern's pool only, and a terminal refusal from it never reaches - the catch-all advertiser. -- A refused subscribe resets rather than hanging, and leaves no state behind. -- A wildcard retracted mid-serve does not disturb the subscription already - running. diff --git a/quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md b/quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md index 53be1396c0..b85a2da169 100644 --- a/quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md +++ b/quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md @@ -1,11 +1,11 @@ -# [S] hang: expose the broadcast wall clock +# [XS] hang: document the broadcast wall clock ## Goal -A browser application can read the broadcast's fixed PTS-to-wall mapping -through `js/hang`, alongside archive timeline records when present, so an application that knows its -viewers share a clock can compute the delay that renders one frame at one -instant everywhere, and a DVR view can map presentation time to wall time. +A browser application can find how to map presentation time to wall time +from `doc/lib/js/hang.md`, so an application that knows its viewers share a +clock can compute the delay that renders one frame at one instant everywhere, +and a DVR view can label its timeline. The library itself never synchronizes playback on wall time. That is the decision behind [#2278](https://github.com/moq-dev/moq/issues/2278): frame @@ -17,16 +17,15 @@ sync exchange over a track. ## Plan -Expose the catalog contract selected by the continuous broadcast clock quest, -using one mapping across tracks and source restarts. The current timeline is -a broadcast-wide segment index, not a per-rendition track. Reuse the existing -consumer and signal machinery where present; do not assume the old `setWall` -producer or create per-record clock epochs. Keep `js/watch` arrival-based Sync -unchanged. Document PTS-to-wall conversion and the requirement that an -application knows whether remote clocks are synchronized. - -Verify application access using the built-in publisher integration, including -a live-only broadcast with no archive timeline. +The API already exists: the catalog root's optional `clock` (`ClockSchema`, +`Clock`) and `wallClockTime(clock, pts, ptsTimescale)` in +`js/hang/src/catalog/clock.ts`, exported from `@moq/hang/catalog`. Only the +docs are missing. Add a short section to `doc/lib/js/hang.md`: read `clock` +from a catalog root, convert a frame's PTS with `wallClockTime`, note that a +live-only broadcast carries it without an `archive` entry, that the mapping is +fixed for the broadcast's life, and that comparing wall times across machines +requires the application to know their clocks are synchronized. Keep +`js/watch` arrival-based Sync unchanged. ## Closes diff --git a/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md b/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md index 38e28ccf8d..946a68a0a6 100644 --- a/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md +++ b/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md @@ -33,6 +33,11 @@ grant; `Options::bandwidth` documents that their own loop; the capture driver's `_reservation` goes away. Public entry points for a manual ceiling stay `Producer::set_bitrate` and `Encoder::set_bitrate`. +- The audio-codecs line branch moves the Opus encoder behind a backend seam: + the rate setter and its floor live in + `rs/moq-audio/src/encode/backend/libopus.rs` there, and + `Encoder::set_bitrate` dispatches through the backend. Target whichever + shape is on the base when this starts. - Floor: `set_opus_bitrate` refuses anything outside `opus::bitrate_floor(codec_rate, frame_size).max(500)` to `300_000 * channels` (`encoder.rs`, `rs/moq-audio/src/opus.rs`). `Policy::min` defaults to a tenth of diff --git a/quest/m1/2850-js-net-give-reader-a-synchronous-decode-so-the-publisher.md b/quest/m1/2850-js-net-give-reader-a-synchronous-decode-so-the-publisher.md index 4496a84f70..9f04697e6c 100644 --- a/quest/m1/2850-js-net-give-reader-a-synchronous-decode-so-the-publisher.md +++ b/quest/m1/2850-js-net-give-reader-a-synchronous-decode-so-the-publisher.md @@ -26,11 +26,11 @@ buffered bytes run out. `tryDecode` returns undefined and consumes nothing in that case, and `decode`/`decodeMaybe` are the one async driver that fills and retries. The primitives (`u62`, `u53`, `read`, `string`, ...) are that driver applied to the `Cursor` reads, and the group and FETCH frame loops drain every -buffered frame with `tryDecode` (`js/net/bench/frames.ts`). The 23 +buffered frame with `tryDecode` (`js/net/bench/frames.ts`). The 22 `static async decode` message decoders under `js/net/src/lite/`, plus four `decodeMaybe` variants, still await a primitive per field. -- Convert all 23 decoders to a single synchronous body over a `Cursor`, with +- Convert all 26 decoders to a single synchronous body over a `Cursor`, with the async form as `reader.decode(...)` rather than a second copy. `Message` in `lite/message.ts` becomes a sync size-prefixed wrapper. - The publisher drains controls synchronously in its loop and diff --git a/quest/m1/2924-moq-relay-tls-rotation-is-not-atomic-across-thread-per.md b/quest/m1/2924-moq-relay-tls-rotation-is-not-atomic-across-thread-per.md index 1ec7587a17..00547d3bf6 100644 --- a/quest/m1/2924-moq-relay-tls-rotation-is-not-atomic-across-thread-per.md +++ b/quest/m1/2924-moq-relay-tls-rotation-is-not-atomic-across-thread-per.md @@ -21,8 +21,8 @@ own `tls::reload_certs` watcher, and snapshots its own mTLS roots. endpoint. `uring::Workers::bind` reads one pair once and never reloads. The primitive already exists: `ServeCerts` implements -`rustls::server::ResolvesServerCert` (`rs/moq-tokio/src/tls.rs:2857`) and -`reload_certs` (`:2931`) swaps its contents from the file watcher. What is +`rustls::server::ResolvesServerCert` in `rs/moq-tokio/src/tls.rs`, and +`reload_certs` there swaps its contents from the file watcher. What is missing is sharing it. - Build the `ServeCerts` and its watcher once, on the shared runtime, in diff --git a/quest/m1/2964-quic-workers-dropping-one-split-server-resizes-the.md b/quest/m1/2964-quic-workers-dropping-one-split-server-resizes-the.md index 43f4a0afed..891110d96d 100644 --- a/quest/m1/2964-quic-workers-dropping-one-split-server-resizes-the.md +++ b/quest/m1/2964-quic-workers-dropping-one-split-server-resizes-the.md @@ -25,8 +25,9 @@ failing. Check the socket group and connection-ID steering on a surviving session while unused handles are dropped, and prove all serving stops when the group terminates. Wire the tests into normal or nightly CI. -Public API: no further `moq-tokio` ownership change. The prerequisite may -change `moq-sock`'s 0.1.x API. Wire: no format change. Close #2964 only when +Public API: no further `moq-tokio` ownership change. The hardened group +already exists (`Group::bind` and `Group::complete` over `Claim` in +`rs/moq-sock/src/shard.rs`). Wire: no format change. Close #2964 only when both the dev ownership proof and this integration are complete. ## Closes diff --git a/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md b/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md index 0db7792501..5369139899 100644 --- a/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md +++ b/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md @@ -15,22 +15,21 @@ but the Rust and JavaScript models violate different parts of that invariant. ### Rust resets sequences when a dynamic producer is replaced A closed dynamic track is removed from the broadcast's weak cache. The next -subscription creates a fresh `track::Request` (`rs/moq-net/src/model/track.rs:3779`), -and `Request::new` creates a fresh `TrackState`. Because `max_sequence` -(`:199`) is empty, both `append_group` (`:1184`) and `append_datagram` -(`:1216`) restart at sequence 0. +subscription creates a fresh `track::Request` (`rs/moq-net/src/model/track.rs`), +and `Request::new` creates a fresh `TrackState`. Because its `max_sequence` +is empty, both `append_group` and `append_datagram` restart at sequence 0. That conflicts with the relay's logical track splicing. -`resume::Producer::takeover` (`rs/moq-net/src/model/resume.rs:328`) retains +`resume::Producer::takeover` (`rs/moq-net/src/model/resume.rs`) retains the previous live edge and starts a replacement at `latest + 1`. Groups from a restarted producer are therefore filtered until its counter catches up, causing the same playback stall fixed for JavaScript in #2953. -The takeover tests in `resume.rs` (`takeover_computes_boundary` `:2313`, -`takeover_splices_mid_group` `:3257`, -`takeover_splices_a_replacement_that_resends_the_head` `:3346`, -`takeover_rolls_past_a_finished_group` `:3467`, -`takeover_after_empty_segment_keeps_live_edge` `:3617`) create their +The takeover tests in `resume.rs` (`takeover_computes_boundary`, +`takeover_splices_mid_group`, +`takeover_splices_a_replacement_that_resends_the_head`, +`takeover_rolls_past_a_finished_group`, +`takeover_after_empty_segment_keeps_live_edge`) create their replacement groups with explicit sequences, so none of them exercises `append_group()` on a restarted producer; using it there would create group 0 and leave the subscriber stalled. Those are the tests to extend. Explicit group @@ -40,7 +39,7 @@ making the catch-up window longer. ### JavaScript permits concurrent same-name dynamic producers `BroadcastProducer.subscribe()` calls the internal `subscribe` with -`register = false` (`js/net/src/broadcast.ts:49-55`). Multiple publishing-side +`register = false` (`js/net/src/broadcast.ts`). Multiple publishing-side subscriptions for the same name therefore enqueue independent requests and create independent `track.Producer` instances. @@ -59,8 +58,7 @@ multiple subscribers fanning out from it. one request and share its accepted producer. - Subscription options from all subscribers remain aggregated on that request. `track::Request` already does this in Rust: it carries `prev_subscription` - (`track.rs:3786`) and re-combines the aggregate whenever a subscriber - changes (`:3905-3919`). + and re-combines the aggregate whenever a subscriber changes. - After that producer closes, a later request creates a new producer but continues the group and datagram sequence namespace for that broadcast and name. diff --git a/quest/m1/3056-watch-video-decoder-captures-the-rewind-generation-at.md b/quest/m1/3056-watch-video-decoder-captures-the-rewind-generation-at.md index 059adeefb5..ac20db956b 100644 --- a/quest/m1/3056-watch-video-decoder-captures-the-rewind-generation-at.md +++ b/quest/m1/3056-watch-video-decoder-captures-the-rewind-generation-at.md @@ -31,6 +31,19 @@ The container signals a playhead generation, not a codec reset: native decode stops flushing on it. This quest is whether watch still calls `decoder.reset()` to drop in-flight WebCodecs chunks when that generation bumps. +Reproduce before fixing; this is likely a false positive. Since +[#3711](https://github.com/moq-dev/moq/pull/3711) timelines only move +forward: a discontinuity continues from the live edge and the Rust consumer +refuses a rewind (`TimestampRewind`), so every chunk queued before the bump is +stamped below the new group, not against a distant new timeline. A +`decoder.reset()` would also contradict the documented "not a decoder flush" +contract of the discontinuity counter (`container::Consumer::discontinuity`, +and `continuous` in `js/hang/src/container/consumer.ts`), and the same +finding was ruled a false positive for native play in +[#4374](https://github.com/moq-dev/moq/pull/4374). If a stale frame cannot be +made to surface in a browser harness, close #3056 with that evidence and +delete this quest instead of adding the reset. + ## Closes - [#3056](https://github.com/moq-dev/moq/issues/3056) - close this issue when the quest finishes diff --git a/quest/m1/3489-ts-import-stream-liveness.md b/quest/m1/3489-ts-import-stream-liveness.md index abae2dc67b..8107a665b5 100644 --- a/quest/m1/3489-ts-import-stream-liveness.md +++ b/quest/m1/3489-ts-import-stream-liveness.md @@ -32,8 +32,10 @@ audio half is visible, late, through #3372's resync line. resync message. The SRT gateway reports nothing today; that surface is [SRT import stats](/quest/m1/srt-import-stats.md). - Name and shape the counters so the TR 101 290 quest adopts them as its - `PID_error` check, and leave the catalog `stalled` bit alone; that is the - ladder and client stats work. + `PID_error` check, and leave the catalog `stalled` bit alone: the importer + already sets it for a quiet video PID (`Stream::tick` after each decode + batch, #3630). These counters add no timeout; anything that must bound a + wait on a silent PID (the shared-shift quest) brings its own. - Tests with the issue's stimulus shape: suppress one PID's PES while keeping its PCR and continuity legal, assert the row's count stops and the gap grows; audio and SCTE-35 arms. diff --git a/quest/m1/announce-live-apps.md b/quest/m1/announce-live-apps.md index b9e7a4a995..c6f9969516 100644 --- a/quest/m1/announce-live-apps.md +++ b/quest/m1/announce-live-apps.md @@ -10,7 +10,9 @@ origin stream opened before the first connection goes live. ## Plan -- #4261 (on `dev`) adds the `live` event; the consumers it touches +- #4261 (on `dev`) adds the `live` event, and #4266 the same marker in the + bindings. Open #4384 renames the announce events to Start/Update/End/Live; + follow its names if it lands first. The consumers it touches (`demo/web/src/index.ts`, `js/room/src/room.ts`, `js/watch/src/broadcast.ts`, `js/moq-boy`) skip it today. - Page load: an origin stream opened before any session connects has no @@ -32,11 +34,3 @@ origin stream opened before the first connection goes live. Public API: when `@moq/net` emits `live` changes; any state signal on `@moq/room` or `@moq/watch` is additive. Lands on `dev` with #4261. Wire: none. - -## Required - -- [JS caught up](/quest/m1/js-announce-caught-up.md) - the `live` marker (#4261) - -## Related - -- [Bindings caught up](/quest/m1/announce-live-bindings.md) - the same marker in the bindings diff --git a/quest/m1/announce-live-bindings.md b/quest/m1/announce-live-bindings.md deleted file mode 100644 index fe2464bbab..0000000000 --- a/quest/m1/announce-live-bindings.md +++ /dev/null @@ -1,33 +0,0 @@ -# [M] Bindings see when an announce consumer has caught up - -## Goal - -moq-ffi, libmoq, and every wrapper (`py`, `swift`, `kt`, `go`, `dart`) yield -the same flat announce event Rust does: `Announced`, `Updated`, or `Retracted` -carrying the announce, or `Live` once every route live at subscribe time has -been delivered. A binding app can list what is live and stop, with no timer. - -## Plan - -Mirror the Rust `announce::Event` from -[Caught up](/quest/m1/cli-inspect/caught-up.md) one to one: - -- moq-ffi: `MoqAnnounceConsumer::next` returns `MoqAnnounceEvent`, a uniffi - enum with the four variants, replacing `MoqAnnounceUpdate::active()`. Stop - filtering `Live` in `rs/moq-ffi/src/origin.rs`. -- libmoq: the `on_announce` handle's `moq_announce_update.kind` gains a LIVE - value with no route fields, replacing the active flag. Stop skipping - `Event::Live` in `rs/libmoq/src/origin.rs`. -- Hand-written wrappers and `doc/lib/{py,swift,kt,go,dart,c}` follow, with a - test per binding that an empty origin still yields `Live`. - -Public API: breaking in moq-ffi, libmoq's C ABI, and every wrapper, so it -retargets to `dev` with the Rust break. Wire: none. - -## Required - -- [Caught up](/quest/m1/cli-inspect/caught-up.md) - settles the event shape this mirrors - -## Related - -- [JS caught up](/quest/m1/js-announce-caught-up.md) - the same event in `@moq/net` diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index cf79f69604..dd334aa40b 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -13,10 +13,19 @@ After normal catalog discovery, an HLS media playlist is generated by downloading only the timeline, never media objects. The catalog's root `archive` entry and the `moq-archive` object layout have -landed. The rest of the line is additive on top of them. +landed on main; the line may still reshape both. ## Plan +### Compatibility + +Decided (2026-09-28): the line may break the hang `archive` catalog entry and +the recording format in place on `main`, without a version bump or a `dev` +detour (for example the track-timeline rework in #4280). No archives exist +yet, so nothing recorded or published depends on either shape. This is an +exception to the main/dev rule for this line only; once a release ships +recordings, later format changes go through the entry's format version. + ### Landed The segment engine is in `rs/moq-mux/src/timeline.rs`: diff --git a/quest/m1/audio-codecs/encode-backend.md b/quest/m1/audio-codecs/encode-backend.md index 5c6dbc1073..8d18a9a16f 100644 --- a/quest/m1/audio-codecs/encode-backend.md +++ b/quest/m1/audio-codecs/encode-backend.md @@ -28,6 +28,12 @@ quest adds AAC through platform encoders; no software AAC dependency is selected backend's first packet. A backend that reports its own header (a magic cookie, `csd-0`, `MF_MT_USER_DATA`) must produce one equal to the synthesized ASC, asserted in its tests. +- The line branch's `aac-encode-refusals` quest makes `Config::encode` refuse + a channel count no channelConfiguration names instead of writing stereo, so + that synthesis is fallible and `Producer` refuses such a layout at + construction. JS matches: `@moq/hang`'s `audioSpecificConfig` throws for the + same counts (#4119, on the line). This absorbs the former m2 + aac-encode-refusal quest, a duplicate of that line quest. - Frame size is the codec's (1024 samples for AAC), so `frame_duration` is validated per codec rather than against the Opus table. - Bitrate updates go through the backend; one that cannot change rate diff --git a/quest/m0/audio-quality-harness/native.md b/quest/m1/audio-quality-native.md similarity index 91% rename from quest/m0/audio-quality-harness/native.md rename to quest/m1/audio-quality-native.md index a614645dd3..ff4d16ab81 100644 --- a/quest/m0/audio-quality-harness/native.md +++ b/quest/m1/audio-quality-native.md @@ -40,7 +40,11 @@ budget. Only then compare end-to-end totals, with the backend-dependent stages isolated: this lane deliberately accepts real device callback noise, so a difference in totals alone proves nothing about the estimator. +Standalone in m1 rather than a child of the m0 [Audio quality +harness](/quest/m0/audio-quality-harness/README.md) line (decided in the +2026-09-28 quest audit): nothing in m0 waits on it. The native jitter target +it grades is done on the jitter target line. + ## Required - [Browser](/quest/m0/audio-quality-harness/browser.md) - defines the metric schema, the budget file, and the extracted shaper -- [Native jitter target](/quest/m0/audio-jitter-target/native.md) - the estimator this lane grades and compares against the browser; without it there is no target series and the budgets would be set against a playout path that holds nothing diff --git a/quest/m1/audio-warmup.md b/quest/m1/audio-warmup.md index 926c6321a7..1b50e058d5 100644 --- a/quest/m1/audio-warmup.md +++ b/quest/m1/audio-warmup.md @@ -15,14 +15,16 @@ frames decode independently and set nothing; HE-AAC is out of scope. publish `warmup` for Opus; `js/publish` does the same for its Opus track. - `rs/moq-audio/src/decode` already trims Opus `pre_skip` at stream start; the warmup trim is the same mechanism keyed on the container consumer's - non-continuous signal, and the subscription's maximum age grows by `warmup` - as the video consumer quest does. `js/watch` audio mirrors it. + non-continuous signal (added in Rust by the open-GOP quest), and the + subscription's maximum age grows by `warmup` as the video consumer quest + does. `js/watch` audio mirrors it. - Tests in both languages: a mid-stream join discards exactly the warmup span and a continuous listener loses nothing. ## Required - [Catalog warmup](/quest/m1/catalog-warmup.md) - the field this reads and writes +- [Open-GOP leading pictures](/quest/m1/open-gop-leading-pictures.md) - adds the Rust non-continuous signal the trim keys on ## Related diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index dcba80f86f..b95317d571 100644 --- a/quest/m1/auth/README.md +++ b/quest/m1/auth/README.md @@ -108,6 +108,8 @@ existing lite-06 ALPN. grant does not, and REQUEST_UPDATE refreshes it - [moq-transport](/quest/m1/auth/moq-transport.md) - the same exchange as a setup-option extension on draft-17+, specified in a new draft +- [Expired token error](/quest/m1/auth/expired-error.md) - an expired token + reports `Error::Expired`, not `Unauthorized`, in Rust, JS, and the bindings - [Bindings](/quest/m1/auth/bindings.md) - grants and tokens reach every binding through moq-ffi and libmoq - [Token in band](/quest/m1/auth/token-in-band.md) - the credential can leave diff --git a/quest/m2/auth-expired-error.md b/quest/m1/auth/expired-error.md similarity index 61% rename from quest/m2/auth-expired-error.md rename to quest/m1/auth/expired-error.md index 013ab441fe..cdd88e4446 100644 --- a/quest/m2/auth-expired-error.md +++ b/quest/m1/auth/expired-error.md @@ -14,6 +14,11 @@ refusal as final. `EXPIRED_AUTH_TOKEN`, and back again when refusing. - Carry it through moq-ffi's error mapping and each wrapper. +Moved from m2 into the auth line: relay tokens refuse an expired +token with `AUTH_ERROR { Expired }`, so without this moq-net cannot tell it +apart from `Unauthorized`. + ## Required -- [In-band auth](/quest/m1/auth/README.md) - the AUTH streams that carry these codes +- [Lite stream](/quest/m1/auth/lite.md) - the lite AUTH streams that carry `Expired` +- [moq-transport](/quest/m1/auth/moq-transport.md) - the extension that carries `EXPIRED_AUTH_TOKEN` diff --git a/quest/m1/bbr-ack-cleanup.md b/quest/m1/bbr-ack-cleanup.md index 702b7b2124..3439bf801e 100644 --- a/quest/m1/bbr-ack-cleanup.md +++ b/quest/m1/bbr-ack-cleanup.md @@ -31,8 +31,8 @@ reclamation. Let the implementation choose the simplest representation that handles sparse packet numbers, reordering, and separate Initial, Handshake, and Data spaces. Bound retained state without repeatedly sweeping live entries or scanning large unused packet-number gaps. Keep the existing -sampling and congestion policies unchanged; loss-sample repair and ECN -response belong to their own quests. +sampling and congestion policies unchanged; loss-sample repair belongs to +its own quest. Extend existing tests for ordered, reordered, duplicate, and batched ACKs; loss and spurious loss; expiry; overlapping packet numbers in different @@ -50,11 +50,11 @@ substitute one hardware-specific millisecond limit for the scaling check. Land the fix in the fork, offer it upstream or record why not, publish an immutable fork release, and pin the corrected dependency chain here before -completing this quest. Do not wait for the broader QUIC stack release or the -ECN fix. Update internal packet-lifetime comments inline; no new user guide +completing this quest. Do not wait for the broader QUIC stack release. Update internal packet-lifetime comments inline; no new user guide is needed. ## Related -- [Loss sampling](/quest/m1/quic/bbr-loss-parity.md) - preserve packet metadata needed by the separate loss-sample repair +- [Loss sampling](/quest/m1/quic/bbr-loss-parity.md) - preserve packet metadata needed by the separate loss-sample repair; both edit `bbr3/mod.rs`, so sequence them +- [BBR starvation edges](/quest/m1/quic/bbr-app-limited-edges.md) - also edits `bbr3/mod.rs`; one owner there at a time - [Benchmark comparisons](/quest/m1/performance-comparisons.md) - reusable measurement guidance, not a prerequisite for this fix diff --git a/quest/m1/bbr-idle-burst.md b/quest/m1/bbr-idle-burst.md deleted file mode 100644 index 480cbad830..0000000000 --- a/quest/m1/bbr-idle-burst.md +++ /dev/null @@ -1,31 +0,0 @@ -# [S] BBR idle burst - -## Goal - -After a long keep-alive-only idle, a BBRv3 (`delay`) sender paces its next -burst near the bandwidth it learned before, not at one packet per RTT. A -regression test proves it. - -## Plan - -- The likely fix already shipped: moq-dev/noq#5 tells the controller of - starvation before the next send, released in moq-noq 1.3.1 and pinned by - #4206. The reporter ran 1.3.0. Nobody has reproduced the stall on either. -- In the fork, add a virtual-time transport test: learn the bandwidth, run - keep-alive only for about five minutes, send 250 KB, and assert the pacing - rate stays at or above about 0.9x the earlier max bandwidth. It must fail on - 1.3.0 and pass on 1.3.1. The existing label tests do not check the rate. -- If 1.3.1 still stalls, find the remaining cause (a stale `bw_shortterm` or a - ProbeRTT effect after idle) and fix it in the fork. Dropping the estimate - after long idle is a policy change for the m2 study, not this quest. -- The `iroh` feature uses upstream noq, which lacks the fix; offering it there - belongs to the upstream quest. - -## Closes - -- [#4219](https://github.com/moq-dev/moq/issues/4219) - the first send after an idle period is paced at a trickle - -## Related - -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer the starvation fix to n0-computer/noq -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - broader media measurements diff --git a/quest/m1/bench-ci.md b/quest/m1/bench-ci.md index b317185d53..4df11aa246 100644 --- a/quest/m1/bench-ci.md +++ b/quest/m1/bench-ci.md @@ -23,8 +23,8 @@ a GitHub App: - PR: one job builds base and head on the same runner and runs only the selected targets, saving and comparing Criterion baselines. Selection is the - changed crates plus their dependents, from the impact map that - [Thin justfiles](/quest/m1/tooling/justfiles.md) lands. No selected bench + changed crates plus their dependents, from the impact map that the + [tooling line](/quest/m1/tooling/README.md) lands. No selected bench means no job. Extend `bench/run.sh` with a Criterion-only, crate-scoped mode behind a recipe instead of writing a second runner. Hosted runners vary by about 3%, so the comment highlights only changes Criterion calls @@ -45,7 +45,7 @@ a GitHub App: ## Required -- [Thin justfiles](/quest/m1/tooling/justfiles.md) - owns the diff-to-crate impact map the PR job reuses +- [Tooling](/quest/m1/tooling/README.md) - owns the diff-to-crate impact map the PR job reuses ## Related diff --git a/quest/m1/binding-surface.md b/quest/m1/binding-surface.md index e789e32710..35bafa5f2a 100644 --- a/quest/m1/binding-surface.md +++ b/quest/m1/binding-surface.md @@ -2,7 +2,7 @@ ## Goal -moq-ffi, libmoq, and every wrapper (Python, Go, Swift, Kotlin, Dart) expose +moq-ffi and every wrapper (Python, Go, Swift, Kotlin, Dart) expose `decode::Options::delay` and `decode::Consumer::delay()` so applications can configure and observe audio playout delay. @@ -11,9 +11,9 @@ configure and observe audio playout delay. - One PR, each wrapper touched once, in its own idiom: durations as the language's duration type where the wrapper already uses one, handles over flat methods where a surface has more than one call. -- Add the libmoq implementation and tests in `rs/libmoq`, then regenerate - `moq.h`. Keep the additions compatible with the published C ABI. -- Update `doc/lib/{py,swift,kt,go,dart,c}` in the same PR. +- moq-ffi only, not libmoq: the [generated C](/quest/m1/c/README.md) and + C++ bindings inherit it from moq-ffi. +- Update `doc/lib/{py,swift,kt,go,dart}` in the same PR. - Test configuration and observed delay in every wrapper that has tests. ## Required diff --git a/quest/m1/broadcast-remove.md b/quest/m1/broadcast-remove.md deleted file mode 100644 index f95e2b8349..0000000000 --- a/quest/m1/broadcast-remove.md +++ /dev/null @@ -1,21 +0,0 @@ -# [S] Remove finish - -## Goal - -On `dev`, the deprecated broadcast end APIs are gone in every language, and -waiting for a broadcast to close says only that it closed. - -## Plan - -This is a published API break, so it targets `dev`. - -- Remove `broadcast::Producer::finish`, `abort`, and `Consumer::is_finished`, - and the `finished` and `abort` fields they read. -- `Consumer::closed()`, `Consumer::poll_closed`, and `Dynamic::closed()` return - `()` rather than an `Error` cause. -- Remove the deprecated binding `finish` methods, `moq_publish_finish`, and - JS's `close(abort)` parameter. - -## Required - -- `main` merged into `dev` once #4031 lands, so the deprecations exist there diff --git a/quest/m1/browser-benchmarks.md b/quest/m1/browser-benchmarks.md index 06043fac8d..b8ed38d6ef 100644 --- a/quest/m1/browser-benchmarks.md +++ b/quest/m1/browser-benchmarks.md @@ -2,21 +2,24 @@ ## Goal -A reproducible browser suite measures JS transport and media costs that the Rust -Criterion targets and native relay load generator do not exercise. +A reproducible real-browser suite measures JS transport and media costs that +the Rust Criterion targets, the native relay load generator, and the Bun +microbenchmarks do not exercise. ## Plan -`bench/run.sh::criterion_targets` discovers Cargo targets only. Existing JS unit -tests validate behavior, and `test/wasm` validates browser interop, but neither -provides a repeatable JS performance comparison. Reuse the existing relay/browser -harness pieces and add a focused recipe with artifacts under the benchmark -conventions. Microbenchmarks may run in Bun; browser conclusions must come from -an identified browser version on a real WebTransport connection. +The JS microbenchmarks already exist: the four Bun sweeps in `js/net/bench` +(`broadcasts`, `reader`, `frames`, `track`) run nightly and cover the origin +map, fragmented `Reader` reads, group frame decode, and track retention. This +quest is only the real-browser half: conclusions come from an identified +browser version on a real WebTransport connection. `test/wasm` validates +browser interop but measures nothing. Reuse the existing relay/browser harness +pieces and add a focused recipe with artifacts under the benchmark conventions. -- Cover `js/net/src/stream.ts` with buffered controls, fragmented varints, and - payloads from small audio through large keyframes. Sweep chunk sizes and record - CPU, wall time, allocation volume, GC pauses, and bytes copied where measurable. +- Measure the `js/net/src/stream.ts` path in the browser over WebTransport, + with payloads from small audio through large keyframes, recording CPU, wall + time, allocation volume, GC pauses, and bytes copied where measurable. Add a + Bun sweep only for a cost the browser run finds and the four miss. - Cover CMAF encode/decode with fixed audio/video fixtures and multiple samples. Keep fixture generation and relay startup outside timed intervals. - Add publish/watch scenarios measuring delivered/decoded/presented frames, diff --git a/quest/m1/c/README.md b/quest/m1/c/README.md index 8ed159fb22..92aec50f5d 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -27,23 +27,22 @@ Decided: (NULL on success) with `moq_error_message()`, results come through out-params, records are owned structs with `moq__free`, and names are the uniffi names in snake case. -- The generated package takes over the `moq-c` name and `moq::c` target that - #4288 gives the hand-written crate, so C users migrate once. Its first release - is 0.8.0, a minor bump over the hand-written 0.7.x. +- The line targets `dev`: #4288 already renamed the hand-written crate to + `moq-c` (`rs/moq-c`) there, and the generated package takes over that name + and `moq::c` target, so C users migrate once. Its first release is 0.8.0, a + minor bump over the hand-written 0.7.x. - Docs change inline: the consumer quest rewrites `doc/lib/c`, and retirement adds an upgrade note. No separate guide. The line owns the end-to-end check: every `doc/lib/c` sample and the C interop client build and run against the released 0.8.0 archive, not only in-tree. -The hand-written crate's open feature quests are parked in m3 until this line -retires it: [fetch](/quest/m3/libmoq-fetch.md), -[hidden](/quest/m3/libmoq-hidden.md), -[CMake library](/quest/m3/libmoq-cmake-lib.md), and -[shutdown](/quest/m3/libmoq-shutdown.md). +The hand-written crate gets no more feature work: its shutdown, CMake library, +and fetch quests were abandoned for this line, and hidden is done on dev. ## Required +- [C++ through moq-ffi](/quest/m1/cpp/README.md) - the generator fork, package recipe, and OBS move this line builds on - [C backend](/quest/m1/c/backend.md) - the fork emits an ergonomic C header and implementation from moq-ffi, with its own tests - [moq-c package](/quest/m1/c/package.md) - the generated header ships as `moq-c` 0.8.0 with `moq::c`, pkg-config, and a release workflow - [C consumers](/quest/m1/c/consumers.md) - the C interop client and `doc/lib/c` samples move onto the generated API @@ -51,6 +50,4 @@ retires it: [fetch](/quest/m3/libmoq-fetch.md), ## Related -- [C++ through moq-ffi](/quest/m1/cpp/README.md) - the generator fork and package recipe this line extends -- [libmoq becomes moq-c](/quest/m1/moq-c.md) - the rename whose name this line inherits - [FFI shape](/quest/m1/ffi-shape/README.md) - reshapes moq-ffi, which the generated C then follows for free diff --git a/quest/m1/c/package.md b/quest/m1/c/package.md index 686ecc5ea8..3a8a602f62 100644 --- a/quest/m1/c/package.md +++ b/quest/m1/c/package.md @@ -5,7 +5,8 @@ The generated C ships as the `moq-c` package, version 0.8.0: a release archive with the header, the moq-ffi staticlib, a CMake config exporting `moq::c`, and `moq-c.pc`, built the same way `cpp/moq` builds the C++ package. It replaces the -hand-written crate's artifacts under the same names. +hand-written `rs/moq-c` crate's artifacts (renamed from libmoq on dev by #4288) +under the same names. ## Plan @@ -24,5 +25,4 @@ hand-written crate's artifacts under the same names. ## Required -- [libmoq becomes moq-c](/quest/m1/moq-c.md) - the rename that frees the `moq-c` name and `moq::c` target this package takes over - [C backend](/quest/m1/c/backend.md) - the generator output this packages diff --git a/quest/m1/c/retire.md b/quest/m1/c/retire.md index cb3aa49e20..5a821f75e3 100644 --- a/quest/m1/c/retire.md +++ b/quest/m1/c/retire.md @@ -8,9 +8,11 @@ it are deleted. `doc/setup/upgrade.md` tells C users how to move. ## Plan -- Fold in any retirement step #4288 already planned for the renamed crate. -- Decide the parked m3 libmoq quests: delete each whose outcome the generated - API already provides, and say so in the PR. +- #4288 renamed libmoq to `moq-c` on dev and left a code-free `rs/libmoq` stub + whose last release points at `moq-c`; dev's + `quest/m1/libmoq-retire.md` deletes that stub. This quest retires the + hand-written `rs/moq-c` itself, so fold in or delete that quest, whichever + is still open. ## Required diff --git a/quest/m1/cache-expiry-growth.md b/quest/m1/cache-expiry-growth.md index fc2ea10f9d..0aafd2b2c1 100644 --- a/quest/m1/cache-expiry-growth.md +++ b/quest/m1/cache-expiry-growth.md @@ -18,7 +18,12 @@ cached groups, not a leak elsewhere. An earlier run saw growth on IETF only, but lite was then 30x slower per round, so it wrote far fewer groups; compare by groups written, not by wall time. -Reproduce with a focused test first. Unexpired groups at higher throughput, +#4378 (after these runs) fixed one candidate cause: an ended track's latest +group was exempt from idle expiry and the pool sweep never reached an ended +track, so stale consumers pinned its groups. Re-measure on current `main` +first; if RSS now plateaus, delete this quest. + +Otherwise reproduce with a focused test. Unexpired groups at higher throughput, expiry not running on some path, or groups held outside the pool's accounting would each explain it. Fix what is actually wrong. Consider whether an unbounded default is the right default for an origin at all. diff --git a/quest/m1/cache-wall-eviction.md b/quest/m1/cache-wall-eviction.md index 6451b320b8..083a2857e5 100644 --- a/quest/m1/cache-wall-eviction.md +++ b/quest/m1/cache-wall-eviction.md @@ -18,6 +18,12 @@ instead of only on a write. The pool's idle expiry is out of scope. groups past `max_age` until the pool's idle expiry (`Pool::gc`, driven by the origin driver without a write) or byte pressure reclaims them. `max_age_does_not_drive_wall_eviction` pins that behaviour. +- JS already does the opposite: `#prune` in `js/net/src/track.ts` evicts a + group once it has been idle on the wall clock (`performance.now`) past + `maxAge`, on its own timer, and `js/net/bench/track.ts` benches the publish + cost against the retained window. So `max_age` means different things per + language today. The decision settles both: either Rust gains a wall term or + JS moves to media time, and the loser's tests and docs change with it. - Prototype the alternative behind a bench-only switch: each track keeps a deadline for its oldest group on `max(wall elapsed, pts)`, armed on the timers the origin driver already runs, and evicts on expiry. diff --git a/quest/m1/captions-msf.md b/quest/m1/captions-msf.md index eb68c70696..5079a87ee4 100644 --- a/quest/m1/captions-msf.md +++ b/quest/m1/captions-msf.md @@ -2,16 +2,18 @@ ## Goal -An MSF catalog's caption, subtitle, and sign-language tracks survive -conversion into a hang catalog. Today they are dropped with a warning, so a +An MSF catalog's caption and subtitle tracks survive conversion into a hang +catalog. Today they are dropped with a warning, so a round trip through MSF silently loses every caption. ## Plan `from_msf` skips any track "with no role, with an unsupported role (caption, subtitle, sign language, audio description, custom roles)". Now that the hang -`text` section exists with a `TextRole` deliberately mirroring the MSF role -registry, three of those map straight across and stop being unsupported. +`text` section exists with a `TextRole` borrowing the MSF role names, two of +those map straight across and stop being unsupported: `TextRole` has only +`Subtitle`, `Caption`, and `Unknown(String)`, which preserves any other role +verbatim. Map the roles, and derive the rest of `TextConfig` from what MSF carries: language onto `lang`, the display name onto `label`, and the packaging onto @@ -23,7 +25,9 @@ garbage. Prove the round trip in both directions, so an MSF catalog converted to hang and back keeps its caption tracks, roles, and languages. Audio description is deliberately left unmapped: it is an audio rendition with a role, not timed -text, and mapping it into `text` would be wrong. +text, and mapping it into `text` would be wrong. Sign language is likewise a +video rendition, so it stays unmapped too. A custom role maps to +`TextRole::Unknown` only when the track's codec is a text format. Per cross-package sync, mirror any schema movement in `js/msf`. diff --git a/quest/m1/capture-control.md b/quest/m1/capture-control.md index 0072552113..958853788f 100644 --- a/quest/m1/capture-control.md +++ b/quest/m1/capture-control.md @@ -1,4 +1,4 @@ -# [S] Capture Control: settled name, loud cut, prompt cancel +# [M] Capture Control: settled name, loud cut, prompt cancel, catalog clock ## Goal @@ -14,6 +14,12 @@ on `dev` only, get their final shape before release: startup probe, `capture::open`, `Sink::open`, or an encode is in flight. Today those awaits never see the handle close, so a camera or permission prompt can outlive its owner. +- Capture publishers stamp on the clock their catalog advertises, with no + separate clock to pass. Today both `CaptureOptions` carry their own + `clock: moq_mux::Clock`, and `Default` builds a fresh one, so a caller + relying on the default publishes against a mapping the catalog never + advertised. `moq import capture` passes `catalog.clock()`, the only correct + value. ## Plan @@ -29,6 +35,12 @@ Decided: `CutUnsupported`, and record the choice here. - Race every await in the driver against the controls closing, rather than only the idle wait, so the probe and the demand-driven opens both cancel. +- Drop the `clock` field from both options; `Control::new` already takes the + catalog producer, so it reads `catalog.clock()`. Update moq-cli and any + binding that forwards a clock. The clock fixtures in both crates already + pass the catalog's clock, so they keep grading the same path. This absorbs + the former m2 capture-clock-source quest: it breaks the same `dev` options, + so one break lands instead of two. This is a `dev` break layered on #4184; land it on `dev` before the release that first publishes these handles. @@ -36,8 +48,3 @@ that first publishes these handles. Tests: a backend without forced keyframes surfaces `CutUnsupported` to the caller; dropping the last `Control` during a slow fake open or probe returns from `Driver::run` without finishing the open. - -## Related - -- [Video keyframe flag](/quest/m1/video-keyframe-flag.md) - the same cut throttle, counting cadence keyframes -- [Capture clock source](/quest/m2/capture-clock-source.md) - drops the `clock` field from these options diff --git a/quest/m1/cli-given-flags.md b/quest/m1/cli-given-flags.md index 2b270033e9..1bb750ba4e 100644 --- a/quest/m1/cli-given-flags.md +++ b/quest/m1/cli-given-flags.md @@ -2,8 +2,7 @@ ## Goal -`moq fetch`, `moq ls` (on the [CLI inspect](/quest/m1/cli-inspect/README.md) -line), and the local verbs behind `Invocation::reject` refuse any listener +`moq fetch`, `moq ls` (#4032, on `dev`), and the local verbs behind `Invocation::reject` refuse any listener flag they would never use, as they already do for `--listen` and the cluster flags. Today `MoqSide::given()` in `rs/moq-cli/src/args.rs` lists only some of them, so `--listen-version`, `--listen-tls-*`, `--listen-preferred-*`, and diff --git a/quest/m1/cli-inspect/README.md b/quest/m1/cli-inspect/README.md deleted file mode 100644 index 718aa7fb50..0000000000 --- a/quest/m1/cli-inspect/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# CLI inspection - -## Goal - -`moq ls` answers "what is live on this relay" and `moq fetch` reads one group -of a track, over MoQ with the session's own auth, instead of a separate `curl` -to the relay's HTTP `/announced` and `/fetch` endpoints. A guide shows an -operator how to inspect a relay: what is live, how to watch it change, how to -read a group, and where the stats live. - -Non-goals: listing tracks or catalogs, showing routes, hops, or sources, and -changing the relay's HTTP endpoints. - -## Plan - -This README owns the guide, written once both verbs land: a new -`doc/bin/inspect.md` covering `moq ls` and `--follow`, `moq fetch`, their -`curl` equivalents, and reading the relay's stats track, linked from `doc/bin/cli.md`, -`doc/bin/relay/http.md`, and the site sidebar. - -## Required - -- [Caught up](/quest/m1/cli-inspect/caught-up.md) - moq-net's announce consumer yields a `Live` marker once the initial set has landed, and shell completion drops its settle timer -- [ls](/quest/m1/cli-inspect/ls.md) - `moq ls` prints the live set and exits, or follows changes, as paths or JSON lines diff --git a/quest/m1/cli-inspect/caught-up.md b/quest/m1/cli-inspect/caught-up.md deleted file mode 100644 index dcec2f6787..0000000000 --- a/quest/m1/cli-inspect/caught-up.md +++ /dev/null @@ -1,41 +0,0 @@ -# [M] An announce consumer knows when it has caught up - -## Goal - -A Rust consumer of `origin::Consumer::announced()` is told, once and in -order, that the routes live at subscribe time have all been delivered, so -"list what is live" needs no guess. Today the stream replays the current -routes and continues live with no boundary, and moq-cli's `--broadcast` -completion (`rs/moq-cli/src/complete.rs`) stops on a 30ms settle / 500ms -budget timer instead. - -## Plan - -- `AnnounceConsumer::next()` yields an enum: each route update as today, - then a single `Live` marker once the initial set has been delivered, then - live updates. `Live` comes after every replayed update, so a caller that - stops at it has seen the whole set. This changes a published return type, - so the quest lands on `dev`. -- The fact is per source, but consumers read a merged origin, which today - has no source registry. Each remote announce subscription (a session's - subscriber, a cluster peer) holds a pending guard from the origin until - its initial set has landed; in-process routes are replayed synchronously - and are never pending. A cursor yields `Live` once every guard pending at - subscribe time has cleared. A source that closes before clearing drops - its guard, so a dead session cannot stall the marker. No cost while no - guard is pending. -- A source clears on the wire's boundary: `AnnounceOk.active` on lite-05+, - `AnnounceInit` on lite-01/02. Versions without one (lite-03/04, IETF) - clear on a settle timer owned by the session, the one place it lives. -- Move completion onto `Live`; the timer survives only in the session for - marker-less versions. Look at the other settle and deadline loops - (`moq-bench` startup, relay cluster discovery, test `settle()` helpers) - and move any that want the initial set. -- Test: a relay with N announced broadcasts yields `Live` after exactly N - updates on each lite version that carries a count, an empty relay yields - it immediately, and a session that closes before its count arrives does - not block it. - -Public API: breaking on moq-net (`next()` returns an enum). Wire: none. JS -parity is [a separate quest](/quest/m1/js-announce-caught-up.md), and an -IETF count is [another](/quest/m1/ietf-announce-count.md). diff --git a/quest/m1/cli-inspect/ls.md b/quest/m1/cli-inspect/ls.md deleted file mode 100644 index a5791a8d6a..0000000000 --- a/quest/m1/cli-inspect/ls.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] The ls verb lists active announcements - -## Goal - -`moq --connect ls [prefix]` prints the broadcasts live under `prefix`, -one path per line relative to the connect URL's root, and exits once caught -up. `--follow` first prints the announce stream's initial replay, one `+ path` -per active route, then `+ path` / `- path` as broadcasts come and go. `--json` -prints one `{"path": .., "active": bool}` per line in either mode. Like the -relay's `/announced`, it lists announced prefixes, which by convention are -broadcast paths. An `Updated` event (a new route for a path already live) is -not printed. `--follow` runs until interrupted; if the announce stream ends, -it exits non-zero. - -## Plan - -- A MoQ verb beside import/export/play, not a stageable one: it consumes the - origin and never publishes. It is a subscriber-only session, so it works - with a subscribe-only token. -- Update `doc/bin/cli.md`: add the verb to the table, and point the Debugging - section at `moq ls` next to `curl /announced`. The verb table still lists - `token` where the code has `auth`; fix it while there. -- Test: against an in-process relay, snapshot mode prints exactly the - announced set and exits, `--follow` prints the initial replay then a `-` when - a publisher leaves, and `--json` lines parse. - -## Required - -- [Caught up](/quest/m1/cli-inspect/caught-up.md) - snapshot mode exits on the consumer's `Live` marker, so `ls` follows it onto `dev` diff --git a/quest/m1/close-codes.md b/quest/m1/close-codes.md index 4e52f19d62..8478905c3b 100644 --- a/quest/m1/close-codes.md +++ b/quest/m1/close-codes.md @@ -11,6 +11,10 @@ WebTransport. Never `Transport("connection closed")` or its own `Internal`. Both bugs are upstream; fix them at the source, release, and bump the pins. +Targets `dev`: #4262 (open) does this, and the qmux release pulls in +`web-transport-trait` 0.5 (`SendStream::set_priority` takes `i32`), a Rust +API break. It reports `@moq/qmux` already keeps the first close. + - qmux 0.5.1 (`moq-dev/web-transport`) lets later writes overwrite the recorded close in `session.rs`: the WS Close frame read after APPLICATION_CLOSE (reader loop, backend `send_replace`), a local `close()` diff --git a/quest/m1/cluster-routing.md b/quest/m1/cluster-routing.md index 13d66d28a8..4c82131756 100644 --- a/quest/m1/cluster-routing.md +++ b/quest/m1/cluster-routing.md @@ -104,13 +104,21 @@ make MoQ's common case. registry's replay lands, on every new prefix rather than in a rare race. - What remains of announce compression's hop-tail half (`Hop Base` and `Hop Keep` in the lite draft) once only cluster boundaries carry hops. +- What replaces `--hop` first-hop failover. Today two publishers sharing a Hop + ID are one source that relays fail over between at a group boundary + (`doc/bin/cli.md` "Redundant publishers", + `doc/concept/use-case/contribution.md`). Inside a cluster no announcement + carries a hop list, so two encoders on different ingest relays become two + origins. Keep the documented behavior or change the docs in the same PR. ## Required +- [Local origin](/quest/m0/local-origin.md) - workers stop reading hop chains before they go +- [Wildcard](/quest/m0/wildcard/README.md) - the specificity, pool spread, and reply identity this selection builds on - moq.pro's routing simulator reports ([quest](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/routing-simulator.md)) ## Related - [Skip unchanged announce updates](/quest/m0/announce-update-dedupe.md) - cuts duplicate updates on today's routing -- [Local origin](/quest/m0/local-origin.md) - workers stop reading hop chains before they go -- [Wildcard](/quest/m0/wildcard/README.md) - the specificity, pool spread, and reply identity this selection builds on +- [Redundant ingest](/quest/m2/redundant-ingest.md) - builds on the `--hop` failover this must keep or replace +- [Routing cost domains](/quest/m2/routing-cost-domains.md) - cost across the cluster boundaries this keeps path vector diff --git a/quest/m1/cpp/cancel.md b/quest/m1/cpp/cancel.md index a3974224bb..8faad9eacf 100644 --- a/quest/m1/cpp/cancel.md +++ b/quest/m1/cpp/cancel.md @@ -2,33 +2,25 @@ ## Goal -A caller can tell a cancelled or consumed future is dead before reading it, -and a read of one fails with a message naming the misuse. Today the generated -`uniffi::Future` declares `void cancel() noexcept` on an lvalue, so -`future.cancel(); future.get();` compiles and aborts with nothing to check -first; `cpp/moq/README.md` documents the abort instead of giving callers a -guard. +Callers are told to check `valid()` before reading a future that may be +cancelled, consumed, or moved from, and the tests prove it goes false in each +case. ## Plan -- Mirror `std::future`, the idiom C++ callers know: once cancelled, consumed - by `then()`, or moved from, a future is invalid and `valid()` returns false. - Calling `get()` on an invalid future is a precondition violation, as it is - for `std::future`, and it aborts with a message naming `cancel`/`then`/move - rather than a bare abort. Decided with the maintainer during the #4307 - review. -- Rejected: `&&`-qualifying `cancel()`, since `std::move(f).cancel(); f.get();` - still compiles; and `get()` returning an error, since expected mode has no - error value for it: generic `E` has no invalid-state variant, and the line - README already rejects `std::variant`. -- Decided in [#4100](https://github.com/moq-dev/moq/pull/4100): cancel - abandons the future, so no continuation runs. That still holds. -- The `Future` template lives in the kixelated `uniffi-bindgen-cpp` fork, so - this is a fork change and a new `-kixelated.N` tag, then the pin bump in - `flake.nix` and every place its comment lists. -- Update the README to point callers at `valid()`, and check it in the - in-tree callers that can hold a cancelled future (`cpp/moq`, the probe, - `cpp/obs`). Test `valid()` after `cancel()`, after `then()`, and after a - move. +The fork half is done on the line: the `-kixelated.2` generator's +`uniffi::Future` has `valid()`, false once `get()`, `then()`, `cancel()`, or a +move took its state, like `std::future`, and a read of an invalid future aborts +with "it was consumed or cancelled". `cpp/moq/test/probe.cpp` checks `valid()` +after `cancel()`. Decided in the #4307 review; `&&`-qualifying `cancel()` and an +error-returning `get()` stay rejected. -Public API: additive `valid()` on the unreleased C++ package. Wire: none. +What remains: + +- `cpp/moq/README.md` (the Error and Cancellation sections) still only says a + misused future aborts; point callers at `valid()` instead. +- Extend the probe to check `valid()` after `then()` and after a move. +- Check the in-tree callers that can hold a cancelled future (`cpp/moq`, + `cpp/obs`) against `valid()`. + +Public API: none beyond the unreleased C++ package's `valid()`. Wire: none. diff --git a/quest/m1/data-capture-bindings.md b/quest/m1/data-capture-bindings.md index 11f969227f..29b364bb69 100644 --- a/quest/m1/data-capture-bindings.md +++ b/quest/m1/data-capture-bindings.md @@ -39,7 +39,10 @@ no optional arguments), and a `_with_x` twin is ruled out. The broadcast clock `now()` is additive; `window::Producer::push` accepts `Timed`, source-compatible. Wire: none. +## Required + +- [JSON and flate namespaces](/quest/m1/ffi-shape/json.md) - moves the data producers this changes, so the two breaks land in order rather than colliding + ## Related -- [FFI shape](/quest/m1/ffi-shape/README.md) - moves the data producers into a json namespace - [Generated C bindings](/quest/m1/c/README.md) - replaces libmoq, so C inherits this from moq-ffi diff --git a/quest/m1/decoded-frames.md b/quest/m1/decoded-frames.md deleted file mode 100644 index 64a63052d7..0000000000 --- a/quest/m1/decoded-frames.md +++ /dev/null @@ -1,47 +0,0 @@ -# [M] Preserve decoded frame ownership across bindings - -## Goal - -Bindings retain the existing `moq_video::Frame` and its backing surface until -the consumer is finished. A native consumer can access platform surfaces; -portable bindings can request CPU pixels without a second frame or conversion implementation. - -## Plan - -`moq_video::Frame` already owns a timestamp and `Surface`, with native backing, -resizing, and CPU conversions. Reuse it. The eager conversion to remove is in -`rs/libmoq/src/video.rs`: `consume_task` calls `surface.into_i420()` before -inserting a byte-only frame into the handle slab. - -Retain the existing Frame through the binding's owned handle. Define release, -borrowed-view validity, thread/device affinity, and GPU-completion ownership. -A native view cannot outlive the frame or permit producer-pool reuse while a -consumer is still using it. Cancellation and delayed completion retain the same -ownership guarantees. CPU conversion uses existing Surface methods on demand. -Do not create a parallel surface enum, decoder, resize engine, or cleanup -callback protocol. - -The moq-ffi boundary carries the owned frame; libmoq mirrors it through the -Cross-Package Sync table. Native views are exposed where OBS, on the generated -C++ over moq-ffi, needs them; other bindings expose portable pixels until a -consumer asks. Shared ownership does not require every -language to expose every platform surface. - -Define and implement the shared binding contract here. OBS owns graphics -imports and presentation; the FFI video consumer owns rendition subscription -and portable delivery. The C decoder output layout has landed; consume its -format and size controls without another struct layout change. Adding -fields to a published C struct is not automatically additive. - -Test handle release, conversion failures, cancellation, delayed consumption, -and retained ownership through the existing libmoq/FFI test lanes. Platform -adapters own their hardware import proof. Update `moq.h`, affected wrappers, -and C/binding documentation; run `just test interop --all` in CI. - -Public API: owned frame access and conversion at the binding boundary. Wire: -none. Consume the settled main frame/output contracts without replacing them. - -## Related - -- [OBS source](/quest/m1/obs-moq-video/source.md) - consumes native views through the generated C++ -- [Mobile ownership](/quest/m2/mobile-ownership.md) - deferred platform capture and native mobile integration diff --git a/quest/m1/doc-samples-go-dart.md b/quest/m1/doc-samples-go-dart.md index af68aa5f44..6680e0a034 100644 --- a/quest/m1/doc-samples-go-dart.md +++ b/quest/m1/doc-samples-go-dart.md @@ -20,11 +20,16 @@ knows neither language. imports are compile errors. Handle what the samples actually use; prefer adjusting a sample so it reads naturally and still compiles over growing the extractor. -- Wire each into its language's `check` recipe (`go/justfile`, - `dart/justfile`) the way `py/justfile` and `rs/justfile` call it, and add - `doc/lib/samples.sh` and the doc page to the paths that trigger those - checks in CI. +- Wire each into its language's check script, `sh/go/check.sh` and + `sh/dart/check.sh` on the tooling line, the way `sh/kt/check.sh` and + `sh/py/samples.sh` call `samples.sh`. Add `doc/lib/go/`, `doc/lib/dart/`, + and `doc/lib/samples.sh` to the `go` and `dart` patterns of the impact + map in `sh/dispatch.sh`, as the `py`, `kt`, and `swift` ones already have. - Prove it by renaming one wrapper method locally and watching each check fail on the doc sample. Public API: none. Wire: none. + +## Required + +- [Tooling](/quest/m1/tooling/README.md) - the `sh//` check scripts and the impact map this extends diff --git a/quest/m1/drain/README.md b/quest/m1/drain/README.md index 284fd4aa71..786b65f106 100644 --- a/quest/m1/drain/README.md +++ b/quest/m1/drain/README.md @@ -58,4 +58,4 @@ by the stop deadline and encoder reconnect. ## Related -- [pop-skipping](/quest/m1/pop-skipping/README.md) - its same-PoP link price and full eligible pairing become important when a deployment adds a second relay per PoP +- [Cluster routing](/quest/m1/cluster-routing.md) - the configured topology and link costs a second relay per PoP joins diff --git a/quest/m1/dropped-sources.md b/quest/m1/dropped-sources.md index 08a0ce43bb..4c20212884 100644 --- a/quest/m1/dropped-sources.md +++ b/quest/m1/dropped-sources.md @@ -27,7 +27,7 @@ Public API: none expected; error values consumers observe change. Wire: none. ## Required -- [Unauthorized](/quest/m1/auth/unauthorized.md) - #4179 supplies shared Unauthorized errors and revoked-stream handling +- [Auth](/quest/m1/auth/README.md) - its Unauthorized quest (#4179, done on the line) supplies shared Unauthorized errors and revoked-stream handling ## Related diff --git a/quest/m1/epoch.md b/quest/m1/epoch.md index d829397624..1ebdeaacdd 100644 --- a/quest/m1/epoch.md +++ b/quest/m1/epoch.md @@ -21,7 +21,7 @@ line builds on, and it replaces the e2ee-local `moq_e2ee::Epoch`. like `@alice` is valid today and stays valid: strict parsing already keeps it from reading as an epoch. Rejecting it instead would break the path contract and land on `dev`. Check how the split interacts with - [path patterns](/quest/m1/path-patterns.md) and + path patterns (done on the [auth line](/quest/m1/auth/README.md)) and hidden broadcasts (a leading `.`, see `doc/concept/moq-lite.md`). - `moq-e2ee` uses the shared type. Update [draft-lcurley-moq-e2ee](/drafts/draft-lcurley-moq-e2ee.md) so the path is diff --git a/quest/m1/export-linger.md b/quest/m1/export-linger.md index d8e40d1f0f..fe0f3a0808 100644 --- a/quest/m1/export-linger.md +++ b/quest/m1/export-linger.md @@ -32,7 +32,3 @@ once and exits 1 with `json: dropped`, even while `moq_tokio` is mid-reconnect - Tests: a relay-backed CLI test that restarts the publisher within the linger and sees output resume, one that lets it expire and checks exit 1, and a clean FIN that exits 0. - -## Closes - -- [#3926](https://github.com/moq-dev/moq/issues/3926) - close this issue when the quest finishes diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index 6e20982499..346223a423 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -3,7 +3,7 @@ ## Goal A binding consumer finds each layer where Rust keeps it: moq-net at the root, -and `media`, `json`, `audio`, and `video` as their own namespaces, each type +and `media`, `json`, `flate`, `audio`, and `video` as their own namespaces, each type constructed from the lower-layer handle it wraps. `BroadcastProducer` and `BroadcastConsumer` stop carrying every layer's verbs, and every moq-ffi type and verb maps to a Rust one. A docs page shows the layers in each language. @@ -18,7 +18,10 @@ Settled shape: - Groups by role, not crate: the root is moq-net (client, server, session, origin, broadcast, track, group); `media` merges hang and moq-mux, since a binding never sees that split (catalog, import producers, container - consumers); `json`, `audio`, and `video` own their producers and consumers. + consumers); `json`, `flate`, `audio`, and `video` own their producers and + consumers. `flate` holds the opaque snapshot and stream tracks moq-ffi + publishes as `publish_binary_*` today (#4137), named after the crate they + fold into in [moq-binary folds into moq-flate](/quest/m1/flate-binary.md). - A layer's type is constructed from the handles its Rust constructor takes, not reached through an accessor on the broadcast: JSON wraps a track (`moq_json::snapshot::Producer::new(track, config)`), so it also works on a @@ -32,14 +35,16 @@ Settled shape: namespaces. - `demand()` is the one way to watch subscribers; producers drop their `name`/`is_used`/`used`/`unused` duplicates. -- libmoq renames its C symbols to the same groups (`moq_json_*`, - `moq_media_*`), with `cpp/obs` adapting. +- moq-ffi only. libmoq and `cpp/obs` are out of scope: the + [generated C](/quest/m1/c/README.md) and [C++](/quest/m1/cpp/README.md) + bindings inherit this shape from moq-ffi, so reshaping the hand-written C + ABI would break C users twice. -Each child reshapes one group end to end: moq-ffi, all five wrappers, libmoq, -and the `doc/lib` samples, per the cross-package table. This README owns the +Each child reshapes one group end to end: moq-ffi, all five wrappers, and the +`doc/lib` samples, per the cross-package table. This README owns the work no child does: -- A layers guide under `doc/lib` mapping net, media, json, audio, and video to +- A layers guide under `doc/lib` mapping net, media, json, flate, audio, and video to each language's module, linked from every binding page. - The bindings section of the following release's upgrade page: old call to new call per language. @@ -47,8 +52,7 @@ work no child does: ## Required -- [Release](/quest/m0/release.md) - the restructure follows the release rather than riding it -- [JSON](/quest/m1/ffi-shape/json.md) - the pilot: json becomes its own namespace wrapping a track in every binding and sets the per-language pattern +- [JSON](/quest/m1/ffi-shape/json.md) - the pilot: json and flate become their own namespaces wrapping a track in every binding and set the per-language pattern - [Net](/quest/m1/ffi-shape/net.md) - client and server take config records, snapshots are records, and the verbs match moq-net - [Media](/quest/m1/ffi-shape/media.md) - catalog, import, and container consume move under `media` - [Codecs](/quest/m1/ffi-shape/codec.md) - audio and video encoders and decoders move under their own namespaces with one constructor shape diff --git a/quest/m1/ffi-shape/codec.md b/quest/m1/ffi-shape/codec.md index 1f72300318..a5668cfc19 100644 --- a/quest/m1/ffi-shape/codec.md +++ b/quest/m1/ffi-shape/codec.md @@ -17,9 +17,16 @@ key apart from its rendition; pick one convention for both. Go's through `demand()` only. Both groups stay behind their cargo features and off wasm. -libmoq's codec symbols follow. - -Public API: breaking in every binding and libmoq. Wire: none. +The video encoder's output mirrors moq-video's `encode::Gop`: +`MoqVideoEncoderOutput.gop: Option` becomes a `MoqVideoGop` enum with a +`Keyframe { interval }` variant, defaulting to keyframes at two seconds, and +documented as non-exhaustive like the core. The wrappers expose it as an enum +their callers construct, not one they are asked to match, so +[intra-refresh bindings](/quest/m2/intra-refresh/bindings.md) adds the refresh +variant additively instead of breaking `gop` a second time. Go gets no uniffi +default, so its zero value must read as keyframe mode. + +Public API: breaking in every binding. Wire: none. ## Required diff --git a/quest/m1/ffi-shape/json.md b/quest/m1/ffi-shape/json.md index 0ff6f35227..3cbf8969d6 100644 --- a/quest/m1/ffi-shape/json.md +++ b/quest/m1/ffi-shape/json.md @@ -1,11 +1,12 @@ -# [M] JSON gets its own namespace in every binding +# [M] JSON and flate get their own namespaces in every binding ## Goal -JSON tracks live under `json` in moq-ffi and every wrapper, constructed from a -track producer or consumer as in `moq-json`, and `BroadcastProducer`/`BroadcastConsumer` lose -`publish_json_*`/`subscribe_json_*`. The per-language namespace pattern this -sets is what the other children copy. +JSON tracks live under `json` and opaque tracks under `flate` in moq-ffi and +every wrapper, constructed from a track producer or consumer as in `moq-json` +and `moq-flate`, and `BroadcastProducer`/`BroadcastConsumer` lose +`publish_json_*`/`subscribe_json_*` and `publish_binary_*`. The per-language +namespace pattern this sets is what the other children copy. ## Plan @@ -25,10 +26,10 @@ keep doing so, and the rest may follow. Watch for Go import cycles: a subpackage takes the root's broadcast handle, so the root must not import it back. -libmoq's JSON symbols move to `moq_json_*`. +`flate` is the same shape over opaque bytes: moq-ffi's `binary.rs` +(`publish_binary_snapshot`, `publish_binary_stream`, #4137) moves under it, +mirroring `moq_flate::{snapshot, stream}` once +[moq-binary folds into moq-flate](/quest/m1/flate-binary.md). If that fold has +not landed, name the namespace `flate` anyway rather than `binary`. -Public API: breaking in every binding and libmoq. Wire: none. - -## Required - -- [Release](/quest/m0/release.md) - the restructure follows the release rather than riding it +Public API: breaking in every binding. Wire: none. diff --git a/quest/m1/ffi-shape/media.md b/quest/m1/ffi-shape/media.md index e936b29668..e2a0a53714 100644 --- a/quest/m1/ffi-shape/media.md +++ b/quest/m1/ffi-shape/media.md @@ -24,9 +24,7 @@ once the shape is in front of you, and prefer one path. Go's `FetchMediaGroup` takes an options struct. Media producers watch subscribers through `demand()` only. -libmoq's media symbols move to `moq_media_*`, and `cpp/obs` adapts. - -Public API: breaking in every binding and libmoq. Wire: none. +Public API: breaking in every binding. Wire: none. ## Required diff --git a/quest/m1/ffi-shape/net.md b/quest/m1/ffi-shape/net.md index b91d2770ac..cfcf2da2e7 100644 --- a/quest/m1/ffi-shape/net.md +++ b/quest/m1/ffi-shape/net.md @@ -36,9 +36,7 @@ are renamed. this line removes, but dropping it costs every quick-start a hop (raised in #3959). -libmoq's affected symbols follow. - -Public API: breaking in every binding and libmoq. Wire: none. +Public API: breaking in every binding. Wire: none. ## Required diff --git a/quest/m1/flate-binary.md b/quest/m1/flate-binary.md new file mode 100644 index 0000000000..812c11c613 --- /dev/null +++ b/quest/m1/flate-binary.md @@ -0,0 +1,50 @@ +# [M] moq-binary folds into moq-flate + +## Goal + +Opaque binary tracks live in `moq-flate` and `@moq/flate`: the `snapshot` and +`stream` modes `moq-binary` and `@moq/binary` provide today move there beside +the group-scoped codec, and `moq-binary` and `@moq/binary` are deleted. One +package owns compressed and opaque tracks, so there is no second "compressed +track" wrapper to build. + +## Plan + +Decided in the 2026-09-28 quest audit: `moq-binary` already composes +`moq-flate` into per-group windows (each group one sync-flushed DEFLATE +stream), which is what the m2 flate line planned to add as a new track wrapper. +Folding the two removes the duplicate instead of building it. + +- Rust: move `rs/moq-binary/src/{snapshot,stream}` and `Compression` into + `moq-flate` as `moq_flate::{snapshot, stream}`, keeping the codec + (`Encoder`/`Decoder`) at the root. `moq-flate` gains the `moq-net` + dependency. Delete `rs/moq-binary` and its workspace member, and repoint + `moq-mux` (`src/binary.rs`, `src/error.rs`) and `rs/libmoq` if it still + exists. +- JS: move `js/binary/src/{snapshot,stream,compression.ts}` into `@moq/flate` + as `Snapshot` and `Stream`, adding the `@moq/net` and `@moq/signals` + dependencies, and delete `js/binary`. +- Wire and catalog: unchanged. The hang catalog's `binary` section and + `moq_mux::binary` keep their names; they describe the track's content, and + the catalog section is wire. +- moq-ffi: rename `binary.rs` and its `publish_binary_*`, `MoqBinaryConfig`, + and producer types after `flate`, so every binding names the crate it wraps. + If [FFI shape](/quest/m1/ffi-shape/README.md) has already given them a + `flate` namespace, follow it instead. +- Open: whether `Compression::None` survives the move. An uncompressed opaque + track still needs a home, so the recommendation is to keep it and document + that the crate name is not a promise every track is deflated. +- Docs: fold `doc/lib/rs/moq-binary.md` into a `moq-flate` page and + `doc/lib/js/binary.md` into a `@moq/flate` page, fix `doc/.vitepress/config.ts`, + `doc/lib/{rs,js}/index.md`, `doc/concept/hang.md`, the android workflow path + filter, and add an upgrade note in `doc/setup/upgrade.md`. Grep for + `moq-binary`, `moq_binary`, and `@moq/binary`. + +Public API: breaking. `moq-binary` and `@moq/binary` are published and +deleted, and moq-ffi's binary names change, so this lands on `dev`. +`moq-flate` and `@moq/flate` grow additively. Wire: none. + +## Related + +- [Compressed tracks](/quest/m2/flate/README.md) - the hand-written wrappers expose these tracks +- [FFI shape](/quest/m1/ffi-shape/README.md) - gives the flate tracks their binding namespace diff --git a/quest/m1/frame-slot-charge.md b/quest/m1/frame-slot-charge.md new file mode 100644 index 0000000000..d7c199e5aa --- /dev/null +++ b/quest/m1/frame-slot-charge.md @@ -0,0 +1,53 @@ +# [S] Frame slots are cached for free + +## Goal + +A group's frame slots are charged against the cache pool, so a track producing +many small frames per group is billed for what it holds. The fixed per-group +overhead already covers the first few slots; this is the growth past them, and +the capacity a released group keeps. + +## Plan + +Re-planned from the closed #3546 against the current accounting in +`rs/moq-net/src/model/group.rs`. + +`GroupState::cache` and its `cache::Charge` count frame payload bytes only. The +frames live in a `VecDeque`, and `CACHE_OVERHEAD` prices exactly +`FRAME_SLOTS` (4) of its slots, the capacity `VecDeque` rounds the first frame +up to. Two gaps follow: + +- The deque grows geometrically past those four slots, and every slot beyond + them is `size_of::()` the pool never sees. It only bites where many + small frames share one group (chat, telemetry); a group of kilobyte video + frames is dominated by payload. +- `GroupState::release` (on abort, too-large, or a dropped producer) calls + `frames.clear()`, which keeps the capacity, then zeroes `cache` and clears + the charge. A consumer still holding the group pins those slots uncharged. + Dropping the deque's storage on release is likely enough here. + +The fix is not a bigger constant. The charge has to follow the deque's +capacity: charge the growth at each `charge.add` site (`write_frame`, the +`write_frames` loop, `create_frame`, and `create_frame_owned`), keeping +`FRAME_SLOTS` as the part `CACHE_OVERHEAD` already paid. + +Decide what `MAX_CACHE_BYTES` compares against before touching any of it. +`GroupState::would_overflow` reads `cache` as a payload limit, and the tests +and its doc ("maximum total size of frames") read it that way, so folding +slots into `cache` silently changes the per-group ceiling. Either keep a +separate payload counter for that check or restate the limit; do not let one +field mean both. + +`rs/moq-net/tests/group_charge.rs` weighs the process with a counting +allocator and is the place to prove it: add a many-small-frames shape beside +the one-frame-per-group case, and a unit test that crosses a deque growth +boundary and asserts the pool charge moved. + +Found by CodeRabbit on #3523, which fixed the one-frame-per-group undercount +that was OOM-killing relays serving chat, and deliberately left out of it. + +Public API: none, unless `MAX_CACHE_BYTES` is restated. Wire: none. + +## Related + +- [Relay memory](/quest/m1/relay-memory.md) - the per-announcement half of the same question, whose figures also predate the current accounting diff --git a/quest/m1/gateway-live-clock.md b/quest/m1/gateway-live-clock.md deleted file mode 100644 index ee39a7b1c4..0000000000 --- a/quest/m1/gateway-live-clock.md +++ /dev/null @@ -1,32 +0,0 @@ -# [S] Gateways publish on the broadcast clock - -## Goal - -`moq-srt`, `moq-rtmp`, and HLS import map source timestamps onto the -catalog's broadcast clock, as `moq import` does since -[#4122](https://github.com/moq-dev/moq/pull/4122). Today only the CLI calls -`.live()`; the gateways publish the encoder's timestamps verbatim, so the -advertised wall time is wrong for a source whose PTS does not start near zero, -and an encoder reconnect that restarts its timestamps rewinds or is refused. - -## Plan - -Decided: each gateway opts into `live()`. The importer default stays -verbatim, since tests and callers that pin `Config::with_clock` rely on it. - -Guidance: - -- SRT: `rs/moq-srt/src/ts.rs` builds the `ts::Import`. RTMP: the server's - publish path and the pull path in `dial.rs` build `FlvImport`. -- HLS import (`rs/moq-hls/src/import.rs`) runs one fMP4 importer per - rendition, and `live()` gives each its own anchor, so renditions would map - their first frames to different instants. They share one source clock and - need one mapping: share the `Anchor` across the broadcast's importers, or - pick another way to anchor the playlist once. Audio and video in separate - HLS renditions are the case to test. -- A reconnect that reuses the broadcast is where this pays off; check each - gateway's reconnect keeps the same importer (and anchor) or deliberately - starts a new one. -- Tests per gateway: a source starting at a large PTS publishes near the - broadcast clock's now, and a restart to zero continues forward. -- Docs: the gateway pages under `doc/bin/` that describe timestamps. diff --git a/quest/m1/gpu-ci.md b/quest/m1/gpu-ci.md index 0c0a8a054e..64bacaeeec 100644 --- a/quest/m1/gpu-ci.md +++ b/quest/m1/gpu-ci.md @@ -26,16 +26,19 @@ skipping inside the Nix shell. tests (Android, D3D11, PipeWire). Inside that selection a missing GPU fails the test instead of returning early. Keep the no-driver tests (`missing_driver_errors_instead_of_panicking`) outside it. -- `just rs nvidia`: symlink only those three libraries (by soname) from - `/usr/lib/x86_64-linux-gnu` into a private directory, put that on - `LD_LIBRARY_PATH`, and run that selection. Fail when a library is missing - instead of skipping. `just rs vulkan-cuda` puts the whole host directory on - the path, which lets host libraries shadow the Nix ones; fold it into this - recipe, since its `vulkan_cuda_` tests are the same kind. Those also need +- `just rs nvidia`, a one-line recipe over `sh/rs/nvidia.sh` in the tooling + line's `sh//` layout: symlink only those three libraries (by + soname) from `/usr/lib/x86_64-linux-gnu` into a private directory, put that + on `LD_LIBRARY_PATH`, and run that selection. Fail when a library is missing + instead of skipping. `just rs vulkan-cuda` (`sh/rs/vulkan-cuda.sh`) puts the + whole host driver directory on the path, which lets host libraries shadow + the Nix ones; fold it into this script and recipe, since its `vulkan_cuda_` + tests are the same kind. Those also need the Vulkan loader to find the host NVIDIA ICD: point it at the ICD manifest and expose the driver libraries it names, or keep them in their own recipe. -- Nightly: a job in `.github/workflows/nightly.yml` runs `just rs nvidia` on - the self-hosted runner. A self-hosted runner on a public repository must +- Nightly: a job in `.github/workflows/nightly.yml` runs `nix develop + --command just rs nvidia` on the self-hosted runner, a recipe and not a + script path, like every other workflow step. A self-hosted runner on a public repository must never run untrusted code: only `schedule` and `workflow_dispatch`, with the job gated to `refs/heads/main`, never `pull_request`; a dedicated label only this job selects; read-only `permissions`. Read GitHub's self-hosted runner @@ -49,6 +52,7 @@ Public API: none. Wire: none. ## Required +- [Tooling](/quest/m1/tooling/README.md) - the `sh/` script layout and recipe-only workflows this follows - A self-hosted runner is registered for moq-dev/moq on the maintainer's host, with the NVIDIA driver ## Related diff --git a/quest/m1/gpu-pool-reservation.md b/quest/m1/gpu-pool-reservation.md deleted file mode 100644 index d0fad928ec..0000000000 --- a/quest/m1/gpu-pool-reservation.md +++ /dev/null @@ -1,33 +0,0 @@ -# [S] GPU frame pool back-pressure is a reservation, not an error - -## Goal - -A caller feeding imported Vulkan frames through `moq_video::frame::cuda::Converter` -can drop a frame when the bounded GPU pool is full without matching an error. -Exhaustion is expected back-pressure; only real failures are errors. - -## Plan - -`Converter::convert` and `cuda::Frame::resize` take a pooled buffer and report -a full pool as `Error::Unsupported` with a prose message (#3869), so the CARLA -bridge in moq.pro can only drop-and-continue by matching the text. - -Split the reservation from the work, the way the bandwidth allocator hands out -a `Reservation`: `Converter::reserve() -> Option` returns `None` when -every buffer is live, and `Slot::convert(&vulkan::Frame) -> Result` -does the GPU work on the held buffer, failing only for a genuine error. The -slot returns its buffer to the pool on drop, converted or not. Apply the same -shape to the resize pool. Keep the pool itself crate-private and its capacity -bound unchanged. - -Tests on the injected allocator: `reserve` yields exactly `capacity` slots and -then `None`, dropping an unconverted slot frees it, and a failed conversion -does not leak the buffer. Update the `just rs vulkan-cuda` hardware test and -`doc/lib/rs/moq-video.md` inline. - -Public API: `moq-video` 0.1.x, breaking for `Converter::convert` callers, so -it targets dev. Wire: none. - -## Related - -- [Bandwidth allocator](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md) - the reservation-handle precedent diff --git a/quest/m1/ietf-announce-count.md b/quest/m1/ietf-announce-count.md deleted file mode 100644 index 5c5b92695f..0000000000 --- a/quest/m1/ietf-announce-count.md +++ /dev/null @@ -1,24 +0,0 @@ -# [M] An IETF namespace subscription says how many routes it replays - -## Goal - -A moq-transport session that negotiates a new extension learns the size of -the initial set a SUBSCRIBE_NAMESPACE replays, the way `AnnounceOk.active` -tells a moq-lite session, so the announce `Live` marker fires on the count -instead of the settle timer. Without the extension the timer stays. - -## Plan - -- Specify the extension in a `drafts/` document (a new one, or an existing - lcurley extension draft if one fits), with its setup parameter and where - the count rides, and validate with `just drafts check`. -- Implement it in `rs/moq-net`'s IETF publisher and subscriber; mirror in - `js/net` if its IETF path carries announces. -- Test: a negotiated session clears on the count with N routes and with none; - an un-negotiated one still clears on the timer. - -Public API: none. Wire: a new opt-in extension. - -## Required - -- [Caught up](/quest/m1/cli-inspect/caught-up.md) - the per-source clear this feeds diff --git a/quest/m1/ietf-max-age.md b/quest/m1/ietf-max-age.md deleted file mode 100644 index 04c6f11ce2..0000000000 --- a/quest/m1/ietf-max-age.md +++ /dev/null @@ -1,47 +0,0 @@ -# [M] Max age is optional and travels as MAX_CACHE_DURATION - -## Goal - -A track's max age is set only by its publisher. `None` means the publisher set -no limit, and the value crosses moq-transport as MAX_CACHE_DURATION, so it -survives an IETF hop the way it already survives moq-lite 05+. -`origin::Config::default_max_age` goes away. - -## Plan - -- `track::Info::max_age` becomes `Option`, defaulting to `None`. - `None` retains groups until the cache pool or `origin::Config::cache_duration` - evicts them. `Some(0)` keeps only the live edge. Mirror this in JS - (`maxAge` optional, drop `DEFAULT_MAX_AGE_MS`). -- Remove `track::DEFAULT_MAX_AGE` and `origin::Config::default_max_age`. A - track whose wire carries no max age gets `None`, never a local fallback. - Audit the callers that rely on the 5s default (moq-rtmp, moq-gst, moq-mux, - hang, JS lite tests) and give each an explicit value wherever it matters. - HLS/DASH egress asks for its window from the publisher; the old 5s was too - short for it anyway (`moq import` already sends 30s). -- IETF: MAX_CACHE_DURATION (0x04) is milliseconds, a message parameter through - draft 15 and a track property from draft 16. The subscriber reads it on - every draft, and absent means `None`, which is what the drafts mean by - omission. The publisher sends `Some(n)` as n and omits it for `None`, but - only on draft 17+: older moq-net peers reject 0x04 on draft 15 and trailing - properties on draft 16, so those drafts stay receive-only. Today Rust drops the property and JS - parses but never uses it. -- MAX_CACHE_DURATION is wall-clock and max age is media time with the newest - group always kept, so the mapping is approximate. Accept that rather than - modeling a second clock. -- moq-lite-07 (still WIP, off by default) makes TRACK_INFO's Max Age - optional: the value plus one, with 0 meaning none. Lite05/06 have no - absent value, so `None` is a sentinel both languages can represent: send - 2^53-1 (`Number.MAX_SAFE_INTEGER`) and read any value at or above it as - `None`. Decided over the largest varint (2^62-1) because JS reads Max Age - as a u53, so that sentinel would not survive a JS hop - ([#4188](https://github.com/moq-dev/moq/pull/4188)). Update - `drafts/draft-lcurley-moq-lite.md` in the same PR. -- EXPIRES stays 0 on send and ignored on receive. It is subscription lifetime, - not retention. -- Breaks the published `track::Info` and `origin::Config`, so the PR retargets - to `dev`. Update `doc/concept/moq-lite.md` and the affected rustdoc and JS - docs in the same PR. -- Test: Rust-to-Rust and Rust-to-JS sessions over IETF and lite-07 carry - `None`, `Some(0)`, and a non-zero max age end to end, including through a - relay hop, and drafts 15 and 16 decode but never send it. Run `just test interop --all`. diff --git a/quest/m1/ietf-uni-stream-types.md b/quest/m1/ietf-uni-stream-types.md index 9efb7e0ed5..6b0ca089c9 100644 --- a/quest/m1/ietf-uni-stream-types.md +++ b/quest/m1/ietf-uni-stream-types.md @@ -8,19 +8,17 @@ so it ships on main. ## Plan -`rs/moq-net/src/ietf/session.rs` routes every non-SETUP uni stream to -`run_uni_group` (`:708`), which rejects padding and unknown types alike while +`run_unis` in `rs/moq-net/src/ietf/session.rs` routes every non-SETUP uni +stream to `run_uni_group`, which rejects padding and unknown types alike while leaving the session alive. That stream-only rejection reaches the wire as -INTERNAL_ERROR on both branches, because nothing registers a code for it: on -dev the handler maps a session-scoped error to `StreamError::Internal` and -aborts the reader (`:697-704`); on main it is `reader.stop(to_stream_code(&err))` -(`:583` there), which falls through to INTERNAL_ERROR the same way. +INTERNAL_ERROR, because nothing registers a code for it: the handler maps a +session-scoped error to `StreamError::Internal` and aborts the reader. draft-21 settles what each stream type means: a stream whose type the endpoint does not recognize MUST close the session, and a padding stream (type 0x132B3E28) MUST be discarded, which an endpoint may do by cancelling -it. The tree negotiates up to draft-20 (`rs/moq-net/src/ietf/version.rs:12`), -so apply that split to every supported draft and check the earlier ones for +it. The tree negotiates drafts 14 through 22 (`ietf::Version` in +`rs/moq-net/src/ietf/version.rs`), so apply that split to every supported draft and check the earlier ones for the padding type value and whether draining is required. - Classify stream types before spawning a group handler. Handle PADDING per @@ -28,15 +26,16 @@ the padding type value and whether draining is required. stream-only code, and propagate a genuinely unknown type to the session driver as a protocol violation. Keep ordinary group failures scoped to their streams. -- The test `unknown_uni_type_does_not_claim_the_session_closed` - (`session.rs:1267`) asserts the current behavior, an INTERNAL_ERROR stop and +- The test `unknown_uni_type_does_not_claim_the_session_closed` in + `session.rs` asserts the current behavior, an INTERNAL_ERROR stop and no session close, and flips: an unknown type now closes the session and stops nothing on its own. - Add a padding test asserting a stream-only cancel with no session close, and - keep `a_group_for_a_retired_alias_is_stopped_with_cancelled` (`:1255`), which + keep `a_group_for_a_retired_alias_is_stopped_with_cancelled`, which pins that a dropped group never closes the session. Consult [draft-21 section 11.5](https://www.ietf.org/archive/id/draft-ietf-moq-transport-21.html) for the wording, and [draft-19 section 3.4 and section 11.5.1](https://www.ietf.org/archive/id/draft-ietf-moq-transport-19.html) plus [draft-20 section 11.5.1](https://www.ietf.org/archive/id/draft-ietf-moq-transport-20.html) -for the drafts the tree negotiates. +and [draft-22](https://www.ietf.org/archive/id/draft-ietf-moq-transport-22.html) +for the other drafts the tree negotiates. diff --git a/quest/m1/iroh-lite-wip.md b/quest/m1/iroh-lite-wip.md index e733761633..f6be7e23dc 100644 --- a/quest/m1/iroh-lite-wip.md +++ b/quest/m1/iroh-lite-wip.md @@ -16,9 +16,9 @@ constant rather than the configured `Versions` (`versions.alpns()`, which config and then ignored on iroh. Thread the configured versions through instead. -The relay's WebSocket listener and `moq-ffi`'s transport also read -`moq_net::ALPNS` directly. Check whether they have the same hole; fix them -here if it is the same small change, otherwise report it. +`moq-ffi`'s transport (`rs/moq-ffi/src/transport.rs`) also offers +`moq_net::ALPNS` directly; fix it here too. The relay's WebSocket listener +already honors the configured versions. Scope is those two files. Decided by the maintainer: the finalized lite-07 ALPN stays `moq-lite-07`, as `drafts/draft-lcurley-moq-lite.md` already says. Peers from the yanked diff --git a/quest/m1/js-announce-caught-up.md b/quest/m1/js-announce-caught-up.md deleted file mode 100644 index c568f1fdbb..0000000000 --- a/quest/m1/js-announce-caught-up.md +++ /dev/null @@ -1,20 +0,0 @@ -# [S] @moq/net announce consumers know when they have caught up - -## Goal - -`@moq/net`'s announce consumer yields the same `Live` marker the Rust -consumer gets from [Caught up](/quest/m1/cli-inspect/caught-up.md), with -the same ordering and per-source semantics, so a browser app can render "no -broadcasts" instead of a spinner that never resolves. - -## Plan - -Mirror the Rust shape, name, and marker-less fallback: one flat event, -`Announced`, `Updated`, `Retracted` (each carrying the announce), or `Live`, -replacing the update's `kind` field. Test the same cases -in JS. Public API: the announce consumer's yield type changes, so it lands -with the Rust break. Wire: none. - -## Required - -- [Caught up](/quest/m1/cli-inspect/caught-up.md) - settles the API shape this mirrors diff --git a/quest/m1/js-ietf-datagram.md b/quest/m1/js-ietf-datagram.md index 417609fe0a..db210e7b25 100644 --- a/quest/m1/js-ietf-datagram.md +++ b/quest/m1/js-ietf-datagram.md @@ -26,4 +26,4 @@ accept `OBJECT_DATAGRAM` as the drafts define; no project draft changes. ## Required -- [Datagram groups](/quest/m1/moxygen/datagram.md) - the Rust side this mirrors and interops with +- [moxygen interop](/quest/m1/moxygen/README.md) - its datagram groups quest (done on the line) is the Rust side this mirrors and interops with diff --git a/quest/m1/js-subscribe-abandonment.md b/quest/m1/js-subscribe-abandonment.md index df3ed49137..771a8da131 100644 --- a/quest/m1/js-subscribe-abandonment.md +++ b/quest/m1/js-subscribe-abandonment.md @@ -9,13 +9,15 @@ of receiving an abandonment error. The setup path is the same on main, so the fix lands there. -- `waitAbandoned` (`js/net/src/ietf/subscriber.ts:423-428`) checks demand and - resolves through `Promise.race`; another microtask can attach a viewer before - the catch closes the producer at `:449`. Reproduce that ordering in - `js/net/src/ietf/subscriber.test.ts`: the case at `:673` covers only the - established serving loop. +- `waitAbandoned` (in `#runSubscribe`, `js/net/src/ietf/subscriber.ts`) + checks demand and resolves through `race`; another microtask can attach a + viewer before the catch awaits `sessionCause` and rejects the request. + Reproduce that ordering in `js/net/src/ietf/subscriber.test.ts`: "returning + demand survives a blocked unsubscribe" covers only the established serving + loop. - Recheck demand and commit the close in the same synchronous continuation, - the way the serving loop does (`:511-523`). When demand returns, keep the + the way the serving loop after SUBSCRIBE_OK does (its `producer.used` + re-check before `producer.close()`). When demand returns, keep the existing setup operation and timeout budget. - Cover abandonment before SUBSCRIBE_OK, demand returning before the commit, and late setup completion. Verify cancellation and alias cleanup still happen diff --git a/quest/m1/keyframe-trigger.md b/quest/m1/keyframe-trigger.md deleted file mode 100644 index 67e430de51..0000000000 --- a/quest/m1/keyframe-trigger.md +++ /dev/null @@ -1,37 +0,0 @@ -# [M] On-demand keyframe trigger - -## Goal - -An application publishing through the built-in capture path can ask for a -keyframe. `Encoder::cut()`, `Sink::cut()`, and the ffi/libmoq `cut` already -force one, refusing with `CutUnsupported` when a backend cannot, but the -turnkey capture paths have no way in. - -## Plan - -`Backend::encode(frame, cut)` honors a cut (NVENC via the `FORCEIDR` picture -flag with `repeatSPSPPS` so the IDR carries its parameter sets, deliberately -not `pictureType` which `enablePTD` ignores; openh264, VAAPI, VideoToolbox and -Media Foundation the same way) and `can_cut()` answers at open whether it can. -What is missing is a caller-facing trigger on the turnkey paths: - -- `publish_capture` relies on every backend opening with a keyframe and - otherwise rides the GOP cadence; its `Options` carry no trigger. -- `js/publish`'s encode path already calls `encoder.encode(frame, { keyFrame })`, - but `lastKeyframe` is a closure-local `let` with no external trigger. - `Config.keyframeInterval` is cadence, not on demand. - -Give both a trigger the caller owns: a handle on the Rust capture path, and a -Signal the JS encode effect reads instead of its local variable. Coalesce -requests, so several arriving within one frame interval produce one IDR rather -than a run of them, and rate limit at the publisher so a caller in a loop -cannot pin the encoder at all-IDR. - -Additive on both sides, so it lands on `main`. Real callers exist regardless -of whether a wire-level request ever ships: a resume, a recording cut, a -rendition switch, and an application that knows its own tune-in moment. - -## Related - -- [GOP overhead](/quest/m2/gop-overhead.md) - whether a long GOP driven by a - keyframe request is worth designing at all diff --git a/quest/m1/kt-jvm-exit.md b/quest/m1/kt-jvm-exit.md index 62f5681d8f..71fbe5e115 100644 --- a/quest/m1/kt-jvm-exit.md +++ b/quest/m1/kt-jvm-exit.md @@ -34,7 +34,3 @@ out of scope: it kills the process rather than destroying the VM. exit code. Run it in the existing `kt` CI job. Public API: none; the hook is internal to the binding. Wire: none. - -## Related - -- [libmoq shutdown](/quest/m3/libmoq-shutdown.md) - the same hazard class for the C ABI and the OBS plugin diff --git a/quest/m1/ladder/README.md b/quest/m1/ladder/README.md index 79943bff1e..0c242cc396 100644 --- a/quest/m1/ladder/README.md +++ b/quest/m1/ladder/README.md @@ -27,9 +27,13 @@ What remains is the publisher side. The allocator estimate by `track::Info::priority`, filling a tier before the next sees a bit and splitting max-min fair within one. A controller that assigns descending priorities down the ladder gets correct allocation from that -alone. Send order is the same number, not a separate question: -`Priority::cmp` (`rs/moq-net/src/lite/priority.rs`) ranks by track priority -first, so the same number decides what to produce and what to send first; +alone. Send order is a different number today: `Priority::cmp` +(`rs/moq-net/src/lite/priority.rs`) ranks first by `Priority.track`, which is +the subscriber's priority from SUBSCRIBE (`msg.priority` in +`rs/moq-net/src/lite/publisher.rs`), not the publisher's `Info::priority`. +The controller quest makes the publisher's number the tiebreak after it, so +the same number decides what to produce and, among equal subscriber +priorities, what to send first; [Scope track priority](/quest/m1/track-priority-scope.md) settles what that ranking means on the first mile versus a cluster session before the controller depends on it. diff --git a/quest/m1/lite-count-settle.md b/quest/m1/lite-count-settle.md deleted file mode 100644 index 2cb41dcf52..0000000000 --- a/quest/m1/lite-count-settle.md +++ /dev/null @@ -1,28 +0,0 @@ -# [S] lite-07 subscribers settle on the stream count - -## Goal - -On moq-lite-07, Rust and JS subscribers stop waiting for a subscription's tail -once they have read the headers of as many group streams as SUBSCRIBE_END -counts, so a group the publisher skipped or never opened costs no grace. A -late stream below the end is still accepted within the grace, which stays for -a stream reset before its header arrived. lite-05 and -06 keep the DROP -accounting JS already has and Rust track tail adds. - -## Plan - -- Rust and JS subscribers now settle on the received header count for lite-07. - lite-05 and -06 keep their range and DROP accounting. The grace still covers - counted streams reset before their headers arrive. -- Local Rust and JS regressions cover late streams, missing/reset streams, - skipped sequences, and zero streams. JS also verifies a reset after its header - and groups still being read across the subscription's FIN. -- Remaining: add the count-specific Rust-JS case to track-tail interop. That - harness currently exposes a relay start-floor defect: when a newer group - arrives first, earlier in-flight groups can be lost. Finish the count proof - once that defect is resolved; keep this quest and its PR open until then. - -## Related - -- [Track tail interop](/quest/m1/track-tail-interop.md) - the Rust-JS case this adds its count check to -- [Reliable stream reset](/quest/m1/quic/reliable-reset.md) - makes the count exact by keeping a reset stream's header diff --git a/quest/m1/merge-queue.md b/quest/m1/merge-queue.md index 002b723959..d84f2cee10 100644 --- a/quest/m1/merge-queue.md +++ b/quest/m1/merge-queue.md @@ -17,9 +17,10 @@ and there is no merge queue, so nothing re-runs them on the combined tree. queue tests the combination once, at merge time. - Make the workflows ready: every workflow providing a required check (today `Check` and `Test` in `.github/workflows/check.yml`) also runs on - `merge_group`. `just ci` scopes by diffing against `GITHUB_BASE_REF`, - which a merge group does not set, so pass the group's base - (`github.event.merge_group.base_sha`) explicitly. Check the concurrency + `merge_group`. `just ci check|test` runs `sh/dispatch.sh`, which scopes + by diffing against its `BASE` argument, else `origin/$GITHUB_BASE_REF`. A + merge group sets no `GITHUB_BASE_REF`, so pass the group's base + (`github.event.merge_group.base_sha`) as `BASE`. Check the concurrency group and the `closed`-only skip still behave for queue refs. - Document it in `CONTRIBUTING.md`: PRs merge through the queue, a dequeued PR means the combination failed, and how agents enqueue (the @@ -29,3 +30,7 @@ and there is no merge queue, so nothing re-runs them on the combined tree. settings to use rather than changing it. Public API: none. Wire: none. + +## Required + +- [Tooling](/quest/m1/tooling/README.md) - `just ci` and `sh/dispatch.sh`, the entry point the queue runs diff --git a/quest/m1/moq-c.md b/quest/m1/moq-c.md deleted file mode 100644 index ba18262125..0000000000 --- a/quest/m1/moq-c.md +++ /dev/null @@ -1,33 +0,0 @@ -# [M] libmoq becomes moq-c - -## Goal - -The C bindings ship as `moq-c`, matching `moq-cpp` and the other bindings: the -crate, its directory, release tags, CMake package, and pkg-config file all use -the name. C code and link lines do not change: the header stays `moq.h` and the -library file keeps its current name. The published `libmoq` crate gets one last -release that points users at `moq-c`. - -## Plan - -- Rename the crate `libmoq` to `moq-c` (`rs/libmoq` to `rs/moq-c`), its release - tags from `libmoq-v*` to `moq-c-v*`, and its workflow. The CMake package - becomes `find_package(moq-c)` and the pkg-config file `moq-c.pc`. The C++ - package is already `moq-cpp` (#4187), so the two install side by side. -- Keep the `[lib] name` so the library file and `moq.h` are unchanged. -- A published package rename is a break, so this lands on `dev`. Release - tooling (release-plz, alert and nightly workflows, cachix) must follow the - new name and tag; check every workflow that names libmoq. -- Update every consumer and reference: `cpp/obs` (`find_package`), interop - clients, `doc/lib/c`, `doc/bin/obs.md`, the root `AGENTS.md` Cross-Package - Sync table, and quests that name libmoq. Grep the whole repository. -- Publish a final `libmoq` release whose README and description point at - `moq-c`, then stop publishing it. The maintainer cuts releases; the PR only - prepares it. - -Public API: the C ABI is unchanged; the crate, package, and tag names change. -Wire: none. - -## Related - -- [C++ through moq-ffi](/quest/m1/cpp/README.md) - the `moq-cpp` package this name mirrors diff --git a/quest/m1/mux-wasm-target.md b/quest/m1/mux-wasm-target.md deleted file mode 100644 index 17fc354710..0000000000 --- a/quest/m1/mux-wasm-target.md +++ /dev/null @@ -1,26 +0,0 @@ -# [S] moq-mux compiles for wasm32 - -## Goal - -`cargo check -p moq-mux --target wasm32-unknown-unknown` passes. Two things -in the crate compile natively only through workspace feature unification and -break on the wasm target, which is what stops anything above `moq-net` from -reaching the browser through Rust. - -## Plan - -Found by the #2907 spike; both are latent bugs independent of any browser -work. - -- `tokio::time::Instant` in `rs/moq-mux/src/codec/{av1,h264,h265}/split.rs`: - `moq-mux` declares tokio with only the `macros` feature, so this compiles - natively only because another crate turns the runtime on. Use - `web_async::time`, the migration `moq-net` already made. -- `pub trait Stream: Send + 'static` in `rs/moq-mux/src/catalog/stream.rs`: - moq-net's wasm stats types are `Rc>`, so the supertrait cannot - be satisfied. Apply the `MaybeSend` treatment `moq-net` has in - `src/util.rs`. -- Add the crate to the wasm clippy lane in `rs/justfile` beside `moq-wasm` - and `moq-net`, so it stays green. - -Public API: none. Wire: none. diff --git a/quest/m1/obs-moq-video/README.md b/quest/m1/obs-moq-video/README.md index cdc4cdcbe4..1e41ad84c8 100644 --- a/quest/m1/obs-moq-video/README.md +++ b/quest/m1/obs-moq-video/README.md @@ -2,11 +2,11 @@ ## Goal -Remove the MoQ OBS plugin's dependency on OBS/system FFmpeg ABI versions by decoding subscribed video with moq-video. Add audio playback and publishing with moq-audio, and opt-in video publishing with moq-video. OBS retains scene composition, audio mixing, and output timing. This integration serves MoQ publishing and playback, not general OBS recording or other streaming outputs. +Remove the MoQ OBS plugin's dependency on OBS/system FFmpeg ABI versions by decoding subscribed video with moq-video and subscribed audio with moq-audio. Add audio publishing with moq-audio, and opt-in video publishing with moq-video. OBS retains scene composition, audio mixing, and output timing. This integration serves MoQ publishing and playback, not general OBS recording or other streaming outputs. ## Plan -Portability and FFmpeg removal lead. The current MoQ source uses libavcodec, libavutil, and libswscale for video; it has no audio playback. Its swresample linkage is unused. Video replacement can therefore remove direct FFmpeg dependencies without waiting for audio. The plugin reaches codecs through the generated C++ package over moq-ffi (the migration quest linked below lands first); build `libmoq_ffi` statically with only the codec features needed here; OS frameworks and runtime GPU drivers remain valid dependencies. Verify plugin imports instead of promising a completely static OBS plugin. +Portability and FFmpeg removal lead. The current MoQ source decodes video with libavcodec, libavutil, and libswscale, and audio with libavcodec (`moq_source_decode_audio_frame` in `cpp/obs/src/moq-source.cpp`). Its swresample linkage is unused. FFmpeg linkage goes away only once both the video source replacement and the audio decode replacement land. The stranded audio branch (#3498, merged only into `codex/obs-audio-receive-base`) is abandoned; the audio playback quest replans it on the generated C++. The plugin reaches codecs through the generated C++ package over moq-ffi (the migration quest linked below lands first); build `libmoq_ffi` statically with only the codec features needed here; OS frameworks and runtime GPU drivers remain valid dependencies. Verify plugin imports instead of promising a completely static OBS plugin. Attempt GPU delivery immediately, starting on macOS. Windows and Linux can ship independently. Prefer direct surface reuse, allow GPU conversion/blits, and automatically fall back to CPU delivery when import is unavailable or fails. Stats must show the actual decoder/encoder, delivery path, and fallback reason. Retaining a texture handle is insufficient unless pool ownership and synchronization also prevent reuse while work is in flight. @@ -14,12 +14,12 @@ Initial video decoding covers H.264, HEVC, and AV1 where moq-video has an availa Publishing remains opt-in, with one **Use MoQ encoders** choice for video and audio. Keep the existing OBS encoder mode. Internal OBS encoder adapters call moq-video/moq-audio, preserving OBS's A/V handling and the existing encoded MoQ output. The combined choice is enabled only when both adapters are present. Start with H.264, supported HEVC, and Opus; defer AV1/AAC encoding and PCM publishing UI. Keep bitrate separate from **Low latency**, **Balanced** (default), and **Quality** presets. Presets describe supported buffering/compression controls, not an end-to-end delay promise. -The quests separate portable decoding, platform GPU delivery, audio, and publishing so each can land and be validated independently. The existing CPU decode path is a fallback primitive, not a GPU implementation: it explicitly converts every surface to I420. Native frame ownership must cross the FFI boundary without that conversion. +The quests separate portable decoding, platform GPU delivery, audio, and publishing so each can land and be validated independently. moq-ffi's decoded frames own their surface and convert to CPU pixels only on request (#4094, on `dev`); a native decode exposes the platform surface as a borrowed view, which each platform quest extends to its own surface type. ## Required -- [Video source replacement](/quest/m1/obs-moq-video/source.md) - remove FFmpeg and attempt macOS GPU delivery immediately, with a working CPU fallback on other platforms -- [Audio playback](/quest/m1/obs-moq-video/audio-playback.md) - add synchronized subscribed audio through moq-audio +- [Video source replacement](/quest/m1/obs-moq-video/source.md) - remove the FFmpeg video decode and attempt macOS GPU delivery immediately, with a working CPU fallback on other platforms +- [Audio playback](/quest/m1/obs-moq-video/audio-playback.md) - replace the FFmpeg audio decode with moq-audio - [Windows decoded frames](/quest/m1/obs-moq-video/decode-windows.md) - present decoded D3D11 surfaces in OBS without CPU readback - [Linux decoded frames](/quest/m1/obs-moq-video/decode-linux.md) - present supported native decoded surfaces with visible CPU fallback - [Linux bundle](/quest/m1/obs-moq-video/linux-bundle.md) - attach a portable Linux x86_64 tarball to every obs-moq release once FFmpeg is gone diff --git a/quest/m1/obs-moq-video/audio-playback.md b/quest/m1/obs-moq-video/audio-playback.md index d7510a8cef..d7da76a3f4 100644 --- a/quest/m1/obs-moq-video/audio-playback.md +++ b/quest/m1/obs-moq-video/audio-playback.md @@ -1,15 +1,20 @@ -# [L] Add moq-audio playback to the OBS source +# [L] Replace the OBS source's FFmpeg audio decode with moq-audio ## Goal -Subscribed MoQ audio plays through OBS alongside video using statically linked moq-audio codec code, including Opus, AAC-LC, and PCM. Audio playback can ship independently of video replacement and publishing. +The MoQ source decodes subscribed audio with statically linked moq-audio instead of libavcodec, covering Opus, AAC-LC, and PCM, and keeps its multichannel speaker placement. Audio can ship independently of video replacement and publishing. ## Plan -- The current source is video-only. Reuse the moq-ffi audio consumer (`subscribe_audio`, a `next()` future per frame, cancel on drop), then feed converted PCM to `obs_source_output_audio`. Keep device playback inside OBS; do not enable moq-audio device capture/playback features or open a second audio device. -- Map catalog tracks, sample rates, channel layouts, and timestamps explicitly. Support mono/stereo initially, with explicit rejection or a tested OBS conversion for other layouts. Share the source's media timebase with video, preserve reconnect and rendition changes, and bound audio buffering. `latency_max_ms` controls stalled-group skipping, not desired A/V playout delay. +- Today the source already plays audio: `moq_source_subscribe_audio` in `cpp/obs/src/moq-source.cpp` receives encoded frames through libmoq's `moq_consume_audio`, and `moq_source_decode_audio_frame` decodes them with libavcodec before `obs_source_output_audio`. Replace that decode, not the playback path. +- Decode through the generated C++ package with `MoqBroadcastConsumer::decode_audio` (a `MoqAudioConsumer` with a `next()` future per frame, cancel on drop), then feed its PCM to `obs_source_output_audio`. `decode_audio` accepts Opus and AAC-LC today; add PCM there rather than in the plugin. Keep device playback inside OBS; do not enable moq-audio device capture/playback features or open a second audio device. +- Map catalog tracks, sample rates, channel layouts, and timestamps explicitly. Channel layouts: map each channel count to the default layout moq-audio uses, the WAVE convention (3 is 2.1, 4 is quad, 6 is 5.1, 8 is 7.1), picking the nearest OBS `speaker_layout`, and refuse counts OBS cannot place rather than guess. This replaces `audio_layout_to_speakers`, which maps from FFmpeg layouts, and absorbs the former m2 obs-wave-layout quest. Note the mapping in `doc/bin/obs.md`. +- Share the source's media timebase with video, preserve reconnect and rendition changes, and bound audio buffering. `latency_max_ms` controls stalled-group skipping, not desired A/V playout delay. - Preserve OBS mixer, monitoring, mute, and volume behavior. Release every frame on output, conversion failure, stop, and late completion. Do not hold source state locks across callbacks into OBS. -- Verify audible output and recorded PCM, A/V synchronization with timestamped test media, rate changes, silence, stalls, reconnect, source replacement, and teardown. Exercise Opus, AAC-LC and PCM fixtures, not just callback counts. Update source documentation and Stats. +- Remove the audio FFmpeg includes and codec mapping. Whichever of this and the video source replacement lands second removes the remaining FFmpeg CMake linkage. +- Verify audible output and recorded PCM, A/V synchronization with timestamped test media, rate changes, silence, stalls, reconnect, source replacement, teardown, and a 5.1 fixture placed on the right speakers. Exercise Opus, AAC-LC and PCM fixtures, not just callback counts. Update source documentation and Stats. + +Decided: the earlier receive branch (#3498, merged only into the stranded `codex/obs-audio-receive-base`) is abandoned rather than rebased; this quest starts from the plugin on the generated C++. ## Required @@ -18,3 +23,4 @@ Subscribed MoQ audio plays through OBS alongside video using statically linked m ## Related - [Video source replacement](/quest/m1/obs-moq-video/source.md) - coordinate the shared timestamp and source lifecycle without blocking audio rollout +- [Channel layouts](/quest/m1/audio-codecs/layout.md) - the moq-audio layouts this mapping mirrors diff --git a/quest/m1/obs-moq-video/linux-bundle.md b/quest/m1/obs-moq-video/linux-bundle.md index fb55f31b51..8a10983f3a 100644 --- a/quest/m1/obs-moq-video/linux-bundle.md +++ b/quest/m1/obs-moq-video/linux-bundle.md @@ -6,15 +6,16 @@ Every `obs-moq-v*` release attaches a Linux x86_64 tarball that loads into a sto ## Plan -- The only reason `obs-build` in `.github/workflows/libmoq.yml` skips Linux is FFmpeg: the source links nix/distro libavcodec, which is not portable. The FFmpeg removal is the blocker; once the plugin is C++ over libmoq plus libobs and Qt6, a Linux build has no extra runtime dependency that OBS itself does not already carry. +- The only reason `obs-build` in `.github/workflows/libmoq.yml` skips Linux is FFmpeg: the source links nix/distro libavcodec for both video and audio, which is not portable. The FFmpeg removal (video source replacement plus audio playback) is the blocker; once the plugin is C++ over moq-ffi plus libobs and Qt6, a Linux build has no extra runtime dependency that OBS itself does not already carry. - Build on `ubuntu-24.04` (glibc 2.39, the floor OBS's own Linux packages target) against the libobs and Qt6 headers OBS's plugin template uses; the template's `.deb` recipe is the reference. Ship the plain archive layout the other platforms use (`obs-moq-*-x86_64-unknown-linux-gnu.tar.gz` with `bin/64bit/obs-moq.so` and `data/`), extractable into `~/.config/obs-studio/plugins/obs-moq/`. A `.deb` is optional and separate. - Flatpak OBS cannot load a plugin from the host filesystem. Verify a real Flatpak install and support it only if the sandbox's runtime ABI matches, documenting the `~/.var/app/com.obsproject.Studio/config/obs-studio/plugins/` path. Otherwise, state plainly that Flatpak is unsupported. -- `cpp/obs/build.sh --target x86_64-unknown-linux-gnu` produces the tarball, and the matrix in `obs-build` gains the row; nothing else in the release pipeline changes. Keep the nightly `just obs ci` compile as the PR gate. +- `cpp/obs/build.sh --target x86_64-unknown-linux-gnu` produces the tarball, and the matrix in `obs-build` gains the row; nothing else in the release pipeline changes. Keep the nightly `just obs ci` compile as the PR gate; #4370 (open) compiles the OBS plugin on every PR, which covers the Linux compile if it lands first. - Verify by loading the tarball into the oldest supported OBS 32 release and current stable on Ubuntu 24.04 and one non-Debian distro (Fedora), publishing and subscribing against a relay, and inspecting the `.so` with `ldd` for nothing beyond libobs, Qt6, glibc, and OS libraries. ## Required -- [Video source replacement](/quest/m1/obs-moq-video/source.md) - removes the FFmpeg linkage that makes a Linux binary non-portable +- [Video source replacement](/quest/m1/obs-moq-video/source.md) - removes the FFmpeg video linkage that makes a Linux binary non-portable +- [Audio playback](/quest/m1/obs-moq-video/audio-playback.md) - removes the FFmpeg audio linkage ## Related diff --git a/quest/m1/obs-moq-video/rate-control.md b/quest/m1/obs-moq-video/rate-control.md index c4a8aeda4d..3f98e38e03 100644 --- a/quest/m1/obs-moq-video/rate-control.md +++ b/quest/m1/obs-moq-video/rate-control.md @@ -12,8 +12,9 @@ configured rate. ## Plan -Uses the reservation surface from libmoq (`moq_session_bandwidth`, -`moq_bandwidth_reserve`, `moq_reservation_grant`). Apply grants through +Uses the moq-ffi reservation surface through the generated C++ package +(`MoqSession::bandwidth`, `MoqBandwidth::reserve`, `MoqReservation::grant`), +not libmoq, since the plugin moves off libmoq first. Apply grants through the shape `moq_mux::rate::Control` uses (drops at once, raises ramp, hysteresis) rather than pushing every change into `obs_encoder_update`; whether that policy sits in moq-ffi behind the reservation or in the plugin depends on @@ -24,3 +25,7 @@ whether a second binding wants it. Verify against a shaped uplink and with ## Required - [OBS migration](/quest/m1/cpp/obs.md) - the plugin is on the generated C++ first + +## Related + +- [Audio follows the grant](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md) - the shared rate policy audio adopts; OBS audio still reserves only diff --git a/quest/m1/obs-moq-video/source.md b/quest/m1/obs-moq-video/source.md index 15d9450789..e0178523f9 100644 --- a/quest/m1/obs-moq-video/source.md +++ b/quest/m1/obs-moq-video/source.md @@ -1,24 +1,24 @@ -# [XL] Replace OBS source FFmpeg decoding with moq-video +# [XL] Replace OBS source FFmpeg video decoding with moq-video ## Goal -The MoQ source loads and plays supported video without directly linking FFmpeg libraries. Attempt macOS GPU delivery in the first implementation; automatically fall back to CPU delivery when native presentation is unavailable. Windows and Linux initially retain a portable CPU path. +The MoQ source loads and plays supported video without FFmpeg's video libraries (libswscale, and libavcodec/libavutil for video). Attempt macOS GPU delivery in the first implementation; automatically fall back to CPU delivery when native presentation is unavailable. Windows and Linux initially retain a portable CPU path. ## Plan - Replace `cpp/obs/src/moq-source.cpp` video decode and conversion with moq-video through the generated C++ package: the moq-ffi video consumer for the CPU path, including frame ownership and cancellation semantics. Support H.264, HEVC, and available AV1 decoding; report unsupported VP8/VP9 explicitly until their follow-up lands. -- Extend the owned frame abstraction to retain native surfaces across the FFI boundary. The current CPU consumer calls `into_i420()` unconditionally; a native path must avoid it. Prefer an options object and owned opaque frame handle with explicit release and optional CPU conversion over parallel platform-specific subscription APIs. Document device/thread affinity, borrowed views, synchronization, cancellation, and pool lifetime. Do not introduce caller cleanup callbacks. +- Decode with `MoqVideoDecoderOutput.native` set: each `MoqVideoDecodedFrame` retains the decoder's surface, `native()` borrows it (`PixelBuffer` on macOS) for as long as the frame lives, and `pixels(format)` is the CPU fallback. Hold the frame until OBS's GPU work reading it completes; held frames hold decoder pool slots. This surface landed on `dev` (#4094). - Implement the macOS presentation probe immediately: retain VideoToolbox PixelBuffer/IOSurface storage, inspect OBS graphics import and rendering support, and convert to OBS's expected color format on the GPU if necessary. Adapt the source render path to import textures on the graphics thread; preserve source timing instead of simply drawing the newest frame. An asynchronous CPU source API alone does not prove native GPU delivery. - Bound decoded frames retained by the render thread. On import failure, switch to the existing I420 delivery path and show the reason in Stats. Keep fallback stable for the stream/device configuration rather than retrying every frame; re-probe on a relevant configuration change or restart. Device loss and resize must retire old surfaces only after rendering completes. - Preserve timestamps, range/primaries, stride and plane layout, catalog/rendition changes, reconnect, visibility/deactivation behavior, and existing source settings. Carry frame-generation identity so late callbacks cannot display frames from a replaced source. -- Remove FFmpeg includes, CMake discovery/linkage, unit stubs, compile recipe requirements, and unused swresample linkage. Update OBS build/install docs and `doc/lib/cpp` together. libobs/Qt and native OS/GPU dependencies remain. -- Validate new code with decoded pixels and moving timestamps, GPU copy/readback traces, and p50/p95 decode-to-presentation delay. Exercise CPU fallback, unsupported codec, GPU import failure, device loss, repeated start/stop, rendition change, and delayed terminal completion. Verify no AVCodec/AVUtil/SWScale/SWResample imports using platform binary inspection. Load the artifact against the oldest supported OBS release and current stable release, using the repo's supported version policy at implementation time. +- Remove the video FFmpeg includes and swscale linkage. Audio still decodes through libavcodec until the audio playback quest replaces it, so whichever of the two lands second removes the remaining FFmpeg includes, CMake discovery/linkage, unit stubs, compile recipe requirements, and the unused swresample linkage. Update OBS build/install docs and `doc/lib/cpp` together. libobs/Qt and native OS/GPU dependencies remain. +- Validate new code with decoded pixels and moving timestamps, GPU copy/readback traces, and p50/p95 decode-to-presentation delay. Exercise CPU fallback, unsupported codec, GPU import failure, device loss, repeated start/stop, rendition change, and delayed terminal completion. Verify no SWScale imports (and no AVCodec/AVUtil/SWResample imports once audio playback has landed) using platform binary inspection. Load the artifact against the oldest supported OBS release and current stable release, using the repo's supported version policy at implementation time. ## Required - [OBS migration](/quest/m1/cpp/obs.md) - the plugin is on the generated C++ before decode changes -- [Decoded frame ownership](/quest/m1/decoded-frames.md) - the decoded-frame consumer moq-ffi lacks, retaining the existing frame and defining native-view lifetime before OBS imports it ## Related - [VP8/VP9 decoding](/quest/m1/obs-moq-video/vpx.md) - restores deferred codec coverage independently +- [Audio playback](/quest/m1/obs-moq-video/audio-playback.md) - removes the audio half of the FFmpeg linkage diff --git a/quest/m1/open-gop-leading-pictures.md b/quest/m1/open-gop-leading-pictures.md index 8c53a747a8..c1f84922ea 100644 --- a/quest/m1/open-gop-leading-pictures.md +++ b/quest/m1/open-gop-leading-pictures.md @@ -25,16 +25,23 @@ everyone. sample of a group to `keyframe`, and `js/watch/src/video/decoder.ts` submits it as `"key"`): for the first group after any non-continuous transition, skip delta frames stamped before that group's keyframe. That covers a - subscribe, a declared discontinuity, and a latency skip: `#checkLatency` + subscribe, a declared discontinuity, and a latency skip: `#checkMaxAge` records the skip through `#gap` and `next()` reports the next frame with `continuous: false`. Latency skip also bumps playhead generation (startup delay) but does not flush the decoder. Leading pictures after that non-continuous transition are still skipped, as above; a viewer that skipped into a later GOP lacks its references just like a cold join. Every continuous group is passed through untouched. -- The same rule in the Rust decode path (`moq-video` decode consumers), with - an equivalent non-continuous signal from `container::Consumer`, so native - playback and the transcoder tune in the same way. +- The same rule in the Rust decode path (`moq-video` decode consumers), so + native playback and the transcoder tune in the same way. +- This quest owns the Rust non-continuous signal, which audio warmup and + consumer warmup reuse rather than each adding one. Today + `moq_mux::container::Consumer::poll_read` returns a bare frame, and + `discontinuity()` is a counter bumped on a declared marker group, an + unproven delivered hole, or a latency skip, but not on the subscribe itself. + Add the equivalent of JS `continuous`: false on the first frame after the + subscribe and after every bump, true otherwise. It changes the moq-mux + consumer API, so pick main or dev by whether the shape is additive. - Tests: a synthetic group with a keyframe followed by two earlier-stamped deltas is trimmed on the first group and kept on the second; and a viewer that plays continuously, then latency-skips into a later open GOP, has that @@ -48,3 +55,4 @@ everyone. ## Related - [Consumer warmup](/quest/m2/intra-refresh/consumer-warmup.md) - the `recovery_frame_cnt > 0` case this rule does not cover +- [Audio warmup](/quest/m1/audio-warmup.md) - keys its Opus pre-roll trim on the same signal diff --git a/quest/m1/opus-conceal.md b/quest/m1/opus-conceal.md index 507d45a24c..cac6f8255e 100644 --- a/quest/m1/opus-conceal.md +++ b/quest/m1/opus-conceal.md @@ -11,6 +11,10 @@ last real one, instead of 120 ms. The API is unchanged and lands on main. libopus's `frame_size` on every call, and libopus conceals exactly that many samples. Record the last packet's sample count (`opus_packet_get_nb_samples`) and pass it for an empty packet; it is always a multiple of 2.5 ms. +- The audio-codecs line branch moves this code to + `rs/moq-audio/src/decode/backend/libopus.rs` (same `max_frame_size`); + land the fix wherever the code lives when this starts, and port it on the + line's next merge from main otherwise. - Refuse loss before any packet has decoded, rather than surfacing libopus's `BUFFER_TOO_SMALL`. - Document on `decode` how much an empty packet conceals. diff --git a/quest/m1/p2p/README.md b/quest/m1/p2p/README.md index bb0550f52c..b0817e2379 100644 --- a/quest/m1/p2p/README.md +++ b/quest/m1/p2p/README.md @@ -95,8 +95,8 @@ the same independence without fighting the browser. Rust already does the serving-side work: `best_route` re-runs on every table change and a live subscription re-splices onto a cheaper route with the same first hop at a group boundary, while an anonymous chain never wins. The JS -origin re-selects on provider change but ranks newest-first; that is -[route cost in the JS origin](/quest/m1/route-cost.md). The JS handshake +origin ranks the same way (`compareRoutes` in `js/net/src/origin.ts`: cost, +then fewest hops, then newest). The JS handshake already declares a random hop id, so browser hops are identified; the roster id is that hop id, held once per origin rather than once per session. @@ -110,16 +110,8 @@ relay's route ties the relay and loses on chain length. [Cost across scopes](/quest/m1/p2p/cost-scopes.md) writes the rule before the watcher depends on it. -### Fallback: a Rust + WASM in-tab hop - -If JS transit or ranking proves hard, the browser side can be moq-net in -WASM: `moq-wasm` already runs it over `web-transport-wasm`. A WASM hop would -hold the relay and peer sessions in Rust, giving one implementation of -signaling, policy, ranking, transit, and migration, and TS apps would connect -to it over an in-memory transport. It costs a web-sys data channel poll -transport, an in-memory bridge into `@moq/net`, and parsing every frame twice -in the tab. Recorded here so that decision is made with numbers, not -re-derived. +No Rust + WASM in-tab hop: [rs2ts](/quest/m1/rs2ts/remove-wasm.md) removes the +WASM build, so the browser side stays TypeScript. ### Risks @@ -148,7 +140,6 @@ re-derived. ## Related - [Peer grants](/quest/m1/auth/peer-grant.md) - the hop-bound credential a direct session presents; HMAC keys issue none -- [Route cost in the JS origin](/quest/m1/route-cost.md) - the watcher-side route pick this line needs - [One port](/quest/m1/one-port/README.md) - the relay answers STUN on its QUIC port - [E2EE](/quest/m1/e2ee/README.md) - what a peer would need if the token scope stopped being the trust boundary - [qmux on the QUIC core](/quest/m1/quic/qmux.md) - the stream core the unordered follow-up rides diff --git a/quest/m1/p2p/cost-scopes.md b/quest/m1/p2p/cost-scopes.md index 41f31fbd2b..1aecaa1c19 100644 --- a/quest/m1/p2p/cost-scopes.md +++ b/quest/m1/p2p/cost-scopes.md @@ -49,5 +49,4 @@ implementation quest it needs. ## Related -- [Route cost in the JS origin](/quest/m1/route-cost.md) - the ranking that consumes the rule -- [PoP skipping](/quest/m1/pop-skipping/README.md) - the mesh-side use of warm versus cold +- [Cluster routing](/quest/m1/cluster-routing.md) - the mesh-side use of cost diff --git a/quest/m1/p2p/transit.md b/quest/m1/p2p/transit.md index 9f54cff6aa..20a8f23d9d 100644 --- a/quest/m1/p2p/transit.md +++ b/quest/m1/p2p/transit.md @@ -32,10 +32,6 @@ the id appended and never on A; a chain already containing the id is dropped; a retraction on A retracts on B; two watchers of one tab share one upstream subscription. -## Required - -- [Route cost in the JS origin](/quest/m1/route-cost.md) - cost and hops must be carried on the entry before they can be forwarded - ## Related - [Watch opts in](/quest/m1/p2p/watch.md) - the first topology that needs a forwarding tab diff --git a/quest/m1/p2p/watch.md b/quest/m1/p2p/watch.md index 45bd5aa23b..476879b88d 100644 --- a/quest/m1/p2p/watch.md +++ b/quest/m1/p2p/watch.md @@ -12,7 +12,7 @@ goes away, with no visible interruption. `hang-watch` and `hang-publish` gain a `p2p` attribute that constructs `Peers` on the shared connection's origin with the demo's ICE servers and a `max` from the page; `demo/web` exposes the toggle. The route pick is the -origin's, from [cost ranking](/quest/m1/route-cost.md) under the rule from +origin's, from its cost ranking (`compareRoutes` in `js/net/src/origin.ts`) under the rule from [cost across scopes](/quest/m1/p2p/cost-scopes.md): a peer already carrying the broadcast wins, and its retraction falls back to the relay. @@ -26,5 +26,4 @@ watcher tab re-serving to another watcher through - [Signaling and policy](/quest/m1/p2p/signal.md) - [moq-cli joins](/quest/m1/p2p/cli.md) - the native hop the second topology shows - [Transit in the JS origin](/quest/m1/p2p/transit.md) -- [Route cost in the JS origin](/quest/m1/route-cost.md) - [Cost across scopes](/quest/m1/p2p/cost-scopes.md) diff --git a/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md b/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md index a4883e683e..a5fccaa027 100644 --- a/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md +++ b/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md @@ -21,7 +21,10 @@ the tokio worker path: That is `clock_gettime`. Roughly 2.5% of relay CPU spent reading the clock. The profile is the since-deleted quiche driver's; re-measure on noq before -and after. +and after. The closed, unmerged prototype +[#3136](https://github.com/moq-dev/moq/pull/3136) froze the clock per turn +behind an RAII guard on that driver; its shape and tests are a starting +point. Where the reads are: diff --git a/quest/m1/perf/3201-moq-uring-use-sendmsg-zc-for-large-udp-gso-trains.md b/quest/m1/perf/3201-moq-uring-use-sendmsg-zc-for-large-udp-gso-trains.md index d0f0e32311..5315677c57 100644 --- a/quest/m1/perf/3201-moq-uring-use-sendmsg-zc-for-large-udp-gso-trains.md +++ b/quest/m1/perf/3201-moq-uring-use-sendmsg-zc-for-large-udp-gso-trains.md @@ -8,7 +8,13 @@ beats `SendMsg` end to end before it is on by default. ## Plan -Follow-up to #2875. +Follow-up to #2875. The closed, unmerged prototype +[#3224](https://github.com/moq-dev/moq/pull/3224) (on the quiche-era dev +tree) did this together with #3204's fixed buffers, opt-in behind +`udp::Config::send_zc_threshold`. On loopback it was 6 to 9% slower, but +`IORING_SEND_ZC_REPORT_USAGE` showed the kernel copying there, so loopback +measures only the forced-copy overhead: the sweep needs a remote peer +through a physical NIC. The UDP path already assembles up to 64 KiB GSO trains in stable pool buffers, then submits `SendMsg` and recycles the buffer at the first CQE. Large trains are the promising case for `SENDMSG_ZC`; individual QUIC datagrams are likely below the copy-avoidance crossover. diff --git a/quest/m1/perf/3204-moq-uring-register-tx-pool-buffers-for-zero-copy-sends.md b/quest/m1/perf/3204-moq-uring-register-tx-pool-buffers-for-zero-copy-sends.md index 72f7cc1a8a..86f4ebb14a 100644 --- a/quest/m1/perf/3204-moq-uring-register-tx-pool-buffers-for-zero-copy-sends.md +++ b/quest/m1/perf/3204-moq-uring-register-tx-pool-buffers-for-zero-copy-sends.md @@ -9,6 +9,15 @@ measurable gain over plain `SendMsgZc` or the change is dropped. ## Plan Follow-up to #2875 and dependent on the `SENDMSG_ZC` experiment in #3201. +The closed, unmerged prototype [#3224](https://github.com/moq-dev/moq/pull/3224) +built this; reuse its lease and quarantine handling. + +Fixed buffers on `SENDMSG_ZC` need Linux 6.15 (vectored registered-buffer +support), above moq-uring's 6.12 floor, so the fixed path must fall back: +#3224 retried with ordinary `SENDMSG` on `EINVAL` or `EOPNOTSUPP` and +disabled later attempts on that ring. #3224 also filled the SQE `buf_index` +with a raw-SQE adapter because the `io-uring` crate did not expose it for +this opcode; check the current crate first. The TX pool already owns stable `Box<[u8]>` allocations and grows lazily. If zero-copy send wins, registering those allocations lets send SQEs reference fixed buffers and can reduce repeated page accounting on the large-train path. diff --git a/quest/m1/perf/README.md b/quest/m1/perf/README.md index 025eda859d..95ae136cdd 100644 --- a/quest/m1/perf/README.md +++ b/quest/m1/perf/README.md @@ -3,7 +3,7 @@ ## Goal Reduce relay CPU per session, raise the per-worker throughput ceiling, and -hold tail latency on the dev thread-per-core stack by eliminating measured +hold tail latency on the thread-per-core stack by eliminating measured hot-path costs: redundant copies, locks, atomics, clock reads, allocations, and syscalls. Not io_uring specific: anything on the relay's hot path qualifies, including the shared moq-net model layer and kio. @@ -14,6 +14,10 @@ outcome that abandons the quest. ## Plan +Quests branch from main unless they say otherwise; +[Run to quiescence](/quest/m1/perf/uring-quiescence.md) needs dev, where +`kio`'s `Tasks::poll` changed (#4156). + Planning quests can settle their contracts independently. Facts from the 2026-09 hot-path survey, so quests don't re-litigate them: diff --git a/quest/m1/perf/egress-keepalive.md b/quest/m1/perf/egress-keepalive.md index d07b00d9b3..208b9efa9c 100644 --- a/quest/m1/perf/egress-keepalive.md +++ b/quest/m1/perf/egress-keepalive.md @@ -23,7 +23,7 @@ before optimizing the remaining publisher overhead. Extend the existing group and track Criterion targets and add a bounded session regression to normal CI. Keep -`slow_batch_reader_survives_expiry_with_keep_alive`, and cover expiry scans +`slow_prefetch_reader_survives_expiry` (`rs/moq-net/src/model/track.rs`), and cover expiry scans while a batch drains, cancellation, and eventual expiry after reads stop. Report delivered bytes, refresh cost, CPU, and throughput for paired runs; fewer refresh calls alone are not evidence of a win. diff --git a/quest/m1/perf/egress-requeue.md b/quest/m1/perf/egress-requeue.md index ed9ae1d090..90ba398717 100644 --- a/quest/m1/perf/egress-requeue.md +++ b/quest/m1/perf/egress-requeue.md @@ -29,7 +29,3 @@ Linux. Latency must not regress at the chosen budget. A no-win keeps 1. The [quiescence quest](/quest/m1/perf/uring-quiescence.md) sweeps this budget together with its pass budget; land whichever runs first and fold the other's sweep in. - -## Closes - -- [#3120](https://github.com/moq-dev/moq/issues/3120) - close this issue when the quest finishes diff --git a/quest/m1/perf/uring-one-enter.md b/quest/m1/perf/uring-one-enter.md index 4cd8e3f239..5ff2baf3de 100644 --- a/quest/m1/perf/uring-one-enter.md +++ b/quest/m1/perf/uring-one-enter.md @@ -10,7 +10,7 @@ a ratio of two totals. ## Plan -Branch from dev. The loop in `Worker::block_on` +Branch from main. The loop in `Worker::block_on` (rs/moq-uring/src/worker.rs:164-187) runs one task pass, then `pump` (`submit()` at worker.rs:250, then reap and dispatch), then `maybe_park`, whose enter (`submit_and_wait(1)` or a timed `enter(to_submit, 1, diff --git a/quest/m1/perf/uring-quiescence.md b/quest/m1/perf/uring-quiescence.md index 529aae15c7..88b4879722 100644 --- a/quest/m1/perf/uring-quiescence.md +++ b/quest/m1/perf/uring-quiescence.md @@ -15,7 +15,8 @@ exhaustion, or to a quantum, before touching the ring. ## Plan -Branch from dev. Keep the fairness the one-pass rule protects: a forward wake +Branch from dev: `kio`'s `Tasks::poll` changed only there (#4156 merged +`Pollable` into `Task`), and the pass budget builds on that version. Keep the fairness the one-pass rule protects: a forward wake chain must not starve the caller's other arms, and one connection's backlog must not starve the socket. diff --git a/quest/m1/performance-comparisons.md b/quest/m1/performance-comparisons.md index e9fcaf8b3a..ab8a640e72 100644 --- a/quest/m1/performance-comparisons.md +++ b/quest/m1/performance-comparisons.md @@ -38,7 +38,7 @@ while extending this harness rather than creating another benchmark runner. ## Required -- [Thin justfiles](/quest/m1/tooling/justfiles.md) - finish benchmark script relocation before changing its lifecycle +- [Tooling](/quest/m1/tooling/README.md) - the recipe and script layout `just bench` runs under ## Related diff --git a/quest/m1/performance-profiles.md b/quest/m1/performance-profiles.md index eb52ef23b1..e0c5da304c 100644 --- a/quest/m1/performance-profiles.md +++ b/quest/m1/performance-profiles.md @@ -11,7 +11,7 @@ cost. Profiling is opt-in and has no production overhead when disabled. `bench/run.sh` already owns the builds, relay PID, workload, and host samples, but has no profiler integration. Reuse that lifecycle instead of adding a second launcher. `Cargo.toml` already has a `profiling` profile and -`rs/moq-native/src/jemalloc.rs` already supports on-demand heap dumps. +`rs/moq-tokio/src/jemalloc.rs` already supports on-demand heap dumps. - Add a focused `just` recipe selecting workload, duration, and capture mode through one configuration. Reuse locked builds and the existing profiling Cargo profile; @@ -46,3 +46,4 @@ verify current supported versions and pin any newly installed tools. - [Benchmark comparisons](/quest/m1/performance-comparisons.md) - repeatable results and artifact metadata - [Relay memory](/quest/m1/relay-memory.md) - retained route and announcement memory +- [Release profile](/quest/m1/release-profile.md) - also changes `[profile.profiling]`; land one, then rebase the other diff --git a/quest/m1/pop-skipping/README.md b/quest/m1/pop-skipping/README.md deleted file mode 100644 index e3c1185615..0000000000 --- a/quest/m1/pop-skipping/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Cache-aware PoP skipping - -## Goal - -Give an unpopular broadcast a short cold path without sacrificing the backhaul -deduplication a sparse mesh gets once the broadcast is warm. Every relay connects -to all healthy relays in its own PoP, its base-graph neighbor PoPs, and the PoPs at -graph distance two. A cold subscriber uses the direct distance-two session rather -than forwarding through an idle intermediate; a relay with any warm track advertises -the cheaper route, pulling later subscribers back onto the copy the cluster already -has. Full eligible-PoP pairing is accepted for now; on the topology this was -designed against it takes live persistent relay sessions from 29 to 78 (of 120 for -a full mesh). - -For `sjc0 -> dal0 -> iad0`, with the publisher in IAD: - -1. Cold SJC sees direct `sjc0 -> iad0` at 5 and `sjc0 -> dal0 -> iad0` at 6, so it - takes the direct session on price rather than on a tie-break. -2. Another SJC relay reaches that warm copy over the same-PoP link for 1, against 5 - to open its own. -3. A DAL subscriber initially pulls directly from IAD at cost 3. DAL then ranks - ahead of SJC because its cold route is cheaper (3 against 5), so SJC migrates at - a group boundary and both share DAL's one IAD pull. -4. SEA makes the same local decision and can join the already-warm aggregation - tree rather than opening another copy from IAD. - -## Plan - -### Where this stands after prefix routes - -[moq#3225](https://github.com/moq-dev/moq/pull/3225) made an announcement a -route over a path *prefix* and deleted the machinery this questline had -already landed: the warm-cost discount, `COST_LINGER`, the -`(cold, hash)` adoption gate, the handover hold, and the per-broadcast front -that hosted all of it. A relay now forwards accumulated costs only. - -What survives is the part that was expensive to get right: - -- `origin::Cost { warm, cold }` on the wire, decoded on lite-06, with - `Cost::UNKNOWN` reading an inexpressible cold as the ceiling rather than as - free. -- `route_order`, which still breaks a warm tie on the lower cold cost ahead of - hop count. -- `DRAIN_COST`, and the hop list as the loop check. -- The link-price decisions below, which were rulings about the topology rather - than about the code that read them. - -So this questline is no longer "add a rank to a working discount". It is -re-deriving warmth on a route model that prices prefixes, then putting the -adoption gate back on top. - -### The tension prefix routes introduce - -Warmth is a property of one broadcast. A route covers a prefix, which is a -claim about a set of paths. A relay carrying `pid/foo.hang` knows nothing about -the rest of `pid/`, so there is no honest way to discount the prefix route it -already advertises: doing so would attract subscribers for every cold path -underneath it. - -### Decisions - -- **A carrying relay advertises the exact broadcast path as its own route, - priced warm.** Per-broadcast warmth becomes a more specific route rather than - a discount on a broader one. Nothing new goes on the wire, and the existing - selection rule already prefers it. -- **Selection keeps specificity ahead of cost.** `best_server` filters to the - longest covering prefix and only then orders by `route_order`, and the lite - draft says the same. This matches longest-prefix-match everywhere it appears - (IP forwarding, BGP, DNS closest encloser, URL routers), and for the same - reason: routes of different prefix length describe different destination - sets, so comparing their costs asks "what does this broadcast cost" against - "what would anything under here cost". Cost decides between routes that cover - the same thing, which is exactly where the warm/cold pair was designed to - work. -- **A draining carrier retracts its exact-path route rather than repricing - it.** Under specificity-first, forgoing the discount is not enough: a route - priced at the ceiling still wins on specificity and keeps attracting - subscribers a drain is trying to move. Retraction drops the claim, and the - broader route the content is still reachable through takes over. This - replaces the draft's ceiling-exemption paragraph, which was written when the - discount rode a single per-broadcast advertisement. -- **Adoption and resume need different identities.** Adoption keys on the - *last* hop of a route, the peer that advertised it and the parent a relay - would be adopting; resuming a subscription keys on the *first* hop, who - produced the content, which alternate routes to one publisher deliberately - share. The pre-#3225 front kept both (`FrontState.publisher` for the first, - `handover_allowed` and the hold for the last), with `same_identity` comparing - either and refusing `Hop::UNKNOWN` on both. [moq#3312](https://github.com/moq-dev/moq/pull/3312) restored that comparison rule and the - publisher half as the first-hop resume; [Rank](/quest/m1/pop-skipping/rank.md) owns - the carrier half rather than reusing the wrong one. -- The operator hand-authors one undirected base graph. Same-PoP connectivity is - unconditional, not a self-edge operators must remember. The radius-two closure - and shortest base-graph distance are derived and validated. -- The initial reference link costs are local 1, base neighbor 3, and distance-two - skip 5. The invariant that buys PoP skipping is `skip < 2 * neighbor`: a cold - skip must beat the equivalent two-edge path outright, so 5 against 6 works while - anything from 6 up preserves the old cold path and defeats this quest. Keeping - the skip strictly below rather than equal to the two-edge path means the choice - is made on price, not on the hop-count tie-break below it. -- No link is free, including a same-PoP one. A local transfer still costs a NIC, a - hop of latency, and a copy; what is genuinely free is bytes already flowing, and - that is the warm route's job, not the link price's. A floor of 1 also prices - chain *length*, so a PoP converges on a flat tree around one puller instead of - daisy-chaining at no cost, and it means adopting a parent strictly increases the - adopter's own cold cost, which leaves the per-broadcast hash as a tie-break - between equally-placed relays rather than the only thing keeping the order - strict. -- Warmth is broadcast-wide: demand for any track makes the broadcast warm. When - demand drains, the exact-path route follows the existing 30-second spliced-track - lifecycle (`TRACK_IDLE_LINGER`) while at least one track copy remains retained. - Do not add a second timer with a different definition; `COST_LINGER` was that - timer and is already gone. A retained track has canceled its upstream - subscription, so "warm" here intentionally means reusable route/track state plus - hysteresis, not that every future byte is already in memory. -- Provider economics are directional and dominate locality in a mixed-provider - deployment. Conceptually the metric is `(serving provider egress class, - topology distance)`: pulling from an unmetered relay can be cheaper than the - reverse direction, while equal provider classes retain the 1/3/5 locality - order. Keep these components structured until the final peer cost is encoded; - choose an encoding whose economic stride exceeds the maximum accumulated - topology distance allowed by the bounded hop list, which at a 32-entry chain - and a 5-cost worst link is 160. -- Cluster sessions use `moq-lite-06`, explicitly. Lite05 silently drops - the cost; the MoQT Cluster extension is not the chosen cluster wire for this - questline. -- Fleet rollout stays downstream: moq.pro owns rendering the priced radius-two - topology, the two-phase Lite06 cluster cutover, and the staging and live - route-verification proofs. This questline completes when the mechanism above - lands here. - -### Peer reconfiguration - -The URL-backed half is complete in -[moq#2874](https://github.com/moq-dev/moq/pull/2874): canonical identity stays -separate from dial configuration, so changing `?cost=` or an inline credential -replaces the active session while an identical render is a no-op. It also -preserves the last-good topology on malformed input, keeps an identical fallback -session alive, redacts credentials from parse errors, and tracks overlapping -gossip paths so an old unannounce cannot stale a replacement. The remaining -boundary is structured policy that does not live in the URL: the two directional -costs of one bidirectional session, which one `?cost=` cannot split. - -## Required - -- [Warm advertise](/quest/m1/pop-skipping/warm-advertise.md) - a carrying relay - advertises the exact broadcast path as a warm route, and retracts it when - draining or idle -- [Rank](/quest/m1/pop-skipping/rank.md) - rank warm relay candidates by cold - cost and adopt only downhill, with a hold that outlasts cost propagation -- [Peer reconfigure](/quest/m1/pop-skipping/peer-reconfigure.md) - structured - peer entries carry the charged cost, the declared cost, and the credential, - and a change redials, proving both sides of an asymmetric link - -## Related - -- [drain](/quest/m1/drain/README.md) - a second relay per PoP makes the same-PoP link price and its connection cardinality operationally important -- [wildcard](/quest/m0/wildcard/README.md) - it reuses this questline's route cost, and needs a cluster on Lite06 -- [relay-memory](/quest/m1/relay-memory.md) - a denser mesh multiplies whatever a non-selected route costs diff --git a/quest/m1/pop-skipping/peer-reconfigure.md b/quest/m1/pop-skipping/peer-reconfigure.md deleted file mode 100644 index 21313053e4..0000000000 --- a/quest/m1/pop-skipping/peer-reconfigure.md +++ /dev/null @@ -1,70 +0,0 @@ -# [M] Peer reconfigure - -## Goal - -A cluster peer entry can carry structured policy that has no home in a URL: -the price this relay charges to pull from the peer, the price it declares to -the peer for the reverse direction, and the credential. Changing any of it at -runtime replaces the live session the same way a `?cost=` change does today, -and an identical render stays a no-op. - -## Plan - -The typed public configuration, URL/object normalization, credentials, and -symmetric policy are supplied by dev. This quest removes the explicit refusal -of asymmetric costs and wires their distinct meanings without another change -to the peer configuration type. Preserve the compatibility and validation -rules below rather than implementing a second parser. - -[moq#2874](https://github.com/moq-dev/moq/pull/2874) landed the URL half: -`?cost=` and an inline `?jwt=` are dial configuration, the query-less URL is -the identity, and a changed render replaces the session while preserving -inactive fallbacks and the last-good topology. What it cannot express is an -asymmetric link from one side. One `?cost=N` is both what this relay charges -locally to pull from the peer and what it declares in SETUP as its own egress -price, so pricing the two directions differently needs the peer to list us -with its own `?cost=`. - -Decisions: - -- Peer entries in `cluster.connect` and `connect_api` accept an object beside - the bare URL string, deserialized untagged into the existing `DialTarget`: - `url` (required, still the canonical identity), `cost` (what this relay - charges to pull from the peer, the routing input), `egress` (what it declares - in SETUP as its own price toward the peer, defaulting to `cost`), and - `token` (replaces an inline `?jwt=`). `DialTarget` grows `egress` and a - normalized credential, with an inline `?jwt=` and an object `token` parsed - to the same representation, and its equality covers every field, so an - `egress`-only or `token`-only update is a change and the token is never - dropped. Unknown fields reject the whole list, so a typo keeps the - last-good topology exactly as a malformed URL does, and so does an object - whose `url` still carries `?cost=` or `?jwt=`: policy has one home per - form, and a mixed entry is rejected rather than given a precedence a - migration could silently get wrong. -- Gossip and mDNS keep advertising URLs only. Their allowlist admits `?cost=` - alone, and the draft already makes a declared price an assertion the - receiver may override, so a peer never needs to push structured policy at us. -- Any field change replaces the session, the rule [moq#2874](https://github.com/moq-dev/moq/pull/2874) - set for `?cost=`. A URL entry and an object entry that normalize to the same - `DialTarget` are one entry, deduplicated as `parse_peer_list` already does; - two entries for one identity with differing policy are the conflict that - rejects the list, whatever their forms. Reconciling cost in place without a - redial is deliberately not done: it would be a second code path for one - field. -- No per-peer wire version. ALPN negotiation picks the best common version per - session and the global `--version` list is the only pin; a per-peer - override is a fleet cutover concern that stays downstream. - -The split lands in `moq_tokio::Client` as separate charged and declared costs -(today `with_cost` sets both), and in the relay's session setup so the routing -side reads the charged value while SETUP carries the declared one. - -Tests: the asymmetric link in both directions, two relays each pricing the -other differently, with routes ranked per side from the charged value and the -declared value visible on the far side; a `connect_api` update that changes -only `egress` or only `token` redials that peer and no other; an identical -object render is a no-op; equivalent URL and object forms of one peer -deduplicate while differing policies for one identity conflict; an unknown -field, or an object whose `url` carries `?cost=` or `?jwt=`, keeps the -previous list. Update `doc/bin/relay/cluster.md` and -`doc/bin/relay/config.md` with the object form. diff --git a/quest/m1/pop-skipping/rank.md b/quest/m1/pop-skipping/rank.md deleted file mode 100644 index 955bf6f0bc..0000000000 --- a/quest/m1/pop-skipping/rank.md +++ /dev/null @@ -1,91 +0,0 @@ -# [L] Rank - -## Goal - -A relay adopts another relay's warm copy only when that relay is strictly -closer to the publisher, so a PoP converges on one aggregation point instead of -a coin flip, and no two relays can adopt each other. - -## Plan - -Once [Warm advertise](/quest/m1/pop-skipping/warm-advertise.md) lands, two -relays carrying one broadcast both advertise it warm and tie: warm cost cannot -separate them, and only the deterministic hash would, so the aggregation root is -picked at random. In the `sjc0 -> dal0 -> iad0` topology that lets SJC (two -links from IAD) win over DAL (one link), and the cluster carries the extra -backhaul it was supposed to remove. - -Break that tie on cold cost, which is the relay's own distance to the publisher -with warm discounts removed and is already on the wire and already ranked below -warm in `route_order`. Adoption descends `(cold cost, hash(broadcast path, relay -hop id))`: lower cold wins, equal cold takes the lower hash, and a relay keeps -its own upstream rather than adopting a peer that ranks above it. Including the -broadcast path in the hash spreads ownership instead of making one relay win -every broadcast. A relay advertises its *own* rank, not that of a parent it -adopted, so every warm edge descends and cycles cannot form. - -Adopting a parent adds that link to the adopter's cold cost, so it can only rank -above its parent afterwards. That is what makes descent automatic, and it is why -no link may be priced free. - -### The hold, and why it is not optional - -Cold is a value each relay reports about itself, so a report still crossing the -mesh can be lower than what its sender would say now. Rings of relays can each -rank a stale neighbour below themselves and all let go at once, leaving the -broadcast with no source. Rising costs are the whole hazard; if costs only fell, -a stale value would only make a peer look worse than it is. A GOAWAY prices a -route at the ceiling while neighbours still remember it cheap, so the trigger is -a rolling restart, not an exotic race. - -Hold a re-parent onto another relay long enough for the costs it rests on to -land, and re-evaluate when the hold expires rather than committing to the -decision that armed it. The sizing rule is "longer than an announcement crosses -the mesh", plus a stable per-relay spread so a PoP does not reconsider on one -instant. The hold covers only trading a working upstream for a better one: -an idle relay is pulling nothing, a one-hop chain is the publisher itself, and -leaving a drained or vanished route stays immediate. - -### Two identities, not one - -Adoption is a statement about the adjacent relay, and that is a different -identity from the first-hop resume rule [moq#3312](https://github.com/moq-dev/moq/pull/3312) landed. -The pre-#3225 front kept both, because they answer different questions: - -- The **first** hop is who produced the content. Alternate routes to one - publisher deliberately share it, which is what makes them spliceable, and it - is what the resume rule keys on. -- The **last** hop is the peer that advertised the route: the parent a relay - would be adopting. `handover_allowed` and the hold both keyed on it, and the - rank hash was taken over it (`fnv_key(name, [peer])`). - -Use the last hop here. Keying adoption on the first hop would make every carrier -of one broadcast look like the same peer, so a changed parent would go -undetected and two relays could adopt each other, which is the failure the hold -exists to prevent. - -What this quest reuses from the resume rule is the comparison rather than the -field: `Hop::UNKNOWN` identifies nobody and never matches itself, so two -anonymous relays must not pass for one relay reconnecting and skip the gate. - -Update `drafts/draft-lcurley-moq-lite.md` in the same change, restoring the -adoption-rank rule that -[moq#3278](https://github.com/moq-dev/moq/pull/3278) removed. - -### Tests - -The asymmetric chain (DAL outranks SJC regardless of hash), the equal-cost race -(exactly the lower hash keeps its upstream while the other adopts it), a -three-node transitive tree, simultaneous updates, a ring of relays each holding -a stale cheaper report (which must not leave the broadcast sourceless), route -loss and reversion, and an unknown-cold peer losing to a known cheap one in both -hash directions. A live track must migrate only at a group boundary and must not -see announcement churn. - -Write every gate test over both hash directions, or it proves nothing beyond a -lucky hash. - -## Required - -- [Warm advertise](/quest/m1/pop-skipping/warm-advertise.md) - there is nothing - to rank until two relays can both advertise one broadcast as warm diff --git a/quest/m1/pop-skipping/warm-advertise.md b/quest/m1/pop-skipping/warm-advertise.md deleted file mode 100644 index 39a23daaef..0000000000 --- a/quest/m1/pop-skipping/warm-advertise.md +++ /dev/null @@ -1,51 +0,0 @@ -# [L] Warm advertise - -## Goal - -A relay carrying a broadcast advertises that broadcast's exact path as its own -route, priced warm, so later subscribers reach the copy the cluster already has -instead of opening another one. It retracts that route when the broadcast goes -idle or its own path starts draining. - -## Plan - -Warmth is per-broadcast and a route covers a prefix, so the discount cannot ride -the prefix route a relay already advertises: carrying `pid/foo.hang` says -nothing about the rest of `pid/`. Advertise the exact path instead, as a second, -more specific route. `best_server` already filters to the longest covering -prefix before ordering by cost, so the warm route wins for that one broadcast -and changes nothing for its neighbors. - -Price it the way the deleted discount did: warm zero (the ingress is already -paid for), cold forwarded accumulated, since cold prices the path this relay -would have to open if it were not already carrying. Two relays both carrying -then tie on warm and are separated by cold, which is what -[Rank](/quest/m1/pop-skipping/rank.md) builds on. - -Lifecycle follows the state that already exists rather than a new timer. -The route appears when a track under the broadcast has demand and stays while -any track retains its source copy inside `TRACK_IDLE_LINGER`; it is retracted -once the last copy expires. `COST_LINGER` was the parallel five-second timer -with a different definition and is already gone with -[moq#3225](https://github.com/moq-dev/moq/pull/3225); do not reintroduce it. - -Retract rather than reprice when the serving path drains. Under -specificity-first selection a ceiling-priced exact-path route still outranks -every broader route, so a drain that only repriced would keep attracting the -subscribers it is trying to move. Dropping the claim lets the broader route the -content is still reachable through take over. Update -`drafts/draft-lcurley-moq-lite.md` in the same change: this replaces the -ceiling-exemption paragraph that -[moq#3278](https://github.com/moq-dev/moq/pull/3278) removed, and it is a behavior change the -draft has to carry. - -Make both Lite and IETF publishers advertise from one warmth signal, so a -cluster selecting Lite06 and one on the Cluster extension mean the same thing by -warm. - -Tests: two tracks with staggered demand keep one warm route alive; demand -returning during retention does not churn the advertisement; the route is -retracted after the last copy expires and is not re-advertised afterwards; a -draining path retracts rather than repricing, and a subscriber on it moves to -the broader route; a second relay carrying the same broadcast ties on warm and -is separated by cold. diff --git a/quest/m1/processor/README.md b/quest/m1/processor/README.md index 102b88eef7..041fc85997 100644 --- a/quest/m1/processor/README.md +++ b/quest/m1/processor/README.md @@ -4,7 +4,8 @@ A customer runs a worker in its own environment, connects outbound to a MoQ deployment, reads only eligible source media, and publishes an on-demand -contribution at `/.pro`. The platform supplies +contribution under the processor's own prefix, mirroring the source path +(for example `./`). The platform supplies registration, scoped credentials, routing, demand, status, and usage visibility; it does not upload or execute customer code. @@ -14,14 +15,23 @@ service contract, and credential minting) stay downstream in moq.pro. The contract is not vision-specific: captioning, moderation, telemetry extraction, and custom transforms use the same worker lifecycle. +## Plan + +Decided: derived output follows [Wildcard](/quest/m0/wildcard/README.md)'s +service-prefix layout, not `/.pro`. Suffix routing is +dropped everywhere, and a prefix claim needs the variable part of the path +trailing, so the processor claims its prefix and mirrors the source path +beneath it. The source's catalog reaches the output through a +cross-broadcast reference ([media contract](/quest/m1/processor/media-contract.md)). + ## Required - [Processor media contract](/quest/m1/processor/media-contract.md) - define contribution references, source relations, and correlation in the Hang catalog - [Advertise-only authorization](/quest/m1/processor/advertise-auth.md) - a - worker may advertise its contribution suffix without receiving permission to - publish arbitrary matching paths + worker may advertise its service prefix without receiving permission to + publish arbitrary paths under it - [Expiring media grants](/quest/m1/processor/grant-lease.md) - enforce short-lived exact grants on already-open consumer and producer handles @@ -31,4 +41,5 @@ and custom transforms use the same worker lifecycle. publishes frame-correlated detections and proves demand, reconnect, failover, and teardown end to end - [Wildcard advertisements](/quest/m0/wildcard/README.md) - lets a dormant - processor advertise what it could serve without enumerating live sources + processor advertise what it could serve without enumerating live sources, + and sets the service-prefix layout diff --git a/quest/m1/processor/advertise-auth.md b/quest/m1/processor/advertise-auth.md index 26c1d8faaa..1c8fc2bead 100644 --- a/quest/m1/processor/advertise-auth.md +++ b/quest/m1/processor/advertise-auth.md @@ -2,18 +2,24 @@ ## Goal -A v1 worker credential can advertise an allowed wildcard without receiving -permission to publish any path matching it. Relays enforce advertise and -publish as independent capabilities before external processor credentials are +A v1 worker credential can advertise an allowed prefix without receiving +permission to publish any path under it. Relays enforce advertise and publish +as independent capabilities before external processor credentials are minted. ## Plan -Add an explicit advertise pattern union to the v1 claims, token SDKs, origin -scope, and relay authorization model. Wildcard authorization checks that -scope rather than borrowing the publish union. A concrete announcement or -publish request still requires publish permission, so an advertise-only worker -cannot bypass the demand exchange. +Add an explicit advertise prefix scope to the v1 claims, token SDKs, origin +scope, and relay authorization model. [Wildcard](/quest/m0/wildcard/README.md) +checks advertisements against `moq_auth::Claims.publish` today; this quest +gives them their own scope instead of borrowing the publish one. A concrete +announcement or publish request still requires publish permission, so an +advertise-only worker cannot bypass the demand exchange. + +Decided: the advertise scope is prefix-only. Advertising is prefix-only on +every wire (Wildcard's decision) and suffix routing is dropped, so leading-star +and suffix advertise patterns have nothing to authorize. Token claim patterns +keep their suffix support for publish and subscribe. Preserve current customer credentials in the wire and authorization design: existing claims retain their current publish-implies-advertise behavior, while @@ -21,7 +27,11 @@ the new v1 claim separates the capabilities. Land the claims, SDK, origin-scope, relay authorization, and tests without combining the release or the moq.pro (downstream) pin rollout into this quest. -Cover containment, rebasing, leading-star and suffix patterns, missing versus -empty advertise scope, v0 compatibility, token revalidation, concrete announce, -publish, FETCH, and a wildcard demand that receives only an exact short-lived -publish grant. +Cover containment, rebasing, missing versus empty advertise scope, v0 +compatibility, token revalidation, concrete announce, publish, FETCH, and a +prefix demand that receives only an exact short-lived publish grant. + +## Required + +- [Wildcard](/quest/m0/wildcard/README.md) - the prefix advertisements this scopes +- [Auth](/quest/m1/auth/README.md) - the v1 claims and relay authorization this extends diff --git a/quest/m1/processor/grant-lease.md b/quest/m1/processor/grant-lease.md index d58b3b226d..66ee0bf16a 100644 --- a/quest/m1/processor/grant-lease.md +++ b/quest/m1/processor/grant-lease.md @@ -18,9 +18,15 @@ missing, late, broader, or mismatched refresh closes them. Reconnecting with an expired grant is denied as it is today. Keep deadline enforcement in the relay authorization owner rather than a -cooperative worker timer. Cover an idle open handle, active source reads, -active publication, refresh before expiry, refresh after demand ends, relay -clock skew within the token policy, disconnect races, HTTP and HLS rejection of +cooperative worker timer. Build on what exists: `Grant::deadline` +(`rs/moq-auth/src/grant.rs`, #4237) already pins an accepted grant to a fixed +deadline, and dev's `moq_auth::lease` (#3943) re-checks a session on cadence +and reports why it ended. Extend those to the handles a grant opened rather +than adding a second timer. No clock-skew grace: open #4368 makes expiry +exact and drops `CLOCK_SKEW`, so a deadline in the past is expired. + +Cover an idle open handle, active source reads, active publication, refresh +before expiry, refresh after demand ends, disconnect races, HTTP and HLS rejection of the worker audience, and unrelated traffic continuing through an existing pooled HLS consumer after a worker grant expires. diff --git a/quest/m1/processor/media-contract.md b/quest/m1/processor/media-contract.md index 72d53ebec7..f2f042408f 100644 --- a/quest/m1/processor/media-contract.md +++ b/quest/m1/processor/media-contract.md @@ -8,9 +8,14 @@ can resolve without eagerly subscribing to processor output. ## Plan -This quest also defines the generic catalog-level contribution reference -itself: a catalog entry that names another contribution and its relative -broadcast without opening it, which the processor contract specializes. +Build on hang's existing per-rendition relative `broadcast` reference +(`broadcast` on the video rendition in `rs/hang/src/catalog/video/mod.rs`, +with its audio, text, JSON, and binary counterparts): it already names another +broadcast relative to the catalog's, and Rust rejects one that escapes. The +processor output lives under the processor's prefix, mirroring the source +path, so the source catalog reaches it through that relative reference. Add +only what it lacks: a catalog-level contribution entry that names a whole +contribution without opening it, which the processor contract specializes. The processor publishes a normal Hang contribution rather than an arbitrary fragment for an edge to merge. Output renditions carry their own schema and an @@ -26,3 +31,7 @@ Land Rust and JavaScript catalog bindings, resolver behavior, and fixtures for video, audio, text, missing output, malformed relations, lazy resolution, and relative-path escape. The release and the moq.pro (downstream) pin rollout stay out of this quest. + +## Related + +- [JS catalog path](/quest/m1/js-catalog-path.md) - JS rejects an escaping `broadcast` reference like Rust diff --git a/quest/m1/publish-codec-string.md b/quest/m1/publish-codec-string.md index b86f6c557b..b19ce6fe49 100644 --- a/quest/m1/publish-codec-string.md +++ b/quest/m1/publish-codec-string.md @@ -22,7 +22,9 @@ Guidance: - The catalog is built from the resolved config today, before any frame is encoded. Either hold the rendition out of the catalog until the first output reports its `decoderConfig`, or update it then; keep the stall and - jitter reporting working either way. + jitter reporting working either way. Holding it out must go through the + reservation gate the #2075 quest adds to `#runCatalog`, or it recreates the + partial first snapshot that quest prevents. - Check what each browser returns for a bare hint. If one echoes the hint back, derive the string from the bitstream (SPS for H.264/H.265, the sequence header for AV1, the uncompressed header for VP9), or fail loud @@ -31,3 +33,7 @@ Guidance: catalog follows it. - Tests with the fake `VideoEncoder`: a bare-hint probe whose output reports a full string publishes the full string. + +## Required + +- [#2075](/quest/m1/2075-mirror-catalog-reservation-gating-in-moq-hang-js-hang.md) - the catalog reservation gate this rendition hold must go through diff --git a/quest/m1/qos/README.md b/quest/m1/qos/README.md index a5f0720f65..3bf61cf061 100644 --- a/quest/m1/qos/README.md +++ b/quest/m1/qos/README.md @@ -31,6 +31,12 @@ The counters and channels land here. The moq.pro (downstream) dashboard work, including the health badge, connection-health drill-down, and stream preflight, consumes them downstream. +Decided (2026-09-28): the whole line, including the client stats line, targets +`dev`. The moq-stats schema change +([#4145](https://github.com/moq-dev/moq/pull/4145)) breaks the published +`moq-stats` crate, and a line cannot close with part of it on `main` and part +on `dev`. + ## Required - [Starvation](/quest/m1/qos/starvation.md) - per broadcast, how far behind diff --git a/quest/m1/qos/final-lag-sample.md b/quest/m1/qos/final-lag-sample.md index 6ee05196af..6b4727e4d2 100644 --- a/quest/m1/qos/final-lag-sample.md +++ b/quest/m1/qos/final-lag-sample.md @@ -26,6 +26,13 @@ interval disappears with it. Public API: none. Wire: none, only the histogram's values change. +Overlaps the lag-splice quest on the line branch +([#4381](https://github.com/moq-dev/moq/pull/4381)), which also changes how +`FrontierInner::sample` holds `unsampled` weight and when a spliced segment's +source stops being sampled. Land them in sequence on the same sampler, not in +parallel, and make the final drop-time sample keep any weight lag-splice +defers. + ## Required - [Starvation](/quest/m1/qos/starvation.md) - the sampler this extends (#4298) diff --git a/quest/m1/qos/stats/README.md b/quest/m1/qos/stats/README.md index 929e56f1c3..b74ec7f73f 100644 --- a/quest/m1/qos/stats/README.md +++ b/quest/m1/qos/stats/README.md @@ -46,7 +46,8 @@ Decisions settled while planning: authorization, or route-selection input. - **A published break goes through dev**, because `moq-stats` is a published crate and the generic producer is a breaking change, and the moq-json rework there is what the - producers build on. + producers build on. The parent QoS line targets `dev` with it (#4145), so + every quest here does too. ## Required @@ -58,7 +59,3 @@ Decisions settled while planning: crate, and the browser publisher and player report through it - [Encoder feedback](/quest/m1/qos/stats/encoder-feedback.md) - a Rust encoder subscribes to its viewers' stats and adapts its bitrate - -## Closes - -- [#3608](https://github.com/moq-dev/moq/issues/3608) - close this issue when the questline finishes diff --git a/quest/m1/qos/stats/encoder-feedback.md b/quest/m1/qos/stats/encoder-feedback.md index 11518d8cc7..f6391b0b7c 100644 --- a/quest/m1/qos/stats/encoder-feedback.md +++ b/quest/m1/qos/stats/encoder-feedback.md @@ -27,7 +27,9 @@ prefix. Keyframe requests stay out. a stalled share above a threshold steps the target down like a bandwidth drop, recovery follows the existing attack curve, and the estimate stays the ceiling. - Audio follows the same signal with its narrower ladder. + Audio does not follow its grant today (`Options::bandwidth` in + `rs/moq-audio/src/encode/producer.rs` reserves only), so it follows this + signal only once the grant quest lands; until then the loop drives video. - `moq import --feedback ` and `moq transcode --feedback ` wire it; each rung of the ladder reads its own broadcast's track. - Test with the CLI publishing to a relay and two `moq play --stats` viewers @@ -46,5 +48,5 @@ prefix. Keyframe requests stay out. - [Ladder](/quest/m1/ladder/README.md) - the transcode ladder that adapts to its uplink today -- [Keyframe trigger](/quest/m1/keyframe-trigger.md) - the keyframe request - this loop does not send +- [Audio follows the grant](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md) - + the audio rate follow this signal would feed diff --git a/quest/m1/qos/stats/js.md b/quest/m1/qos/stats/js.md index 4a95727e1f..280092e32c 100644 --- a/quest/m1/qos/stats/js.md +++ b/quest/m1/qos/stats/js.md @@ -35,7 +35,3 @@ dashboard reads relay stats through the package instead of its own copies. - [Schema and library](/quest/m1/qos/stats/schema.md) - the wire shape this mirrors - -## Closes - -- [#2735](https://github.com/moq-dev/moq/issues/2735) - close this issue when the quest finishes diff --git a/quest/m1/qos/stats/rust.md b/quest/m1/qos/stats/rust.md index 4073c38fa2..2a9f7ebb07 100644 --- a/quest/m1/qos/stats/rust.md +++ b/quest/m1/qos/stats/rust.md @@ -36,7 +36,3 @@ publisher of a `.hang` broadcast can learn whether its viewers played it. - [Schema and library](/quest/m1/qos/stats/schema.md) - the producer and the media types - -## Closes - -- [#2734](https://github.com/moq-dev/moq/issues/2734) - close this issue when the quest finishes diff --git a/quest/m1/quic/README.md b/quest/m1/quic/README.md index 7280d2ff8f..36c4c19aa2 100644 --- a/quest/m1/quic/README.md +++ b/quest/m1/quic/README.md @@ -8,10 +8,10 @@ MoQ's schedule and offers it upstream when it is general. One core serves the tokio backend, the thread-per-core `moq-uring` backend, iroh, and qmux. The features are per-stream acknowledgment progress, reliable stream resets, hierarchical stream scheduling with per-broadcast fairness, the shared stream -state machine used by qmux, capacity probing for media, per-stream -deadlines, deadline-based and wider limits for relay peers. The experiments that may join them (GCC, FEC, receive -timestamps, kernel pacing, buffer pools, probing, L4S, careful resume) live -in [m2](/quest/m2/README.md). +state machine used by qmux, per-stream deadlines, and wider limits for relay +peers. The experiments that may join them (GCC, FEC, receive timestamps, +kernel pacing, buffer pools, media probing, L4S, careful resume, deadline +keep-alive) live in [m2](/quest/m2/README.md) and do not gate this line. ## Plan @@ -20,15 +20,15 @@ One stack carries every change on MoQ's own QUIC paths; a build with the `iroh` feature also compiles upstream noq, and iroh connections are outside what these quests reach. -The seven BBR correctness fixes follow the fork bootstrap. They are separate -PRs, but one owner should work in the shared controller code at a time. -Their controller-level regressions extend the shared test `Sim` in -`bbr3/mod.rs` with only what each needs, rather than adding another -simulation loop; a fix at the transport boundary still needs a transport -test through the real callbacks. The existing loops stay, since the fork -merges upstream weekly and a port would conflict. -The [BBR release](/quest/m1/quic/bbr-release.md) delivers them without waiting -for the remaining transport features. The +The seven BBR correctness fixes shipped in moq-noq 1.3.1 (#4206). The +remaining BBR quests here and [BBR ACK cleanup](/quest/m1/bbr-ack-cleanup.md) +all edit `bbr3/mod.rs`, so one owner should work there at a time. +Controller-level regressions extend the shared test `Sim` in `bbr3/mod.rs` +with only what each needs, rather than adding another simulation loop; a fix +at the transport boundary still needs a transport test through the real +callbacks. The existing loops stay, since the fork merges upstream weekly and +a port would conflict. Each BBR fix ships in a fork patch release without +waiting for the remaining transport features. The [Google comparison](/quest/m2/quic-bbr-google.md) is a separate study. Rules the line keeps: @@ -51,14 +51,7 @@ This is a transport API change, not a MoQ wire change. ## Required -- [Preserve QUIC packet identity in BBR](/quest/m1/quic/bbr-packet-identity.md) - ACKs and losses identify the right packet across QUIC spaces -- [Finish each BBR ACK sample before using it](/quest/m1/quic/bbr-ack-sampling.md) - current delivery samples reach the model once with consistent metadata -- [Mark application starvation before the next BBR send](/quest/m1/quic/bbr-app-limited.md) - resumed bursts retain correct sample labels -- [Finish BBR bandwidth-probe feedback once](/quest/m1/quic/bbr-probe-feedback.md) - cruise rounds neither age probe history repeatedly nor retain probe-loss classification -- [Recalibrate BBR startup pacing from measured RTT](/quest/m1/quic/bbr-startup-pacing.md) - measured RTT replaces the nominal startup rate for media senders -- [Protect bandwidth samples during BBR ProbeRTT](/quest/m1/quic/bbr-probe-rtt.md) - intentionally reduced sending cannot masquerade as reduced capacity -- [Preserve BBR state across a spurious loss episode](/quest/m1/quic/bbr-loss-undo.md) - consecutive losses preserve the original recovery snapshot -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - publish and pin the corrected controller independently of later features +- [BBR idle burst](/quest/m1/quic/bbr-app-limited.md) - a fork regression proves a burst after a long idle is paced at the learned bandwidth, closing #4219 - [Align BBR loss handling with draft-06](/quest/m1/quic/bbr-loss-parity.md) - losses use their own sample and undo re-enters ProbeUp through Refill - [Mark BBR starvation wherever the source runs dry](/quest/m1/quic/bbr-app-limited-edges.md) - partial polls count, local send caps do not, receiver credit is pinned - [Deliver the application close before io_uring teardown](/quest/m1/quic/uring-close.md) - @@ -99,7 +92,7 @@ This is a transport API change, not a MoQ wire change. - [FEC experiment](/quest/m2/quic-fec.md) - a measured verdict on transport redundancy - [Kernel pacing](/quest/m2/quic-kernel-pacing.md), [Send batching](/quest/m2/quic-send-batching.md), - [Send buffer pools](/quest/m2/quic-buffer-pool.md), [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - + [Send buffer pools](/quest/m2/quic-buffer-pool.md), [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - the syscall, allocation, and controller spikes - [Multipath spike](/quest/m2/multipath-spike.md) - a noq capability that MoQ does not use yet diff --git a/quest/m1/quic/bbr-ack-sampling.md b/quest/m1/quic/bbr-ack-sampling.md deleted file mode 100644 index 63fcb2b098..0000000000 --- a/quest/m1/quic/bbr-ack-sampling.md +++ /dev/null @@ -1,35 +0,0 @@ -# [M] Finish each BBR ACK sample before using it - -## Goal - -Each ACK updates the BBR model and control parameters from a completed, -consistent sample. Bandwidth, delivered bytes, application-limited status, -and inflight describe the same observation. - -## Plan - -In [the audited callback order](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/congestion/bbr3/mod.rs#L1656), on_ack updates the model per -packet before on_end_acks computes delivery_rate and delivered. Current -packet metadata is combined with the preceding ACK's values. Follow the -ordering in [draft section 5.2.3](https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.2.3) and -[Google QUICHE](https://github.com/google/quiche/blob/535a2730e77d47e0dc03746555cc9c34b17bc9e9/quiche/quic/core/congestion_control/bbr2_sender.cc#L271), adapted to noq's event boundary. - -Reproduce both failures in the fork: the first completed 1200-byte/10-ms -sample leaves max_bw at zero; and, with an aged prior bandwidth maximum, a -120 KB/s application-limited sample is admitted under the next burst's -non-limited metadata even though that burst delivers 1.2 MB/s. Verify fresh -samples are consumed once, with the correct labels and post-ACK inflight. - -Cover batched ACKs, reordered ACKs, invalid/too-short intervals, loss-only -events, and idle restart. Preserve the transport's RTT and loss event -ordering while fixing the source of stale state. Reuse the existing -controller simulations and add regressions to the fork's CI. Coordinate -with packet identity work if the callback contract changes; settle the API -with the maintainer and update its consumers and docs in the same PR. - -## Related - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver the corrected controller to MoQ -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer general fixes upstream -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measure the corrected controller on media traffic -- [Packet identity](/quest/m1/quic/bbr-packet-identity.md) - shares the controller boundary diff --git a/quest/m1/quic/bbr-app-limited-edges.md b/quest/m1/quic/bbr-app-limited-edges.md index 58a62a00fa..fc47d4d255 100644 --- a/quest/m1/quic/bbr-app-limited-edges.md +++ b/quest/m1/quic/bbr-app-limited-edges.md @@ -29,11 +29,10 @@ when a transmit poll sends nothing. In moq-dev/noq Transport-boundary tests through the real callbacks: a partial poll that drains, a sender blocked only by `send_window`, and a stream blocked by -receiver credit. Stacks on the seven fixes' noq branches until they merge; -does not gate [the BBR release](/quest/m1/quic/bbr-release.md). No public -API or wire change is intended. +receiver credit. Builds on the seven fixes released in moq-noq 1.3.1. No public API or wire +change is intended. ## Related -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - includes the first app-limited fix this extends -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measures natural draining on the corrected controller +- [BBR idle burst](/quest/m1/quic/bbr-app-limited.md) - the first app-limited fix this extends +- [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - measures natural draining on the corrected controller diff --git a/quest/m1/quic/bbr-app-limited.md b/quest/m1/quic/bbr-app-limited.md index dca67a99a8..f0ea70c90c 100644 --- a/quest/m1/quic/bbr-app-limited.md +++ b/quest/m1/quic/bbr-app-limited.md @@ -1,38 +1,39 @@ -# [M] Mark application starvation before the next BBR send +# [S] Prove a BBR burst after a long idle is paced at the learned bandwidth ## Goal -Packets sent after application starvation carry the correct historical -application-limited label, even when no ACK arrives during the idle gap. -The bandwidth model cannot mistake a source-limited sample for capacity. +After minutes of keep-alive-only idle, a BBRv3 (`delay`) sender paces its +next burst near the bandwidth it learned before, not at a trickle, and a +regression test in the fork proves it. ## Plan -In noq `1a26a8b064d21e316fe6769f068617975bd8a27b`, an empty unblocked -[transmit poll](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/connection/mod.rs#L1337) records starvation, but BBR receives the marker only in -[on_end_acks](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/congestion/bbr3/mod.rs#L1732). -A resumed send can be stamped before that notification. Google's -[QUICHE BBR3](https://github.com/google/quiche/blob/535a2730e77d47e0dc03746555cc9c34b17bc9e9/quiche/quic/core/congestion_control/bbr3_sender.cc#L505) notifies its sampler immediately when application limited. - -Reproduce through the transport boundary: send and ACK one 1200-byte packet -with a 10-ms RTT, run an empty unblocked transmit poll, wait until 30 ms to -send another packet, then ACK it 10 ms later. There is no intervening ACK -to notify the controller of starvation. The existing public-callback -reproduction yields a non-limited sample; preserve a failing regression -without privately seeding the sampler marker. This establishes a label bug, -not a measured throughput regression. - -Communicate starvation before subsequent sends, preserving the delivery -boundary that ends the sampler's limited phase. Distinguish producer -starvation from cwnd, pacing, anti-amplification, receiver credit, and local -buffer limits; do not silently change receiver-limited policy. Cover streams, -datagrams, resumed backlog, repeated empty polls, and ACK batching. Coordinate -any controller event changes with the packet identity and ACK sampling fixes. -Keep state private where possible; document any public Controller change and -its consumers. No wire change is intended. Wire regressions into fork CI. +moq-dev/noq#5 (in moq-noq 1.3.1; main pins 1.3.2) fixed the label bug this +quest was opened for. The transport calls `Controller::on_app_limited` on +every empty poll that nothing held back, and BBR marks starvation before the +next send. Its tests cover streams, datagrams, batched ACKs, and backlogs +held by the window or the pacer. What is left is +[#4219](https://github.com/moq-dev/moq/issues/4219), reported on 1.3.0 and +not yet reproduced on either version: the first 250 KB after five idle +minutes took 5.2 s at 50 ms RTT, against 0.5 s with CUBIC. + +The fix plausibly covers it. The fork's max-bw filter ages only on +non-app-limited samples, and before #5 every other keep-alive was stamped +non-app-limited. Nothing measures it, though. Add a virtual-time transport +test in the fork: learn the bandwidth, idle on keep-alives for about five +minutes, send 250 KB, and assert the pacing rate stays at or above about +0.9x the earlier max bandwidth. Check that it fails on 1.3.0. + +If it still stalls, find the remaining cause, such as ProbeRTT entered during +the idle or a stale `bw_shortterm`, and fix it in the fork. Dropping the +estimate after a long idle is a policy change for the m2 study, not this +quest. The `iroh` feature uses upstream noq, which lacks #5; offering it +there belongs to the upstream quest. + +## Closes + +- [#4219](https://github.com/moq-dev/moq/issues/4219) - the first send after an idle period is paced at a trickle ## Related -- [Finish each BBR ACK sample](/quest/m1/quic/bbr-ack-sampling.md) - a separate ordering defect in the same callback lifecycle -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver correct labels before policy experiments -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer the general fix upstream +- [Upstream the fork](/quest/m1/quic/upstream.md) - offer the starvation fix upstream diff --git a/quest/m1/quic/bbr-loss-parity.md b/quest/m1/quic/bbr-loss-parity.md index 647d1123a5..7b36bf4f53 100644 --- a/quest/m1/quic/bbr-loss-parity.md +++ b/quest/m1/quic/bbr-loss-parity.md @@ -33,5 +33,4 @@ no `Controller` or wire change. ## Related -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - the corrected baseline this builds on - [Upstream the fork](/quest/m1/quic/upstream.md) - offers this fix alongside the seven diff --git a/quest/m1/quic/bbr-loss-undo.md b/quest/m1/quic/bbr-loss-undo.md deleted file mode 100644 index ceae058e03..0000000000 --- a/quest/m1/quic/bbr-loss-undo.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] Preserve BBR state across a spurious loss episode - -## Goal - -Declaring a loss episode spurious restores the state saved before that -episode, even when several packets were declared lost. Later losses in the -same episode cannot overwrite the original recovery snapshot. - -## Plan - -[note_loss](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/congestion/bbr3/mod.rs#L1513) saves undo state on every lost packet, including -after an earlier packet reduced the model. With a 100,000-byte long-term -inflight bound, a 10,000-byte BDP, a 20,000-byte cwnd, and two successive -1200-byte packet losses during ProbeUp, undo retained 7000 bytes rather than -the original bound. Existing single-loss simulations miss this case. - -Align snapshot lifetime with recovery semantics using -[Google Linux's recovery entry](https://github.com/google/bbr/blob/90210de4b779d40496dee0b89081780eeddf2a60/net/ipv4/tcp_bbr.c#L2240) and -[draft section 5.5.11](https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.5.11). Verify saved model bounds, -window, and any restorable phase across multiple losses in one event and -across ACK events. A new episode must get a new snapshot; real loss must -still constrain sending. Cover ProbeRTT interaction and add regressions to -the fork's CI. Keep the fix internal with no wire change. - -## Related - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver the corrected controller to MoQ -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer general fixes upstream -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measure the corrected controller on media traffic diff --git a/quest/m1/quic/bbr-packet-identity.md b/quest/m1/quic/bbr-packet-identity.md deleted file mode 100644 index c3ecbc27f8..0000000000 --- a/quest/m1/quic/bbr-packet-identity.md +++ /dev/null @@ -1,32 +0,0 @@ -# [M] Preserve QUIC packet identity in BBR - -## Goal - -BBR associates every send, ACK, and loss with the correct packet across -Initial, Handshake, and application-data spaces. Reused packet numbers never -alias or invalidate an ordered lookup. - -## Plan - -The audit of noq-proto 1.3.0 and upstream `1a26a8b` found separate internal -packet queues, but [every public Controller callback forces Data](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/congestion/bbr3/mod.rs#L1881). -Sending Initial packets 0 and 1 followed by Handshake packet 0 makes an ACK -for the Handshake packet select Initial packet 0 instead. - -Fix identity at the transport/controller boundary in moq-dev/noq. Cover -coalesced sends, repeated packet numbers across spaces, losses, key discard, -and path ownership through the real transport callbacks. Private helpers -that accept a space while the public path discards it are insufficient. - -Land a regression that sends the overlapping sequence above and verifies the -ACK uses the Handshake packet's send time and delivery snapshot. Check every -controller consumer if the public Controller contract changes, including -other controllers and custom implementations. Settle any public API change -with the maintainer before implementation; document it inline. No QUIC wire -change is intended. Run the regressions in the fork's CI. - -## Related - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver the corrected controller to MoQ -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer general fixes upstream -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measure the corrected controller on media traffic diff --git a/quest/m1/quic/bbr-probe-feedback.md b/quest/m1/quic/bbr-probe-feedback.md deleted file mode 100644 index 7dc6f54b08..0000000000 --- a/quest/m1/quic/bbr-probe-feedback.md +++ /dev/null @@ -1,28 +0,0 @@ -# [S] Finish BBR bandwidth-probe feedback once - -## Goal - -Finishing a bandwidth probe advances the bandwidth-history window once. -Later cruise losses are not classified as feedback from that probe. - -## Plan - -[adapt_long_term_model](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/congestion/bbr3/mod.rs#L991) leaves ack_phase at ProbeStopping and -bw_probe_samples set. Every subsequent cruise round can advance cycle_count, -and a later loss can reduce the long-term model as if it came from probing. -Compare [Google Linux](https://github.com/google/bbr/blob/90210de4b779d40496dee0b89081780eeddf2a60/net/ipv4/tcp_bbr.c#L1664), -[Google QUICHE](https://github.com/google/quiche/blob/535a2730e77d47e0dc03746555cc9c34b17bc9e9/quiche/quic/core/congestion_control/bbr2_probe_bw.cc#L119), and the draft's -AdaptLongTermModel transition. - -Exercise a completed loss-free probe followed by several cruise rounds. -Assert the filter advances only once and retains the intended probe-cycle -history. Inject a later cruise loss and verify only the appropriate -short-term response applies. Include application-limited feedback and -ProbeRTT entry so neither leaves stale probe classification. Land these -regressions in the fork's CI without changing public APIs or the wire. - -## Related - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver the corrected controller to MoQ -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer general fixes upstream -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measure the corrected controller on media traffic diff --git a/quest/m1/quic/bbr-probe-rtt.md b/quest/m1/quic/bbr-probe-rtt.md deleted file mode 100644 index 955816ea5e..0000000000 --- a/quest/m1/quic/bbr-probe-rtt.md +++ /dev/null @@ -1,30 +0,0 @@ -# [S] Protect bandwidth samples during BBR ProbeRTT - -## Goal - -Packets deliberately rate-limited by ProbeRTT cannot lower the bandwidth -model as if they measured a network capacity reduction. Normal sampling -resumes after the protected delivery interval. - -## Plan - -[handle_probe_rtt](https://github.com/n0-computer/noq/blob/1a26a8b064d21e316fe6769f068617975bd8a27b/noq-proto/src/congestion/bbr3/mod.rs#L1182) omits the application-limited marker present -in [Google Linux](https://github.com/google/bbr/blob/90210de4b779d40496dee0b89081780eeddf2a60/net/ipv4/tcp_bbr.c#L920) and -[draft section 5.3.4.3](https://www.ietf.org/archive/id/draft-ietf-ccwg-bbr-06.html#section-5.3.4.3). The audited controller -sends unmarked packets during ProbeRTT even with a backlogged application. -QUICHE's BBR3 ProbeRTT also lacks an explicit marker; the requirement here -is the draft/Linux behavior, not universal Google parity. - -Reproduce that unmarked send, then test entry, the full ProbeRTT interval, -exit, and delayed ACKs for packets sent during the interval. Verify reduced -samples do not replace a learned capacity solely because ProbeRTT reduced -the window, while a legitimate higher sample can still raise the model. -Do not mark the connection application-limited forever or change the -ProbeRTT policy merely to mask a sampling bug. Add the regressions to the -fork's CI; keep the fix internal with no wire change. - -## Related - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver the corrected controller to MoQ -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer general fixes upstream -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measure the corrected controller on media traffic diff --git a/quest/m1/quic/bbr-release.md b/quest/m1/quic/bbr-release.md deleted file mode 100644 index 8966e3ae3d..0000000000 --- a/quest/m1/quic/bbr-release.md +++ /dev/null @@ -1,36 +0,0 @@ -# [M] Release the BBR correctness fixes - -## Goal - -Published MoQ consumers receive the seven corrected BBR behaviors through -immutable releases of the noq fork and its adapters. Fixes do not wait for -qmux, stream scheduling, or other unrelated QUIC features. - -## Plan - -After the fork bootstrap, publish the corrected dependency chain and update -MoQ's manifest and lockfile pins. Record the parent commit, carried patches, -and upstream status. Follow the existing fork packaging rules; no mutable -branch or workspace-only patch may stand in for a release. - -Verify the fork's regression suite and MoQ's default and supported runtime -builds against the released artifacts, including compatibility of controller -consumers if a callback API changed. Run a media-shaped transfer through the -real QUIC stack to confirm handshake sampling, pacing, and recovery work -together. This integration check is required; the broader media-flow study -and Google comparison do not gate these bug fixes. - -## Required - -- [Preserve QUIC packet identity in BBR](/quest/m1/quic/bbr-packet-identity.md) -- [Finish each BBR ACK sample before using it](/quest/m1/quic/bbr-ack-sampling.md) -- [Mark application starvation before the next BBR send](/quest/m1/quic/bbr-app-limited.md) -- [Finish BBR bandwidth-probe feedback once](/quest/m1/quic/bbr-probe-feedback.md) -- [Recalibrate BBR startup pacing from measured RTT](/quest/m1/quic/bbr-startup-pacing.md) -- [Protect bandwidth samples during BBR ProbeRTT](/quest/m1/quic/bbr-probe-rtt.md) -- [Preserve BBR state across a spurious loss episode](/quest/m1/quic/bbr-loss-undo.md) - -## Related - -- [Release the stack](/quest/m1/quic/release.md) - later feature releases follow the same packaging rules -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - broader media measurements after the corrected release diff --git a/quest/m1/quic/bbr-startup-pacing.md b/quest/m1/quic/bbr-startup-pacing.md deleted file mode 100644 index 13f6adfe69..0000000000 --- a/quest/m1/quic/bbr-startup-pacing.md +++ /dev/null @@ -1,33 +0,0 @@ -# [S] Recalibrate BBR startup pacing from measured RTT - -## Goal - -The first available measured RTT replaces BBR's nominal 1-ms startup -pacing estimate. A media sender that stays application-limited does not keep -an inflated pacing rate for its entire session. - -## Plan - -The audited 12-KB initial window retains a 33,276,000-byte/s pacing rate -after a measured 10-ms RTT. Startup only raises its rate, and an -application-limited flow need not leave Startup. [Google Linux](https://github.com/google/bbr/blob/90210de4b779d40496dee0b89081780eeddf2a60/net/ipv4/tcp_bbr.c#L443) -reinitializes pacing once an RTT measurement becomes available. - -Review and reuse [upstream PR #802](https://github.com/n0-computer/noq/pull/802) -if still applicable, preserving its author's attribution rather than -reimplementing it. Check the actual ACK/RTT callback order: the configured -initial RTT is not evidence of a measurement, and the first ACK can precede -the transport RTT update. Recalculate the send quantum consistently. - -Add CI regressions for measured RTTs above and below 1 ms, a continuously -application-limited source, and a secondary path initialized after the -handshake. Preserve subsequent bandwidth-driven Startup growth. Report any -public RTT-estimator API change and document it inline; no wire change is -intended. - -## Related - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - deliver the corrected controller to MoQ -- [Upstream the fork](/quest/m1/quic/upstream.md) - offer general fixes upstream -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - measure the corrected controller on media traffic -- [noq #800](https://github.com/n0-computer/noq/issues/800) - existing upstream report; do not duplicate it diff --git a/quest/m1/quic/deadline.md b/quest/m1/quic/deadline.md index 4c518af2e4..8450cadfa4 100644 --- a/quest/m1/quic/deadline.md +++ b/quest/m1/quic/deadline.md @@ -30,9 +30,10 @@ Implement in the fork. ACK-frequency extension noq already implements) on the next packet and arm a shortened probe at `max(deadline - rtt - now, min_pto)`. Never probe past the congestion window; the probe is a scheduling choice, not extra credit. -- moq-net: `Subscription::serve_group` sets the deadline from the - subscription's latency target and the group's expiry, whichever is sooner; - a subscription with neither sets none. The reset error code maps to the +- moq-net: the per-group `GroupServe` machine in the lite and IETF + publishers (`lite/publisher.rs`, `ietf/publisher.rs`) sets the deadline + when it opens the stream, from the subscription's latency target and the + group's expiry, whichever is sooner; a subscription with neither sets none. The reset error code maps to the existing group-expired code on the MoQ wire, so a viewer sees the same signal it sees for a relay-side expiry today. diff --git a/quest/m1/quic/peer-limits.md b/quest/m1/quic/peer-limits.md index 88eecc44be..7c36f74bc2 100644 --- a/quest/m1/quic/peer-limits.md +++ b/quest/m1/quic/peer-limits.md @@ -30,7 +30,8 @@ Tests: a cluster session sees the raised `MAX_STREAMS` after SETUP and a viewer session does not; the io_uring path applies the same values; a `peer` table below the defaults is refused at resolve time. -## Related +## Required - [io_uring flow control](/quest/m1/uring-flow-control-windows.md) - the - static windows on the same workers + io_uring workers hardcode their windows today, so the peer values have + nothing to raise there until the static windows reach them diff --git a/quest/m1/quic/release.md b/quest/m1/quic/release.md index d2f3878932..fee1d41bdf 100644 --- a/quest/m1/quic/release.md +++ b/quest/m1/quic/release.md @@ -24,8 +24,6 @@ the parent applies. ## Required -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - preserve the corrected controller in later stack releases - - [Reliable stream reset](/quest/m1/quic/reliable-reset.md) - the WebTransport-required transport extension - [Hierarchical stream scheduling](/quest/m1/quic/scheduler.md) - the new diff --git a/quest/m1/quic/upstream.md b/quest/m1/quic/upstream.md index 320c4d357d..294e9c0e44 100644 --- a/quest/m1/quic/upstream.md +++ b/quest/m1/quic/upstream.md @@ -15,7 +15,7 @@ lands in the fork on MoQ's schedule; once a feature has shipped in a MoQ release and its shape has stopped moving, split it into an upstream PR with the tests it landed with. -Offer the seven [BBR correctness fixes](/quest/m1/quic/bbr-release.md) and +Offer the seven BBR correctness fixes (moq-noq 1.3.1, #4206) and their [loss](/quest/m1/quic/bbr-loss-parity.md) and [starvation](/quest/m1/quic/bbr-app-limited-edges.md) follow-ups with their regressions before promoting BBR as the default. Reuse existing @@ -36,17 +36,16 @@ Then the feature proposal order, each linked to its producing quest: on, so its regressions are found upstream and not only here; 1. per-stream acknowledgment progress ([ACK progress](/quest/m1/quic/ack-progress.md)); 2. `RESET_STREAM_AT` ([reliable reset](/quest/m1/quic/reliable-reset.md)); -3. keep-alive by deadline ([keep-alive](/quest/m2/quic-keep-alive.md)); -4. hierarchical send groups ([scheduler](/quest/m1/quic/scheduler.md)); -5. careful resume as a `Controller` wrapper ([careful resume](/quest/m2/quic-careful-resume.md)); -6. ECT(1) marking and its accounting ([L4S](/quest/m2/quic-ecn.md)); -7. per-stream deadlines ([deadlines](/quest/m1/quic/deadline.md)); -8. the measured media-headroom mechanism ([probe](/quest/m2/quic-probe.md)); -9. the qmux crate over the shared stream state machine ([qmux](/quest/m1/quic/qmux.md)). +3. hierarchical send groups ([scheduler](/quest/m1/quic/scheduler.md)); +4. per-stream deadlines ([deadlines](/quest/m1/quic/deadline.md)); +5. the qmux crate over the shared stream state machine ([qmux](/quest/m1/quic/qmux.md)). -The next experiments (receive timestamps, GCC, FEC, kernel pacing, send -batching, buffer pools, the BBR3 app-limited check) join the list only with -a positive verdict. +The m2 features (keep-alive by deadline, careful resume as a `Controller` +wrapper, ECT(1) marking, the media-headroom mechanism) are offered when they +land, but do not gate this quest: an m1 quest must not wait on m2 work. The +next experiments (receive timestamps, GCC, FEC, kernel pacing, send +batching, buffer pools, the natural-drain check) join the list only with a +positive verdict. Record in this quest what upstream accepted, what it asked to see as an extension crate, and what it declined; a declined change stays in the fork @@ -55,22 +54,22 @@ offered and answered. ## Required -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - the corrected controller and its regression evidence - [Align BBR loss handling with draft-06](/quest/m1/quic/bbr-loss-parity.md) - [Mark BBR starvation wherever the source runs dry](/quest/m1/quic/bbr-app-limited-edges.md) - [Per-stream ACK progress](/quest/m1/quic/ack-progress.md) - [Reliable stream reset](/quest/m1/quic/reliable-reset.md) -- [Keep-alive by deadline](/quest/m2/quic-keep-alive.md) - [Hierarchical stream scheduling](/quest/m1/quic/scheduler.md) -- [Careful resume on reconnect](/quest/m2/quic-careful-resume.md) -- [L4S on the backbone](/quest/m2/quic-ecn.md) - [Per-stream deadlines](/quest/m1/quic/deadline.md) -- [Discover media headroom](/quest/m2/quic-probe.md) - [qmux on the QUIC stream state machine](/quest/m1/quic/qmux.md) ## Related - [Receive timestamps](/quest/m2/quic-receive-ts.md), [GCC](/quest/m2/quic-gcc.md), - [FEC](/quest/m2/quic-fec.md), [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - + [FEC](/quest/m2/quic-fec.md), [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - experiments that join the list with a positive verdict +- [Keep-alive by deadline](/quest/m2/quic-keep-alive.md), + [Careful resume on reconnect](/quest/m2/quic-careful-resume.md), + [L4S on the backbone](/quest/m2/quic-ecn.md), + [Discover media headroom](/quest/m2/quic-probe.md) - m2 features offered + upstream when they land diff --git a/quest/m1/raw-stream-codes.md b/quest/m1/raw-stream-codes.md index a4c1b517da..adf730736e 100644 --- a/quest/m1/raw-stream-codes.md +++ b/quest/m1/raw-stream-codes.md @@ -15,8 +15,9 @@ on stream errors. WebTransport sessions keep the mapping they need. `web-transport-iroh` and `web-transport-quinn` (moq-dev/web-transport) do the same. A raw peer's code 5 reads as `None` or another value, and ours reaches it as a large HTTP/3 code. -- [Close codes](/quest/m1/close-codes.md) fixed the same mix-up for - `ApplicationClosed` (noq#11, #4262). Let each stream know whether its +- [Close codes](/quest/m1/close-codes.md) fixes the same mix-up for + `ApplicationClosed` (noq#11 is released; #4262 is still open on `dev`, so + this follows it there unless trait 0.5 reaches main first). Let each stream know whether its session is raw and skip the mapping there, in all three adapters, with a round-trip test per adapter against a plain QUIC peer. - Release the fixed crates and bump the pins here in the same quest; published diff --git a/quest/m1/relay-memory.md b/quest/m1/relay-memory.md index a3c5f6fd82..b4d9037702 100644 --- a/quest/m1/relay-memory.md +++ b/quest/m1/relay-memory.md @@ -28,17 +28,14 @@ committed, since they need `#[doc(hidden)]` size probes on private types. Rebuild them from this description and restate the per-broadcast and per-route cost, the per-peer session bookkeeping (`announce_ids`, `held`, `watched`), and the shed threshold on a degree-5, 1 GB node, before anyone -quotes a number again. Two committed directions depend on the answer: -chat-shaped traffic (one broadcast per channel or per chatter) and -[PoP skipping](/quest/m1/pop-skipping/README.md), which triples average -degree and adds a second, more specific route per carried broadcast. +quotes a number again. Chat-shaped traffic (one broadcast per channel or +per chatter) depends on the answer. -Asking peers for announcements only while something watches was considered -and dropped: every loop-free way to forward coalesced interest through a -cyclic mesh (a hop budget, an originator set, cost-decreasing interest over -coarse claims) adds teardown churn or new wire state, for a saving nobody -has measured. Shrink the table itself instead. +On-demand announcements are now [Cluster routing](/quest/m1/cluster-routing.md)'s +plan: a relay learns only the prefixes its own clients request, which bounds +the table by demand. These numbers size that saving. ## Related +- [Cluster routing](/quest/m1/cluster-routing.md) - on-demand announcements shrink the table this measures - [Perf](/quest/m1/perf/README.md) - the hot-path work that owns the remaining per-cell cost diff --git a/quest/m1/remove-live.md b/quest/m1/remove-live.md new file mode 100644 index 0000000000..e193202ff9 --- /dev/null +++ b/quest/m1/remove-live.md @@ -0,0 +1,49 @@ +# [M] Importers publish stream timestamps; the catalog clock maps them to wall + +## Goal + +The fMP4, MPEG-TS, and FLV importers in `rs/moq-mux` have no `live()`: every +importer publishes the stream's own timestamps verbatim (after PTS unwrap), +and the catalog's root `clock` is what maps them to wall time. `moq import` +(`rs/moq-cli/src/publish.rs`) stops calling it. An encoder that restarts its +timestamps starts a new broadcast epoch instead of being re-anchored forward +onto the old one. The SRT, RTMP, and HLS gateways, which reuse these +importers, get the same behavior. + +## Plan + +Decided (2026-09-28, replacing the gateway live-clock quest, which planned +the opposite: every gateway opting into `live()`): + +- Timestamps stay verbatim from the stream. Rewriting them onto an + arrival-time anchor breaks same-hop importers, which must derive every + timestamp from the input alone (the hop-aligned import quest in + [#4388](https://github.com/moq-dev/moq/pull/4388)), and hides the source's + own timeline from anything downstream. +- The catalog clock carries the mapping. An importer establishes the root + `clock` so the stream's PTS converts to wall time (the first frame is live + on arrival), rather than translating each timestamp. Open: whether the + importer sets the catalog clock from its first frame (the mapping is fixed + at construction today, `moq_mux::Clock` and `catalog::Config::with_clock`) + or the caller builds the catalog once the first PTS is known. Every track of + one input, and every rendition of one HLS import, shares that one mapping. +- An encoder restart (a PTS rewind or a signalled time-base discontinuity) is + a new epoch, not a forward re-anchor: the importer ends the broadcast with + an error, and the caller republishes, which the broadcast epoch line turns + into a fresh `@`. Until that line lands, a restart fails loud. +- Delete `live()` from `ts::Import`, `fmp4::Import`, and `flv::Import`, the + crate-private `clock::Anchor`, and `SourceMap` if nothing else uses it. + fMP4 passthrough stops rewriting `tfdt`. A published `moq-mux` API break, + so this targets `dev`. + +Tests: per importer, a source starting at a large PTS publishes that PTS and +a catalog clock that maps it to near the arrival time; a rewind ends the +broadcast with an error. Update `doc/lib/rs/moq-mux.md` (the `live()` +paragraph), `doc/bin/cli.md`, and the gateway pages under `doc/bin/`, and +replace `ts_import_publishes_on_the_broadcast_clock` in moq-cli. + +## Related + +- [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - the new epoch an encoder restart becomes +- [Catalog wall clock](/quest/m1/catalog-wall-clock.md) - the PTS-to-wall conversion this relies on, at full precision +- [TS import shared shift](/quest/m1/ts-import-shared-shift.md) - the TS re-anchor shift that must stay input-derived diff --git a/quest/m1/route-cost.md b/quest/m1/route-cost.md deleted file mode 100644 index 9268d3d06f..0000000000 --- a/quest/m1/route-cost.md +++ /dev/null @@ -1,27 +0,0 @@ -# [S] Route cost in the JS origin - -## Goal - -The `@moq/net` origin serves a path through the best route it knows, ranked by -cost and then hop count the way Rust's origin does, instead of the newest -announce. The lite-06 route cost that arrives on every announce is read rather -than dropped. - -## Plan - -`announce.ts` already decodes `Cost { warm, cold }` and `hop.ts` already -carries the hop chain; neither reaches `OriginState.remote`, which keeps -providers newest-first. Carry both on the provider, rank with the same order -as `route_order` in `rs/moq-net/src/model/origin.rs` (cost, chain length, a -deterministic tiebreak), including `Cost::UNKNOWN` for an announce that -carries no cost (free to reach, cold path at the ceiling, so hop count decides -as it did before route cost existed), and re-pick when the chosen route is -retracted. - -Tests in `js/net` with the mock transport pair: two sessions announcing the -same path at different costs, the cheaper one serves, its retraction moves -the consumer to the other. - -## Related - -- [P2P](/quest/m1/p2p/README.md) - the watcher-side route pick that line needs diff --git a/quest/m1/rs2ts/remove-wasm.md b/quest/m1/rs2ts/remove-wasm.md index 2f8d0a2c21..56a7f6b490 100644 --- a/quest/m1/rs2ts/remove-wasm.md +++ b/quest/m1/rs2ts/remove-wasm.md @@ -10,8 +10,7 @@ deleted: the browser runs moq-net as generated TypeScript instead. Decided in planning: keep the experiment until generated lite ships, then delete it rather than polish it. Remove its entries from the size report, the justfiles, the wasm clippy lane (keep `moq-net` and `moq-mux` there if -anything still targets wasm32), and the docs, and update the P2P questline's -note about `moq-wasm`. +anything still targets wasm32), and the docs. Public API: removes the unpublished `@moq/wasm` package. Wire: none. diff --git a/quest/m1/session-death.md b/quest/m1/session-death.md index 22ac9be5c5..0eec5aa84a 100644 --- a/quest/m1/session-death.md +++ b/quest/m1/session-death.md @@ -15,6 +15,11 @@ Rust and JS end a session's tracks and groups the same way: https://github.com/moq-dev/moq/pull/4120 made a dying session end its tracks with the session's error in both languages, and left two gaps. +Since then #4351 and #4378 changed Rust abort semantics: an abort keeps the +finished groups and drops only the open ones, so readers get what finished +and then the abort, or a clean end once the declared end settled. #4385 (open) +mirrors that in JS. Build the clean end below on those semantics. + Decided by the maintainer: - **A local close is a close, not an error.** JS already ends tracks cleanly diff --git a/quest/m1/signal-race.md b/quest/m1/signal-race.md deleted file mode 100644 index 90695f5847..0000000000 --- a/quest/m1/signal-race.md +++ /dev/null @@ -1,30 +0,0 @@ -# [S] Signal.race releases its listeners when it loses a race - -## Goal - -`Signal.race` from `@moq/signals` no longer leaves a `changed` listener on -each signal when its own promise is raced and loses. Today it disposes them -only when one of the signals changes, so `js/net/src/origin.ts`'s -`#changed()`, raced against `closed` in `connection/forward.ts`, keeps -listeners for as long as the table stays quiet. Awaiting it directly still -works, with the same signature. - -## Plan - -Make it consistent with the free `race()` and `effect.race` from -[JS retention](https://github.com/moq-dev/moq/pull/4085), which already -subscribe to `Once`/`GetPromise` values and dispose them when the race -settles. `Signal.race` returns that same kind of awaitable instead of a -native promise: it attaches its signal listeners when first awaited or -subscribed, and releases them when it settles or when its last subscriber -detaches. Racing it through `race()` or `effect.race` then cleans up both -sides. Settle the exact return type at PR time, keeping `await` source -compatible; if the change is a published type break, stop and bring it back. - -Audit the callers in `js/net` (`announced`, `broadcast`, `group`, `origin`, -`track`, both subscribers) for the ones that race the result, and add a -listener-count test for the `origin.ts` case that fails today. - -## Required - -- JS retention (#4085) merged, which adds the free `race()` this builds on diff --git a/quest/m1/signed-priority.md b/quest/m1/signed-priority.md index 12ea0eb5ee..9fcfa54d10 100644 --- a/quest/m1/signed-priority.md +++ b/quest/m1/signed-priority.md @@ -15,10 +15,15 @@ Decided with the maintainer: - moq-lite carries `p + 128` (flip the top bit). The mapping is one-to-one and keeps order, so a given byte means what it means today; only the API number changes. The default becomes byte 128. -- IETF carries `128 - p`, saturating. An unset priority goes out as the - draft's usual 128, and an absent IETF priority decodes to 0. The cost is - that i8 -128 and -127 share byte 255, and IETF bytes 0 and 1 both decode to - 127. Pin both ends in tests. +- IETF carries `127 - p`. Both mappings are one-to-one over the whole `i8` + range, with no saturation. An unset priority goes out as IETF 127, not the + draft's usual 128, and an absent IETF priority decodes to 0, the unset + default. Pin both ends in tests. +- Invariant, kept from the [moxygen line](/quest/m1/moxygen/README.md)'s + default-priority quest (#4273): one urgency on both wires, so the IETF byte + is always `255 -` the lite byte. Mapping IETF as `128 - p` to hit the + draft's 128 would break it, which is why the default is one step off the + draft there. - hang's built-in priorities move above 0, so hang media outranks a track that never set one. Something like catalog 40, text 30, audio 20, video 10; the spacing is the implementer's call. Rust and JS keep matching values. @@ -36,8 +41,12 @@ moq-archive's `Info::priority` follows. Its version-1 `.info` stores the `doc/concept/moq-lite.md` (the 0..255 knob) and `doc/concept/standard.md` (IETF 128 maps to 127) move to the new range and mapping. -Report the wire impact in the PR: none in format, but the default byte on -moq-lite moves again. +Report the wire impact in the PR: none in format, but the default byte moves +again, from 127 to 128 on moq-lite and from 128 to 127 on IETF. + +## Required + +- [Moxygen compatibility](/quest/m1/moxygen/README.md) - ships the 127 default and the one-urgency invariant this re-maps ## Related diff --git a/quest/m1/track-demand.md b/quest/m1/track-demand.md index 40ed280607..90be7ad3e9 100644 --- a/quest/m1/track-demand.md +++ b/quest/m1/track-demand.md @@ -20,7 +20,3 @@ line, so moq-net breaks once. Its PR retargets to `dev`. Public API: breaking in moq-net and the layer crates, and in `@moq/net`. Wire: none. - -## Required - -- [Release](/quest/m0/release.md) - the break follows the release diff --git a/quest/m1/track-tail-hardening.md b/quest/m1/track-tail-hardening.md index d6fbb68bf4..f1e912491c 100644 --- a/quest/m1/track-tail-hardening.md +++ b/quest/m1/track-tail-hardening.md @@ -53,9 +53,8 @@ decide in the PR, applying the choice to both languages: and the end waits out the whole grace (JS was reported to wait and Rust not, but Rust's `covers(owed)` reads the same way despite the comment in `route_datagram`; confirm with a test first). Recommendation: no, datagrams - are best effort. On lite-07 the SUBSCRIBE_END stream count - ([lite-count-settle](/quest/m1/lite-count-settle.md)) settles without - looking at sequences, which makes this moot there; older versions keep the + are best effort. On lite-07 the SUBSCRIBE_END stream count (#4224) + settles without looking at sequences, which makes this moot there; older versions keep the grace as the documented stopgap. - **Does a group at or past the declared end abort the track, or only that group?** Rust aborts the track with `ProtocolViolation`; JS aborts the group @@ -75,5 +74,4 @@ drafts already require. ## Related - [Track tail interop](/quest/m1/track-tail-interop.md) - the Rust-JS check that both sides now agree -- [lite-07 count settle](/quest/m1/lite-count-settle.md) - replaces sequence coverage with the stream count on lite-07 - [Session death](/quest/m1/session-death.md) - how a tail ends when the session dies under it diff --git a/quest/m1/track-tail-interop.md b/quest/m1/track-tail-interop.md index 7489cbdfb3..4e9bb23e53 100644 --- a/quest/m1/track-tail-interop.md +++ b/quest/m1/track-tail-interop.md @@ -25,5 +25,24 @@ What stood in the way when the Rust half landed: first frame. It needs a mode that reads a track to its end and reports how it ended and which groups it saw. +Also cover the stream count: + +- On moq-lite-07 add a count case: both subscribers settle on the + SUBSCRIBE_END stream count (#4224), so a group the publisher skipped or + never opened ends the track without waiting out the grace. Folded in from + the lite-count-settle quest, whose local Rust and JS regressions landed; + this Rust-JS case was all it had left. +- The harness exposed a relay start-floor defect: when a newer group arrives + first, earlier in-flight groups can be lost. #4387 fixes it, so the count + proof waits on it. + QUIC on localhost rarely reorders, so this is a smoke check that the end is delivered and clean. The ordering race itself stays in the unit tests. + +## Required + +- #4387 merges: a relayed subscription resolves its start from its source, so earlier in-flight groups are not lost (it adds quest/m1/relay-late-joiner-history.md) + +## Related + +- [Reliable stream reset](/quest/m1/quic/reliable-reset.md) - makes the count exact by keeping a reset stream's header diff --git a/quest/m1/transport-upgrade/README.md b/quest/m1/transport-upgrade/README.md index b933806797..094817c6e0 100644 --- a/quest/m1/transport-upgrade/README.md +++ b/quest/m1/transport-upgrade/README.md @@ -34,8 +34,8 @@ forbidden to a moq-transport client). The origin's multi-route front prefers the newest of two equal routes and `resume` splices each track at a group boundary, capping the old segment so the old session's subscription ends at the boundary on its own. The JavaScript handover is the -[client goaway](/quest/m1/drain/client-goaway.md) quest's, so the JS half -requires it. +[drain](/quest/m1/drain/README.md) line's (client goaway, done there), so the +JS half requires it. Shared decisions: diff --git a/quest/m1/transport-upgrade/js.md b/quest/m1/transport-upgrade/js.md index 4cb58a3c59..f5c8562035 100644 --- a/quest/m1/transport-upgrade/js.md +++ b/quest/m1/transport-upgrade/js.md @@ -11,8 +11,9 @@ nothing changes. ## Plan -Lands in `js/net`, after the [client goaway](/quest/m1/drain/client-goaway.md) -quest ships the handover it reuses: dial the replacement while the old session +Lands in `js/net`, after the [drain](/quest/m1/drain/README.md) line ships +the GOAWAY handover it reuses (client goaway is done on the line, JS +group-boundary handover is its remaining child): dial the replacement while the old session keeps serving, swap the origin wiring once it is established, leave the old session to close on its own or at the handover cap. See the [questline](/quest/m1/transport-upgrade/README.md) for the shared decisions. @@ -31,6 +32,9 @@ session to close on its own or at the handover cap. See the may not name a redirect URI, and an empty one is legal) and closes at the configured cap. A WebTransport attempt that fails after WebSocket won is logged at debug and the session stays on WebSocket. +- A self-sent GOAWAY must gate new requests on the old session too, not only + a received one: [JS GOAWAY requests](/quest/m1/drain/js-goaway-requests.md) + covers the received case, so check it also covers this path. - On a successful upgrade delete the URL from `websocketWon`. - Tests in the browser harness against the in-tree relay: with the WebTransport dial delayed past the head start, a watched track keeps every @@ -43,4 +47,5 @@ session to close on its own or at the handover cap. See the ## Required -- [Client goaway](/quest/m1/drain/client-goaway.md) - the handover this upgrade reuses +- [Drain](/quest/m1/drain/README.md) - the GOAWAY handover this upgrade reuses +- [JS GOAWAY requests](/quest/m1/drain/js-goaway-requests.md) - no new request opens on a session that is going away diff --git a/quest/m1/ts-export-byte-schedule.md b/quest/m1/ts-export-byte-schedule.md index 4eb0f15454..8427325a68 100644 --- a/quest/m1/ts-export-byte-schedule.md +++ b/quest/m1/ts-export-byte-schedule.md @@ -27,7 +27,3 @@ UDP sink is out of scope; delivery stays with an external tool. it in the existing TS test recipe against a CBR fixture. - `doc/bin/cli.md`: say that export pads to `mpegts.muxRate` on a constant-rate schedule and what latency that adds. - -## Closes - -- [#3925](https://github.com/moq-dev/moq/issues/3925) - close this issue when the quest finishes diff --git a/quest/m1/ts-import-shared-shift.md b/quest/m1/ts-import-shared-shift.md index 6dd1fa513a..e5c9190bd3 100644 --- a/quest/m1/ts-import-shared-shift.md +++ b/quest/m1/ts-import-shared-shift.md @@ -25,10 +25,16 @@ Guidance: itself leaves a later-arriving stream below its edge, and growing again then drifts the two. Decided: hold each stream's post-wrap frames until every live stream of the program has shown its new PTS, then take the - maximum growth once. A stream that stays silent is bounded by the - existing liveness timeout (#3489), not waited on forever. The - `Anchor`/`Lane` split behind `live()` in `moq_mux::clock` solves the same - problem for restarts and may be reusable. + maximum growth once. +- The hold needs its own bound: #3489 adds per-PID counters, not a timeout, + and the catalog `stalled` bit (`Stream::tick`, #3630) covers video only and + runs on the catalog's timer. Decided: bound the hold on the program clock + (PCR advance since the first stream's new generation, not wall time), and + commit the shift over the streams seen so far when it expires. A stream + that returns later applies the committed shift, clamped to its edge as + below. The `Anchor`/`Lane` split behind `live()` in `moq_mux::clock` solves + a similar problem for restarts, but the remove-live quest deletes it, so + copy what helps rather than depending on it. - A stream whose own edge is still above the shifted timestamp after the shared growth (its tail ran longer) is the case that forces growing by the maximum. Landing on its edge is accepted today; keep that trade-off. @@ -42,3 +48,4 @@ Guidance: ## Related - [#3489](/quest/m1/3489-ts-import-stream-liveness.md) - per-PID liveness in the same importer; touches `Stream` but not the shift +- [Remove live()](/quest/m1/remove-live.md) - deletes the restart anchor; a wrap shift stays input-derived diff --git a/quest/m1/video-keyframe-flag.md b/quest/m1/video-keyframe-flag.md deleted file mode 100644 index 7c97c29d20..0000000000 --- a/quest/m1/video-keyframe-flag.md +++ /dev/null @@ -1,24 +0,0 @@ -# [S] Encoded video knows its keyframes - -## Goal - -`moq_video::encode::Encoded` says whether an access unit is a keyframe, so the -capture `Control::cut()` throttle counts every keyframe, including the -encoder's own GOP cadence. Today it sees only forced and opening keyframes, so -a cut requested just after a cadence keyframe still forces another one. - -## Plan - -- Every backend already knows whether it produced a keyframe; carry it on - `Encoded`. Whether that is additive depends on whether `Encoded` is - `#[non_exhaustive]`; if it is not, this goes to `dev`. -- The throttle in the capture driver treats any keyframe as satisfying a - pending cut and restarting the spacing window, matching what JS already does. -- Test with a backend whose GOP produces a keyframe right before a requested - cut, asserting no extra keyframe. - -Public API: one field or accessor on `Encoded`. Wire: none. - -## Related - -- [#4184](https://github.com/moq-dev/moq/pull/4184) - the `Control::cut()` this completes diff --git a/quest/m1/watch-audio-time-stretch.md b/quest/m1/watch-audio-time-stretch.md index 15e13d7cff..0ceb0850b6 100644 --- a/quest/m1/watch-audio-time-stretch.md +++ b/quest/m1/watch-audio-time-stretch.md @@ -9,8 +9,9 @@ audio or rendering silence. Convergence is inaudible at ordinary drift and burst sizes. Boundaries: no packet loss concealment; an underrun still renders a ramped -gap. The target estimator and the ring's slack and re-stall are #3517 on -dev; the clock the stretch converges toward is +gap. The target estimator and the ring's slack and re-stall are the +[audio jitter target](/quest/m0/audio-jitter-target/README.md) line (#3517 +was closed in favor of it); the clock the stretch converges toward is [Plan: A/V clock](/quest/m0/plan-av-clock.md). ## Plan diff --git a/quest/m2/1838-tr-101-290-monitoring-requirements-broadcast-contribution.md b/quest/m2/1838-tr-101-290-monitoring-requirements-broadcast-contribution.md index ab9b803e0d..f77cafbf26 100644 --- a/quest/m2/1838-tr-101-290-monitoring-requirements-broadcast-contribution.md +++ b/quest/m2/1838-tr-101-290-monitoring-requirements-broadcast-contribution.md @@ -1,95 +1,44 @@ -# [M] TR 101 290 monitoring: requirements (broadcast/contribution health metrics) +# [S] Plan TR 101 290 monitoring ## Goal -Implement and verify the behavior tracked in [#1838](https://github.com/moq-dev/moq/issues/1838) -within the issue's stated scope and boundaries. +The TR 101 290 stream-health requirements in +[#1838](https://github.com/moq-dev/moq/issues/1838) become scoped +implementation quests with the open questions answered. This quest writes +quests, not code. ## Plan -Use the public issue's scope, implementation notes, and acceptance criteria -below as the starting plan. Reconcile paths and assumptions with the current -tree before implementation. +Run `/quest-plan` against the issue and the current tree. The issue is a +requirements list (ETSI P1/P2/P3 checks, per-check counters, a per-stream +health roll-up) and asks for agreement before any skeleton; its parent +proposal #1799 is closed. -### Issue context +Settled by the issue, carry over: -#### Goal +- TS-level checks run at the TS edges (`moq import ts`, `moq export ts`, + `moq-srt`), never in the media-agnostic relay. +- In today's media-aware lane our muxer regenerates PAT, PMT, PCR, and CC, + so egress checks validate our own output and SI checks are not applicable. + Full P1 to P3 conformance only means something for an opaque whole-mux + lane, which nothing plans yet. +- Results surface through the existing stats plumbing (`moq-stats`), not a new + transport. No remediation (FEC, 2022-7) and no GUI. -For MoQ to replace satellite/contribution links and hand off to IRDs, it needs the -stream-health telemetry operators expect from SRT / Zixi / RIST. ETSI TR 101 290 is the -industry yardstick for MPEG-TS integrity. This issue specifies *what* to monitor and -*where* to surface it, so we can agree scope before building a skeleton. Part of #1799. -No implementation here, requirements only. +Questions the planning session must answer: -#### Where monitoring runs (design question) - -The relay core is media-agnostic by design, so TS-level monitoring belongs at the **TS -edges**, not in the relay: - -- **Ingest** (e.g. `moq-srt`, `moq import ts`): validate the incoming contribution - feed and expose its health. -- **Egress** (e.g. `moq-srt` m=request, `moq export ts`): validate the - TS we hand downstream. - Important nuance from the two-lane model (#1799): -- **Media-aware lane** (today): PAT/PMT/PCR/CC are *regenerated* by our muxer, so at - egress TR 101 290 mostly validates *our own output* (still valuable, catches muxer - regressions like the DTS issue #1836). SI tables beyond PAT/PMT don't exist in this - lane, so those checks are N/A. -- **Opaque whole-mux lane** (future, Option B): the original PCR cadence, CC, and SI - survive, so TR 101 290 validates the *real* contribution feed faithfully. This is where - full P1/P2/P3 conformance is meaningful. - Proposed: implement the checks once over a TS byte/packet stream, run them at both edges, - and surface results through the existing stats plumbing (cf. #1783 connection stats, - \#1671 per-track stats; `moq-net`/`moq-relay` stats modules). - -#### Checks to implement (configurable thresholds; ETSI defaults) - -##### Priority 1 (loss of these = not decodable) - -- TS\_sync\_loss (loss of sync after N consecutive bad sync bytes; default 5) -- Sync\_byte\_error (sync byte != 0x47) -- PAT\_error / PAT\_error\_2 (PAT absent, repetition > 0.5 s, PID 0 wrong table\_id, scrambled) -- Continuity\_count\_error (CC discontinuity / wrong increment / illegal duplicate) -- PMT\_error / PMT\_error\_2 (PMT repetition > 0.5 s, scrambled) -- PID\_error (a referenced PID not seen within a user-defined window) - -##### Priority 2 (recommended continuous monitoring) - -- Transport\_error (TEI bit set) -- CRC\_error (PAT/PMT/CAT/NIT/EIT/BAT/SDT CRC) -- PCR\_repetition\_error (PCR interval > 40 ms) -- PCR\_discontinuity\_indicator\_error (> 100 ms jump w/o discontinuity\_indicator) -- PCR\_accuracy\_error (PCR drift outside +/- 500 ns) -- PTS\_error (PTS repetition interval > 700 ms) -- CAT\_error (CAT required when scrambling present) - -##### Priority 3 (application dependent; mostly opaque-lane only) - -- NIT/SDT/EIT/TDT/RST\_error, SI\_repetition\_error, unreferenced\_PID - -#### Surfacing - -- Per-check counters + last-event timestamp, per PID where applicable. -- Aggregate per-stream "health" suitable for an operator dashboard (green/amber/red per - priority, akin to an SRT/Zixi stats panel). -- Exposed through the existing stats API so relay/CLI consumers can read it; no new - bespoke transport. - -#### Out of scope (for now) - -- Remediation (FEC, 2022-7) - separate egress work. -- Full DVB SI semantic validation beyond presence/repetition/CRC. -- A GUI; this is the metrics source, not a dashboard. - -#### Open questions for discussion - -1. Does monitoring live in a dedicated `moq-ts`/`moq-monitor` crate, or inside the - ingest/egress crates that already touch TS? -2. Which subset is MVP, P1 + the PCR/CC/PTS parts of P2? -3. Configuration surface (thresholds, which PIDs, sampling) - CLI flags vs config file. - A private reference implementation of these checks exists and can inform thresholds and - edge cases; happy to share it as background (not as a code drop). +1. Where the checks live: a new crate, or beside the TS container in + `rs/moq-mux/src/container/ts`. +2. The MVP subset: P1 plus the PCR, CC, and PTS parts of P2 is the issue's + suggestion. +3. The configuration surface (thresholds, PIDs, sampling) and how the + counters map onto `moq-stats` tracks. +4. Whether the opaque whole-mux lane is wanted at all; without it, P3 is out. ## Closes - [#1838](https://github.com/moq-dev/moq/issues/1838) - close this issue when the quest finishes + +## Related + +- [TS import liveness](/quest/m1/3489-ts-import-stream-liveness.md) - the stream-stall case this model names `PID_error` diff --git a/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md b/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md index aa313e8dba..d72e102a55 100644 --- a/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md +++ b/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md @@ -1,79 +1,42 @@ -# [XL] moq-video: carry PipeWire DMA-BUFs safely into the Vulkan renderer +# [M] moq-video: validate PipeWire DMA-BUFs into the Vulkan renderer ## Goal -Implement and verify the behavior tracked in [#2819](https://github.com/moq-dev/moq/issues/2819) -within the issue's stated scope and boundaries. +The Linux zero-copy spine from [#2819](https://github.com/moq-dev/moq/issues/2819), +`PipeWire DMA-BUF -> Surface::DmaBuf -> Vulkan import -> render shader`, is +proven on real hardware, and a V4L2 camera feeds `Surface::DmaBuf` too. ## Plan -Use the public issue's scope, implementation notes, and acceptance criteria -below as the starting plan. Reconcile paths and assumptions with the current -tree before implementation. - -### Issue context - -#### Goal - -Complete the Linux zero-copy surface spine from #2481 as one producer-to-consumer series: - -`PipeWire DMA-BUF -> moq_video::Surface::DmaBuf -> Vulkan import -> render shader` - -This is the first Linux producer and consumer for the public DMA-BUF surface contract. VAAPI encode/decode and V4L2 M2M can reuse the same contract afterward. - -#### Required lifetime invariant - -Duplicating a DMA-BUF fd preserves the allocation, not the pixels. Returning a dequeued PipeWire buffer lets the compositor overwrite that allocation while a queued frame or GPU import still reads it. - -The PipeWire producer must therefore retain the dequeued buffer until the last `DmaBuf` clone drops, then return it to the stream on the PipeWire loop thread. The renderer must retain its clone until the GPU submission completes. An fd-only wrapper is racy and is not acceptable. - -#### Phase 1: surface and producer - -- \[x] Add the non-default `dmabuf` feature, enabled by `pipewire`, `vaapi`, and `render`. -- \[x] Add `Surface::DmaBuf` with a concrete public payload, typed DRM format, modifier, dimensions, plane offsets/strides, and mint-on-access `export() -> OwnedFd`. -- \[x] Keep backend export/download behavior private so no public implementable trait freezes backend internals. -- \[x] Negotiate DMA-BUF plus shared-memory fallback in PipeWire, preferring packed RGB for the first complete Vulkan path and retaining NV12 support. -- \[x] Retain the dequeued PipeWire buffer through the surface lifetime and return it on the loop thread. -- \[x] Keep the universal I420 fallback for linear NV12 and RGB DMA-BUFs. Reject non-linear CPU mapping instead of interpreting tiled memory as rows. -- \[x] Add unit coverage for descriptors, NV12 stride removal, buffer negotiation, and return-on-last-drop. - -#### Phase 2: renderer consumer - -- \[x] Add a Linux Vulkan DMA-BUF importer behind the non-default render feature. -- \[x] Import single-plane XRGB/ARGB/XBGR/ABGR through wgpu 30's `VULKAN_EXTERNAL_MEMORY_DMA_BUF` HAL path. -- \[x] Retain the producer surface until the GPU submission completes, so PipeWire cannot overwrite in-flight pixels. -- \[x] Preserve the renderer's CPU fallback and three-strike fast-path retirement. -- \[x] Document the wgpu device feature required for DMA-BUF import. A custom device-creation helper is unnecessary with wgpu 30. -- \[ ] Import multi-plane NV12 with explicit DRM modifier plane layouts. -- \[ ] Copy imported NV12 Y/UV planes into wgpu-sampleable R8/RG8 textures without touching the CPU. -- \[ ] Handle unsupported Intel tiling with a VAAPI VPP re-tile path. `Processor` blits exist (moq-vaapi 0.1.0); this item is the renderer using one when a modifier will not import. - -#### Validation gates - -- \[x] macOS `moq-video --all-features` compile and tests remain green. -- \[x] The wgpu 30 packed DMA-BUF HAL import compiles in an isolated Vulkan-enabled API check. -- \[x] Linux renderer-only cross-build (`x86_64-unknown-linux-gnu`, `--features render`). -- \[ ] Native Linux `--features pipewire,render` compile and tests. -- \[ ] Shared-memory PipeWire fallback still captures. -- \[ ] Intel or AMD desktop: packed DMA-BUF capture renders with zero CPU download. -- \[ ] Modifier mismatch exercises VPP re-tiling on hardware that needs it. -- \[ ] Holding several frames cannot produce torn/reused content or exhaust the pool permanently. -- \[ ] Hardware tests ship ignored with a reason where CI lacks the device. - -The first packed-RGB vertical slice is implemented locally. Native Linux validation is still required before opening a PR, and the zero-copy tracker item stays open until the real hardware gates pass. +Built already: the `dmabuf` feature and `Surface::DmaBuf`, PipeWire DMA-BUF +negotiation with the shared-memory fallback, the dequeued buffer retained +until the last clone drops (#2839), packed RGB import in +`rs/moq-video/src/render/dmabuf.rs`, and NV12 import (#3331), which aliases +the buffer as an `R8` luma and an `RG8` chroma texture with no copy. VA-API +encode takes a `Surface::DmaBuf` directly. + +What remains: + +- Hardware gates, as ignored tests with a reason where CI lacks the device: + native Linux `--features pipewire,render` tests; the shared-memory fallback + still captures; packed and NV12 DMA-BUF capture renders with zero CPU + download on an Intel or AMD desktop; holding several frames never shows + reused content or exhausts the PipeWire pool for good. +- Modifier mismatch: `render/dmabuf.rs` downloads a buffer whose modifier the + driver will not import, and re-tiles nothing. A VA-API VPP re-tile is only + worth adding if a measured capture source lands on such a modifier; record + the modifiers seen and decide. +- V4L2 capture still converts to I420 on the CPU. Export its buffers with + `VIDIOC_EXPBUF` (the ioctl is in `moq-v4l`, unused) as a `Surface::DmaBuf` + so a camera reaches VA-API or NVENC without a copy. Refs #2481, #1837. -This also covers the Linux zero-copy capture input gap: V4L2 and PipeWire -convert to I420 on the CPU today (YUYV, BGRA), so V4L2 `VIDIOC_EXPBUF` export -and PipeWire DMA-BUF negotiation are what feed a `Surface::DmaBuf` straight -into VAAPI or NVENC. - ## Closes - [#2819](https://github.com/moq-dev/moq/issues/2819) - close this issue when the quest finishes ## Related -- [Capture multi-plane PipeWire cameras](/quest/m2/pipewire-camera-planes.md) - separate memory blocks from a camera, which is the capture offer rather than this renderer import -- [#2893: video: validate PipeWire DMA-BUF capture on KDE hardware](/quest/m3/2893-video-validate-pipewire-dma-buf-capture-on-kde-hardware.md) - related open work +- [Capture multi-plane PipeWire cameras](/quest/m2/pipewire-camera-planes.md) - separate memory blocks from a camera, the capture offer rather than this import +- [#2893: video: validate PipeWire DMA-BUF capture on KDE hardware](/quest/m3/2893-video-validate-pipewire-dma-buf-capture-on-kde-hardware.md) - the KDE portal capture that timed out diff --git a/quest/m2/703-experimental-webgpu-renderer.md b/quest/m2/703-experimental-webgpu-renderer.md deleted file mode 100644 index cf78cdd882..0000000000 --- a/quest/m2/703-experimental-webgpu-renderer.md +++ /dev/null @@ -1,26 +0,0 @@ -# [M] Experimental WebGPU renderer - -## Goal - -Implement and verify the behavior tracked in [#703](https://github.com/moq-dev/moq/issues/703) -within the issue's stated scope and boundaries. - -## Plan - -Use the public issue's scope, implementation notes, and acceptance criteria -below as the starting plan. Reconcile paths and assumptions with the current -tree before implementation. - -### Issue context - -**WARNING** This is a pre-mature optimization or gimmick at best. - -We currently use [Canvas2D](https://github.com/kixelated/moq/blob/ff7cf92679e16c1d18ca5862aa4c7f73417e3c36/js/hang/src/watch/video/renderer.ts#L97) to render individual frames. This is pretty basic but works. - -All major browsers support WebGPU now. It has a [copyExternalImageToTexture](https://developer.mozilla.org/en-US/docs/Web/API/GPUQueue/copyExternalImageToTexture) method that apparently copies a `VideoFrame` to a renderable texture. This apparently avoids a copy so it might be faster than Canvas2D but probably not. - -The main benefit of WebGPU is being able to do *other* stuff, like run AI models on pixel data without copying to the CPU. Or rendering a person's face on a teapot. Or using shaders for gimmicky effects. None of this is really generic enough for a MoQ library but maybe somebody wants to have some fun. - -## Closes - -- [#703](https://github.com/moq-dev/moq/issues/703) - close this issue when the quest finishes diff --git a/quest/m2/823-svc-support.md b/quest/m2/823-svc-support.md deleted file mode 100644 index 7445ef6016..0000000000 --- a/quest/m2/823-svc-support.md +++ /dev/null @@ -1,22 +0,0 @@ -# [M] SVC support? - -## Goal - -Implement and verify the behavior tracked in [#823](https://github.com/moq-dev/moq/issues/823) -within the issue's stated scope and boundaries. - -## Plan - -Use the public issue's scope, implementation notes, and acceptance criteria -below as the starting plan. Reconcile paths and assumptions with the current -tree before implementation. - -### Issue context - -WebCodecs supports SVC, but somebody should actually test it out. - -If it works, we could add a field to the catalog indicating `layer`. The media download logic will get more complicated but it should be possible to support. - -## Closes - -- [#823](https://github.com/moq-dev/moq/issues/823) - close this issue when the quest finishes diff --git a/quest/m2/aac-encode-refusal.md b/quest/m2/aac-encode-refusal.md deleted file mode 100644 index 4f7a7371bf..0000000000 --- a/quest/m2/aac-encode-refusal.md +++ /dev/null @@ -1,22 +0,0 @@ -# [S] AAC encode refuses a channel count it cannot name - -## Goal - -Writing an AudioSpecificConfig for a channel count that no AAC -channelConfiguration names is an error, not a stereo config with a warning, -in `moq_mux::codec::aac::Config::encode`, as `@moq/hang`'s -`audioSpecificConfig` already refuses since #4119. This mirrors the parse -side, which since #4093 refuses reserved values instead of guessing stereo. - -## Plan - -`Config::encode` becomes fallible, a published API break, so this targets -`dev`. Counts with a PCE-free configuration map as today. Refuse the others, -matching JS; writing channelConfiguration 0 with a PCE derived from the layout -is a later additive change in both languages. Test every count from 1 to 8 and -one beyond. - -## Related - -- [AAC PCE](https://github.com/moq-dev/moq/pull/4093) - the parse half -- [Layout](/quest/m1/audio-codecs/layout.md) - the layout a PCE would be derived from diff --git a/quest/m2/capture-clock-source.md b/quest/m2/capture-clock-source.md deleted file mode 100644 index ddd4d10888..0000000000 --- a/quest/m2/capture-clock-source.md +++ /dev/null @@ -1,20 +0,0 @@ -# [S] Capture publishers read the catalog's clock - -## Goal - -`moq_video::encode::publish_capture` and `moq_audio::encode::Publication` stamp -on the clock their catalog advertises, with no separate clock to pass. Today -each takes its own `moq_mux::Clock`, and `PublicationOptions::default()` builds -a fresh one, so a caller relying on the default publishes audio against a -mapping the catalog never advertised. `moq import capture` passes -`catalog.clock()` to both, which is the only correct value. - -## Plan - -Drop the `clock` parameter from video `publish_capture` and the `clock` field -from `PublicationOptions`, reading `catalog.clock()` instead. Update moq-cli and -any binding that forwards a clock. The clock fixtures in both crates already -pass the catalog's clock, so they keep grading the same path. - -Public API: breaking in published `moq-video` and `moq-audio`, so it targets -`dev`. Wire: none. diff --git a/quest/m2/cat/README.md b/quest/m2/cat/README.md index 689f7da37f..d68c4ec7d5 100644 --- a/quest/m2/cat/README.md +++ b/quest/m2/cat/README.md @@ -44,9 +44,8 @@ Boundaries decided while planning: Order: the SETUP option already reaches the auth server as `moq_auth::Request.token`; verification comes first, then our clients present one. Everything rides `moq_auth::Request` and -`moq auth serve`, which shipped on dev. The JWT types sit at the crate root; -the verify quest moves them under `moq_auth::jwt` so `cat` is a sibling -module rather than a set of prefixed names. +`moq auth serve`. The JWT types stay at the crate root, so the published +`moq-auth` names do not break; `cat` is an additive module beside them. ## Required diff --git a/quest/m2/cat/verify.md b/quest/m2/cat/verify.md index 46df67ba0e..e606294f04 100644 --- a/quest/m2/cat/verify.md +++ b/quest/m2/cat/verify.md @@ -11,14 +11,16 @@ tokens. Every claim we do not evaluate refuses the token naming the claim. ## Plan -The JWT types (`Claims`, `Key`, `Jwk`, `KeyId`, `Algorithm`, the key set, -`authorize`) sit at the `moq_auth` root today. Move them under -`moq_auth::jwt` first, keeping their names, so `cat` is a sibling module -rather than a set of prefixed names; `@moq/auth` stays flat. +The JWT types (`Claims`, `Key`, `Jwk`, `KeyId`, `Algorithm`, `Scope`, the +key set) sit at the root of the published `moq-auth` 0.1.x, and they stay +there: `cat` is an additive module beside them, so this lands on `main`. +Moving them under `moq_auth::jwt` would break every published caller; if it +is still wanted, it is its own `dev` change, not part of this quest. Decided +in the 2026-09-28 quest audit. `@moq/auth` stays flat. - Crates: `coset` for COSE and CWT claims, `ciborium` for CBOR; HMAC through the `aws-lc-rs` the crate already links, ES256 through `p256`. Keys reuse - `moq_auth::jwt::Key` files: a JWK with `alg` maps to the COSE algorithm + `moq_auth::Key` files: a JWK with `alg` maps to the COSE algorithm (`HS256` to HMAC 256/256, `ES256` to -7, `EdDSA` to -8, `RS256` to -257); a JWK whose algorithm has no COSE mapping is refused at load naming it. - `cat::Claims { issuer, audience, subject, expires, not_before, issued, @@ -46,7 +48,7 @@ rather than a set of prefixed names; `@moq/auth` stays flat. than widening a fetch-only token into a live subscription. `ClientSetup` and `ServerSetup` add nothing. The namespace fields become one `moq_pattern::Pattern` segment each, the mapping `rs/moq-pattern` documents under "CAT / C4M", relative - to the dialed path exactly as `jwt::Claims::root` is: `Exact(f)` is the + to the dialed path exactly as the JWT `Claims::root` is: `Exact(f)` is the literal segment, `Prefix(f)` is `f*`, `Suffix(f)` is `*f`, a named namespace without `exact_depth` appends `/**`, and an absent namespace is bare `**` with nothing appended. A field value containing `/` or `*` diff --git a/quest/m2/cpp-conan.md b/quest/m2/cpp-conan.md index a4d1dfe8d4..19e36c2ac2 100644 --- a/quest/m2/cpp-conan.md +++ b/quest/m2/cpp-conan.md @@ -2,16 +2,17 @@ ## Goal -A consumer adds the moq Conan remote, requires `moq/`, and gets the +A consumer adds the moq Conan remote, requires `moq-cpp/`, and gets the prebuilt package for their `os`, `arch`, and `compiler` without a Rust toolchain or the bindgen fork. A fresh consumer project installs it in CI on Windows, macOS, and Linux. ## Plan -- A `moq` recipe on a moq-dev remote (Artifactory or a GitHub-hosted `conan` - index) that packages the prebuilt release tarball per setting and exports - the CMake target from `package_info`. +- A `moq-cpp` recipe, named after the package, on a moq-dev remote + (Artifactory or a GitHub-hosted `conan` index) that packages the prebuilt + release tarball per setting and exports the `moq::cpp` CMake target from + `package_info`. - The recipe reads the release manifest the vcpkg quest introduced, so one release bumps both recipes; `release-cpp.yml` publishes to the remote after the tarballs land. diff --git a/quest/m2/cpp-vcpkg.md b/quest/m2/cpp-vcpkg.md index 40b98e73fc..36ffc7427f 100644 --- a/quest/m2/cpp-vcpkg.md +++ b/quest/m2/cpp-vcpkg.md @@ -3,13 +3,14 @@ ## Goal A consumer adds `moq-dev/vcpkg-registry` to `vcpkg-configuration.json`, -depends on `moq`, and gets the prebuilt package for their triple without a +depends on `moq-cpp`, and gets the prebuilt package for their triple without a Rust toolchain or the bindgen fork. A fresh consumer project installs it in CI on Windows, macOS, and Linux. ## Plan -- `moq-dev/vcpkg-registry`: a git registry with a `moq` port whose portfile +- `moq-dev/vcpkg-registry`: a git registry with a `moq-cpp` port, named after + the package (`find_package(moq-cpp)`, target `moq::cpp`), whose portfile downloads the per-target release tarball from `release-cpp.yml` by version and hash, installs headers, the static library, and the CMake config, and declares `supports` for exactly the release matrix. Versioning follows the diff --git a/quest/m2/flate/README.md b/quest/m2/flate/README.md index 2438bf27d7..58936a70b3 100644 --- a/quest/m2/flate/README.md +++ b/quest/m2/flate/README.md @@ -2,30 +2,21 @@ ## Goal -Any track can be DEFLATE-compressed per group from every language, not only -the JSON modes. A native or C caller publishes and consumes a compressed track -of opaque frames the same way a Rust or browser caller does, and the bytes on -the wire are identical across all of them. +Opaque tracks, compressed per group or not, are published and consumed the +same way from every language, not only Rust and JS, and the bytes on the wire +are identical across all of them. ## Plan -`moq-flate` and `@moq/flate` today are a bare codec: `Encoder`/`Decoder` with -`frame(bytes) -> bytes` and a shared window the caller scopes to a group by -hand. Only `moq-json` composes them, so the bindings reach compression through -`compression: bool` on the JSON configs and nothing else. A telemetry, caption, -or sensor track of raw frames has no compressed form outside Rust and JS, and -even there the caller re-derives the group discipline from the crate docs. - -The line adds a track wrapper to the crate first, then binds that wrapper. The -codec objects stay as they are; the wrapper owns the per-group window so a -caller cannot desynchronize it. The wire format does not change: a wrapper -group is the raw sync-flushed stream the codec already emits, so a wrapped -producer interoperates with a hand-composed consumer and with `moq-json`. +`moq-flate` and `@moq/flate` absorb `moq-binary`'s snapshot and stream modes +in [moq-binary folds into moq-flate](/quest/m1/flate-binary.md), so the crate +already owns the per-group window a caller could otherwise desynchronize. The +track wrapper this line once planned was dropped for that reason. What +remains is reaching those tracks from the hand-written binding wrappers. No wire, catalog, or relay impact. Compression stays invisible to `moq-net`; a compressed track is announced, routed, and cached like any other. ## Required -- [Track wrapper](/quest/m2/flate/track.md) - `moq-flate` and `@moq/flate` wrap a track so each group is one compression window without caller bookkeeping -- [Bindings](/quest/m2/flate/bindings.md) - moq-ffi and libmoq publish and subscribe compressed tracks, mirrored through every wrapper +- [Bindings](/quest/m2/flate/bindings.md) - the hand-written wrappers expose flate tracks diff --git a/quest/m2/flate/bindings.md b/quest/m2/flate/bindings.md index c83c3df214..ef28b41d31 100644 --- a/quest/m2/flate/bindings.md +++ b/quest/m2/flate/bindings.md @@ -2,55 +2,35 @@ ## Goal -moq-ffi and libmoq publish and subscribe a compressed track of opaque frames, -and every wrapper (Python, Swift, Kotlin, Go, Dart, C) reaches it. A track -written from C decodes in the browser with `@moq/flate` and vice versa. +The hand-written wrappers (Python, Swift, Kotlin, Go, Dart) expose flate +tracks, the snapshot and stream opaque tracks moq-ffi already generates, in +their own idiom beside the JSON entry. A track published from a wrapper +decodes in the browser with `@moq/flate` and vice versa. ## Plan -Bind the track wrapper, not the codec: the bindings' job is to make the group -discipline unrepresentable to misuse, and a bare `frame()` call across an FFI -boundary invites the desync the wrapper exists to prevent. - -moq-ffi, next to `json.rs` and named the same way: - -- `MoqBroadcastProducer::publish_flate(name, MoqFlateConfig) -> MoqFlateProducer` - with `append_group() -> MoqFlateGroupProducer`, `finish()`, `abort(code)`. -- `MoqFlateGroupProducer::write_frame(MoqFrame)`, `finish()`, `abort(code)`. - A failed write aborts the group and the handle refuses further writes. -- `MoqBroadcastConsumer::subscribe_flate(name, MoqFlateConfig) -> MoqFlateConsumer` - with `next_group() -> Option`, `cancel()`. -- `MoqFlateGroupConsumer::read_frame() -> Option`, `cancel()`. -- `MoqFlateConfig { level = 6, max_frame_size = 64 MiB }` as `#[uniffi(default)]` - literals, with the same drift test `json.rs` keeps against the crate defaults. - -Frames cross as `MoqFrame` so the transport timestamp survives, as on the raw -track API; only the payload is compressed. Explicit groups rather than a flat `append(bytes)` because the window resets -at the boundary and the caller chooses where that is; a helper that rolls -groups on a size or count budget can follow if a consumer asks. - -libmoq mirrors `moq_publish_json_*` and `moq_consume_json_*`: -`moq_publish_flate`, `moq_publish_flate_group`, `moq_publish_flate_frame`, -`moq_publish_flate_group_finish`, `moq_publish_flate_finish`, -`moq_consume_flate`, `moq_consume_flate_group`, `moq_consume_flate_frame`, -`moq_consume_flate_frame_free`, `moq_consume_flate_close`, with a -`moq_flate_config` struct. Follow the terminal-status callback contract for -the consume side and regenerate `moq.h`. - -Wrappers per the Cross-Package Sync table: the uniffi bindings regenerate; -`go/wrapper/moq/json.go`, `py/moq-rs/moq/{publish,subscribe}.py`, -`swift/Sources/Moq/Json.swift`, `kt/moq`'s `Json.kt` with its `Aliases.kt` -re-exports and `Flows.kt` extensions, and `dart/moq` each gain a hand-written -sibling. Document in `doc/lib/{c,py,swift,kt,go,dart}` beside the JSON entry. - -Tests: a moq-ffi round trip next to `json_snapshot_roundtrip`, a libmoq C -round trip in `src/test.rs`, and one cross-language check that a C-published -group decodes with the shared vector from the track quest. Run -`just test interop --all`. - -Public API impact: additive on moq-ffi, libmoq, and every wrapper; `main`. -Wire impact: none. +moq-ffi publishes opaque tracks today (`publish_binary_snapshot` and +`publish_binary_stream`, #4137), renamed after `flate` by +[moq-binary folds into moq-flate](/quest/m1/flate-binary.md). Only the +generated bindings reach them; no wrapper does. This quest binds the existing +track modes, not the bare codec: a `frame()` call across the FFI boundary +invites the window desync the track modes exist to prevent. + +- moq-ffi has no consume side for these tracks. Add it next to the JSON + consumers so each wrapper can read what it writes. +- Wrappers per the Cross-Package Sync table: `go/wrapper/json.go`, + `py/moq-rs/moq/{publish,subscribe}.py`, `swift/Sources/Moq/Json.swift`, + `kt/moq`'s `Json.kt` with its `Aliases.kt` re-exports, and + `dart/moq/lib/src/aliases.dart` each gain a flate sibling. If + [FFI shape](/quest/m1/ffi-shape/README.md) has landed, follow its `flate` + namespace instead. +- Document in `doc/lib/{py,swift,kt,go,dart}` beside the JSON entry. +- Tests: a round trip in each wrapper that has tests, and one cross-language + check that a wrapper-published group decodes with `@moq/flate`. Run + `just test interop --all`. + +Public API: additive on moq-ffi and every wrapper. Wire: none. ## Required -- [Track wrapper](/quest/m2/flate/track.md) - the surface being bound +- [moq-binary folds into moq-flate](/quest/m1/flate-binary.md) - the flate snapshot and stream tracks and their moq-ffi names diff --git a/quest/m2/flate/track.md b/quest/m2/flate/track.md deleted file mode 100644 index cec5104a6b..0000000000 --- a/quest/m2/flate/track.md +++ /dev/null @@ -1,55 +0,0 @@ -# [M] Flate track wrapper - -## Goal - -`moq_flate::track::{Producer, Consumer}` and the matching `@moq/flate` classes -wrap a `moq-net` track so every group is one compression window. A caller -writes and reads plain frames; the wrapper compresses, decompresses, and -resets the window at each group boundary. A Rust producer and a browser -consumer (and the reverse) round-trip byte-identical frames. - -## Plan - -Mirror the `moq-json` layering: the codec layer stays for callers that own -their own groups, and the new track layer owns a `moq_net::track::Producer` -or `Subscriber` plus one codec per live group. - -Shape, matching `moq-net` names so the wrapper reads like the track it wraps: - -- `track::Producer::new(track, Config)`, `append_group() -> group::Producer`, - `create_group(sequence)`, `finish()`, `abort(code)`, `consume()`. -- `group::Producer`: `write_frame(timestamp, &[u8])`, `finish()`, - `abort(code)`. Holds the `Encoder`; dropping it without `finish` aborts, - per the refcount idiom. A write that fails after the encoder advanced (an - oversized frame, a closed group) leaves the window ahead of the consumer, - so it is terminal: the group aborts and the handle refuses further writes. -- `track::Consumer::new(subscriber, Config)`, `next_group() -> group::Consumer` - (plus `recv_group` if the raw consumer distinguishes them), `update(...)`. -- `group::Consumer`: `read_frame() -> Option`, the transport - timestamp intact and the payload inflated. Holds the `Decoder`. -- `Config { level, max_frame_size }` with the crate defaults; one struct shared - by both sides, the consumer reading only `max_frame_size`. A frame past the - cap is an error that aborts the group, never a truncated frame. - -No datagrams: a datagram has no window to share, so `append_datagram` is not -on the wrapper. Timestamps pass through untouched: `moq-net` frames carry one -on both sides, and a timed opaque track (telemetry, captions) must round-trip -as the same track. Only the payload is compressed. - -Decide whether `moq-json`'s `stream` and `window` modes should move onto the -group wrapper. They already keep one encoder per group, so it is likely a -deletion, but `stream::Error::Desync` exists because the codec layer can -encode without writing; the wrapper closes that gap by making a failed write -terminal instead. Take the deletion if it is clean, otherwise leave `moq-json` -alone and note why. - -Verify with a shared test vector: a fixed frame sequence compressed by Rust, -checked into both test suites, and decoded by the JS wrapper, with the reverse -direction generated by JS. The existing codec tests stay. - -Public API impact: additive on `moq-flate` and `@moq/flate`; lands on `main`. -Wire impact: none. - -## Related - -- [Bindings](/quest/m2/flate/bindings.md) - binds this wrapper diff --git a/quest/m2/gop-overhead.md b/quest/m2/gop-overhead.md index 67383329de..9c9a8649b3 100644 --- a/quest/m2/gop-overhead.md +++ b/quest/m2/gop-overhead.md @@ -33,11 +33,6 @@ a verdict on whether a long GOP plus a keyframe request is worth designing. A verdict of "2 seconds is fine" is a valid, expected outcome and completes this quest. -## Related - -- [Keyframe trigger](/quest/m1/keyframe-trigger.md) - the publisher-side half a - keyframe request would drive, useful on its own - ## Closes - [#2284](https://github.com/moq-dev/moq/issues/2284) - close this issue when the quest finishes diff --git a/quest/m2/intra-refresh/README.md b/quest/m2/intra-refresh/README.md index 54744d74f0..3bfb94cd11 100644 --- a/quest/m2/intra-refresh/README.md +++ b/quest/m2/intra-refresh/README.md @@ -44,7 +44,7 @@ Decisions the quests share: - [Encode config](/quest/m2/intra-refresh/encode-config.md) - refresh mode extends the settled GOP contract; the producer cuts groups per sweep and publishes `warmup` - [NVENC refresh](/quest/m2/intra-refresh/nvenc-refresh.md) - the NVENC backend encodes refresh mode for H.264 and HEVC - [V4L2 refresh](/quest/m2/intra-refresh/v4l2-refresh.md) - the V4L2 backend encodes refresh mode -- [Bindings](/quest/m2/intra-refresh/bindings.md) - ffi, libmoq, and every wrapper expose the `Gop` enum +- [Bindings](/quest/m2/intra-refresh/bindings.md) - moq-ffi and every wrapper expose refresh mode, additive on the ffi-shape `Gop` enum - [Export sync flags](/quest/m2/intra-refresh/export-sync-flags.md) - fmp4, MKV, and HLS stop advertising a refresh group start as a sync sample ## Related diff --git a/quest/m2/intra-refresh/bindings.md b/quest/m2/intra-refresh/bindings.md index 880327b1a8..78bbc95863 100644 --- a/quest/m2/intra-refresh/bindings.md +++ b/quest/m2/intra-refresh/bindings.md @@ -1,27 +1,25 @@ -# [M] Bindings expose the Gop enum +# [S] Bindings expose refresh mode ## Goal -Every binding names the group structure the way the core does: the ffi record, -the libmoq C struct, and the Python, Swift, Kotlin, Dart, and Go wrappers take -a keyframe interval or a refresh cycle and nothing else, and `cut()` keeps its -meaning in both modes. This replaces the published `gop` integer in moq-ffi -and changes the libmoq C struct layout, so it targets `dev`. +Every binding names the group structure the way the core does: the moq-ffi +record and the Python, Swift, Kotlin, Dart, and Go wrappers take a keyframe +interval or a refresh cycle and nothing else, and `cut()` keeps its meaning in +both modes. ## Plan -- `rs/moq-ffi/src/video.rs`: `MoqVideoEncoderOutput.gop: Option` becomes - a `MoqVideoGop` enum record mirroring `Gop`, defaulting to keyframes at two - seconds. `MoqVideoProducer::cut()` already has the right name. -- `rs/libmoq/src/video.rs`: `moq_video_encoder_output` gains a - `moq_video_gop` discriminant beside `gop`, zero meaning keyframes, and - `moq.h` is regenerated (build.rs does not do it on source-only changes). - `cpp/obs/src` follows the header. +- [Codecs](/quest/m1/ffi-shape/codec.md) already replaces the `gop` integer + with a `MoqVideoGop` enum mirroring the non-exhaustive `Gop`, so this adds + the refresh variant beside `Keyframe` in `rs/moq-ffi/src/video.rs`, which + is additive and lands on `main`. `MoqVideoProducer::cut()` already has the + right name. +- moq-ffi only: the generated C and C++ bindings inherit the variant. - Hand-written wrappers and docs per the cross-package table: `py/moq-rs`, - `swift/`, `kt/`, `dart/moq`, `go/wrapper/moq`, and `doc/lib/{py,swift,kt,go,dart,c}`. - Go gets no uniffi default, so its zero value must read as keyframe mode. + `swift/`, `kt/`, `dart/moq`, `go/wrapper`, and `doc/lib/{py,swift,kt,go,dart}`. - Run `just test interop --all` for the cross-language check. ## Required -- [Encode config](/quest/m2/intra-refresh/encode-config.md) - the core enum this mirrors +- [Encode config](/quest/m2/intra-refresh/encode-config.md) - the core refresh variant this mirrors +- [Codecs](/quest/m1/ffi-shape/codec.md) - the `MoqVideoGop` enum this extends diff --git a/quest/m2/intra-refresh/consumer-warmup.md b/quest/m2/intra-refresh/consumer-warmup.md index 41326377ff..932c5420d7 100644 --- a/quest/m2/intra-refresh/consumer-warmup.md +++ b/quest/m2/intra-refresh/consumer-warmup.md @@ -22,11 +22,10 @@ replaces both: the rule is timestamp arithmetic on the group start. - Withhold rule, keyed on the same non-continuous signal in both consumers: `js/hang/src/container/consumer.ts` `next()` reports `continuous: false` after a subscribe, a declared discontinuity, or any skip (`#gap`). The Rust - `rs/moq-mux/src/container` `Consumer` has no such signal: `read()` returns a - bare frame and `discontinuity()` covers only empty groups and rewinds, so - this quest adds a per-delivery continuity flag there, set on a sequence gap - and on a latency skip, and `rs/moq-video/src/decode/consumer.rs` propagates - it. For the first + `moq_mux::container::Consumer` gains the equivalent in the open-GOP quest + (today `poll_read` returns a bare frame and only the `discontinuity()` + counter moves); this quest reuses it, and + `rs/moq-video/src/decode/consumer.rs` propagates it. For the first group after that signal, every frame is decoded (the decoder needs them to build reference state) and frames stamped below `group.start + warmup` are not presented; frames stamped at or above that boundary are, so the recovery @@ -41,13 +40,15 @@ replaces both: the rule is timestamp arithmetic on the group start. in `js/hang`. Other codecs never set `warmup`, so no check is needed there. - Join earlier: the subscription's maximum age becomes the latency target plus `warmup`, so the group start lands `warmup` before the target and the first - presented frame is on time. JS sets `Subscription.latencyMax` in `js/net`; - Rust sets `latency_max` on the decode consumer's subscription - (`rs/moq-video/src/decode/consumer.rs`), not `Subscription::group_start`, - which is aggregated across subscribers and rewinds the track for everyone. -- Latency skipping must not shed the warmup span it deliberately joined: - `#checkLatency` in the JS container consumer and `with_latency` in Rust - compare the buffered span against the target, and frames still inside a + presented frame is on time. JS sets the subscription's `maxAge` in `js/net`; + Rust adds it to the decode consumer's `Options::max_age`, which reaches the + subscription through `Subscription::with_max_age` + (`rs/moq-video/src/decode/consumer.rs`), not `Subscription::start`, which + is aggregated across subscribers and rewinds the track for everyone. +- Max-age skipping must not shed the warmup span it deliberately joined: + `#checkMaxAge` in the JS container consumer and the max-age budget in Rust + (`Consumer::poll_read`, set by `set_max_age`) compare the buffered span + against the target, and frames still inside a withheld warmup count as decode-only, not buffered. - Tests in both languages: a synthetic three-group track with `warmup` where a cold join presents nothing before start plus `warmup` and everything after; @@ -59,7 +60,4 @@ replaces both: the rule is timestamp arithmetic on the group start. ## Required - [Catalog warmup](/quest/m1/catalog-warmup.md) - the field this reads - -## Related - -- [Open-GOP leading pictures](/quest/m1/open-gop-leading-pictures.md) - trims frames stamped before the keyframe; this trims frames after the start, on the same signal +- [Open-GOP leading pictures](/quest/m1/open-gop-leading-pictures.md) - adds the Rust non-continuous signal this keys on, and trims frames stamped before the keyframe where this trims frames after the start diff --git a/quest/m2/js-discontinuity.md b/quest/m2/js-discontinuity.md index 36e41aada4..0270c141b2 100644 --- a/quest/m2/js-discontinuity.md +++ b/quest/m2/js-discontinuity.md @@ -1,29 +1,22 @@ -# [S] JS names the break discontinuity() +# [XS] JS discontinuity() writes no estimated end ## Goal -`@moq/hang`'s container producer spells its two operations the way Rust -`moq_mux::container::Producer` does: `cut(end?)` closes the current group, -and `discontinuity()` closes it and writes the empty marker group that tells -subscribers to re-anchor. Today JS's public `cut()` writes the marker, so the -same name means a routine group close in Rust and a timeline break in JS. +`@moq/hang`'s `discontinuity()` without an explicit end closes the group with +no cadence-estimated end, as Rust `moq_mux::container::Producer::discontinuity` +does. Whatever resumes can land sooner than one estimated frame later (a +capture swap), and an end past it reads as a rewind to every consumer. ## Plan -[#4045](https://github.com/moq-dev/moq/pull/4045) deletes -`quest/m1/js-publish-discontinuity.md`, since #3982 already put the marker -in `cut()`; this rename is the only remaining JS work. +The rename landed on `dev` in #4141 and kept the old behavior: in +`js/hang/src/container/legacy.ts`, `discontinuity(end?)` calls `#close(end)`, +which fills a missing `end` with `#end + #interval`. Rust's `discontinuity()` +calls `close(None, None)` and clears the cadence first. -In `js/hang/src/container/legacy.ts`, rename the public `cut(end?)` to -`discontinuity()` (taking the same optional end) and keep the routine close -private until a caller needs it public. Move `js/publish/src/video/encoder.ts` -and any other caller over, and keep data tracks skipping a sequence the way -Rust's `discontinuity()` does if a JS data-track caller appears. Rust is -untouched. +Give the break its own close path that passes no estimate when the caller +gave no end, leaving the routine close's estimate alone. Test that a +discontinuity after a steady cadence writes no end past the last frame. -Match Rust's break, too: without an explicit end, `discontinuity()` closes the -group with no cadence-estimated duration marker. Whatever resumes can land -sooner than one estimated frame later (a capture swap), and a marker past it -reads as a rewind to every consumer. - -Public API: breaking in published `@moq/hang`, so it targets `dev`. Wire: none. +Public API: behavior change in `@moq/hang`'s `discontinuity()`, which exists +only on `dev`, so it targets `dev`. Wire: none. diff --git a/quest/m2/latency-ledger.md b/quest/m2/latency-ledger.md index 9c5f1edad4..ed059c529b 100644 --- a/quest/m2/latency-ledger.md +++ b/quest/m2/latency-ledger.md @@ -13,8 +13,12 @@ The audio quality harness lands on ad-hoc debug probes, which is the right trade to get it running. This quest promotes them. - Take the stage schema the harness already defines (capture, encode, publish - flush, network, jitter buffer, decode, render) and expose it the way - `moq-stats` exposes relay traffic: an observable readout, not a callback. + flush, network, jitter buffer, decode, render) and expose it as fields of + `hang::Stats`, the media extension the client stats schema adds to + `moq-stats` (`rs/hang/src/stats.rs`, and its `@moq/stats` mirror), rather + than a second readout: an observable value a `.stats` broadcast already + carries, not a callback. Decided so viewers report latency the same way + they report stalls. - Both languages, matching names, per the repo's cross-language rule. Scrutinise each exported item: a stage nobody outside can act on stays internal. - Switch the harness over, deleting the probes it replaces. A ledger with no @@ -30,6 +34,7 @@ trade to get it running. This quest promotes them. ## Required - [Audio quality harness](/quest/m0/audio-quality-harness/README.md) - defines the stage schema and lands the probes this promotes +- [Schema and library](/quest/m1/qos/stats/schema.md) - adds `hang::Stats`, which this extends ## Related diff --git a/quest/m2/mobile-ownership.md b/quest/m2/mobile-ownership.md index 973b75bf56..14e17f4c94 100644 --- a/quest/m2/mobile-ownership.md +++ b/quest/m2/mobile-ownership.md @@ -24,8 +24,3 @@ capture quests in this questline start only once this is settled. `moq-video`, so replace them with the required platform-owned implementation quests if the answer is option 1. Update [mobile completion](/quest/m2/mobile-completion.md) to require those replacements before abandoning the Rust capture quests. - -## Related - -- [Decoded frame ownership](/quest/m1/decoded-frames.md) - established shared frame lifetime; reuse it for any later native mobile views -- [Decoded frame ownership](/quest/m1/decoded-frames.md) - independently supplies portable pixels from Rust decoding diff --git a/quest/m2/multipath-spike.md b/quest/m2/multipath-spike.md index f26d6804fe..6e7646f68b 100644 --- a/quest/m2/multipath-spike.md +++ b/quest/m2/multipath-spike.md @@ -57,5 +57,8 @@ Naming trap: `Path` in moq-net is the broadcast namespace path, and `Client::with_path` is the MoQ SETUP resource path. A network path needs a different name. -Target `dev`, where the crate is `moq-tokio`; the rename has not reached -`main`. +Target `main`: `apply_transport` lives in `rs/moq-tokio/src/noq.rs` there +too, and the max-paths knob is additive. + +Public API: an additive transport setting in `moq-tokio`. Wire: none; multipath +is negotiated by QUIC transport parameters, below MoQ. diff --git a/quest/m2/obs-wave-layout.md b/quest/m2/obs-wave-layout.md deleted file mode 100644 index b62afa7030..0000000000 --- a/quest/m2/obs-wave-layout.md +++ /dev/null @@ -1,17 +0,0 @@ -# [XS] OBS channel layouts - -## Goal - -The OBS source maps a channel count to the same default layout moq-audio does, -the WAVE convention (3 is 2.1, 4 is quad, 6 is 5.1, 8 is 7.1), so a -multichannel broadcast plays with its speakers where the publisher put them. - -## Plan - -`audio_layout_to_speakers` in `cpp/obs/src/moq-source.cpp` maps from FFmpeg -layouts today. Map each count to the nearest OBS `speaker_layout` and refuse -the ones OBS cannot place rather than guess. Note the mapping in `doc/bin/obs.md`. - -## Related - -- [Audio codecs](/quest/m1/audio-codecs/README.md) - the channel layouts this mirrors diff --git a/quest/m2/pipewire-camera-planes.md b/quest/m2/pipewire-camera-planes.md index d33e8fb114..5cc58a94f8 100644 --- a/quest/m2/pipewire-camera-planes.md +++ b/quest/m2/pipewire-camera-planes.md @@ -2,7 +2,7 @@ ## Goal -A PipeWire camera that delivers I420 or NV12 in separate memory blocks produces frames. Single-block cameras keep working. Other pixel formats stay unsupported. Importing multi-plane NV12 into Vulkan stays with the PipeWire DMA-BUF quest. +A PipeWire camera that delivers I420 or NV12 in separate memory blocks produces frames. Single-block cameras keep working. Other pixel formats stay unsupported. The renderer already imports multi-plane NV12 DMA-BUFs (#3331); this is the shared-memory capture offer. ## Plan @@ -13,4 +13,4 @@ Unit-test the offer, a multi-block NV12 buffer, and a multi-block I420 buffer, w ## Related - [Validate PipeWire cameras on a portal and a Pi](/quest/m3/pipewire-camera-hardware.md) - the pass that shows whether a real Pi or portal camera delivers separate planes -- [PipeWire DMA-BUFs into Vulkan](/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md) - multi-plane NV12 import in the renderer +- [PipeWire DMA-BUFs into Vulkan](/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md) - the hardware validation of the DMA-BUF path diff --git a/quest/m2/quic-bbr-google.md b/quest/m2/quic-bbr-google.md index 869b3c8fa7..63c90992fe 100644 --- a/quest/m2/quic-bbr-google.md +++ b/quest/m2/quic-bbr-google.md @@ -34,11 +34,7 @@ not an end-to-end network measurement. Persist a reproducible harness in CI separate implementation quest for any adopted change rather than silently expanding this study into a controller rewrite. -## Required - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - measure a baseline without the seven known defects - ## Related -- [BBR3 app-limited](/quest/m2/quic-bbr-app-limited.md) - reuse media profiles and measurements +- [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - reuse media profiles and measurements - [Upstream the fork](/quest/m1/quic/upstream.md) - share useful findings with upstream diff --git a/quest/m2/quic-bbr-app-limited.md b/quest/m2/quic-bbr-natural-drain.md similarity index 95% rename from quest/m2/quic-bbr-app-limited.md rename to quest/m2/quic-bbr-natural-drain.md index d6b7a29846..561b4bea8b 100644 --- a/quest/m2/quic-bbr-app-limited.md +++ b/quest/m2/quic-bbr-natural-drain.md @@ -40,10 +40,6 @@ harness in CI, with broader network cases at least nightly, and a verdict with pinned sources/configurations. Any adopted production policy gets a separate implementation quest; this study does not silently change defaults. -## Required - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - measure the corrected controller through MoQ's dependency chain - ## Related - [Discover media headroom](/quest/m2/quic-probe.md) - preserving an estimate and discovering spare capacity are separate problems diff --git a/quest/m2/quic-probe.md b/quest/m2/quic-probe.md index 5926256767..f1e8ef3e06 100644 --- a/quest/m2/quic-probe.md +++ b/quest/m2/quic-probe.md @@ -44,12 +44,8 @@ is not proof of full path capacity. Persist regressions in CI and broader network scenarios at least nightly. A measured no-go is a valid outcome; retain the baseline and record why before exposing an ineffective option. -## Required - -- [Release BBR fixes](/quest/m1/quic/bbr-release.md) - exclude known controller defects from the experiment - ## Related -- [Natural media drains](/quest/m2/quic-bbr-app-limited.md) - separate ProbeRTT policy experiment +- [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - separate ProbeRTT policy experiment - [FEC experiment](/quest/m2/quic-fec.md) - repetition competes for the redundancy budget - [GCC egress experiment](/quest/m2/quic-gcc.md) - delay control changes what headroom means diff --git a/quest/m2/redundant-ingest.md b/quest/m2/redundant-ingest.md index 6713d87dd3..96ce3c0644 100644 --- a/quest/m2/redundant-ingest.md +++ b/quest/m2/redundant-ingest.md @@ -4,20 +4,26 @@ Decide whether and how two live publishers of identical content share one broadcast, so viewers survive losing one faster than the QUIC keep-alive. -Epochs let the publishers claim one identity (the same `@`), but -today's #3312 rule splices only across routes with the same first hop, so -two ingest hosts are two identities. The study may end in a no-go. +The study may end in a no-go. ## Plan -Open questions: whether an explicitly shared epoch may splice across first -hops at a group boundary, and what makes that safe (group sequences aligned -across encoders, a matching catalog). Also who declares the incumbent dead -early: a failover service that retracts it, or active-active delivery to the -relay. Weigh them against the moq-transport rule that multiple publishers of -a namespace must each be asked (#3697) and the cluster draft. Output: a +Start from what is documented today (`doc/bin/cli.md` "Redundant +publishers"): two encoders sharing a Hop ID (`--hop 42`) are one first hop, +so relays hold both routes and fail over at a group boundary under the #3312 +same-first-hop rule, provided the tracks are identical with aligned groups. + +Open questions: how that maps onto epochs (the pair claiming one +`@`), what enforces the alignment the docs only ask for (group +sequences, a matching catalog), and who declares the incumbent dead early: a +failover service that retracts it, or active-active delivery to the relay. +[Cluster routing](/quest/m1/cluster-routing.md) drops hop lists inside a +cluster and must decide what replaces this failover; follow its answer. +Weigh them against the moq-transport rule that multiple publishers of a +namespace must each be asked (#3697) and the cluster draft. Output: a decision, with a quest for the chosen mechanism. ## Related +- [Cluster routing](/quest/m1/cluster-routing.md) - decides what replaces first-hop failover inside a cluster - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - explicit epochs are what a redundant pair would share diff --git a/quest/m2/routing-cost-domains.md b/quest/m2/routing-cost-domains.md index 9e045ed5e4..58e0ab5f1f 100644 --- a/quest/m2/routing-cost-domains.md +++ b/quest/m2/routing-cost-domains.md @@ -3,10 +3,13 @@ ## Goal Settle how independently operated MoQ networks exchange reachability without -adding incomparable costs. Produce a reviewed design and scoped implementation -quests, not a protocol implementation. Cloudflare, moq.pro, and self-hosted -relays can retain their own business policy; no RTT/loss-driven repricing or -automatic performance failover is authorized by this work. +adding incomparable costs, at the cluster boundaries where +[Cluster routing](/quest/m1/cluster-routing.md) keeps path vector with cluster +ids as hops. Cost inside one cluster is cluster-routing's. Produce a +reviewed design and scoped implementation quests, not a protocol +implementation. Cloudflare, moq.pro, and self-hosted relays can retain their +own business policy; no RTT/loss-driven repricing or automatic performance +failover is authorized by this work. ## Plan @@ -46,16 +49,14 @@ Explain how warm-route marginal savings interact with border policy without pretending those savings erase upstream delay. State tradeoffs, migration and mixed-version behavior, and the limits of any convergence claim. -Reconcile the existing directional charged/declared cost plan rather than -creating a second peer-policy mechanism. Completion is a documented decision, +Reconcile with cluster-routing's configured link costs rather than creating +a second peer-policy mechanism. Completion is a documented decision, worked counterexamples or model checks, and independently completable follow-up quests. Open wire/API choices belong to this design exercise. ## Related -- [Peer reconfigure](/quest/m1/pop-skipping/peer-reconfigure.md) - existing - charged versus declared directional policy -- [PoP skipping](/quest/m1/pop-skipping/README.md) - coordinated fleet economics - and warm-route behavior +- [Cluster routing](/quest/m1/cluster-routing.md) - in-cluster topology and + cost; this designs only what crosses its boundaries - [#3769](https://github.com/moq-dev/moq/pull/3769) - measurement-based pricing prompted the separation of measurement, operator policy, and protocol diff --git a/quest/m2/teleop/README.md b/quest/m2/teleop/README.md index 1b4b44e856..fa7e5f5a4f 100644 --- a/quest/m2/teleop/README.md +++ b/quest/m2/teleop/README.md @@ -24,11 +24,10 @@ server walks the announce stream to fan the viewers in demo and private to it. Every integrator rebuilds the announce fan-in, the operator arbitration, and the latency instrumentation from scratch. -Two things are genuinely missing rather than merely undocumented: the hang -catalog is video plus audio only (location tracks arrived in moq#401 and were -dropped when the catalog became generic), and `moq-video`'s V4L2-M2M encoder -backend is compiled into no released `moq-cli`, so the boards that fly have no -native hardware-encode path anyone can install. +The hang catalog is no longer a gap: it advertises data tracks in its `json` +and `binary` sections beside video and audio. What is genuinely missing is +`moq-video`'s V4L2-M2M encoder backend in a released `moq-cli`, so the boards +that fly have no native hardware-encode path anyone can install. ### Two delivery classes, one session @@ -45,8 +44,9 @@ a lossy link produces latency spikes rather than delivery, and an emulation study on a long-RTT profile measured command staleness more than twice as bad for QUIC reliable streams as for DDS best-effort: correct and useless. -The split is a framing decision, not a subscription flag, and `moq-json` -already implements both halves as its snapshot and stream modes. What that +The split is a framing decision, not a subscription flag, and `moq-json` and +`moq-binary` already implement both halves as their snapshot and stream +modes. What that means for the primitive is in [robot](/quest/m2/teleop/robot.md), and what it means for a protocol multiplexing many message rates onto one link is in [mavlink](/quest/m2/teleop/mavlink.md). @@ -98,8 +98,8 @@ stating plainly because it is what a builder is comparing against. - [V4L2-M2M encoding](/quest/m2/teleop/v4l2-encode.md) - a released `moq-cli` reaches `moq-video`'s hardware encoder on the boards that fly, and the boards worth buying are written down -- [Teleoperation use-case docs](/quest/m2/teleop/docs.md) - `doc/concept/use-case/other.md` - becomes the teleoperation page, with a runnable non-media example beside it +- [Teleoperation use-case docs](/quest/m2/teleop/docs.md) - `doc/concept/use-case/` + gains a teleoperation page, with a runnable non-media example beside it - [ROS 2 bridge](/quest/m2/teleop/ros2.md) - a ROS 2 bridge sibling to the MAVLink one, carrying topics over the same two delivery classes - [Cross-track correlation](/quest/m2/teleop/correlation.md) - a command, the diff --git a/quest/m2/teleop/browser-package.md b/quest/m2/teleop/browser-package.md index c447df9a37..56e4c29449 100644 --- a/quest/m2/teleop/browser-package.md +++ b/quest/m2/teleop/browser-package.md @@ -7,14 +7,14 @@ and delivery classes rather than reimplementing them. ## Plan -Follow the existing split: `net`, `hang`, `json` and `token` each have a Rust +Follow the existing split: `net`, `hang`, `json` and `auth` each have a Rust crate and a TypeScript package, with zod schemas mirroring the Rust types. There is no `@moq/mux`, so the catalog extension goes through the same seam `js/hang/src/catalog/root.ts` uses to extend the root schema. A browser observer cannot degrade the operator's classes (`clamp_combined` -bounds the window at the publisher, and `Subscription::default()` is already -`Duration::ZERO`), so this package exists for reuse, not for safety: the +bounds the window at the publisher, and `Subscription::max_age` already +defaults to `Duration::ZERO`), so this package exists for reuse, not for safety: the catalog schema, the snapshot and append-log shapes, and the instrumentation are the same on both sides and should be written once. diff --git a/quest/m2/teleop/docs.md b/quest/m2/teleop/docs.md index 78c95c178b..96266a4044 100644 --- a/quest/m2/teleop/docs.md +++ b/quest/m2/teleop/docs.md @@ -2,12 +2,12 @@ ## Goal -`doc/concept/use-case/other.md` stops being a zero-byte stub and becomes the -teleoperation page, with a runnable non-media example beside it. +`doc/concept/use-case/` gains a teleoperation page, listed in its +`index.md`, with a runnable non-media example beside it. ## Plan -The docs have no non-media tutorial. `rs/moq-native/examples/{chat,clock}.rs` +The docs have no non-media tutorial. `rs/moq-tokio/examples/{chat,clock}.rs` and `rs/moq-json/examples/telemetry.rs` all publish something that is not audio or video, but none is presented as the way to carry application data, and telemetry.rs measures wire savings rather than teaching the shape. diff --git a/quest/m2/teleop/mavlink.md b/quest/m2/teleop/mavlink.md index f13c360ed7..5d545ee4d3 100644 --- a/quest/m2/teleop/mavlink.md +++ b/quest/m2/teleop/mavlink.md @@ -41,25 +41,24 @@ because the newest group wins regardless of which message it holds. Putting them all in one long-lived group has the opposite failure: nothing can be skipped and head-of-line blocking is back. -So the lossy class is a latest-value snapshot, the shape `moq_json::snapshot` -implements, with the frame as an opaque value. Two details decide whether it -actually delivers latest-value: +So the lossy class is a latest-value snapshot of opaque bytes: +`moq_binary::snapshot` (moving to `moq_flate::snapshot` in +[moq-binary folds into moq-flate](/quest/m1/flate-binary.md)), with the raw +frame as the value. Every update is a self-contained group, so a newer value +never waits behind an older one. One detail decides whether it actually +delivers latest-value: - **Key on `(sysid, compid, msgid)`, not msgid alone.** A vehicle is several components, and an autopilot, a gimbal and a camera all emit `HEARTBEAT` under msgid 0; keying on msgid alone lets them overwrite each other. Decide explicitly what to do about instances distinguished only inside a payload, which the msgid-only rule cannot see. -- **Set the encoder's delta ratio to 0.** By default `moq_json::snapshot` - batches up to `MAX_DELTA_FRAMES` merge patches into one ordered group - (`rs/moq-json/src/snapshot/encoder.rs`), so under congestion an earlier 50 Hz - attitude delta head-of-line blocks a later heartbeat inside that group. A - ratio of 0 makes every change a self-contained snapshot, which is the - latest-value contract; anything else has to justify the added delay. -`moq-flate` is the existing answer if the encoding overhead matters. +Not `moq_json::snapshot`: a MAVLink frame is not JSON, and its default batches +up to `MAX_DELTA_FRAMES` merge patches into one ordered group, so an earlier +50 Hz attitude delta would head-of-line block a later heartbeat. -The reliable class is the append-log shape (`moq_json::stream`): commands, +The reliable class is the append-log shape (the binary `stream` mode): commands, ACKs, mission, parameter and file transfer are stop-and-wait exchanges carried in one ordered group. Note the scope in [robot](/quest/m2/teleop/robot.md): that is gap-free for a live reader, not diff --git a/quest/m2/teleop/robot.md b/quest/m2/teleop/robot.md index 42614ceb11..5dd2735138 100644 --- a/quest/m2/teleop/robot.md +++ b/quest/m2/teleop/robot.md @@ -34,41 +34,46 @@ QUIC stream (`open_uni`, `rs/moq-net/src/lite/publisher.rs`), so frames inside it are ordered and delivered exactly once. That guarantee is scoped, and the crate must say so rather than promise -losslessness. A group caches at most `MAX_GROUP_CACHE` bytes and evicts frames -off the front (`rs/moq-net/src/model/group.rs`); a reader that falls behind the -retained window, or joins late, gets `Error::Lagged` and cannot recover the -start of the log. So the contract is ordered, gap-free delivery for a live -reader that keeps up, and recovery after a reconnect belongs to the application +losslessness. A group holds at most `MAX_CACHE_BYTES` and `MAX_GROUP_FRAMES` +(`rs/moq-net/src/model/group.rs`), and a write past either aborts it with +`Error::GroupTooLarge`; `moq_json::stream` never rolls its one group, so that +bound ends the track, and a link drop or reconnect loses what was in flight. +So the contract is ordered, gap-free delivery for a live reader within one +bounded log, and recovery after a reconnect belongs to the application protocol. That is an acceptable division for MAVLink, whose mission, parameter and file-transfer services already carry their own stop-and-wait retransmission, but it must be stated, not assumed. The framing is where the guarantee lives, not the subscription flags: -- `Subscription::ordered` is a scheduling tie-break whose own doc says groups - may arrive out of order or not at all. It aggregates across subscribers by - `&&`, and the IETF transport does not carry it. A class built on it would not - be reliable, which is why the reliable class is a single group instead. +- `track::Subscriber::ordered()` returns an `Ordered` handle that reads the + groups it receives in sequence order. It is a local cursor, not a delivery + guarantee: groups that aged out or were skipped never arrive. A class built + on it would not be reliable, which is why the reliable class is a single + group instead. - What makes the lossy class lossy on the wire is the publisher's - `Info::latency_max`: `commit_group` calls `evict_expired`, which calls - `slot.group.abort(Error::Old)` (`rs/moq-net/src/model/track.rs`), and an abort - resets the QUIC stream, so stale bytes stop being retransmitted. + `Info::max_age`: `evict_expired` aborts an aged-out group with `Error::Old` + (`rs/moq-net/src/model/track.rs`), and an abort resets the QUIC stream, so + stale bytes stop being retransmitted. - A subscriber cannot weaken either class. `clamp_combined` (`rs/moq-net/src/model/track.rs`) clamps the aggregate window down to - `Info::latency_max`, and `Subscription::default()` is already + `Info::max_age`, and `Subscription::max_age` already defaults to `Duration::ZERO`, so a raw observer subscribing through `moq-net` neither widens the window nor has to be prevented from trying. ### Contents -- The catalog section, through `moq-mux`'s `CatalogExt` and - `RenditionConfig`: a namespaced root section whose entries embed a - `JsonConfig` or `BinaryConfig` beside the robot's own fields, published - through the `moq-mux` data producers. +- The catalog entries. The hang catalog already advertises data tracks in its + `json` and `binary` sections (`rs/hang/src/catalog/{json,binary}.rs`), + written by the `moq-mux` data producers, and each entry carries `extra` + fields. Decide whether the robot's fields ride there or in a namespaced + root section through `moq-mux`'s `CatalogExt` and `RenditionConfig`. No hang schema change. - Announce-prefix fan-in, generalised from `rs/moq-boy/src/input.rs`. -- The two delivery classes, as `moq-json`'s snapshot and stream modes with - the group structure and `Info::latency_max` each one needs. +- The two delivery classes, as the snapshot and stream modes with the group + structure and `Info::max_age` each one needs: `moq-json`'s for JSON, and the + opaque-bytes ones for binary frames (`moq-binary`, folding into `moq-flate` + per [moq-binary folds into moq-flate](/quest/m1/flate-binary.md)). - Per-stage timestamp instrumentation, generalised from moq-boy's `status` track. Check it against the publisher-reported stats broadcast ([client stats](/quest/m1/qos/stats/schema.md), moq#2734) before adding a diff --git a/quest/m2/unreal.md b/quest/m2/unreal.md index 7062b81245..64b5912809 100644 --- a/quest/m2/unreal.md +++ b/quest/m2/unreal.md @@ -26,4 +26,3 @@ editor stability across a play-stop-play cycle. ## Required - [Package](/quest/m1/cpp/package.md) - the tarball the module links -- [Decoded frame ownership](/quest/m1/decoded-frames.md) - the decoded frames the texture needs diff --git a/quest/m3/README.md b/quest/m3/README.md index c2e15747b7..ac3b0631f5 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -20,8 +20,5 @@ condition clears, move the quest to the milestone its work belongs in. - [#2893](/quest/m3/2893-video-validate-pipewire-dma-buf-capture-on-kde-hardware.md) - video: validate PipeWire DMA-BUF capture on KDE hardware - [Embedded video](/quest/m3/video-embedded.md) - EGL import in the renderer, so moq-video presents on a Pi - [Vision worker](/quest/m3/processor-vision.md) - a documented customer-run vision worker proves the processor contract -- [libmoq hidden opt-in](/quest/m3/libmoq-hidden.md) - `moq_origin_announced` takes a `hidden` flag so C callers can list `.`-named broadcasts -- [libmoq shutdown](/quest/m3/libmoq-shutdown.md) - OBS exits cleanly with the plugin loaded: a C ABI `moq_shutdown` stops the libmoq thread before the module is unloaded -- [libmoq CMake library](/quest/m3/libmoq-cmake-lib.md) - the in-tree CMake build links the `libmoq.a` cargo reports, not a hardcoded `target/` path -- [libmoq fetch](/quest/m3/libmoq-fetch.md) - libmoq gains an additive cached-group fetch entry point +- [Suffix announce](/quest/m3/suffix-announce.md) - moq-lite-only suffix announce and interest, benchmarked over the announce table, once a deployment needs a claim a service prefix cannot express - [Upstream forks](/quest/m3/upstream-forks.md) - offer the uniffi generator fixes our cpp, dart, and Python forks carry upstream, lowest priority diff --git a/quest/m3/dpdk.md b/quest/m3/dpdk.md index c9ec3a466f..efaf48e21c 100644 --- a/quest/m3/dpdk.md +++ b/quest/m3/dpdk.md @@ -22,10 +22,7 @@ a bare-metal tier is worth operating. ## Required +- [AF_XDP UDP path](/quest/m2/af-xdp.md) - the no-hardware verdict this waits + on; a kernel path within reach of line rate abandons this quest - A moq.pro relay provider offers SR-IOV or bare-metal hosts the fleet can run on - -## Related - -- [AF_XDP UDP path](/quest/m2/af-xdp.md) - the no-hardware verdict this waits - on diff --git a/quest/m3/libmoq-cmake-lib.md b/quest/m3/libmoq-cmake-lib.md deleted file mode 100644 index 9a9887f2fe..0000000000 --- a/quest/m3/libmoq-cmake-lib.md +++ /dev/null @@ -1,29 +0,0 @@ -# [XS] libmoq CMake finds the library cargo built - -## Goal - -`rs/libmoq/CMakeLists.txt` links the static library cargo actually produced, -whatever `CARGO_TARGET_DIR`, `--target` triple, or profile the build uses. -Today `BUILD_RUST_LIB` assumes `target//libmoq.a`, so any other -layout links a stale library or fails to find one. - -## Plan -Parked in m3 behind [Generated C bindings](/quest/m1/c/README.md): the hand-written libmoq is being replaced by C generated from moq-ffi, which carries this for free. Do it only if the hand-written crate outlives that line. - - -- `cmake/cargo-build.cmake` already parses cargo's JSON messages to find - `moq.h` in its hashed `OUT_DIR`. Read the libmoq `compiler-artifact` - message's staticlib filename from the same output and copy it beside the - header in the CMake binary dir, `ONLY_IF_DIFFERENT`. `RUST_LIB` then points - there. Chosen over forcing `--target-dir` into the build dir, which loses - the shared workspace cache, and over `cargo metadata`, which still guesses - the triple and profile subdirectories. -- The `BUILD_RUST_LIB=OFF` prebuilt path is unchanged. -- Regression test in CI: the existing `cpp/obs` build against `MOQ_LOCAL` - runs with `CARGO_TARGET_DIR` outside `target/`, so a hardcoded path fails - the build. Update `rs/libmoq/README.md` and `doc/lib/c` if they describe - the path. - -## Related - -- [libmoq shutdown](/quest/m3/libmoq-shutdown.md) - the other libmoq packaging fix OBS needs diff --git a/quest/m3/libmoq-fetch.md b/quest/m3/libmoq-fetch.md deleted file mode 100644 index a6c8430696..0000000000 --- a/quest/m3/libmoq-fetch.md +++ /dev/null @@ -1,20 +0,0 @@ -# [S] libmoq: fetch one cached group - -## Goal - -A C embedder can fetch one cached group by sequence through the existing frame, -handle, and terminal-status conventions. The new entry point is additive. - -## Plan -Parked in m3 behind [Generated C bindings](/quest/m1/c/README.md): the hand-written libmoq is being replaced by C generated from moq-ffi, which carries this for free. Do it only if the hand-written crate outlives that line. - - -Mirror `MoqTrackConsumer::fetch_group` from `rs/moq-ffi/src/consumer.rs` in -`rs/libmoq`, supporting raw and container-decoded delivery as the FFI does. -Specify ownership, cancellation, cache misses, and terminal delivery using the -existing consume contracts. Test a hit, miss, and cancelled fetch from a C caller. - -Regenerate `moq.h` and update `doc/lib/c/index.md`, whose capability list already -claims group fetch. Decoder output configuration landed separately on the dev line. -The `moq_group_request_*` tests in `rs/libmoq/src/test.rs` fetch from Rust for -lack of this entry point; switch them to it. diff --git a/quest/m3/libmoq-hidden.md b/quest/m3/libmoq-hidden.md deleted file mode 100644 index 4095d694f6..0000000000 --- a/quest/m3/libmoq-hidden.md +++ /dev/null @@ -1,16 +0,0 @@ -# [XS] libmoq hidden opt-in - -## Goal - -C callers can list hidden broadcasts (a `.`-prefixed segment below the -prefix) the way every other binding can: `moq_origin_announced` takes a -`hidden` flag, mirroring `MoqAnnounceConfig.hidden` in moq-ffi. Today a C -caller can only list them by naming the dot segment in `prefix`. - -## Plan -Parked in m3 behind [Generated C bindings](/quest/m1/c/README.md): the hand-written libmoq is being replaced by C generated from moq-ffi, which carries this for free. Do it only if the hand-written crate outlives that line. - - -- Adding a parameter breaks the C ABI, so this lands on `dev`. -- Update `rs/libmoq/src/{api,origin}.rs`, the libmoq tests, `cpp/obs` if it - calls `moq_origin_announced`, and `doc/lib/c/index.md`. diff --git a/quest/m3/libmoq-shutdown.md b/quest/m3/libmoq-shutdown.md deleted file mode 100644 index 8b4cc4870e..0000000000 --- a/quest/m3/libmoq-shutdown.md +++ /dev/null @@ -1,54 +0,0 @@ -# [M] libmoq can be stopped before its host unloads it - -## Goal - -OBS exits without a crash whether a moq output is running, was just stopped, -or a source still has terminal callbacks outstanding. libmoq parks a -process-wide `libmoq` thread in `block_on` on first use and has no way to -stop it, while OBS `dlclose`s each plugin at shutdown; the plugin links -libmoq statically, so the thread's code is unmapped under it. Any host that -unloads the library has the same exposure. - -## Plan -Parked in m3 behind [Generated C bindings](/quest/m1/c/README.md): the hand-written libmoq is being replaced by C generated from moq-ffi, which carries this for free. Do it only if the hand-written crate outlives that line. - - -Starts on `main`; libmoq's runtime (`rs/libmoq/src/ffi.rs`) is the same on -both branches and the change is additive. - -- Reproduce first: exit OBS with the plugin loaded and an output running, - then again right after stopping it, and again with a source whose - `moq_source_destroy` hit its two-second backstop. Record which of these - crashes, and how, in the PR before changing anything. -- Add `moq_shutdown()` to the C ABI, mirroring `moq_ffi_shutdown` in - `rs/moq-ffi`: it stops and joins the `libmoq` thread. Pending calls resolve - as cancelled, the terminal callback for every still-open handle fires with - a cancelled status before it returns, and any later call returns an error - code rather than restarting the runtime. It is idempotent and must be - called from a host thread, never from a libmoq callback. Native codec and - capture threads belong to their handles and end with them; this call does - not reach into them. -- The OBS wiring (`obs_module_unload` calling the shutdown after the outputs - and sources are destroyed) belongs to [OBS migration](/quest/m1/cpp/obs.md), - where the plugin reaches moq-ffi through the generated C++ and calls - `moq_ffi_shutdown`; this quest gives the plain-C ABI the same call for the - hosts that stay on libmoq. -- Regression tests: a `rs/libmoq/c-tests` fixture that builds libmoq as a - cdylib, `dlopen`s it, opens a session, and `dlclose`s once without - `moq_shutdown` (must fail, proving the hazard) and once with it (clean - exit, thread gone); a `rs/libmoq` unit test that pending and later calls - resolve cancelled and open handles close without panicking afterwards - (mirror `rs/moq-ffi/src/test.rs::shutdown_cancels_and_drops_cleanly`, - in a child process since the stop is process-wide). Wire the new fixture - into `just rs c-tests`. -- Cross-package sync: `moq.h` is cbindgen-generated from the Rust doc - comment; update `doc/lib/c` (the handle and callback contract) and - the Go bindings regenerate from `moq.h` and need no - wrapper: `os.Exit` ends the process without tearing a host runtime down - under the thread. - -Public API: additive on the libmoq C ABI (`moq_shutdown`). Wire: none. - -## Related - -- [Kotlin JVM exit](/quest/m1/kt-jvm-exit.md) - the same hazard class for the moq-ffi bindings diff --git a/quest/m3/suffix-announce.md b/quest/m3/suffix-announce.md new file mode 100644 index 0000000000..906f4a29f9 --- /dev/null +++ b/quest/m3/suffix-announce.md @@ -0,0 +1,38 @@ +# [L] Suffix announce and interest in moq-lite + +## Goal + +A moq-lite subscriber can request announcements by suffix (`**/transcode.pro`) +and a publisher can advertise one, so a fleet-wide service claims every +matching path once instead of per prefix. moq-lite only: IETF sessions stay +prefix-only. Routing only: token claim patterns keep their suffix support +unchanged. + +## Plan + +Decided in the 2026-09-28 quest audit: suffix routing is dropped from every +other quest, which are prefix-only, and lives here as a moq-lite-only +extension. IETF MoQT refuses suffix matching, so a session negotiated as IETF +never carries it and there is no draft to converge with. + +Today `AnnounceRequest` (`rs/moq-net/src/lite/announce.rs`) carries only a +`prefix`, and the origin's route table is a trie keyed by path segment +(`rs/moq-net/benches/origin.rs`). A suffix cannot walk that trie, so a naive +match costs the whole announce table on every announcement and every new +cursor. Benchmark first: extend `rs/moq-net/benches/origin.rs` with suffix +cursors swept over publishers and subscribers. The slope decides between a +reversed-segment index and abandoning the quest. + +The wire field is version-gated like `hidden`. Update `js/net` and +`drafts/draft-lcurley-moq-lite.md` in the same PR. Token patterns +(`moq-pattern`, `moq_auth::Claims`) already match suffixes and do not change. + +## Required + +- A deployment needs a suffix claim that a service prefix (`./...`) + cannot express + +## Related + +- [Wildcard](/quest/m0/wildcard/README.md) - prefix-only advertisements and the service-prefix layout this would extend +- [Path patterns](/quest/m1/path-patterns.md) - owns the pattern dialect a suffix interest would reuse diff --git a/quest/m3/upstream-forks.md b/quest/m3/upstream-forks.md index 9ae49440ac..45856ec20e 100644 --- a/quest/m3/upstream-forks.md +++ b/quest/m3/upstream-forks.md @@ -49,6 +49,10 @@ One outcome per candidate: Public API: none. Wire: none. +## Required + +- The maintainer approves offering the fixes upstream, per post, issue, or PR + ## Related - [Upstream the fork](/quest/m1/quic/upstream.md) - the same practice for the noq fork diff --git a/quest/m3/video-embedded.md b/quest/m3/video-embedded.md index 15fd63162d..a844e2f7df 100644 --- a/quest/m3/video-embedded.md +++ b/quest/m3/video-embedded.md @@ -24,3 +24,8 @@ libcamera source composes with what already exists, because `encode::Producer::publish` is the bring-your-own-Annex-B path and `moq_mux::codec::h264` already handles framing, so shelling out to `rpicam-vid` is an application concern rather than a moq-video source. + +## Required + +- Someone with a Raspberry Pi or similar V4L2 M2M device without a usable + Vulkan driver validates the EGL import on it diff --git a/quest/m3/video-hardware.md b/quest/m3/video-hardware.md index 355e929ae5..c8bf739fab 100644 --- a/quest/m3/video-hardware.md +++ b/quest/m3/video-hardware.md @@ -27,6 +27,12 @@ Precedent for what this catches: NVENC validation on an RTX 3070 Ti found that NVENC rejects stream-ordered pool memory, so buffers registered with it must come from plain `cuMemAlloc`. That is not a bug any amount of review finds. +## Required + +- Someone with the hardware runs it: an Intel GPU exposing the VAAPI low-power + entrypoint, a second render node, a V4L2 capture device with DMA-BUF export, + a Windows machine with MJPEG and YUY2 cameras, and a live camera per platform + ## Related - [Validate PipeWire cameras on a portal and a Pi](/quest/m3/pipewire-camera-hardware.md) - the camera portal and a Pi CSI node, which are a different machine from this list diff --git a/quest/m4/msfts-convergence.md b/quest/m4/msfts-convergence.md index 45af899df9..fb344dd510 100644 --- a/quest/m4/msfts-convergence.md +++ b/quest/m4/msfts-convergence.md @@ -20,7 +20,3 @@ change on either side. Update `drafts/draft-lcurley-moq-mpegts.md` and ## Required - msfts#33 (https://github.com/mondain/msfts/issues/33) settles the ES-level payload unit - -## Closes - -- [#3731](https://github.com/moq-dev/moq/issues/3731) - close this issue when the quest finishes From 01b8374f9b7c49f2a76759b5356cb2c6b94c046b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 12:43:19 -0700 Subject: [PATCH 2/3] chore(quest): audit the quest tree against code, branches, and priorities Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 52 ++++++++++++---------------------- quest/m1/track-demand.md | 4 +-- quest/m2/README.md | 16 ++++------- quest/m2/quic-kernel-pacing.md | 9 ++++-- quest/m2/stats-delta.md | 2 +- 5 files changed, 32 insertions(+), 51 deletions(-) diff --git a/quest/m1/README.md b/quest/m1/README.md index 759b3c5fb7..8d6ad1103b 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -18,27 +18,19 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. ## Required - [Cluster routing](/quest/m1/cluster-routing.md) - an announcement says where a broadcast originates, not how to reach it, and a relay hears only the prefixes its clients asked for -- [lite-07 count settle](/quest/m1/lite-count-settle.md) - moq-lite-07 subscribers stop waiting for a subscription's tail once SUBSCRIBE_END's stream count is reached -- [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped` - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` - [Go cancel test](/quest/m1/go-origin-gc.md) - the Go request-cancel test keeps its origin alive, so the collector can't close it mid-test - [Worker socket count](/quest/m1/worker-socket-count.md) - the moq-tokio worker test counts only its own listener's sockets -- [Signal.race cleanup](/quest/m1/signal-race.md) - `Signal.race` releases its signal listeners when its result loses a race -- [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi, libmoq, and every wrapper configure and observe audio playout delay -- [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, audio, and video namespaces built from the handle below +- [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay +- [moq-binary folds into moq-flate](/quest/m1/flate-binary.md) - on dev, moq-flate and @moq/flate own the opaque snapshot and stream tracks and moq-binary is deleted +- [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below - [Track demand](/quest/m1/track-demand.md) - Rust and JS watch a track's subscribers through `demand()` alone - [Error messages](/quest/m1/error-display.md) - Python, Go, and Dart print `MoqError` with Rust's message, as Kotlin and Swift do -- [Remove finish](/quest/m1/broadcast-remove.md) - on dev, the deprecated broadcast end APIs are gone and `closed()` carries no cause -- [CLI inspection](/quest/m1/cli-inspect/README.md) - `moq ls` lists what is live and `moq fetch` reads a group over MoQ, and a guide shows how to inspect a relay - [Session close](/quest/m1/session-close.md) - a graceful session end withdraws announces and waits one second for the ack - [Drain before close](/quest/m1/drain-before-close.md) - a closing client delivers its queued stream finishes, so `moq import` ends the catalog cleanly over a real relay - [Close codes](/quest/m1/close-codes.md) - a client sees the peer's application close code over WebSocket and raw QUIC, like WebTransport - [Raw stream codes](/quest/m1/raw-stream-codes.md) - raw QUIC stream resets and stops carry the application's code, not an HTTP/3-mapped one -- [JS caught up](/quest/m1/js-announce-caught-up.md) - @moq/net's announce consumer says when the initial set has landed, like Rust - [Live in apps](/quest/m1/announce-live-apps.md) - the demo and `@moq/room` show "no broadcasts" from the `live` marker, which waits for the first session on page load -- [Bindings caught up](/quest/m1/announce-live-bindings.md) - moq-ffi, libmoq, and every wrapper yield the same flat announce event, `Live` included -- [Optional max age](/quest/m1/ietf-max-age.md) - max age is optional, set only by the publisher, and crosses moq-transport as MAX_CACHE_DURATION -- [IETF announce count](/quest/m1/ietf-announce-count.md) - an opt-in moq-transport extension carries the replay count, so IETF announce consumers go live without a timer - [kio waiter overflow](/quest/m1/kio-waiter-lost.md) - a retained `Waiter` past 8 lists stops adding a duplicate entry to lists it already recorded - [Capture re-anchor](/quest/m1/capture-reanchor.md) - a repeating or restarting device clock never rewinds native capture during a fast backlog drain - [Splice edge cases](/quest/m1/splice-edges.md) - an unstamped successor, a pruned segment's boundary group, and a warm head during a takeover are each handled correctly @@ -53,38 +45,37 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Wire compatibility](/quest/m1/wire-compat.md) - a nightly run tests this checkout against the last published release for tokens, session wire, and catalog/container - [Accept-side flags](/quest/m1/cli-given-flags.md) - dial-only and local verbs refuse every `--listen-*` flag instead of ignoring it - [JS catalog path](/quest/m1/js-catalog-path.md) - `@moq/net` broadcast consumers expose their path and `Catalog.watch` rejects escaping references, like Rust +- [#2075](/quest/m1/2075-mirror-catalog-reservation-gating-in-moq-hang-js-hang.md) - @moq/publish gates the first catalog snapshot until every reserved track is described - [Full codec string](/quest/m1/publish-codec-string.md) - browser-published video carries the encoder's full RFC 6381 codec string, so native players decode it - [TS export jitter](/quest/m1/ts-export-jitter.md) - the video reorder bound follows later catalogs and the declared reorder depth, so a late B-frame never reorders TS output; an undeclared stream can reorder once per new maximum depth - [TS import shared shift](/quest/m1/ts-import-shared-shift.md) - unflagged loop wraps move audio and video by one shift, so A/V sync holds across wraps - [PipeWire duplicate cameras](/quest/m1/pipewire-dup-cameras.md) - a webcam lists once with PipeWire enabled - [Catalog wall clock](/quest/m1/catalog-wall-clock.md) - `Clock::wall_clock` keeps the catalog's full precision instead of truncating to milliseconds - [IPv6 TLS names](/quest/m1/ipv6-tls-name.md) - dialing an IPv6 literal completes TLS on WebSocket as on noq, including a bare `::1` host override -- [Capture control](/quest/m1/capture-control.md) - on dev, `encode::Capture` replaces `CaptureOptions`, an unsupported `cut()` errors, and dropping the last `Control` cancels in-flight opens +- [Capture control](/quest/m1/capture-control.md) - on dev, `encode::Capture` replaces `CaptureOptions` without a `clock` field (it reads the catalog's), an unsupported `cut()` errors, and dropping the last `Control` cancels in-flight opens - [Video surface](/quest/m1/video-surface.md) - on dev, moq-ffi's `native` becomes `surface`, refused on platforms with no surface - [HLS discontinuity sequence](/quest/m1/hls-discontinuity-sequence.md) - on dev, `Segment::discontinuity` is the absolute sequence, so every cursor agrees -- [Strict Redirect::resolve](/quest/m1/redirect-resolve.md) - on dev, `Redirect::resolve` can no longer quietly turn a refused redirect into a redial - [RTMP TLS only](/quest/m1/rtmp-tls-only.md) - an RTMP listener configured for TLS can refuse plaintext instead of sniffing and serving it - [HLS linger](/quest/m1/hls-linger.md) - `moq_hls::Server` serves an ended broadcast for its playlist window plus grace, so the moq.pro edge drops its own pool -- [Gateway live clock](/quest/m1/gateway-live-clock.md) - moq-srt, moq-rtmp, and HLS import publish on the broadcast clock, so encoder reconnects don't restart timestamps +- [Remove live()](/quest/m1/remove-live.md) - on dev, importers publish stream timestamps verbatim, the catalog clock maps them to wall time, and an encoder restart becomes a new epoch - [iroh versions](/quest/m1/iroh-lite-wip.md) - `iroh://` negotiates the configured versions, so `moq-lite-07-wip` can be opted into - [Go and Dart doc samples](/quest/m1/doc-samples-go-dart.md) - Go and Dart doc samples compile against their wrappers - - [Data capture in bindings](/quest/m1/data-capture-bindings.md) - moq-ffi and every wrapper pass a data frame's capture time, and the JSON window producer takes one - [Moxygen compatibility](/quest/m1/moxygen/README.md) - one subgroup per group, whole-group FETCH, and one datagram per group, never a full moxygen pass - [JS IETF datagrams](/quest/m1/js-ietf-datagram.md) - `@moq/net` sends and receives datagram groups over moq-transport, like Rust +- [#2991](/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md) - one dynamic producer per track name in both languages, with the sequence namespace surviving a replacement - [JavaScript FETCH](/quest/m1/js-fetch.md) - generic on-demand group serving and IETF FETCH for browser publishers -- [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS, on the catalog and store the release ships +- [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS; the catalog entry and format may break in place, since no archives exist - [Tooling](/quest/m1/tooling/README.md) - justfiles become a one-line menu over `sh/`, one impact map scopes CI, and every workflow step runs a recipe - [Path patterns](/quest/m1/path-patterns.md) - one matcher for every predicate over broadcast paths: tokens, origins, interest - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish +- [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped` - [Shaper virtual time](/quest/m1/shaper-virtual-time.md) - `moq-shaper` tests judge seeded decisions on paused time, not on wall-clock delivery under load - [Rust compressed gate test](/quest/m1/json-compressed-gate-rs.md) - `rs/moq-json` proves a snapshot delta is gated on its encoded size - [Shrink the JS overflow test](/quest/m1/json-rolls-snapshot-test.md) - the `js/json` roll-on-overflow test uses a small `maxGroupBytes` budget instead of megabytes of JSON -- [Decoded frame ownership](/quest/m1/decoded-frames.md) - retain moq-video Frames across bindings, with native views or CPU conversion as needed - [C++ through moq-ffi](/quest/m1/cpp/README.md) - generated C++ over moq-ffi with futures and expected-style errors, shipped as a tarball, vcpkg, and Conan, and adopted by the OBS plugin - [Generated C bindings](/quest/m1/c/README.md) - C generated from moq-ffi ships as `moq-c` 0.8.0 and replaces the hand-written libmoq -- [moq-c](/quest/m1/moq-c.md) - libmoq ships as `moq-c`, beside `moq-cpp`, with its C header and library unchanged -- [OBS native codecs](/quest/m1/obs-moq-video/README.md) - remove FFmpeg decoding dependencies, deliver GPU frames, and use native audio/video encoders +- [OBS native codecs](/quest/m1/obs-moq-video/README.md) - replace FFmpeg video and audio decoding with moq-video and moq-audio, deliver GPU frames, and use native audio/video encoders - [Opus concealment](/quest/m1/opus-conceal.md) - a lost Opus packet conceals the last packet's length, not 120 ms - [Audio codecs](/quest/m1/audio-codecs/README.md) - platform audio codecs, explicit unsupported cases, and channel layouts up to 7.1 - [Opus catalog rate](/quest/m1/opus-catalog-rate.md) - MKV Opus import publishes the 48 kHz codec rate in the catalog, not the OpusHead input rate @@ -92,19 +83,15 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [CMAF surround Opus](/quest/m1/cmaf-opus-surround.md) - fMP4 import and export carry an Opus channel mapping table - [js/hang dOps pre-skip](/quest/m1/js-dops-pre-skip.md) - CMAF encoding in js/hang stops hard-coding a 312-sample pre-skip - [GPU CI](/quest/m1/gpu-ci.md) - NVIDIA tests run nightly on a self-hosted GPU runner, and `just rs nvidia` runs them locally instead of skipping -- [GPU pool reservation](/quest/m1/gpu-pool-reservation.md) - a full GPU frame pool is a `None` reservation the caller drops on, not an error to match - [JS rendition ranking](/quest/m1/js-ranked.md) - `@moq/hang` ranks video renditions like Rust, and `@moq/watch`'s fallback uses it - [Audio rendition pick](/quest/m1/audio-ranked.md) - single-track FLV/RTMP and WHEP serve the best audio rendition, not the first by name - [FLV rebind before header](/quest/m1/flv-rebind.md) - single-track FLV switches to a better rendition announced before the header - [FLV catalog stream](/quest/m1/flv-catalog-stream.md) - on dev, `flv::Export` takes a catalog stream like fmp4, replacing `with_select` -- [Keyframe trigger](/quest/m1/keyframe-trigger.md) - an application can ask the built-in capture encoder for a keyframe -- [Video keyframe flag](/quest/m1/video-keyframe-flag.md) - encoded video marks its keyframes, so a requested cut never forces an extra one after a cadence keyframe -- [QoS](/quest/m1/qos/README.md) - broadcast health: relay starvation and timeliness histograms, and client stats broadcasts from publishers and viewers +- [Own the QUIC stack](/quest/m1/quic/README.md) - the moq-noq fork carries ACK progress, reliable reset, hierarchical scheduling, deadlines, peer limits, and qmux +- [QoS](/quest/m1/qos/README.md) - broadcast health: relay starvation and timeliness histograms, and client stats broadcasts from publishers and viewers, on dev - [Drain](/quest/m1/drain/README.md) - relay restarts drain sessions over GOAWAY instead of hard-dropping them +- [Strict Redirect::resolve](/quest/m1/redirect-resolve.md) - on dev, `Redirect::resolve` can no longer quietly turn a refused redirect into a redial - [Transport upgrade](/quest/m1/transport-upgrade/README.md) - a session that came up over WebSocket moves to QUIC once the QUIC dial lands, handing over at a group boundary -- [Own the QUIC stack](/quest/m1/quic/README.md) - the moq-noq fork carries - ACK progress, reliable reset, hierarchical scheduling, deadlines, probing, - keep-alive, peer limits, careful resume, ECN, and qmux - [P2P](/quest/m1/p2p/README.md) - opted-in clients serve each other over data channels and iroh while the relay stays the rendezvous and the fallback, under application policy - [One port](/quest/m1/one-port/README.md) - a relay speaks QUIC, STUN, WebRTC media, and SRT on one UDP port and HTTP, RTMP, and RTMPS on one TCP port - [Signed priority](/quest/m1/signed-priority.md) - on dev, every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it @@ -130,26 +117,25 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [RTMP interleaving](/quest/m1/rtmp-interleaving.md) - isolate partial messages before optimizing assembly copies - [Cache expiry growth](/quest/m1/cache-expiry-growth.md) - with the default pool, relay memory plateaus at the expiry window on every version - [Plan: cache age-out](/quest/m1/cache-wall-eviction.md) - a swept benchmark decides whether the track cache ages groups out on wall time without a write +- [Frame slot charge](/quest/m1/frame-slot-charge.md) - a group's frame slots past the first four count against the cache pool, including capacity a released group keeps - [Relay memory](/quest/m1/relay-memory.md) - remeasure what an announcement costs after prefix routes -- [PoP skipping](/quest/m1/pop-skipping/README.md) - short cold paths for unpopular broadcasts without losing warm backhaul dedup -- [Route cost in the JS origin](/quest/m1/route-cost.md) - the browser origin ranks routes by cost and hops like Rust instead of newest-first - [Front parking](/quest/m1/origin-front-parks.md) - an unroutable request waits on a front instead of re-asking on every route-table move - [Publish channel count](/quest/m1/publish-audio-channel-count.md) - forcing a channel count on an Audio.Capture stops costing the subscriber gaps of silence - [JS abandonment](/quest/m1/js-subscribe-abandonment.md) - a viewer returning during IETF subscribe setup keeps its track across microtasks - [IETF stream types](/quest/m1/ietf-uni-stream-types.md) - padding streams are discarded stream-only and an unknown uni type closes the session, per draft-21 -- [#2991](/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md) - one dynamic producer per track name in both languages, with the sequence namespace surviving a replacement - [Epoch primitive](/quest/m1/epoch.md) - one `Epoch` type in moq-net and @moq/net, carried as a trailing `@` path segment, shared by e2ee and broadcast epochs - [E2EE](/quest/m1/e2ee/README.md) - TypeScript and Rust peers interoperate over encrypted broadcasts no relay can decrypt - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - each publish of a name gets a fresh `@` epoch, viewers follow the newest live one at once, and bare names still resolve on every version -- [Processor](/quest/m1/processor/README.md) - a customer-run worker publishes an on-demand contribution with scoped access +- [Processor](/quest/m1/processor/README.md) - a customer-run worker publishes an on-demand contribution under its own service prefix with scoped access - [#3056](/quest/m1/3056-watch-video-decoder-captures-the-rewind-generation-at.md) - watch: the video decoder resets on a declared discontinuity - [#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md) - the catalog rotation follows the live camera's orientation -- [#2075](/quest/m1/2075-mirror-catalog-reservation-gating-in-moq-hang-js-hang.md) - @moq/publish gates the first catalog snapshot until every reserved track is described - [#2848](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md) - the Opus producer follows its bandwidth grant through the settled `moq_mux::rate::Control` - [Ladder](/quest/m1/ladder/README.md) - a transcode ladder adapts to the uplink it publishes over, instead of encoding every live rung at its ceiling - [LOC duration marker](/quest/m1/loc-duration-marker.md) - LOC producers write the marker once released consumers skip it -- [#2278](/quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md) - hang: expose the fixed catalog-root broadcast clock without synchronizing library playback to wall time +- [#2278](/quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md) - hang: document reading the catalog-root clock and converting PTS to wall time, without synchronizing library playback to wall time - [Time stretch](/quest/m1/watch-audio-time-stretch.md) - js/watch: the audio ring converges by time-stretching instead of skipping or going silent +- [Native audio quality](/quest/m1/audio-quality-native.md) - the browser lane's profiles, budgets, and metric schema run against `moq play` on a dummy device +- [fMP4 emsg](/quest/m1/emsg.md) - event messages survive fMP4 import, and the timed-metadata contract ID3, SCTE-35, and FLV script tags share is settled with them - [#2279](/quest/m1/2279-hang-typed-scte-35-ad-cue-signaling-carried-opaquely.md) - hang: SCTE-35 cues arrive immediately on an independent metadata track, optionally associated with a rendition - [Caption import](/quest/m1/captions-import.md) - fMP4 and MKV subtitle tracks import as text renditions instead of erroring or being dropped - [MSF caption roles](/quest/m1/captions-msf.md) - an MSF caption, subtitle, or sign-language track survives conversion to a hang catalog @@ -166,7 +152,6 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [SRT import stats](/quest/m1/srt-import-stats.md) - the SRT gateway reports the same per-stream counters instead of nothing - [Text availability](/quest/m1/text-schema.md) - a text track publishes its own coverage index instead of copying the media timeline - [ID3 catalog section](/quest/m1/id3.md) - timed ID3 as a first-class container-neutral catalog section -- [fMP4 emsg](/quest/m1/emsg.md) - event messages survive fMP4 import, and the timed-metadata contract ID3, SCTE-35, and FLV script tags share is settled with them - [FLV script tags](/quest/m1/flv-script.md) - onMetaData and AMF data messages survive RTMP and FLV import - [Release profile](/quest/m1/release-profile.md) - every release build gets fat LTO, one codegen unit, and stripping from the workspace profile instead of three script exports - [Size report](/quest/m1/size-report.md) - a nightly job reports every shipped artifact's size, native and JS, and alerts when one grows @@ -180,7 +165,6 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Kotlin JVM exit](/quest/m1/kt-jvm-exit.md) - a Kotlin/JVM program exits cleanly whatever the moq-ffi runtime thread is doing, like Python does since #3766 - [Dart publish](/quest/m1/dart-publish.md) - the packages are built and dry-run clean but exist nowhere consumers can install from - [Dart codec parity](/quest/m1/dart-codecs.md) - Dart is the one binding that cannot originate media -- [moq-mux on wasm32](/quest/m1/mux-wasm-target.md) - the crate's two wasm blockers are fixed and the target stays in the clippy lane - [#2850](/quest/m1/2850-js-net-give-reader-a-synchronous-decode-so-the-publisher.md) - js/net: decode messages synchronously from buffered bytes and delete the publisher read-ahead queue - [Install moq](/quest/m1/moq-installer.md) - one command installs or upgrades the released CLI on macOS and Linux - [Install URL](/quest/m1/moq-install-url.md) - moq.dev serves the canonical installer at /install.sh diff --git a/quest/m1/track-demand.md b/quest/m1/track-demand.md index 90be7ad3e9..6dc5cea5bc 100644 --- a/quest/m1/track-demand.md +++ b/quest/m1/track-demand.md @@ -15,8 +15,8 @@ moq-binary, moq-relay, moq-transcode, moq-stats, and libmoq. Both waits already surface the track's abort reason, so callers keep their errors. Keep `abort_unused` if its race still needs an owner. JS mirrors the Rust shape in `js/net`. -Lands after the release, alongside the [FFI shape](/quest/m1/ffi-shape/README.md) -line, so moq-net breaks once. Its PR retargets to `dev`. +Lands on `dev` alongside the [FFI shape](/quest/m1/ffi-shape/README.md) +line, so moq-net breaks once. Public API: breaking in moq-net and the layer crates, and in `@moq/net`. Wire: none. diff --git a/quest/m2/README.md b/quest/m2/README.md index f042fcf0b9..ff38a25780 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -31,14 +31,12 @@ upstream release waits in [m4](/quest/m4/README.md). - [Audio loss recovery](/quest/m2/audio-loss-recovery.md) - prove a useful Opus recovery policy before exposing another option - [Opus implementation](/quest/m2/audio-opus-backend.md) - compare current codec quality, CPU, and optional build costs - [Latency ledger](/quest/m2/latency-ledger.md) - a session reports where its end-to-end audio delay went, stage by stage -- [JS discontinuity](/quest/m2/js-discontinuity.md) - JS names its timeline break `discontinuity()` like Rust, so `cut` means the same group close in both +- [JS discontinuity](/quest/m2/js-discontinuity.md) - on dev, JS `discontinuity()` without an end writes no cadence-estimated end, like Rust - [Synced data playback](/quest/m2/watch-data-sync.md) - js/watch releases JSON and binary payloads on the media playhead, and a slow data track holds media back - [Media Foundation decode](/quest/m2/audio-decode-mediafoundation.md) - Windows decodes HE-AAC, multichannel AAC, and what else the MFTs offer - [Media Foundation encode](/quest/m2/audio-encode-mediafoundation.md) - Windows encodes AAC-LC - [MediaCodec decode](/quest/m2/audio-decode-mediacodec.md) - Android decodes HE-AAC, multichannel AAC, and what else the device offers - [MediaCodec encode](/quest/m2/audio-encode-mediacodec.md) - Android encodes AAC-LC -- [AAC encode refusal](/quest/m2/aac-encode-refusal.md) - AAC config encode refuses channel counts it cannot name, on dev -- [OBS channel layouts](/quest/m2/obs-wave-layout.md) - the OBS source maps channel counts to the WAVE default layouts, like moq-audio - [Video codec coverage](/quest/m2/video-codec-coverage.md) - prioritize remaining native AV1 and portable decoder gaps - [#2147](/quest/m2/2147-moq-video-10-bit-hevc-and-av1-support-in-the-nvidia-codec.md) - moq-video: 10-bit HEVC and AV1 support in the NVIDIA codec path - [NVENC buffer pool](/quest/m2/nvenc-pool.md) - NVENC reuses input and output buffers instead of allocating per frame, if a benchmark shows it wins @@ -46,13 +44,13 @@ upstream release waits in [m4](/quest/m4/README.md). - [Direct3D11 render import](/quest/m2/render-d3d11.md) - Windows presents without downloading every frame to system memory - [Intra-refresh GOPs](/quest/m2/intra-refresh/README.md) - video with periodic intra refresh publishes, imports, and tunes in cleanly with one group per sweep and a catalog `warmup` - [Capture multi-plane PipeWire cameras](/quest/m2/pipewire-camera-planes.md) - I420 and NV12 cameras that deliver one memory block per plane -- [#2819](/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md) - moq-video: carry PipeWire DMA-BUFs safely into the Vulkan renderer +- [#2819](/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md) - moq-video: validate PipeWire DMA-BUFs into the Vulkan renderer on hardware, and export V4L2 buffers as DMA-BUFs - [Unreal prototype](/quest/m2/unreal.md) - a UE5 module on the C++ package with exceptions disabled, rendering a subscribed broadcast to a texture - [Unity prototype](/quest/m2/unity.md) - the C# package under IL2CPP, playing subscribed audio - [C# through moq-ffi](/quest/m2/cs/README.md) - generated C# over moq-ffi as a NuGet package with native runtimes - [vcpkg registry](/quest/m2/cpp-vcpkg.md) - a registry we own serves the prebuilt package to `vcpkg` manifests - [Conan remote](/quest/m2/cpp-conan.md) - a remote we own serves the same tarball to `conan install` -- [Compressed tracks](/quest/m2/flate/README.md) - any track compresses per group from every language, not only the JSON modes +- [Compressed tracks](/quest/m2/flate/README.md) - the hand-written binding wrappers expose flate tracks - [Binary delta stats](/quest/m2/stats-delta.md) - an on-demand varint delta flavor of every stats track, if relay encode CPU still matters after the JSON fixes - [#3115](/quest/m2/3115-moqsink-the-publication-has-no-generation-so-a-flush.md) - moqsink: a flushing restart after EOS opens a new publication generation - [Redundant ingest](/quest/m2/redundant-ingest.md) - decide whether two publishers sharing one epoch may splice, and who declares the incumbent dead before the keep-alive does @@ -62,7 +60,7 @@ upstream release waits in [m4](/quest/m4/README.md). - [QUIC FEC](/quest/m2/quic-fec.md) - a measured verdict on transport-level FEC vs retransmission - [Google BBR comparison](/quest/m2/quic-bbr-google.md) - measure growth detection and precautionary probing after the correctness fixes - [WHEP ABR](/quest/m2/whep-abr.md) - a WHEP viewer switches renditions from its own congestion feedback -- [Natural media drains](/quest/m2/quic-bbr-app-limited.md) - whether bounded drain credit avoids ProbeRTT deadline interference +- [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - whether bounded drain credit avoids ProbeRTT deadline interference - [Discover media headroom](/quest/m2/quic-probe.md) - test useful-media pacing before adding redundant probe traffic - [L4S on the backbone](/quest/m2/quic-ecn.md) - an ECT(1) option in the fork, an `ecn` config knob, and a dualpi2 measurement - [Careful resume on reconnect](/quest/m2/quic-careful-resume.md) - a redial starts at the previous connection's rate @@ -75,14 +73,11 @@ upstream release waits in [m4](/quest/m4/README.md). - [Send buffer pools](/quest/m2/quic-buffer-pool.md) - whether pooled send buffers beat Bytes in the stream send path - [AF_XDP UDP path](/quest/m2/af-xdp.md) - the kernel-bypass verdict on today's virtio hosts that gates DPDK - [GOP overhead](/quest/m2/gop-overhead.md) - price the I-frames a short GOP pays for, deciding whether a long GOP plus a keyframe request is worth designing -- [#703](/quest/m2/703-experimental-webgpu-renderer.md) - Experimental WebGPU renderer -- [#823](/quest/m2/823-svc-support.md) - SVC support? -- [#1838](/quest/m2/1838-tr-101-290-monitoring-requirements-broadcast-contribution.md) - TR 101 290 monitoring: requirements (broadcast/contribution health metrics) +- [#1838](/quest/m2/1838-tr-101-290-monitoring-requirements-broadcast-contribution.md) - plan TR 101 290 stream-health monitoring into implementation quests - [Teleoperation](/quest/m2/teleop/README.md) - MoQ carries robot video down and control up on one session as a library capability - [SIP media stack](/quest/m2/sip-stack.md) - terminate one inbound SIP audio call leg and expose it as Opus frames - [Carrier voice](/quest/m2/carrier-voice/README.md) - determine whether MoQ should be the call fabric for programmable carrier voice - [LiveKit WebRTC bridge](/quest/m2/livekit-webrtc-bridge.md) - a go/no-go verdict, backed by a spike, on per-track LiveKit-to-MoQ bridging -- [Expired token error](/quest/m2/auth-expired-error.md) - an expired token reports `Error::Expired`, not `Unauthorized`, in Rust, JS, and the bindings - [Common Access Tokens](/quest/m2/cat/README.md) - a moq-transport client presents a CAT in SETUP and `moq auth serve` admits it with the scope its `moqt` claim names - [Runtime QA hosts](/quest/m2/runtime-qa-hosts.md) - run exact source snapshots on accessible Linux and device hosts with retrievable debug evidence - [Media QA on other engines](/quest/m2/browser-media-qa-engines.md) - the media harness measures a Firefox or WebKit player over the fallback and names what each engine lacks @@ -90,7 +85,6 @@ upstream release waits in [m4](/quest/m4/README.md). - [Windows capture parity](/quest/m2/capture-windows.md) - system audio and screen cursor capture with a settled app-capture policy - [Linux capture parity](/quest/m2/capture-linux.md) - Wayland window/system-audio capture with explicit display-selection and app-capture limits - [Plan capture ergonomics](/quest/m2/capture-ergonomics.md) - scope independent crop and audio mixing quests -- [Capture clock source](/quest/m2/capture-clock-source.md) - capture publishers stamp on the catalog's clock, with no separate clock to pass, on dev - [Audio capture time](/quest/m2/audio-capture-time.md) - native audio stamps a buffer's capture instant, not when the driver reads it - [X11 capture transport](/quest/m2/x11-capture-shm.md) - move X11 capture to shared memory and RandR events instead of a per-frame socket copy - [Capture frame buffers](/quest/m2/capture-frame-buffers.md) - stop rebuilding a full-frame buffer every tick in the X11 and Windows backends diff --git a/quest/m2/quic-kernel-pacing.md b/quest/m2/quic-kernel-pacing.md index af68254040..f8d3b32915 100644 --- a/quest/m2/quic-kernel-pacing.md +++ b/quest/m2/quic-kernel-pacing.md @@ -3,9 +3,12 @@ ## Goal A measured verdict on handing packet pacing to the kernel. Today noq's pacer -is a userspace token bucket, and the io_uring driver ignores its hint -entirely: a GSO train leaves as one burst, bounded only by the congestion -window. Either the kernel paces each train (`SO_TXTIME` with the `etf` or +is a userspace token bucket on both runtimes: `poll_transmit` holds a train +until the pacing timer fires, which the io_uring driver arms through +`poll_timeout` like the tokio driver does, but a released GSO train still +leaves the NIC as one burst. The `flush_one` comment in +`rs/moq-uring/src/quic/noq/connection.rs` saying the driver ignores the hint +is stale from quiche. Either the kernel paces each train (`SO_TXTIME` with the `etf` or `fq` qdisc, per-packet transmit times in the cmsg the driver already builds) and burstiness at the bottleneck drops without costing CPU, or the burst is shown not to matter on the fleet's paths. diff --git a/quest/m2/stats-delta.md b/quest/m2/stats-delta.md index b60e1b9afa..475e7c6532 100644 --- a/quest/m2/stats-delta.md +++ b/quest/m2/stats-delta.md @@ -112,4 +112,4 @@ impact: new on-demand tracks; existing tracks unchanged. - [Stats format page](/doc/concept/stats.md) - where the new flavor is documented - [Client stats](/quest/m1/qos/stats/README.md) - the extension and gauges the format must carry or refuse -- [Compressed tracks](/quest/m2/flate/README.md) - the group-window discipline this flavor repeats +- [Compressed tracks](/quest/m2/flate/README.md) - group-scoped DEFLATE tracks, whose group-window discipline this flavor repeats From 16fcfb6a40de3a9efd0659843661aa91130b26c4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 12:59:15 -0700 Subject: [PATCH 3/3] chore(quest): address review on remove-live and suffix-announce Co-Authored-By: Claude Opus 5.5 --- quest/m1/remove-live.md | 5 +++-- quest/m3/suffix-announce.md | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/quest/m1/remove-live.md b/quest/m1/remove-live.md index e193202ff9..99c040ec2c 100644 --- a/quest/m1/remove-live.md +++ b/quest/m1/remove-live.md @@ -6,8 +6,9 @@ The fMP4, MPEG-TS, and FLV importers in `rs/moq-mux` have no `live()`: every importer publishes the stream's own timestamps verbatim (after PTS unwrap), and the catalog's root `clock` is what maps them to wall time. `moq import` (`rs/moq-cli/src/publish.rs`) stops calling it. An encoder that restarts its -timestamps starts a new broadcast epoch instead of being re-anchored forward -onto the old one. The SRT, RTMP, and HLS gateways, which reuse these +timestamps ends the broadcast with an error instead of being re-anchored +forward onto the old one; turning the republish into a new epoch is the +broadcast epoch line's outcome, not this quest's. The SRT, RTMP, and HLS gateways, which reuse these importers, get the same behavior. ## Plan diff --git a/quest/m3/suffix-announce.md b/quest/m3/suffix-announce.md index 906f4a29f9..5e061f3b39 100644 --- a/quest/m3/suffix-announce.md +++ b/quest/m3/suffix-announce.md @@ -19,9 +19,13 @@ Today `AnnounceRequest` (`rs/moq-net/src/lite/announce.rs`) carries only a `prefix`, and the origin's route table is a trie keyed by path segment (`rs/moq-net/benches/origin.rs`). A suffix cannot walk that trie, so a naive match costs the whole announce table on every announcement and every new -cursor. Benchmark first: extend `rs/moq-net/benches/origin.rs` with suffix -cursors swept over publishers and subscribers. The slope decides between a -reversed-segment index and abandoning the quest. +cursor. Requests hit the same wall: `request_broadcast` resolves through +`best_route`, which walks the prefix trie for the longest covering claim, so +a suffix advertisement must also be inserted where SUBSCRIBE and FETCH +resolution find it, with the same specificity rules as prefix claims. +Benchmark first: extend `rs/moq-net/benches/origin.rs` with suffix cursors +and suffix route lookups, each swept over publishers and subscribers. The +slopes decide between a reversed-segment index and abandoning the quest. The wire field is version-gated like `hidden`. Update `js/net` and `drafts/draft-lcurley-moq-lite.md` in the same PR. Token patterns