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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,30 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [JS group guard](/quest/m1/js-group-guard.md) - a `@moq/net` publisher abandons a group past its max age without an unhandled rejection
- [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`
- [Interop flakes](/quest/m1/interop-flakes.md) - the interop harness passes with other runs sharing the machine
- [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
- [Track demand](/quest/m1/track-demand.md) - Rust and JS watch a track's subscribers through `demand()` alone
- [Kotlin end](/quest/m1/kotlin-end.md) - Kotlin exposes `close()` as `end()`, since `AutoCloseable.close()` takes the name
- [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
- [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

- [Publish delay](/quest/m1/publish-delay.md) - js/publish encoders advertise `delay` behind the earliest rendition, like moq-mux
- [Data jitter](/quest/m1/data-jitter.md) - JSON and binary tracks with a capture time advertise a detected `delay` and `jitter`
- [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
- [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
- [Wildcard](/quest/m1/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
Expand All @@ -59,6 +66,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [mp4-atom dOps mapping](/quest/m1/mp4-atom-dops-mapping.md) - a released mp4-atom reads and writes any `dOps` channel mapping family and table
- [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
Expand Down Expand Up @@ -97,6 +105,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [Closure counters](/quest/m1/closure-counters.md) - a departed node's return never regresses the closure counters a consumer already saw
- [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
- [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
Expand Down
42 changes: 42 additions & 0 deletions quest/m1/announce-live-apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# [M] Apps show "no broadcasts" from the live marker

## Goal

A browser page listing broadcasts shows an empty state once the relay has
said there are none, never a spinner that never resolves and never a false
empty state before the first session answered. The demo watch page and
`@moq/room` use `@moq/net`'s `live` marker, and `@moq/net` settles when an
origin stream opened before the first connection goes live.

## Plan

- #4261 (on `dev`) adds the `live` event; 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
session to wait on, so today it goes `live` at once and broadcasts arrive
after it. Settled: the reconnect loop (`js/net/src/connection/reload.ts`),
which already answers requests through `expect()`, holds the marker until
its first session lands `live` or its first dial gives up. An empty list
then means the relay said so or is unreachable, which a UI can tell apart.
Once a session is up, its own `live` ends the hold: every wire guarantees
one (ANNOUNCE_OK, ANNOUNCE_INIT, or the quiet-stream fallback). A peer that
accepts the announce stream and never answers is a peer bug, so no extra
timeout.
Check whether Rust's reconnecting client has the same gap.
- Apps: loading before `live`, an explicit empty state after it with nothing
announced, and an error state when the connection gives up. Libraries
expose the state as a signal; wording stays in the demo.
- Tests: an origin stream opened before connect is not `live` until the first
session is; a connection that cannot connect ends the wait.

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
39 changes: 39 additions & 0 deletions quest/m1/cache-wall-eviction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [S] Plan: wall-clock age-out in the track cache

## Goal

A benchmark decides whether a track's cache ages groups out without waiting
for a write, on `max(wall, pts)` like other time decisions in moq-net, or
stays the one write-driven exception. The result is an implementation quest
or a recorded reason to keep the exception.

## Plan

Settled scope: a track's `max_age` retention aging groups out on a timer
instead of only on a write. The pool's idle expiry is out of scope.

- Today a track's `max_age` is media time and is applied only when the track
writes: a group ages out when a later one starts (`is_stale` and the expiry
scans in `rs/moq-net/src/model/track.rs`). A track that stops writing keeps
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.
- 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.
- Bench in `rs/moq-net/benches/track.rs`, swept over tracks (1 to 10k) and
cached groups per track (1 to 1k): write-path cost, timer cost per driver
pass, and retained memory for a population of idle tracks. A cost that
grows with the table should show as a slope.
- Weigh the semantics too: `max_age` is media time on purpose, so a congestion
stall cannot age content out (`track::Info::max_age`). A wall term changes
that for a stalled but live publisher.
- Record the numbers and the decision in the PR, then rewrite this quest into
the implementation or delete it.

Public API: none from the plan. Wire: none.

## Related

- [Cache expiry growth](/quest/m1/cache-expiry-growth.md) - relay memory past the expiry window, in the same cache
- [Cache shard](/quest/m1/perf/cache-shard.md) - the pool's shared counters under many workers
49 changes: 49 additions & 0 deletions quest/m1/data-capture-bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# [M] Bindings stamp data frames with a capture time

## Goal

A moq-ffi publisher, and every wrapper over it (Python, Swift, Kotlin, Go,
Dart), can pass a capture time with a JSON or binary snapshot `update` or
stream `append`, so its data tracks advertise `delay` and `jitter` like a Rust
publisher's. Leaving it out keeps today's behaviour. `moq-json`'s `window`
producer takes a capture time too. Settled scope: moq-ffi and its wrappers,
not libmoq.
Comment on lines +9 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include libmoq in capture-time parity

The plan explicitly excludes libmoq, but rs/libmoq/src/api.rs exposes the equivalent JSON and binary update/append APIs, so C callers would be the only binding unable to supply capture times and doc/lib/c would remain stale. Include the C ABI, documentation, and tests in this quest or a required companion.

AGENTS.md reference: AGENTS.md:L92-L96

Useful? React with 馃憤聽/ 馃憥.

@kixelated kixelated Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree. The maintainer settled the scope as moq-ffi and its wrappers, not libmoq. The generated C bindings questline (quest/m1/c/README.md) deletes the hand-written libmoq ABI in favor of C generated from moq-ffi, so C picks this up with no libmoq work. Added that link under Related in e16fc6c.

(Written by Claude Opus 5.5)


## Plan

- #4270 gives the Rust producers `moq_net::Timed<P, T>`, built with
`Timed::from(value).at(t)`. The `moq-mux` data producers take
`Timed<_, Instant>`, map it onto the broadcast clock, and refuse one ahead of
now (`Error::InvalidCapture`). The moq-ffi producers in
`rs/moq-ffi/src/{json,binary}.rs` pass bare values.
- Settled: the capture time is a media timestamp on the broadcast's
timeline. moq-ffi exposes the broadcast clock's `now()` as a timestamp;
callers stamp payloads with values taken from it, and moq refuses one ahead
of now. That keeps a device or process clock out, as the Rust `Instant`
mapping does. The `moq-mux` producers take an `Instant` today, so either
map the timestamp back through the clock inside moq-ffi or give the clock a
typed timestamp moq-mux accepts; keep a raw `Timestamp` from compiling
there. Make it optional on the existing methods, never a `_with_capture`
twin.
- `moq-json` window: `window::Producer::push` stamps `Timestamp::now()`
(`rs/moq-json/src/window/producer.rs`). Accept `Timed` as the snapshot and
stream producers do. Nothing in `moq-mux` publishes window mode, so there is
no estimator to feed.
- Wrappers follow per the cross-package sync table, each with a test that a
past capture time is accepted and a future one refused. Update
`doc/lib/{py,swift,kt,go,dart}`.

Public API: breaking, so it lands on `dev`. A new parameter on the generated
`update` and `append` breaks every published binding caller (Go, for one, has
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

- [Data jitter](/quest/m1/data-jitter.md) - `Timed` and the mux capture path (#4270)

## 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
34 changes: 34 additions & 0 deletions quest/m1/error-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# [M] Every binding prints MoqError's message

## Goal

Python's `str(err)`, Go's `err.Error()`, and Dart's `toString()` on a
`MoqError` return the message from moq-ffi's exported `Display`, as Kotlin's
`toString()` and Swift's `description` already do. No hand-written
per-variant strings and no extra message function.

## Plan

- #4292 adds `#[uniffi::export(Display)]` on `MoqError` in
`rs/moq-ffi/src/error.rs`, on the C++ line. If it has not reached `main`
when this starts, add the same attribute here; it is additive.
- Go and Dart are fixed in their generators; Python in its wrapper:
- Go: the `kixelated/uniffi-bindgen-go` fork. The generated `Error()` prints
`MoqError: <variant>`. Render the exported `Display` for errors, tag, and
bump every pin site the `flake.nix` comment lists.
- Dart: the `kixelated/uniffi-dart` fork. The regenerated bindings already
carry the unused extern; wire it to `toString()`, tag, bump `flake.nix`,
and regenerate `dart/moq_ffi`.
Comment on lines +17 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Split generator releases into prerequisite quests

The Go and Dart work requires fixing and tagging two external generators before this repository can bump their pins and regenerate bindings, but the plan folds those releases into the dependent binding quest. Give each unblocking release/pin operation its own quest and make the applicable wrapper work require it, so this quest is not blocked midway on external releases.

AGENTS.md reference: quest/AGENTS.md:L85-L86

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree. That rule covers a release that unblocks other quests. Here nothing else waits on the generator tags: the fork fix, the tag, the pin bump, and the regenerated bindings are all one change in forks this org owns. bbr-ack-cleanup.md follows the same pattern (fix the fork, release, pin here).

(Written by Claude Opus 5.5)

- Python: upstream `mozilla/uniffi-rs` (0.32.2 here) renders no uniffi
traits on errors (`ErrorTemplate.py`). Settled: no upstream PR. The
hand-written `py/moq-rs` package defines `__str__` on `MoqError` by
calling the exported `Display`, so the message still comes from Rust.
- A test per binding that a known error prints Rust's text (`Closed` prints
`closed`), and `doc/lib/{py,go,dart}` updated where they show error output.

