diff --git a/AGENTS.md b/AGENTS.md index 471b2095f0..0a47fdd317 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ This file is split into nested `AGENTS.md` files based on the language/situation - Fix any outdated docs and comments inline; don't add a separate PR for it. - Add the AI marker `(Written by )` to any posts on GitHub, excluding commit messages that contain `Co-Authored-By:` trailers. - Any AI comments may be challenged, and not confused with human maintainers. +- This repo is public: never mention customers, partners, or prospects (names, volumes, terms) or link to private repos. Justify work on its merits. - Prompt the user to decide when unsure, but always provide recommendations. # Guidelines diff --git a/quest/m0/broadcast-epoch/stats-split.md b/quest/m0/broadcast-epoch/stats-split.md index b633b022a6..81bde5c053 100644 --- a/quest/m0/broadcast-epoch/stats-split.md +++ b/quest/m0/broadcast-epoch/stats-split.md @@ -11,7 +11,7 @@ everything since the epoch began. ## Plan -Decided 2026-10-05 (planned from moq-dev/moq.pro#2202): +Decided 2026-10-05: - **Why.** Every counter is cumulative per entry, and an entry is pruned right after the frame carrying its closing readout. A broadcast that starts and @@ -64,8 +64,7 @@ Decided 2026-10-05 (planned from moq-dev/moq.pro#2202): just-ended children, not every child the prefix has ever seen. Decide while implementing whether a map with hundreds of children needs a cap or paging. -- **Self counters** (decided 2026-10-07, planned from - moq-dev/moq.pro#2165). A prefix track also carries cumulative counters for +- **Self counters** (decided 2026-10-07). A prefix track also carries cumulative counters for members at exactly the prefix, not its descendants, so a reader can show a broadcast that also has nested broadcasts. A reader can't derive them as the rollup less the current children: a pruned child's tail stays in the diff --git a/quest/m0/qmux-credit.md b/quest/m0/qmux-credit.md index ccb4872da6..585a20f00d 100644 --- a/quest/m0/qmux-credit.md +++ b/quest/m0/qmux-credit.md @@ -61,4 +61,3 @@ Public API: none. Wire: none. ## Related - [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - replaces these stream maps later -- [moq.pro: edge-to-core qmux](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/edge-core/qmux-credit.md) - the long-lived cluster links that need this diff --git a/quest/m1/README.md b/quest/m1/README.md index 7e4ab09148..8b662395d9 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -30,6 +30,12 @@ regression, and the audio jitter target line here from m0; moved uring, capture-chain, and first-FETCH pipelining work to m2; and deleted quests that were done or not worth their cost. +Promoted from m2 on 2026-10-09 by maintainer priority: mobile capture and +completion, the capacity probe, the viewer up-switch, and native enabled, +which simulcast rung disable requires. Added the same day: +simulcast rung disable, OBS multitrack, MoQ in obs-studio, and portrait +ladders. + ## Required - [WebGPU renderer](/quest/m1/webgpu-renderer.md) - a new `@moq/video` renders frames through WebGPU in their own colour space, with Canvas2D where WebGPU is missing, for watch and the publish preview @@ -95,6 +101,8 @@ were done or not worth their cost. - [Retire the libmoq stub](/quest/m1/libmoq-retire.md) - the published `libmoq` crate stops after its final release points users at `moq-c` - [VideoToolbox presets](/quest/m1/videotoolbox-presets.md) - VideoToolbox honors Balanced and Quality instead of always reporting LowLatency - [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 +- [OBS multitrack](/quest/m1/obs-multitrack.md) - the obs-moq output publishes OBS's native multitrack video encoders as simulcast renditions, configured like OBS multitrack and over obs-websocket +- [MoQ in obs-studio](/quest/m1/obs-studio/README.md) - a native MoQ output and service merged into obs-studio, once its maintainers agree; the plugin carries it until then - [AudioToolbox decode](/quest/m1/audio-decode-audiotoolbox.md) - macOS and iOS decode HE-AAC, multichannel AAC, and what else the framework offers - [AudioToolbox encode](/quest/m1/audio-encode-audiotoolbox.md) - macOS and iOS encode AAC-LC - [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 @@ -102,6 +110,10 @@ were done or not worth their cost. - [A self-hosted NVIDIA runner is registered](/quest/m1/gpu-runner.md) - the maintainer registers the host that runs the NVIDIA tests - [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 - [Rendition preference](/quest/m1/rendition-preference.md) - automatic selection by ``, `Video::ranked`, and WHEP keeps the highest `preference` that decodes, so a compatibility transcode is only picked when nothing preferred decodes +- [Native enabled](/quest/m1/native-enabled.md) - native players and the ffi/C paths never select a disabled rendition, and move off one disabled mid-playback +- [Simulcast rung disable](/quest/m1/simulcast-rung-disable.md) - JS, Rust/FFI, and OBS publishers stop encoding the top renditions their grant cannot fund, advertise `enabled: false`, and re-enable with hysteresis +- [Discover media headroom](/quest/m1/quic-probe.md) - test useful-media pacing before adding redundant probe traffic +- [Viewer up-switch](/quest/m1/viewer-upswitch.md) - a viewer capped by its small rendition finds headroom through PROBE and moves up - [Own the QUIC stack](/quest/m1/quic/README.md) - quinn hard-forked in-tree as `moq-quic`, carrying BBR, reliable reset, hierarchical scheduling, peer limits, and endpoint sharding - [QoS](/quest/m1/qos/README.md) - broadcast health: relay starvation and timeliness histograms - [Session outcomes](/quest/m1/session-outcomes.md) - the relay's session stats count refusals by reason and ends by kind, per root and tier @@ -145,6 +157,9 @@ were done or not worth their cost. - [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - @moq/qmux rejects an over-limit create when waitUntilAvailable is false, like Chrome - [E2EE](/quest/m1/e2ee/README.md) - TypeScript and Rust peers interoperate over encrypted broadcasts no relay can decrypt - [#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md) - the catalog rotation follows the live camera's orientation +- [Portrait ladder](/quest/m1/portrait-ladder.md) - portrait video plays end to end through transcode, with rungs sized by the source's short side +- [iOS capture](/quest/m1/mobile-capture-ios.md) - Rust captures the camera and screen on iOS, setting the catalog rotation +- [Android capture](/quest/m1/mobile-capture-android.md) - Rust captures through NDK/JNI on Android, reusing the existing codecs and setting the catalog rotation - [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 - [Caption import](/quest/m1/captions-import.md) - fMP4 and MKV subtitle tracks import as text renditions instead of erroring or being dropped @@ -170,6 +185,7 @@ were done or not worth their cost. - [Dart on iOS](/quest/m1/dart-ios.md) - prove the shipped iOS native asset actually loads on a device, which no CI can - [Dart publish](/quest/m1/dart-publish.md) - a `moq-dart-v*` tag publishes `moq` to pub.dev unattended, as `moq_ffi`'s tags already do - [Dart codec parity](/quest/m1/dart-codecs.md) - Dart is the one binding that cannot originate media +- [Mobile completion](/quest/m1/mobile-completion.md) - verify the Swift and Kotlin bindings over Rust-owned capture before closing #700 - [`moq --listen` drain and stats](/quest/m1/cli-serve.md) - a listening CLI session, already admitted through the relay's auth, is counted and drained like a relay's - [#709](/quest/m1/709-automatic-letsencrypt-support.md) - the relay provisions and renews its own ACME certificate through rustls-acme over TLS-ALPN-01, persisted on disk - [Draft 14-16 updates](/quest/m1/ietf-legacy-updates.md) - Rust and JS apply and answer moq-transport 14-16 request updates without ending or leaking the request diff --git a/quest/m1/auth/webtransport-transport.md b/quest/m1/auth/webtransport-transport.md index e311a97b2a..1546d3a0c7 100644 --- a/quest/m1/auth/webtransport-transport.md +++ b/quest/m1/auth/webtransport-transport.md @@ -11,9 +11,8 @@ sees the new value. ## Plan -Decided 2026-10-07, for moq.pro's per-transport session stats (a customer -measuring how often browsers fall back to WebSocket, whose native QUIC -workers share the `quic` count today): +Decided 2026-10-07, for per-transport session stats (how often browsers fall +back to WebSocket, while native QUIC clients share the `quic` count today): - **A variant, not a flag.** Rejected: an additive `webtransport: bool` beside `transport: "quic"`. The variant states the truth. An auth server that diff --git a/quest/m1/bench-buckets.md b/quest/m1/bench-buckets.md index 1aa63ef4c3..461a6def56 100644 --- a/quest/m1/bench-buckets.md +++ b/quest/m1/bench-buckets.md @@ -13,9 +13,9 @@ at raw-sample precision. ## Plan -Decided 2026-10-05 in moq.pro's quest audit: percentiles neither window nor -merge, and moq.pro's load harness sums several generators against one relay, -so the bench emits mergeable buckets. Approved as recommended. +Decided 2026-10-05 by the maintainer: percentiles neither window nor merge, +and a load harness sums several generators against one relay, so the bench +emits mergeable buckets. - `Latency` (`rs/moq-bench/src/stats.rs`) counts 1 ms buckets for the whole run and diffs them each interval for the `latency_interval_*` percentiles @@ -38,12 +38,8 @@ so the bench emits mergeable buckets. Approved as recommended. taken. Update `rs/moq-bench/README.md`. Lands on main, then is backported to `release` as an additive cherry-pick -PR, like [#4882](https://github.com/moq-dev/moq/pull/4882), since moq.pro's load -harness tracks `release` (decided 2026-10-05). The backport carries #5021's +PR, like [#4882](https://github.com/moq-dev/moq/pull/4882), since downstream load +harnesses track `release` (decided 2026-10-05). The backport carries #5021's delta along if `release` does not have it yet. Public API: none (CLI output only). Wire: none. - -## Related - -- [moq.pro: benchmark output pin](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/kick/bench-pin.md) - the load harness that sums these across generators diff --git a/quest/m1/cluster-routing/routes.md b/quest/m1/cluster-routing/routes.md index 96a86610b1..e400bce926 100644 --- a/quest/m1/cluster-routing/routes.md +++ b/quest/m1/cluster-routing/routes.md @@ -92,14 +92,14 @@ Decided 2026-10-01 (see the [line's decisions](/quest/m1/cluster-routing/README. Why not a hold-down: #4644 retracted at once and re-announced a replacement after 1 s, which ended a downstream front whose only path ran through the relay, dropping subscribers for about a second while the relay kept serving; -holding without retracting brought hunting back (moq-dev/moq.pro#2116 has the -measurements, and #4642's 300 ms cursor hold is the production mitigation). +holding without retracting brought hunting back in measurements, and +#4642's 300 ms cursor hold is the production mitigation. Separating "the path changed" (ROUTE) from "the broadcast ended" (ANNOUNCE) removes that trade, and the failover tests must show no front loses its route while a working one exists. Decide whether #4642's hold can then go. -Simulator findings so far, from moq-dev/moq.pro#2152 (open, awaiting the -maintainer's decision on a design revision): +Simulator findings so far (awaiting the maintainer's decision on a design +revision): - Live origin-end costs 134.5 KiB against path vector's 3668.1/1243.0 KiB, but still emits 2150 client updates against an 840 once-only minimum and diff --git a/quest/m1/cluster-routing/sim.md b/quest/m1/cluster-routing/sim.md index f25fdf02d6..dd9547066d 100644 --- a/quest/m1/cluster-routing/sim.md +++ b/quest/m1/cluster-routing/sim.md @@ -2,9 +2,8 @@ ## Goal -Waits on moq.pro's routing simulator. The condition clears when moq.pro's -[Simulate the edge and core layout](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/routing-sim-tiers.md) -reports a ROUTE and ANNOUNCE split candidate (per-node distance vector with +Waits on moq.pro's routing simulator. The condition clears when its edge and +core layout simulation reports a ROUTE and ANNOUNCE split candidate (per-node distance vector with Babel feasibility, path-less announces following the next hop) beside path vector, on the tiered live layout, the tiered synthetic sweeps, and a synthetic drone mesh (lossy links, two uplinks, a partition that heals). @@ -13,12 +12,10 @@ Per scenario (publish, end, link flap, relay loss and restart, uplink loss): messages and bytes by kind, convergence time, stale-path seconds, transient subscribe loops, and whether a two-uplink mesh ever carries CDN traffic. -moq-dev/moq.pro#2136 rewrote that quest for the split (merged 2026-10-02), -and moq-dev/moq.pro#2152 (draft) measures the tiered route and mesh failures. -Its report says the split needs a design revision before its wire ships, so -the condition is now the maintainer's decision on that revision, not only a -merge. To check: #2152 merges, or the maintainer decides the revision. To -advance: ask the maintainer to review #2152's findings. Delete this quest +The simulator now measures the tiered route and mesh failures, and its +report says the split needs a design revision before its wire ships, so the +condition is the maintainer's decision on that revision. To check and +advance: ask the maintainer to review the simulator's findings. Delete this quest once [Routes and announces](/quest/m1/cluster-routing/routes.md) is confirmed or revised by them. diff --git a/quest/m1/data-consumer-timestamps.md b/quest/m1/data-consumer-timestamps.md index b8d95ff60a..9cfc25a21a 100644 --- a/quest/m1/data-consumer-timestamps.md +++ b/quest/m1/data-consumer-timestamps.md @@ -9,9 +9,8 @@ wrappers, moq-ffi, and every binding wrapper. ## Plan -Requested by an external consumer (OneTooMany, Discord), who translates -MAVLink into application telemetry and must keep its timestamps to stay in -sync with video. +An application that translates MAVLink into its own telemetry must keep +each value's timestamp to stay in sync with video. Every `moq_net::Frame` carries an optional timestamp (`None` when untimed, since #4822), but the consumers decode only `frame.payload` and drop it: `moq_mux::{json,binary}::Consumer::next` and the diff --git a/quest/m1/dogfood-assets.md b/quest/m1/dogfood-assets.md index d31b642eb6..eb02abc392 100644 --- a/quest/m1/dogfood-assets.md +++ b/quest/m1/dogfood-assets.md @@ -12,9 +12,7 @@ CSP is out of scope: the dashboard adds no policy. Decided in planning: the maintainer wants hosted mode dogfooded, not just the pin bump. No CSP. moq.dev's half moved to [its own quest tree](https://github.com/moq-dev/moq.dev/blob/main/quest/m0/dogfood-assets.md) -on 2026-10-09. The moq.pro side is part of its -[package adoption](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/ship-dev.md). -pronto/web is skipped: it renders video only and never loads a worklet. +on 2026-10-09. The moq.pro side is part of its package adoption. Guidance: diff --git a/quest/m1/gst-brew-path.md b/quest/m1/gst-brew-path.md index 2675cb935a..84fd7ad985 100644 --- a/quest/m1/gst-brew-path.md +++ b/quest/m1/gst-brew-path.md @@ -11,9 +11,8 @@ the formula installs nothing without an error. The published tap formula The Nix `moq-gst` package (`moq-gst` in `nix/overlay.nix`) also carries the encoders and decoders a moq-gst pipeline needs. Its plugin path has core, base, good, and bad today; add gst-plugins-ugly (`x264enc`) and gst-libav -(`avenc_aac`, `avdec_h264`). moq.pro's -[gst-install-upstream](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/gst-install-upstream.md) -waits on this, so its install can drop its own GStreamer setup. +(`avenc_aac`, `avdec_h264`), so a downstream install can drop its own +GStreamer setup. ## Plan diff --git a/quest/m1/js-data-consumer-timestamps.md b/quest/m1/js-data-consumer-timestamps.md index a5a824b824..ecfb5d3e8f 100644 --- a/quest/m1/js-data-consumer-timestamps.md +++ b/quest/m1/js-data-consumer-timestamps.md @@ -9,8 +9,8 @@ video playhead reaches it. Mirrors ## Plan -Requested by OneTooMany, whose web frontend shows KLV and MAVLink telemetry -beside video and today reads raw frames to recover the timestamps. +A web frontend showing KLV or MAVLink telemetry beside video has to read raw +frames today to recover the timestamps. Decided (2026-10-01): `next()` and the async iterator yield `@moq/net`'s `Timed` (`{ value, at? }`), the type the producers take. diff --git a/quest/m1/kt-ffi-pom.md b/quest/m1/kt-ffi-pom.md index ab30a52c1a..7d293fb249 100644 --- a/quest/m1/kt-ffi-pom.md +++ b/quest/m1/kt-ffi-pom.md @@ -6,9 +6,7 @@ The `dev.moq:moq-jvm` and `dev.moq:moq-android` POMs on Maven Central depend on a published `moq-ffi` variant, so a Maven build resolves the wrapper. Today 0.5.0 and 0.5.1 depend on `moq-ffi-jvm` and `moq-ffi-android` version `0.0.0-dev`, which was never published, so every Maven consumer fails to -resolve. A PR check refuses a generated POM naming that version. moq.pro's -[Kotlin sample](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/kotlin-sample.md) -is a consumer: it builds against the published wrapper. +resolve. A PR check refuses a generated POM naming that version. ## Plan diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index 242936d489..1c56860b04 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -10,9 +10,9 @@ then roll lite-07 out without a wip wire changing under it. ## Plan -Decided 2026-10-05 in moq.pro's quest audit: finalizing lite-07 is upstream -work with its own quest. Until now nothing upstream owned it, while moq.pro's -mesh and customer rollouts waited on it and the +Decided 2026-10-05 by the maintainer: finalizing lite-07 is upstream work +with its own quest. Until now nothing upstream owned it, while downstream +rollouts waited on it and the [mesh condition](/quest/m3/lite07-mesh.md) here waited on moq.pro. This quest waits on nothing outside the repository; the rollouts and the mesh condition follow it. @@ -28,7 +28,7 @@ The route layer of [Cluster routing](/quest/m1/cluster-routing/README.md) (ROUTE_START/UPDATE/END, path-less ANNOUNCE, the hop list dropped) lands in lite-07, decided 2026-10-05. That removes lite-07's `Hop Base`/`Hop Keep` announce compression along with the hop list, and moq.pro's lite-07 rollout -(under Related) has to plan for a lite-07 without hop lists. Only +has to plan for a lite-07 without hop lists. Only [Routes and announces](/quest/m1/cluster-routing/routes.md) gates the cut, not the rest of the cluster-routing line (decided 2026-10-08). @@ -83,4 +83,3 @@ is published; older versions are unchanged. ## Related - [The moq.pro mesh runs lite-07](/quest/m3/lite07-mesh.md) - the deployment condition that follows this -- [moq.pro: lite-07 on cluster dials](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/lite07.md) - the mesh rollout that adopts the finalized version, and the customer rollout after it; moq.pro holds it in m2 until the cut is called diff --git a/quest/m2/mobile-capture-android.md b/quest/m1/mobile-capture-android.md similarity index 65% rename from quest/m2/mobile-capture-android.md rename to quest/m1/mobile-capture-android.md index f978433179..aaf4c1ff5e 100644 --- a/quest/m2/mobile-capture-android.md +++ b/quest/m1/mobile-capture-android.md @@ -18,7 +18,14 @@ reimplement the native layer). `moq-tokio` already reaches into Android through JNI for `tls::init_android`, so the mechanism exists. +Capture sets the catalog `rotation` from the sensor and display orientation, +so a phone held in portrait plays upright; the browser half of that is +[#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md). + +Promoted from m2 on 2026-10-09 by maintainer priority. + ## Related -- [iOS capture](/quest/m2/mobile-capture-ios.md) - the other half of mobile, which +- [#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md) - the same catalog rotation for browser capture +- [iOS capture](/quest/m1/mobile-capture-ios.md) - the other half of mobile, which reuses an existing backend rather than adding one diff --git a/quest/m2/mobile-capture-ios.md b/quest/m1/mobile-capture-ios.md similarity index 70% rename from quest/m2/mobile-capture-ios.md rename to quest/m1/mobile-capture-ios.md index f471518b08..3731a984fc 100644 --- a/quest/m2/mobile-capture-ios.md +++ b/quest/m1/mobile-capture-ios.md @@ -23,7 +23,14 @@ Reuse the `capture::Source` shape the other platforms use rather than growing an iOS-specific entry point, so device enumeration and selection behave the same everywhere. +Capture sets the catalog `rotation` from the device orientation, so a phone +held in portrait plays upright; the browser half of that is +[#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md). + +Promoted from m2 on 2026-10-09 by maintainer priority. + ## Related -- [Android capture](/quest/m2/mobile-capture-android.md) - the other half of +- [#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md) - the same catalog rotation for browser capture +- [Android capture](/quest/m1/mobile-capture-android.md) - the other half of mobile, and a much larger one diff --git a/quest/m2/mobile-completion.md b/quest/m1/mobile-completion.md similarity index 66% rename from quest/m2/mobile-completion.md rename to quest/m1/mobile-completion.md index 03937b3478..1114cb9f86 100644 --- a/quest/m2/mobile-completion.md +++ b/quest/m1/mobile-completion.md @@ -9,20 +9,26 @@ work and deferred mobile phases are complete before #700 closes. ## Plan This quest owns the cross-phase completion proof, not another implementation. -Use the Rust capture path and the existing binding APIs: Rust owns capture -and codecs on mobile, settled in the 2026-09-30 audit. Record the supported +The mobile SDK is the upstream Swift and Kotlin bindings over moq-ffi, with +Rust owning capture and codecs underneath (Rust capture settled in the +2026-09-30 audit; the binding direction decided by the maintainer +2026-10-09). Prove that path rather than a platform-native capture stack +beside it. Record the supported path, limitations, and reproducible device results; wire repeatable coverage into CI and identify the hardware evidence separately. Update the native/mobile getting-started docs with the working path. Do not close #700 merely because its next subset or a design decision finished. +Promoted from m2 on 2026-10-09 by maintainer priority, with both capture +quests. + ## Required - [Dart on iOS](/quest/m1/dart-ios.md) - the Dart iOS asset proof - [Dart codec parity](/quest/m1/dart-codecs.md) - codec-enabled artifacts and Dart video consumer integration -- [iOS capture](/quest/m2/mobile-capture-ios.md) - deliver the selected iOS capture path -- [Android capture](/quest/m2/mobile-capture-android.md) - deliver the selected Android capture and codec path +- [iOS capture](/quest/m1/mobile-capture-ios.md) - deliver the selected iOS capture path +- [Android capture](/quest/m1/mobile-capture-android.md) - deliver the selected Android capture and codec path ## Closes diff --git a/quest/m2/native-enabled.md b/quest/m1/native-enabled.md similarity index 66% rename from quest/m2/native-enabled.md rename to quest/m1/native-enabled.md index b5a3fb4469..0a55074651 100644 --- a/quest/m2/native-enabled.md +++ b/quest/m1/native-enabled.md @@ -16,6 +16,16 @@ rendition selection for video and audio rather than repeating checks at each consumer. Preserve the existing quality ordering among enabled renditions. Test mixed and all-disabled catalogs, plus disable/re-enable transitions. +A rendition disabled mid-playback must move the player to an enabled lower +one. `moq play` picks from `snapshot.video.renditions` without checking +`enabled` (`rs/moq-cli/src/play/media.rs`), and `Playback::wants` +(`rs/moq-cli/src/play/playback.rs`) refuses to reselect while the current +track is still playing, so a disabled rung that keeps its track would leave +it on an idle subscription. Test a high-to-low disable during playback. + +Promoted from m2 on 2026-10-09: publishers disabling simulcast rungs +require it. + A C `moq_consume_audio_enabled` getter is added only if a C consumer needs it; any FFI/C API change updates all wrappers and their docs in the same PR. Update native consumer docs for the selection behavior. diff --git a/quest/m1/obs-multitrack.md b/quest/m1/obs-multitrack.md new file mode 100644 index 0000000000..8df4ffe8bd --- /dev/null +++ b/quest/m1/obs-multitrack.md @@ -0,0 +1,39 @@ +# [M] OBS multitrack video as simulcast renditions + +## Goal + +The obs-moq output publishes OBS's native multitrack video encoders as +simulcast renditions of one broadcast, one rendition per track, each with +its own size and bitrate in the catalog. A user configures them the way OBS +multitrack video is configured, including over obs-websocket. Disabling the +top renditions as the bandwidth grant falls is +[simulcast rung disable](/quest/m1/simulcast-rung-disable.md)'s work, not +this quest's. + +## Plan + +Verify the gap first. The output already declares +`OBS_OUTPUT_MULTI_TRACK_VIDEO` and keeps one track per video encoder +(`video_tracks` in `cpp/obs/src/moq-output.cpp`), so the work is likely +configuration, naming, and catalog correctness rather than new plumbing. +moq-mux already imports enhanced-RTMP multitrack +(`rs/moq-mux/src/container/flv/import.rs`); match how it names and describes +renditions so an OBS stream looks the same whether it arrives over MoQ or +RTMP. + +- Reuse OBS's own multitrack settings rather than adding a MoQ-only ladder + UI, so obs-websocket and existing profiles drive it unchanged. Find out + what OBS needs from a custom service to enable multitrack and record it. +- Each rendition advertises its real encoded size, codec, and maximum + bitrate; disabling one never removes it from the catalog. +- Update `doc/bin/obs.md`, whose "OBS publishes one rendition" section this + changes. + +Verify with `just obs compile` and `just obs test`, and one manual run with +at least two video tracks watched in ``. + +## Related + +- [Simulcast rung disable](/quest/m1/simulcast-rung-disable.md) - the grant-driven rung disable these renditions follow +- [MoQ in obs-studio](/quest/m1/obs-studio/README.md) - the native output this configuration carries into +- [OBS native codecs](/quest/m1/obs-moq-video/README.md) - the encoders behind each track when MoQ encoders are chosen diff --git a/quest/m1/obs-studio/README.md b/quest/m1/obs-studio/README.md new file mode 100644 index 0000000000..07c8dd0c71 --- /dev/null +++ b/quest/m1/obs-studio/README.md @@ -0,0 +1,40 @@ +# MoQ in obs-studio + +## Goal + +A MoQ output and service ship inside obs-studio itself, native rather than a +plugin, and are configurable through obs-websocket like the other streaming +services. Until that merges, the plugin (`cpp/obs`) remains the vehicle and +keeps shipping. + +## Plan + +This README holds the port and merge work: carry the plugin's output and +service into obs-studio's tree in the shape its maintainers ask for, then +land it upstream. Shape it around their answer to +[the condition](/quest/m1/obs-studio/maintainers.md), not ahead of it. + +Agents may prepare the proposal and the code, but never post to obs-studio +(issues, PRs, comments, forums) without the maintainer's explicit approval +for that post. The maintainer does the outreach. + +Open, for when the port starts: + +- How obs-studio takes the Rust-built library: a prebuilt dependency like + its other media libraries is the likely shape, through the generated C++ + package rather than the hand-written libmoq. +- What stays in the plugin: the MoQ Source and the dock may remain a plugin + even once the output is native. + +## Required + +- [obs-studio maintainers agree](/quest/m1/obs-studio/maintainers.md) - condition: agreement in principle to a native MoQ output and service + +## Related + +- [OBS multitrack](/quest/m1/obs-multitrack.md) - multitrack video renditions the native output should carry +- [First C++ package release](/quest/m1/cpp-release.md) - the published C++ package the port would build on +- [OBS native codecs](/quest/m1/obs-moq-video/README.md) - the plugin's codec work, which stays plugin-side +- [Client settings parity](/quest/m1/obs-client-config.md) - the client settings a native service exposes +- [Session report parity](/quest/m1/obs-session-report.md) - the session report a native output surfaces +- [OBS publishes under epochs](/quest/m1/obs-epoch.md) - the per-start epoch the output keeps diff --git a/quest/m1/obs-studio/maintainers.md b/quest/m1/obs-studio/maintainers.md new file mode 100644 index 0000000000..1adf9204e7 --- /dev/null +++ b/quest/m1/obs-studio/maintainers.md @@ -0,0 +1,19 @@ +# [XS] obs-studio maintainers agree to a MoQ output + +## Goal + +The obs-studio maintainers agree in principle to a MoQ output and service in +obs-studio itself. The maintainer does this outreach; agents may draft the +proposal but never post to obs-studio (issues, PRs, comments, forums) +without explicit approval. + +This quest tracks a condition outside the repository. When it holds, delete +this quest and every `Required` entry that links it. + +## Plan + +To check: ask the maintainer whether obs-studio has agreed, and record any +constraints they set (dependency policy, scope, review owners) in the +[questline](/quest/m1/obs-studio/README.md) before deleting this quest. To +advance: offer the maintainer a drafted proposal covering what the output +does, how it builds, and who maintains it. diff --git a/quest/m1/portrait-ladder.md b/quest/m1/portrait-ladder.md new file mode 100644 index 0000000000..75ec8a26f6 --- /dev/null +++ b/quest/m1/portrait-ladder.md @@ -0,0 +1,29 @@ +# [S] Portrait transcode ladders + +## Goal + +Portrait (vertical) video plays correctly end to end through `moq-transcode`, +and transcode rungs are sized by the source's short side, so a 1080x1920 +source gets 720x1280 and 360x640 rungs rather than rungs a third as wide. + +## Plan + +`rs/moq-transcode` defines rungs by height (`rs/moq-transcode/src/ladder.rs`) +and derives width from the source aspect ratio (`resolve_rungs` in +`rs/moq-transcode/src/catalog.rs`). Its own test pins a 1080x1920 source's +360 rung at 202x360, so a portrait source gets landscape-sized pixel counts at +landscape bitrates. Verify the gap end to end before changing it. + +- Treat a rung's configured size as the short side, and apply the never + upscale and below-source checks to the short side too. +- A source coded landscape with a catalog `rotation` keeps its rotation on + every rung, as it does today; check that the short side is taken from the + coded frame so both portrait encodings land on the same rungs. +- Prove one portrait source through publish, transcode, and ``, + plus unit tests for both portrait encodings. + +## Related + +- [#933](/quest/m1/933-video-rotation-metadata-not-propagated-from-mobile-camera.md) - portrait phones publishing a catalog rotation +- [iOS capture](/quest/m1/mobile-capture-ios.md) - native portrait capture +- [Android capture](/quest/m1/mobile-capture-android.md) - native portrait capture diff --git a/quest/m1/qos/README.md b/quest/m1/qos/README.md index 17e4a7521e..623d3b8816 100644 --- a/quest/m1/qos/README.md +++ b/quest/m1/qos/README.md @@ -32,9 +32,8 @@ moq.pro (downstream) dashboard work, including the health badge and the connection-health drill-down, consumes both. The per-broadcast verdict combining client reports, the relay's starvation, -and publisher timeliness lives in moq.pro's -[health badge](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/qos/badge.md). -This line only reports the counters. +and publisher timeliness lives downstream. This line only reports the +counters. Decided (2026-10-06 audit): [stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) lands first, since it is m0 diff --git a/quest/m2/quic-probe.md b/quest/m1/quic-probe.md similarity index 95% rename from quest/m2/quic-probe.md rename to quest/m1/quic-probe.md index afb417ec2a..c756ac8011 100644 --- a/quest/m2/quic-probe.md +++ b/quest/m1/quic-probe.md @@ -44,13 +44,16 @@ 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. +Promoted from m2 on 2026-10-09 by maintainer priority: the viewer up-switch +requires it. + ## Required - [Hard fork](/quest/m1/quic/fork/README.md) - the change lands in `moq-quic`, not the frozen fork ## Related -- [Viewer up-switch](/quest/m2/viewer-upswitch.md) - the viewer side, which requires this +- [Viewer up-switch](/quest/m1/viewer-upswitch.md) - the viewer side, which requires this - [Natural media drains](/quest/m2/quic-bbr-natural-drain.md) - separate ProbeRTT policy experiment - [QUIC receive-timestamps spike](/quest/m3/quic-gcc.md) - measures forward delay, which a later delay-based controller would read headroom from - [noq#811](https://github.com/n0-computer/noq/issues/811) - probing while app-limited, proposed to n0 diff --git a/quest/m1/quic/README.md b/quest/m1/quic/README.md index 4421ec0b39..bffad6c642 100644 --- a/quest/m1/quic/README.md +++ b/quest/m1/quic/README.md @@ -9,8 +9,9 @@ quinn and not noq). One core serves the tokio backend, the thread-per-core BBR correctness, reliable stream resets, hierarchical stream scheduling with per-broadcast fairness, wider limits for relay peers, and endpoint sharding. Per-stream acknowledgment progress, per-stream deadlines, qmux on the shared -stream state machine, and the experiments (the egress profile, media probing, -ECN measurement, deadline keep-alive) live in [m2](/quest/m2/README.md); L4S, +stream state machine, and the experiments (the egress profile, ECN +measurement, deadline keep-alive) live in [m2](/quest/m2/README.md); media +probing is its own [m1 quest](/quest/m1/quic-probe.md); L4S, careful resume, and a receive-timestamps spike toward GCC live in [m3](/quest/m3/README.md). None of them gate this line. @@ -71,7 +72,7 @@ The BBR app-limited fixes land in `moq-quic` without waiting for the switch. - [Scope track priority](/quest/m1/track-priority-scope.md) - the per-broadcast fairness policy on cluster sessions -- [Discover media headroom](/quest/m2/quic-probe.md) - test useful-media pacing before adding redundant probe traffic +- [Discover media headroom](/quest/m1/quic-probe.md) - test useful-media pacing before adding redundant probe traffic - [L4S on the backbone](/quest/m3/quic-ecn.md) - an ECT(1) option in the fork, an `ecn` config knob, and a dualpi2 measurement - [Careful resume on reconnect](/quest/m3/quic-careful-resume.md) - a redial starts at the previous connection's rate - [Keep-alive from the idle timeout](/quest/m2/quic-keep-alive.md) - the keep-alive interval defaults to a fraction of the negotiated idle timeout; an explicit `quic.keep_alive` overrides diff --git a/quest/m1/rendition-preference.md b/quest/m1/rendition-preference.md index 9d23e242a1..e6dc16b4a1 100644 --- a/quest/m1/rendition-preference.md +++ b/quest/m1/rendition-preference.md @@ -14,9 +14,8 @@ decodes. A multitrack RTMP client still receives every rendition. ## Plan -Requested by an external consumer (OneTooMany), who publishes an H.265 source -with its own H.264 transcode beside it, so a viewer that can decode H.265 -should never pull the transcode. +A publisher with an H.265 source and its own H.264 transcode beside it wants +a viewer that can decode H.265 to never pull the transcode. Decided in planning interviews on 2026-10-01: diff --git a/quest/m1/session-outcomes.md b/quest/m1/session-outcomes.md index b2c26bbd02..1fdfe08b32 100644 --- a/quest/m1/session-outcomes.md +++ b/quest/m1/session-outcomes.md @@ -13,8 +13,8 @@ attributes a refusal to the root and tier the auth server resolved. ## Plan -Decided 2026-10-07, for moq.pro's per-project error counts (a customer -rolling out browser playback wants an error rate it cannot see client-side): +Decided 2026-10-07, for per-project error counts downstream (browser +playback needs an error rate it cannot see client-side): - **Refusals reuse moq#4825's reasons** (`refusals.rs`: refused, unavailable, request, forbidden, lan), so `/metrics` and the stats track share one @@ -41,7 +41,7 @@ rolling out browser playback wants an error rate it cannot see client-side): browser close as failed. A lease ending `expired` is the expired kind. - **Wire shape:** `Presence` gains `sessions_refused` and `sessions_failed`, fixed named counters per reason and per kind, so `Presence` stays `Copy` - (what moq.pro#2267 plans against). `sessions_ended` stays + (what downstream aggregators plan against). `sessions_ended` stays the total, so the live count (`sessions_started - sessions_ended`) is unchanged. They ride stats-split's totals and per-root track, not `sessions.json`, which stats-split retires (quest audit, 2026-10-08). diff --git a/quest/m1/simulcast-rung-disable.md b/quest/m1/simulcast-rung-disable.md new file mode 100644 index 0000000000..1de73073b7 --- /dev/null +++ b/quest/m1/simulcast-rung-disable.md @@ -0,0 +1,66 @@ +# [L] Publishers disable simulcast rungs the grant cannot fund + +## Goal + +A publisher encoding several video renditions of one source stops encoding +its top renditions when its bandwidth grant cannot fund them, advertising +each as `enabled: false` in the catalog, and re-enables them with hysteresis +once the grant recovers. The lower renditions keep their share instead of +every rung degrading together. Covers the JS publisher (`js/publish`), the +Rust and FFI publishers (the `rs/moq-video` producer under the `rs/moq-mux` +rate policy), and the OBS plugin. `@moq/watch` already skips disabled +renditions, and [native enabled](/quest/m1/native-enabled.md) brings native +players along first, so a disabled rung never strands a viewer. + +## Plan + +Today each video encoder reserves its configured ceiling and follows its own +grant down (`js/publish/src/video/encoder.ts`, +`rs/moq-video/src/encode/producer.rs`), so a congested uplink squeezes every +rendition at once and a top rung starved of most of its share keeps +encoding. JS also takes a manual `enabled` input that already publishes +`enabled: false` and stops encoding; this quest drives that state from the +grant. + +- One rule, one home. Put the disable and re-enable decision in + `moq_mux::rate` beside `Control`, so audio + ([#2848](/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md)) + and the transcode [ladder controller](/quest/m3/ladder/controller.md) reuse + it. Start from the ladder's band boundary + (`disable = (max + 2 * lower) / 3`, see the + [ladder questline](/quest/m3/ladder/README.md)) unless measurement argues + otherwise. JS mirrors the same rule and constants. +- Catalog state follows what the encoder applied, not what the policy asked. + Never rewrite the advertised maximum bitrate or remove the rendition. +- Recovery must not depend on demand. Once viewers deselect a disabled rung + its demand, and so its grant, goes away; evaluate a hypothetical share + against the current estimate, as the ladder controller plans to. +- Assign descending track priority down the renditions so the allocator + fills lower rungs first; [Scope track priority](/quest/m1/track-priority-scope.md) + settles how that number also orders sends. +- OBS: each OBS video encoder is its own rendition + (`video_tracks` in `cpp/obs/src/moq-output.cpp`), so apply the rule per + encoder through the generated C++ package. It builds on the plugin + following its grant ([Rate control](/quest/m1/obs-moq-video/rate-control.md)); + if that has not landed, split the OBS slice into its own quest instead of + blocking the rest. + +Tests with mocked time: a shrinking grant disables the top rung first and +keeps the lower ones at their share; a recovering grant re-enables it only +past the hysteresis and after its viewers left; a jittering grant near the +boundary does not flap the catalog. Prove one shaped-uplink run in the +browser and one through moq-ffi. + +Public API: the shared rule in `moq_mux::rate` and whatever knobs the +publishers expose for it. Wire: none; `enabled` already exists. + +## Required + +- [Native enabled](/quest/m1/native-enabled.md) - native players move off a rendition disabled mid-playback + +## Related + +- [Ladder controller](/quest/m3/ladder/controller.md) - the same rung disable for generated transcode ladders +- [Rendition preference](/quest/m1/rendition-preference.md) - viewer selection that drops disabled renditions before ranking the rest +- [OBS multitrack](/quest/m1/obs-multitrack.md) - OBS's native multitrack encoders as the renditions this disables +- [Viewer up-switch](/quest/m1/viewer-upswitch.md) - the viewer side of moving between renditions as capacity changes diff --git a/quest/m1/stats/README.md b/quest/m1/stats/README.md index f76a1d2af3..de6a91893d 100644 --- a/quest/m1/stats/README.md +++ b/quest/m1/stats/README.md @@ -127,7 +127,6 @@ connection view and the dashboard flow. ## Related - [QoS](/quest/m1/qos/README.md) - the relay's delivery counters; the - combined per-broadcast verdict reading both lives in moq.pro's - [health badge](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/qos/badge.md) + combined per-broadcast verdict reading both lives downstream - [Encoder feedback](/quest/m3/stats-encoder-feedback.md) - a Rust encoder adapts its bitrate to what its viewers report diff --git a/quest/m1/stats/health.md b/quest/m1/stats/health.md index b8abab53fe..893d70ce8b 100644 --- a/quest/m1/stats/health.md +++ b/quest/m1/stats/health.md @@ -18,17 +18,15 @@ any relay-side session table. Downstream (moq.pro) keeps its project view. ## Plan -Decided 2026-10-05, from moq.pro's -[connection health](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/qos-connection-health.md): -the health sample and its classification are generic, so they move upstream +Decided 2026-10-05: the health sample and its classification are generic, so they move upstream into this line; moq.pro keeps the per-project view and dashboard. Rejected: deferring the model. The model reads the shapes this line settled after [#4510](https://github.com/moq-dev/moq/pull/4510): the publisher's `stats` track named in the catalog, keyed by rendition ID, and viewers' `.echo` -broadcasts. moq.pro's plan predates that and read client stats from a -`.stats` broadcast, which #4510 replaced with these tracks (the relay's own +broadcasts. Older downstream plans read client stats from a `.stats` +broadcast, which #4510 replaced with these tracks (the relay's own `.stats/node/` is unchanged); do not revive the client one. Guidance, to be settled while building: @@ -54,9 +52,7 @@ Guidance, to be settled while building: it shows a better home. A per-broadcast verdict combining client reports, the relay's starvation, -and publisher timeliness lives in moq.pro's -[health badge](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/qos/badge.md), -not here. +and publisher timeliness lives downstream, not here. Prove a degrading publisher self-report, a degrading viewer report, a stale report going unknown, and a counter reset by both a decrease and an epoch @@ -70,8 +66,6 @@ Public API: new health types in `hang` and `@moq/hang`. Wire: none. ## Related -- [moq.pro: connection health](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/qos-connection-health.md) - the per-project view built on this model -- [QoS](/quest/m1/qos/README.md) - the relay's delivery counters, which moq.pro's combined verdict also reads -- [moq.pro: health badge](https://github.com/moq-dev/moq.pro/blob/main/quest/m1/qos/badge.md) - the per-broadcast verdict combining this one with relay lag and publisher timeliness +- [QoS](/quest/m1/qos/README.md) - the relay's delivery counters, which a combined downstream verdict also reads - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - a new epoch is one way a counter reset shows - [TS health counters](/quest/m2/ts-health-stats.md) - container counters kept out of the verdict diff --git a/quest/m1/stats/preflight.md b/quest/m1/stats/preflight.md index d7cede4fbd..ddfc9f23b5 100644 --- a/quest/m1/stats/preflight.md +++ b/quest/m1/stats/preflight.md @@ -18,9 +18,7 @@ dashboard flow. Downstream (moq.pro) owns those. ## Plan -Decided 2026-10-05, from moq.pro's -[stream preflight](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/qos-preflight.md): -the media checks are generic, so they live upstream in this line; moq.pro +Decided 2026-10-05: the media checks are generic, so they live upstream in this line; moq.pro keeps the project-scoped target and dashboard. Rejected: deferring them. Guidance, to be settled while building: @@ -58,4 +56,3 @@ Public API: a preflight check and its report type, and a CLI sink. Wire: none. - [Publisher timeliness](/quest/m1/qos/publisher-timeliness.md) - the relay's view of the same timestamp lateness and monotonicity - [Intra-refresh](/quest/m2/intra-refresh/README.md) - a stream without IDR keyframes, which the sync point checks must not fail -- [moq.pro: stream preflight](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/qos-preflight.md) - the project-scoped target and dashboard flow built on these checks diff --git a/quest/m1/untimed-decisions.md b/quest/m1/untimed-decisions.md index e0fb33ec48..5c22c75901 100644 --- a/quest/m1/untimed-decisions.md +++ b/quest/m1/untimed-decisions.md @@ -4,9 +4,7 @@ kixelated decides two questions [#4822](https://github.com/moq-dev/moq/pull/4822) (the untimed model) raised, and each answer becomes a quest or is dropped. -Requested by an external consumer (OneTooMany), who leaves these calls to the -maintainer. This quest waits on that decision; delete it once both are -answered. +This quest waits on that decision; delete it once both are answered. ## Plan diff --git a/quest/m2/viewer-upswitch.md b/quest/m1/viewer-upswitch.md similarity index 80% rename from quest/m2/viewer-upswitch.md rename to quest/m1/viewer-upswitch.md index 82e3b07128..864de954e7 100644 --- a/quest/m2/viewer-upswitch.md +++ b/quest/m1/viewer-upswitch.md @@ -15,12 +15,15 @@ above it. Decided (2026-10-04): its own quest, after the transport can validate headroom. The viewer sends a PROBE target, the relay pads up to it, and -selection uses the validated estimate. If [quic-probe](/quest/m2/quic-probe.md) +selection uses the validated estimate. If [quic-probe](/quest/m1/quic-probe.md) ends in a measured no-go, revisit this quest rather than leaving it blocked. +Promoted from m2 on 2026-10-09 by maintainer priority, with the capacity +probe it requires. + ## Required -- [Discover capacity](/quest/m2/quic-probe.md) - a validated estimate above the media bitrate +- [Discover capacity](/quest/m1/quic-probe.md) - a validated estimate above the media bitrate ## Closes diff --git a/quest/m2/README.md b/quest/m2/README.md index 34889de49b..bf4b9b8a29 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -67,9 +67,6 @@ consumer yet, waits in [m3](/quest/m3/README.md). - [IETF on the ring](/quest/m2/uring-ietf.md) - the io_uring workers serve moq-transport sessions too, so a uring relay drops no client protocol - [Dropped uring session closes](/quest/m2/uring-drop-close.md) - a moq-uring session dropped without close() closes its connection - [Relay io_uring packages](/quest/m2/relay-io-uring-package.md) - Linux relay packages ship io_uring once the ring is on par with tokio -- [iOS capture](/quest/m2/mobile-capture-ios.md) - Rust captures the camera and screen on iOS -- [Android capture](/quest/m2/mobile-capture-android.md) - Rust captures through NDK/JNI on Android, reusing the existing codecs -- [Mobile completion](/quest/m2/mobile-completion.md) - verify the selected native/mobile path before closing #700 - [Opus implementation](/quest/m2/audio-opus-backend.md) - compare Opus codec quality, CPU, build cost, and the loss recovery each backend offers - [Latency ledger](/quest/m2/latency-ledger.md) - a viewer reports its share of playback delay stage by stage: jitter buffer, decode, render, and device - [Media Foundation decode](/quest/m2/audio-decode-mediafoundation.md) - Windows decodes HE-AAC, multichannel AAC, and what else the MFTs offer @@ -84,8 +81,6 @@ consumer yet, waits in [m3](/quest/m3/README.md). - [#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 - [QUIC I/O boundary](/quest/m2/quic-io-boundary.md) - moq-uring receives from the buffer ring and transmits into caller-owned buffers with no copy, once a profile says where - [BBR media study](/quest/m2/quic-bbr-natural-drain.md) - whether bounded drain credit avoids ProbeRTT deadline interference, and where our BBR differs from Google's -- [Discover media headroom](/quest/m2/quic-probe.md) - test useful-media pacing before adding redundant probe traffic -- [Viewer up-switch](/quest/m2/viewer-upswitch.md) - a viewer capped by its small rendition finds headroom through PROBE and moves up - [Keep-alive from the idle timeout](/quest/m2/quic-keep-alive.md) - the keep-alive interval defaults to a fraction of the negotiated idle timeout; an explicit `quic.keep_alive` overrides - [Socket close](/quest/m2/noq-socket-close.md) - moq-tokio's QUIC endpoint releases its socket on close, so it drops its wrapper - [TS health stats](/quest/m2/ts-health-stats.md) - the TS counters ride the stats plumbing beside the media counters @@ -106,7 +101,6 @@ consumer yet, waits in [m3](/quest/m3/README.md). - [A release carries multi-vendor GPU input](/quest/m2/gpu-release.md) - `release` ships all three so a pinned consumer drops its vendor code - [Malformed moq-transport input](/quest/m2/ietf-malformed-close.md) - malformed draft-18 and draft-21 control input closes the session with the draft's code, or PROTOCOL_VIOLATION where a code is a real burden and the fallback is recorded in `doc/concept/standard.md`, in moq-net and js/net - [moq-transport request codes](/quest/m2/ietf-request-codes.md) - Range Filters (INVALID_FILTER), reserved namespaces, and RENDEZVOUS_TIMEOUT get the draft's answer, or a recorded fallback code, and the deliberate deviations are documented -- [Native enabled](/quest/m2/native-enabled.md) - native players and the ffi/C paths never select a disabled rendition - [Kotlin JVM exit](/quest/m2/kt-jvm-exit.md) - a Kotlin/JVM program exits cleanly whatever the moq-ffi runtime thread is doing, like Python does since #3766 - [JS audio ranking](/quest/m2/js-audio-ranked.md) - @moq/hang ranks audio and video renditions like Rust, enabled first, and HLS lists audio by that rank - [ts::Export catalog stream](/quest/m2/ts-export-catalog.md) - TS export takes (source, catalog) like the other exporters diff --git a/quest/m2/audio-decode-mediacodec.md b/quest/m2/audio-decode-mediacodec.md index 83d612ebb3..ee8e0a3ea8 100644 --- a/quest/m2/audio-decode-mediacodec.md +++ b/quest/m2/audio-decode-mediacodec.md @@ -28,4 +28,4 @@ behind a new optional audio `mediacodec` feature and the decode seam, on `target ## Related -- [Android capture](/quest/m2/mobile-capture-android.md) - the video MediaCodec family this sits beside +- [Android capture](/quest/m1/mobile-capture-android.md) - the video MediaCodec family this sits beside diff --git a/quest/m2/gpu-health.md b/quest/m2/gpu-health.md index f8c0dec199..a8c79740e8 100644 --- a/quest/m2/gpu-health.md +++ b/quest/m2/gpu-health.md @@ -71,4 +71,3 @@ Public API: a device capacity and health snapshot in moq-video. Wire: none. - [VA-API encodes an external Vulkan image](/quest/m2/vaapi-vulkan-import.md) - picks a render node by the same `dev_t` - [Vulkan Video encode on AMD](/quest/m2/vulkan-encode.md) - the AMD backend whose sessions this counts -- [moq.pro: transcode admission](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/transcode/ws3-admission.md) - the admission and health checks that consume this diff --git a/quest/m2/gpu-release.md b/quest/m2/gpu-release.md index a2c7a9519e..333b10dad3 100644 --- a/quest/m2/gpu-release.md +++ b/quest/m2/gpu-release.md @@ -26,8 +26,3 @@ maintainer at release time: - [Vulkan Video encode on AMD](/quest/m2/vulkan-encode.md) - the AMD encoder - [VA-API encodes an external Vulkan image](/quest/m2/vaapi-vulkan-import.md) - the Intel proof - [GPU capacity and health](/quest/m2/gpu-health.md) - keyed by the same device identity; moq.pro's admission still needs vendor code without it - -## Related - -- [moq.pro: Release multi-vendor GPU input upstream](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/gpu-release.md) - tracks this release -- [moq.pro: Pin multi-vendor GPU input](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/pronto-gpu-pin.md) - the Pronto bridge adopts it diff --git a/quest/m2/link-quality.md b/quest/m2/link-quality.md index 7890084112..de8277eae5 100644 --- a/quest/m2/link-quality.md +++ b/quest/m2/link-quality.md @@ -23,10 +23,9 @@ the default and measured cost is opt-in, so the line ships without it. - Hysteresis: only advertise a cost change past a threshold or after it holds, so the route layer sees a few changes per real shift. Report how many ROUTE changes a fading link causes. -- Never mix it into business pricing: moq.pro's - [priced topology](https://github.com/moq-dev/moq.pro/blob/main/quest/m3/priced-topology.md) - keeps RTT out of monetary costs, and that stays true because measured cost - is opt-in on links the operator chooses. +- Never mix it into business pricing: an operator's priced topology keeps + RTT out of monetary costs, and that stays true because measured cost is + opt-in on links the operator chooses. - A lossy-link fixture over the mock transport with mocked time. Public API: a per-link way to ask for measured cost in the peer entry. diff --git a/quest/m2/quic-bbr-natural-drain.md b/quest/m2/quic-bbr-natural-drain.md index a6dd11bde9..ef36a85405 100644 --- a/quest/m2/quic-bbr-natural-drain.md +++ b/quest/m2/quic-bbr-natural-drain.md @@ -81,4 +81,4 @@ is not an end-to-end network measurement. ## Related -- [Discover media headroom](/quest/m2/quic-probe.md) - preserving an estimate and discovering spare capacity are separate problems +- [Discover media headroom](/quest/m1/quic-probe.md) - preserving an estimate and discovering spare capacity are separate problems diff --git a/quest/m2/quic-deadline.md b/quest/m2/quic-deadline.md index 145ae3ee3c..edfa682108 100644 --- a/quest/m2/quic-deadline.md +++ b/quest/m2/quic-deadline.md @@ -60,6 +60,6 @@ Decided in the 2026-09-30 audit: moved to m2. No m1 quest consumes it. - [QUIC receive-timestamps spike](/quest/m3/quic-gcc.md) - measures whether receive timestamps give a forward delay worth replacing the half-RTT estimate - [ACK hook](/quest/m2/quic-ack-hook.md) - the same pattern for a moq-net method backed by `moq-quic` -- [Discover media headroom](/quest/m2/quic-probe.md) - can reuse +- [Discover media headroom](/quest/m1/quic-probe.md) - can reuse retransmission machinery if redundant capacity probes prove worthwhile - [noq#813](https://github.com/n0-computer/noq/issues/813) - the per-stream deadline proposal to n0 diff --git a/quest/m2/rtsp-import.md b/quest/m2/rtsp-import.md index 81c0156db0..3504a8c075 100644 --- a/quest/m2/rtsp-import.md +++ b/quest/m2/rtsp-import.md @@ -12,18 +12,16 @@ The library's entry point ingests one RTSP session into a `broadcast::Producer` the caller supplies, as moq-mux's importers take one, with no reconnect; importer state stays private. It returns when the session ends, fails, or its timeline jumps. The CLI wraps it with reconnect and a -fresh epoch per run. A caller with its own supervisor, such as Pronto's -truck, runs the library directly, including two sessions (two URLs) into one -broadcast. +fresh epoch per run. A caller with its own supervisor runs the library +directly, including two sessions (two URLs) into one broadcast. Non-goals: an RTSP server (push ingest), a relay pulling cameras itself, and codecs nothing in the stack plays (G.711, MJPEG). ## Plan -Moved from m3 to m2 on 2026-10-05: security cameras are moq.pro's priority -customer segment, so the "no consumer" reason for parking it is gone. moq.pro's -camera guide and Pronto's truck consume it. +Moved from m3 to m2 on 2026-10-05 by maintainer priority: IP cameras are a +priority use case, so the "no consumer" reason for parking it is gone. Decided 2026-10-01: @@ -35,7 +33,7 @@ Decided 2026-10-01: nothing until the first keyframe. - AAC maps to moq-mux's AAC importer. Audio the stack cannot play, G.711 above all (common on cameras), is skipped with one clear warning and the - broadcast goes on video-only. Transcoding it waits for a customer who + broadcast goes on video-only. Transcoding it waits for a consumer who needs it. - A broadcast name always means the same content, so a timeline restart is never spliced onto the old broadcast. A new session's RTP time restarts, @@ -55,26 +53,24 @@ Decided 2026-10-01: before `describe`, and log only the redacted URL (`RedactedUrl`). A `--connect` URL is visible to other local users through `/proc` cmdline; accepted for now, and revisit with an env var or file for shared hosts. -- Prior art: moq.pro's `pronto/truck/src/camera.rs` is a working retina pull - client, video only, run against mediamtx and real cameras. Decided 2026-10-05: - The library ingests one session and never reconnects. The caller supplies the `broadcast::Producer` and importer state stays private, so two - sessions, such as the truck's H.264 SD and H.265 HD URLs, can publish + sessions, such as a camera's H.264 SD and H.265 HD URLs, can publish renditions of one broadcast. Two sessions on one catalog each shift onto its clock (`catalog::Timebase`), so they land on one timeline. A session end, a failure, and a timeline jump all return to the caller, - which decides whether to retry. Reason: the truck deliberately exits - for systemd so a stuck Starlink session surfaces, and an in-process retry + which decides whether to retry. Reason: a supervised caller may deliberately + exit for systemd so a stuck uplink surfaces, and an in-process retry inside the library would force it to opt out. - `moq import rtsp` owns reconnect with backoff: each new session gets a fresh - epoch, a fresh catalog, and starts on a keyframe, as above. Rejected: the - truck adopting the crate's reconnect. + epoch, a fresh catalog, and starts on a keyframe, as above. Rejected: every + caller adopting the crate's reconnect. -Test against a local RTSP source serving a known H.264 + AAC clip (Pronto -uses mediamtx), checking the catalog and first frames; a G.711 source that +Test against a local RTSP source serving a known H.264 + AAC clip (such as +mediamtx), checking the catalog and first frames; a G.711 source that publishes video only; two sessions ingested into one caller broadcast, where one session's timestamp jump has the test caller restart both under a new epoch; and a source restarted mid-stream, whose RTP time resets: the library @@ -89,6 +85,3 @@ lands on main. ## Related - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - the fresh epoch a caller and the CLI mint for each restarted broadcast -- [moq.pro's Pronto truck](https://github.com/moq-dev/moq.pro/blob/main/pronto/truck/src/camera.rs) - the prior art this generalizes -- [moq.pro: Pronto truck on moq-rtsp](https://github.com/moq-dev/moq.pro/blob/main/quest/m3/truck-rtsp.md) - runs the library entry point under its own supervisor -- [moq.pro: Camera (RTSP) guide](https://github.com/moq-dev/moq.pro/blob/main/quest/m2/rtsp-guide.md) - documents `moq import rtsp` for customers once a release ships it diff --git a/quest/m2/teleop/README.md b/quest/m2/teleop/README.md index ae18545e74..efa39bbbd7 100644 --- a/quest/m2/teleop/README.md +++ b/quest/m2/teleop/README.md @@ -13,8 +13,7 @@ partner shows up. Decided in the 2026-09-30 audit: the ROS 2 bridge was deleted, cross-track correlation folded into [robot](/quest/m3/teleop-robot.md), V4L2-M2M encoding folded into [CLI packaging](/quest/m2/cli-packaging.md), and the MAVLink -bridge and SITL proof moved to m3. Kyber is a competitor with proprietary -framing, not a transport we replace. +bridge and SITL proof moved to m3. Decided 2026-10-08: the robot primitive and operator arbitration are parked in m3 until a teleoperation consumer needs them, so the line's m2 work is the @@ -60,24 +59,19 @@ means for the primitive is in [robot](/quest/m3/teleop-robot.md), and what it means for a protocol multiplexing many message rates onto one link is in [mavlink](/quest/m3/teleop-mavlink.md). -### Who is already here - -Nobody runs DDS over a WAN. The fight is MoQ against Zenoh over QUIC and MoQ -against Foxglove on WebRTC. - -- **Zenoh** shipped priority-mapped QUIC multistream plus mixed - stream/datagram reliability in v1.9.0 (April 2026) and is a Tier 1 ROS 2 - middleware. Closest thing to our delivery model in the wild, but its WAN - topology is hand-configured router endpoints with no relay or media pipeline. -- **Foxglove** Remote Access went GA in August 2026 as a hand-rolled MoQ: the - device gateway connects outbound, uploads each stream at most once to an SFU - that fans out, uses lossy data channels by default with reliable opt-in per - topic, and adapts video quality. Built on WebRTC because nothing else existed. -- **Kyber** (kyber.tech, Jean-Baptiste Kempf of VLC, $5M seed June 2026) is the - only other party betting on QUIC over WebRTC for machine control. Point to - point with no relay or fan-out story, proprietary framing with no interop, no - ROS or MAVLink integration, at v0.26. The competition is over the narrative, - not the technology. +### What a WAN deployment expects + +DDS discovery assumes a LAN, so a WAN deployment configures its peers by +hand. What remote robotics needs over a WAN: + +- Priority-mapped QUIC streams with mixed stream and datagram reliability, + without hand-configured router endpoints between sites. +- A device that connects outbound, uploads each stream at most once, and + lets a relay fan it out. +- Lossy delivery by default with reliable opt-in per topic, and video that + adapts to the link. +- Open framing that interoperates, with ROS and MAVLink integrations as + adapters on top. ### CGNAT is the pain the ecosystem routes around diff --git a/quest/m2/watch-data-sync.md b/quest/m2/watch-data-sync.md index a29b61193b..bb4ed36710 100644 --- a/quest/m2/watch-data-sync.md +++ b/quest/m2/watch-data-sync.md @@ -20,10 +20,9 @@ releases it. - An untimed track's payloads add no timestamp wait; they apply in order as they arrive. Since 2026-10-05 a track is all timed or all untimed (the untimed model, [#4822](https://github.com/moq-dev/moq/pull/4822)), so no stream mixes the two. -- OneTooMany is the application this waited for (2026-10-01): their web - frontend holds KLV and MAVLink telemetry back to the video playhead with - its own sync code, which this replaces. In m2 rather than m1 because - they aren't blocked. +- This replaces the sync code a web frontend writes today to hold KLV or + MAVLink telemetry back to the video playhead. In m2 rather than m1, since + that workaround exists. - Decided 2026-10-08: the A/V clock and the watch worker land first, since they change what `Sync` waits on and where it runs. diff --git a/quest/m3/lite07-mesh.md b/quest/m3/lite07-mesh.md index 27de6b8048..9320b6618b 100644 --- a/quest/m3/lite07-mesh.md +++ b/quest/m3/lite07-mesh.md @@ -10,8 +10,7 @@ this quest and every `Required` entry that links it. ## Plan -moq.pro's lite-07 rollout is `quest/m2/lite07.md` in moq-dev/moq.pro. It -waits on [Finalize moq-lite-07](/quest/m1/lite07-finalize.md), so this +moq.pro's lite-07 rollout waits on [Finalize moq-lite-07](/quest/m1/lite07-finalize.md), so this condition cannot clear before a release carries the final version. Decided 2026-10-05: [Cluster routing](/quest/m1/cluster-routing/README.md)'s diff --git a/quest/m3/teleop-robot.md b/quest/m3/teleop-robot.md index 00211fedec..ca809f4597 100644 --- a/quest/m3/teleop-robot.md +++ b/quest/m3/teleop-robot.md @@ -9,7 +9,7 @@ the gate for the rest of the questline. ## Plan Decided 2026-10-08: parked in m3. No teleoperation consumer needs the crate -yet; OneTooMany's telemetry need is [data sync](/quest/m2/watch-data-sync.md), +yet; telemetry beside video needs [data sync](/quest/m2/watch-data-sync.md), not this primitive. ### Direction diff --git a/rs/moq-boy/src/main.rs b/rs/moq-boy/src/main.rs index 94e2bf3aa4..4d684bed90 100644 --- a/rs/moq-boy/src/main.rs +++ b/rs/moq-boy/src/main.rs @@ -340,8 +340,7 @@ fn run_emulator( // long the pause was into the future. Published as a marker group that gap // reads as a discontinuity rather than one enormous frame duration, and the // marker becomes the live edge -- so a viewer arriving mid-pause waits for real - // media instead of being served the pre-pause group as if it were live - // (moq-dev/moq.pro#814). + // media instead of being served the pre-pause group as if it were live. session.video_encoder.discontinuity(); audio_encoder.discontinuity()?; diff --git a/rs/moq-mux/src/container/fmp4/export.rs b/rs/moq-mux/src/container/fmp4/export.rs index a24ceba9fe..81094b8508 100644 --- a/rs/moq-mux/src/container/fmp4/export.rs +++ b/rs/moq-mux/src/container/fmp4/export.rs @@ -1071,8 +1071,7 @@ pub(crate) fn apply_codec_durations(frames: &mut [Frame], opus: bool) { /// of that out, so the gap across a boundary is treated as a discontinuity, never a /// duration. Reading it as one makes a single sample last as long as the gap: a recording /// that woke a paused publisher got a 2405 second video sample, whose `EXTINF` put the HLS -/// video timeline 9620 seconds ahead of audio and stalled the player outright -/// (moq-dev/moq.pro#814). +/// video timeline 9620 seconds ahead of audio and stalled the player outright. /// /// The last frame before a boundary falls back to `default_frame` (the catalog framerate / /// sample rate), which is what the gap works out to anyway on a continuous constant-rate @@ -1308,7 +1307,7 @@ mod tests { assert_eq!(durations, vec![(1_000, timescale); 3]); } - /// The regression for moq-dev/moq.pro#814: a subscriber that got a stale cached group + /// The regression for the stalled HLS player: a subscriber that got a stale cached group /// and then jumped to live sees a huge gap across the boundary. That gap is a /// discontinuity, not a duration, so the frame before it must NOT swallow it -- it /// takes `default_frame` and the fragment stays one frame long. diff --git a/rs/moq-mux/src/container/fmp4/fragmenter.rs b/rs/moq-mux/src/container/fmp4/fragmenter.rs index 012d58f138..2cd49d121e 100644 --- a/rs/moq-mux/src/container/fmp4/fragmenter.rs +++ b/rs/moq-mux/src/container/fmp4/fragmenter.rs @@ -568,7 +568,7 @@ mod tests { } // A keyframe may open a new group, and a group boundary is never a duration: the publisher - // may have paused across it (the 2405 second sample of moq-dev/moq.pro#814). The pending + // may have paused across it (which once produced a 2405 second sample). The pending // frame takes the catalog cadence, and the new group re-anchors the decode timeline at its // keyframe's presentation time rather than pretending the stream was continuous. #[test] diff --git a/rs/moq-mux/src/container/fmp4/muxer.rs b/rs/moq-mux/src/container/fmp4/muxer.rs index ee169fe192..8046205b15 100644 --- a/rs/moq-mux/src/container/fmp4/muxer.rs +++ b/rs/moq-mux/src/container/fmp4/muxer.rs @@ -788,8 +788,7 @@ mod tests { // A group boundary is never a duration, even when the groups arrived consecutively: the // publisher may have paused across it (moq-boy runs its PTS on a clock that keeps going - // while the encoder is off), which is what produced a 2405 second sample in - // moq-dev/moq.pro#814. + // while the encoder is off), which is what once produced a 2405 second sample. #[tokio::test] async fn audio_fragment_does_not_absorb_a_pause() { use hang::catalog::AudioCodec; diff --git a/rs/moq-mux/src/container/producer.rs b/rs/moq-mux/src/container/producer.rs index a197c9cc4c..65be578fda 100644 --- a/rs/moq-mux/src/container/producer.rs +++ b/rs/moq-mux/src/container/producer.rs @@ -608,8 +608,8 @@ where /// source, resuming on a re-anchored clock. Without it a break is invisible: the next /// group looks exactly like the one that would have followed, and a consumer bounding a /// sample by the next group's first frame hands it the entire gap as its duration. That - /// produced a 2405 second video sample out of a publisher that had been paused 40 minutes - /// (moq-dev/moq.pro#814). Consecutive sequence numbers can't rule a pause out, so this + /// produced a 2405 second video sample out of a publisher that had been paused 40 minutes. + /// Consecutive sequence numbers can't rule a pause out, so this /// marker is the only thing that can say one happened. /// /// It also fixes what a subscriber joining mid-break sees. A subscription starts at the @@ -1082,7 +1082,7 @@ mod tests { /// A subscription starts at the track's LATEST group, and the marker advances it even /// though it carries nothing. So a subscriber arriving mid-break waits for real media /// rather than being handed the pre-break group as if it were live -- which is how a - /// 40-minute-stale frame reached a VOD recording in moq-dev/moq.pro#814. + /// 40-minute-stale frame once reached a VOD recording. #[tokio::test] async fn discontinuity_moves_the_live_edge_off_stale_content() { let track = track_producer("test", hang::container::track_info(hang::catalog::PRIORITY.audio)); diff --git a/rs/moq-relay/tests/cluster_unknown.rs b/rs/moq-relay/tests/cluster_unknown.rs index c0ddcfd3ba..ec8a25620d 100644 --- a/rs/moq-relay/tests/cluster_unknown.rs +++ b/rs/moq-relay/tests/cluster_unknown.rs @@ -8,7 +8,7 @@ use moq_relay::{Config, Relay}; use url::Url; const TIMEOUT: Duration = Duration::from_secs(10); -const PATH: &str = "opalin/cell-clumsy-octopus/cameras/left.hang"; +const PATH: &str = "robot/cell-clumsy-octopus/cameras/left.hang"; async fn spawn_relay( id: u64, diff --git a/rs/moq-relay/tests/smoke.rs b/rs/moq-relay/tests/smoke.rs index 0d7268aaf9..7ff048af29 100644 --- a/rs/moq-relay/tests/smoke.rs +++ b/rs/moq-relay/tests/smoke.rs @@ -1001,7 +1001,7 @@ async fn spawn_subscribe_only_relay() -> (u16, tokio::task::JoinHandle<()>) { /// handshake instead of being accepted and silently carrying no media. The client /// advertises `Role::Publisher` in its SETUP (derived from `with_publisher`), and the /// relay closes the session because the token has no publish scope. This is the -/// regression guard for moq.pro#338: before the role hint, this connection was +/// regression guard for the role hint: before it, this connection was /// accepted and the publisher streamed into a dropped session forever. #[tokio::test] async fn subscribe_only_public_rejects_publisher_role() {