Public API: the string form of `MoqError` changes in Python, Go, and Dart.
Wire: none.

## Related

- [C++ through moq-ffi](/quest/m1/cpp/README.md) - where #4292 adds the export and C++ `to_string()`
32 changes: 32 additions & 0 deletions quest/m1/go-origin-gc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# [XS] Go cancel test keeps its origin

## Goal

`TestRequestBroadcastCancelKeepsTheOrigin` in `go/wrapper/moq_test.go` passes
under load, not only alone (30/30 in isolation, `MoqError: Closed` under a
loaded run). Fixed at the cause, with no retry or longer timeout.

## Plan

Likely cause, found by reading and not yet reproduced: the test never touches
`origin` after `origin.Dynamic(...)` and `origin.Consume()`, so Go may collect
it mid-test. The generated finalizer then drops the only `MoqOriginProducer`,
the origin's driver finishes once every producer handle is gone
(`origin::Driver::poll` in `rs/moq-net/src/model/origin.rs`), and the next call
on the consumer or the dynamic handle gets `Error::Closed`. The collector runs
more often under load, which fits.

- Reproduce first. `runtime.GC()` after `cancel()` or `GOGC=1` only makes the
collection likely: finalizers run later on their own goroutine. For a
deterministic failure, do what the finalizer does: an internal test calls
the inner handle's `Destroy` before the next call.
- Keep the producer reachable to the end of the test (`runtime.KeepAlive`, as
the file already does for `pending`), and audit the other `go/wrapper` tests
that hold an `OriginProducer` only for setup.
- Users hit the same trap: a Go `OriginProducer` has no `Close`, so its origin
ends whenever the collector reaches it. Say so in its doc comment and
`doc/lib/go`. Whether consumers should keep their producer alive, or the
wrapper should gain an explicit `Close`, is an API call: propose it to the
maintainer rather than ship it here.

Public API: none unless the maintainer picks an API change. Wire: none.
57 changes: 57 additions & 0 deletions quest/m1/gpu-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# [S] NVIDIA tests run on real hardware

## Goal

The NVDEC, NVENC, and CUDA tests run nightly on the maintainer's Linux host
(RTX 3070 Ti) instead of passing without a GPU on hosted runners, and a local
`just rs nvidia` runs them against the host driver instead of silently
skipping inside the Nix shell.

## Plan

- Why they skip: the driver libraries are loaded at runtime (`libcuda` by
cudarc, `libnvidia-encode` in `rs/moq-nvenc/src/safe/api.rs`, `libnvcuvid`
in `rs/moq-nvenc/src/cuvid.rs`), and the tests return early when they are
missing (`hw_available` in `rs/moq-video/src/decode/backend/nvdec.rs`). The
Nix shell's loader path lacks Ubuntu's `/usr/lib/x86_64-linux-gnu`.
- Select every test that needs the GPU, not only names containing `nvdec`,
`nvenc`, or `cuda`: `safe::session::tests::failed_submission_releases_the_session`
in `rs/moq-nvenc` needs hardware and matches none of them. Find them by
their driver probes (`hw_available`, `driver_libs_present`, `Api::get` and
friends in `moq-nvenc` and `moq-video`). Recommendation: follow the
existing `#[ignore = "requires ..."]` convention (as `frame/vulkan_test.rs`
does) and put them in `nvidia` test modules, so hosted CI reports them
ignored instead of passed and one filter, `--run-ignored only -E
'test(/::nvidia::/)'`, selects them all without the other ignored hardware
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
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
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
hardening guidance before wiring it.
- Share the runner with the io_uring one that #4132 plans
(`quest/m1/uring-runner.md` on the drain line, which wants a 6.12+ kernel on
the same host): one registration and one security posture, a label per
capability. Whichever quest lands second reuses the first's job shape.

Public API: none. Wire: none.

## Required

- A self-hosted runner is registered for moq-dev/moq on the maintainer's host, with the NVIDIA driver

## Related

- [Video hardware validation](/quest/m3/video-hardware.md) - hardware paths nothing runs yet
- [Runtime QA hosts](/quest/m2/runtime-qa-hosts.md) - on-demand jobs on hardware hosts, a broader contract than a nightly
Loading
Loading