From acdb122aca7c03832bd3f0346cde511059bc2beb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 26 Sep 2026 10:44:32 -0700 Subject: [PATCH 1/3] quest: claim moxygen/priority Co-Authored-By: Claude Opus 5.5 From 9bf205206229a4f63d24b382257eb4b90802e36b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 26 Sep 2026 11:11:38 -0700 Subject: [PATCH 2/3] feat(net): default an unset track priority to the midpoint track::Info.priority defaulted to 0, the least urgent value, which IETF sent as 255. Default it to 127 so moq-lite carries 127 and IETF carries 128, the draft's usual publisher priority. Mirrored in JS and the FFI. Co-Authored-By: Claude Opus 5.5 --- dart/moq_ffi/lib/src/moq.dart | 2 +- doc/concept/standard.md | 4 +++- js/net/src/ietf/priority.test.ts | 5 +++++ js/net/src/lite/track.test.ts | 2 +- js/net/src/track.test.ts | 4 ++-- js/net/src/track.ts | 12 +++++++---- quest/m1/moxygen/README.md | 1 - quest/m1/moxygen/priority.md | 28 ------------------------- rs/libmoq/src/api.rs | 4 ++-- rs/moq-ffi/src/producer.rs | 5 +++-- rs/moq-net/src/ietf/publisher.rs | 34 +++++++++++++++++++++---------- rs/moq-net/src/lite/track.rs | 2 +- rs/moq-net/src/model/bandwidth.rs | 11 +++++----- rs/moq-net/src/model/track.rs | 13 ++++++++++-- 14 files changed, 65 insertions(+), 62 deletions(-) delete mode 100644 quest/m1/moxygen/priority.md diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index ef1383523e..1459eadb5e 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -1773,7 +1773,7 @@ class MoqTrackInfo { final int? maxAgeUs; final int? timescale; MoqTrackInfo({ - this.priority = 0, + this.priority = 127, this.maxAgeUs = null, this.timescale = null, }); diff --git a/doc/concept/standard.md b/doc/concept/standard.md index f2c7e9929e..a6e9b72476 100644 --- a/doc/concept/standard.md +++ b/doc/concept/standard.md @@ -29,7 +29,9 @@ differences. An IETF publisher declares the track's default priority in `SUBSCRIBE_OK` or `PUBLISH` when that draft carries track properties. Groups without a priority flag inherit it. If the property is absent, the IETF wire default of 128 maps -to model priority 127, where higher values are served first. +to model priority 127, where higher values are served first. A track that +never sets a priority is 127 as well, so it goes out as 128 on IETF and 127 +on moq-lite. On drafts 14–19, the Rust publisher serves relative joining `FETCH` requests with offset zero for `NextObject` subscriptions. The fetch delivers the saved diff --git a/js/net/src/ietf/priority.test.ts b/js/net/src/ietf/priority.test.ts index a05f8b2650..b04da83354 100644 --- a/js/net/src/ietf/priority.test.ts +++ b/js/net/src/ietf/priority.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; +import { infoDefaults } from "../track.ts"; import { fromWire, toWire } from "./priority.ts"; test("IETF subscriber priority is lower first", () => { @@ -17,3 +18,7 @@ test("subscriber priority round trips", () => { expect(fromWire(toWire(priority))).toBe(priority); } }); + +test("an unset track priority is the draft's usual publisher priority", () => { + expect(toWire(infoDefaults().priority)).toBe(128); +}); diff --git a/js/net/src/lite/track.test.ts b/js/net/src/lite/track.test.ts index 6e12024335..f2bf8756a9 100644 --- a/js/net/src/lite/track.test.ts +++ b/js/net/src/lite/track.test.ts @@ -43,7 +43,7 @@ test("TrackInfo round-trips on draft-05", async () => { test("TrackInfo defaults match cross-language wire bytes", async () => { const info = new TrackInfo(infoDefaults()); expect(await bytes((w) => info.encode(w, Version.DRAFT_05))).toEqual( - new Uint8Array([0x06, 0x00, 0x00, 0x53, 0x88, 0x43, 0xe8]), + new Uint8Array([0x06, 0x7f, 0x00, 0x53, 0x88, 0x43, 0xe8]), ); }); diff --git a/js/net/src/track.test.ts b/js/net/src/track.test.ts index 60836121ce..1cd585ed46 100644 --- a/js/net/src/track.test.ts +++ b/js/net/src/track.test.ts @@ -27,9 +27,9 @@ function mockMonotonicTime(initial: number) { }; } -test("priority reads the committed info and is 0 before accept", () => { +test("priority reads the committed info and is the midpoint before accept", () => { const producer = new TrackProducer("video"); - expect(producer.priority).toBe(0); + expect(producer.priority).toBe(127); producer.accept({ priority: 60 }); expect(producer.priority).toBe(60); }); diff --git a/js/net/src/track.ts b/js/net/src/track.ts index 913d1ce70f..86c19d68bd 100644 --- a/js/net/src/track.ts +++ b/js/net/src/track.ts @@ -27,6 +27,10 @@ const MAX_TIMEOUT_MS = 2 ** 31 - 1; /** Default {@link Info.maxAge} window (milliseconds) when the publisher does not set one. */ export const DEFAULT_MAX_AGE_MS = Milli(5000); +// The higher-first midpoint. IETF flips priority (lower first), so this goes out as 128, the +// draft's usual publisher priority, while moq-lite carries 127 as written: one urgency on both. +const DEFAULT_PRIORITY = 127; + /** Maximum buffered datagrams per subscriber; mirrors Rust's bounded send buffer. */ const MAX_DATAGRAMS = 64; @@ -67,7 +71,7 @@ export interface Info { * or non-finite value and a result past `Number.MAX_SAFE_INTEGER`. */ maxAge: Milli; - /** Tie-break priority between subscriptions of equal subscriber priority (`0..=255`). */ + /** Tie-break priority between subscriptions of equal subscriber priority (`0..=255`, higher first). Defaults to `127`. */ priority: number; } @@ -100,7 +104,7 @@ export function infoDefaults(info: Partial = {}): Info { return { timescale: Timescale(info.timescale ?? Timescale.MILLI), maxAge: maxAgeMillis(info.maxAge ?? DEFAULT_MAX_AGE_MS), - priority: priorityByte(info.priority ?? 0), + priority: priorityByte(info.priority ?? DEFAULT_PRIORITY), }; } @@ -435,13 +439,13 @@ export class Producer { } /** - * Publisher priority from the committed {@link Info}, or 0 before {@link accept}. + * Publisher priority from the committed {@link Info}, or the default before {@link accept}. * * Higher is served first. Hang publishers set this from `Catalog.PRIORITY` so * audio outranks video on the wire and in the bandwidth allocator. */ get priority(): number { - return this.#state.info.peek()?.priority ?? 0; + return this.#state.info.peek()?.priority ?? DEFAULT_PRIORITY; } /** diff --git a/quest/m1/moxygen/README.md b/quest/m1/moxygen/README.md index d7d7e72434..937f5ece5f 100644 --- a/quest/m1/moxygen/README.md +++ b/quest/m1/moxygen/README.md @@ -30,7 +30,6 @@ Docs stay inline in the change that makes them stale. No new guide. ## Quests -- [Default track priority](/quest/m1/moxygen/priority.md) - an unset track priority is the midpoint on moq-lite and on IETF, not the least urgent value - [Group FETCH](/quest/m1/moxygen/fetch.md) - an IETF FETCH of whole groups is served from cache or fetched upstream, one group at a time - [Datagram groups](/quest/m1/moxygen/datagram.md) - an IETF datagram that is one object in a group arrives as a moq-lite datagram group diff --git a/quest/m1/moxygen/priority.md b/quest/m1/moxygen/priority.md deleted file mode 100644 index 6efd6aaa5a..0000000000 --- a/quest/m1/moxygen/priority.md +++ /dev/null @@ -1,28 +0,0 @@ -# [S] Default track priority - -## Goal - -A track that never set a priority is the midpoint on both wires. moq-lite -TRACK_INFO carries that midpoint as written. An IETF subgroup header carries -128. A track that set `track::Info.priority` still uses that value. Per-group -priority stays unsupported. - -## Plan - -`track::Info.priority` defaults to 0, higher-first. moq-lite writes it -verbatim, so the wire byte is 0. IETF flips it, so 0 goes out as 255. The -draft's usual publisher priority is 128. - -One model default serves both. 127 is the higher-first midpoint: lite writes -127, IETF writes 128. Do not special-case each wire to the byte 128. That -would make the two encodings different urgencies. - -Rust and JavaScript both pin the old default TRACK_INFO bytes -(`0x06, 0x00, ...`). Update those fixtures with the new default. - -The moxygen 200 versus 201 split by group parity is out of scope. - -## Related - -- [Moxygen compatibility](/quest/m1/moxygen/README.md) - the line this belongs to -- [Track priority scope](/quest/m1/track-priority-scope.md) - fairness across owners, not this default diff --git a/rs/libmoq/src/api.rs b/rs/libmoq/src/api.rs index 7d5cfdae51..d221f6f441 100644 --- a/rs/libmoq/src/api.rs +++ b/rs/libmoq/src/api.rs @@ -491,8 +491,8 @@ pub struct moq_datagram { /// Publisher-side raw track properties. /// /// A null [moq_publish_track] `info` pointer uses the moq-net defaults. -/// A zero-initialized struct also uses those defaults, except `priority` where -/// zero is the default itself. +/// A zero-initialized struct also uses those defaults, except `priority`, which +/// has no presence flag: zero is the least urgent, and 127 is the moq-net default. #[repr(C)] #[allow(non_camel_case_types)] pub struct moq_track_info { diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index 6456335841..12c9304f51 100644 --- a/rs/moq-ffi/src/producer.rs +++ b/rs/moq-ffi/src/producer.rs @@ -11,11 +11,12 @@ use crate::media::{MoqAudioInit, MoqContainerFormat, MoqContainerInit, MoqFrame, /// Publisher-side track properties, mirroring [`moq_net::track::Info`]. /// /// Construct with the fields you care about; the rest use raw-track defaults -/// (priority 0, the publisher's default max age, microsecond timescale). +/// (priority 127, the publisher's default max age, microsecond timescale). #[derive(Clone, uniffi::Record)] pub struct MoqTrackInfo { /// Priority, used only to break ties between subscriptions of equal subscriber priority. - #[uniffi(default = 0)] + /// Higher is more urgent; the default 127 is the midpoint. + #[uniffi(default = 127)] pub priority: u8, /// Maximum age of a non-latest group before the publisher evicts it, in /// microseconds. Null uses the default. This is the publisher-side half of diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index 6177770669..2648a61b8a 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -2476,10 +2476,31 @@ mod group_priority_test { /// every moq-transport peer. #[tokio::test] async fn group_header_carries_the_publisher_priority() { + let header = serve_group_header(track::Info::default().with_priority(hang_audio_priority())).await; + assert_eq!( + header.publisher_priority, + priority::to_wire(hang_audio_priority()), + "the wire is lower-first, so audio must encode below video" + ); + assert!( + priority::to_wire(hang_audio_priority()) < priority::to_wire(hang_video_priority()), + "audio outranks video on the wire" + ); + } + + /// A track that never set a priority is the draft's usual publisher priority, 128, + /// not 255, the least urgent value a peer like moxygen would deprioritize. + #[tokio::test] + async fn group_header_defaults_to_the_midpoint() { + let header = serve_group_header(track::Info::default()).await; + assert_eq!(header.publisher_priority, 128); + } + + /// Serve one group of a track with `info` and decode the subgroup header it opens with. + async fn serve_group_header(info: track::Info) -> ietf::GroupHeader { let log = crate::lite::test_transport::Log::default(); let session = SinkSession::new(log.clone()); - let info = track::Info::default().with_priority(hang_audio_priority()); let track = track::Producer::new(std::sync::Arc::new(crate::broadcast::Info::default()), "test", info); let subscriber = track.subscribe(None); @@ -2500,16 +2521,7 @@ mod group_priority_test { let written = log.writes.lock().unwrap().clone(); let mut buf = bytes::Bytes::from(written); - let header = ietf::GroupHeader::decode(&mut buf, Version::Draft14).expect("a group header"); - assert_eq!( - header.publisher_priority, - priority::to_wire(hang_audio_priority()), - "the wire is lower-first, so audio must encode below video" - ); - assert!( - priority::to_wire(hang_audio_priority()) < priority::to_wire(hang_video_priority()), - "audio outranks video on the wire" - ); + ietf::GroupHeader::decode(&mut buf, Version::Draft14).expect("a group header") } /// `hang::catalog::PRIORITY` isn't reachable from `moq-net` (hang depends on it, not the diff --git a/rs/moq-net/src/lite/track.rs b/rs/moq-net/src/lite/track.rs index 68f2e10d48..1d4ad41da0 100644 --- a/rs/moq-net/src/lite/track.rs +++ b/rs/moq-net/src/lite/track.rs @@ -133,7 +133,7 @@ mod test { let mut buf = Vec::new(); info.encode(&mut buf, Version::Lite05).unwrap(); - assert_eq!(buf, [0x06, 0x00, 0x00, 0x53, 0x88, 0x43, 0xe8]); + assert_eq!(buf, [0x06, 0x7f, 0x00, 0x53, 0x88, 0x43, 0xe8]); } #[test] diff --git a/rs/moq-net/src/model/bandwidth.rs b/rs/moq-net/src/model/bandwidth.rs index 735ab78339..58977552ea 100644 --- a/rs/moq-net/src/model/bandwidth.rs +++ b/rs/moq-net/src/model/bandwidth.rs @@ -232,9 +232,9 @@ impl Allocator { /// decision about what to *produce*, and there is no single subscriber /// priority to read when several are watching one track. /// - /// That last part is what carries the common case, since publishers leave - /// `priority` at its default today: one tier of audio and video still serves - /// audio's small reservation in full before video takes the remainder. + /// That last part carries a publisher that leaves `priority` at its default: + /// one tier of audio and video still serves audio's small reservation in full + /// before video takes the remainder. /// /// The reservation lasts as long as the returned [`Reservation`]: hold it for as /// long as the sender is publishing, change the ceiling with @@ -663,9 +663,8 @@ mod tests { assert_eq!(allocate(bps(1_000_000), &wants, 1), Some(bps(0))); } - /// Publishers don't set [`track::Info::priority`] today (it defaults to 0 and - /// `hang::container::track_info` leaves it there), so audio and video land in - /// one tier. That has to come out right anyway, and it does: max-min fair + /// A publisher that doesn't set [`track::Info::priority`] puts audio and video + /// in one tier. That has to come out right anyway, and it does: max-min fair /// satisfies the small claim first, so audio still gets its full reservation /// and video takes the rest. Priority only changes the answer once a tier's /// smaller claims outgrow an even split. diff --git a/rs/moq-net/src/model/track.rs b/rs/moq-net/src/model/track.rs index d4f3216a6d..105ce7c636 100644 --- a/rs/moq-net/src/model/track.rs +++ b/rs/moq-net/src/model/track.rs @@ -34,6 +34,10 @@ use std::{ /// Default [`Info::max_age`] when the publisher doesn't set one. pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(5); +// The higher-first midpoint. IETF flips priority (lower first), so this goes out as 128, the +// draft's usual publisher priority, while moq-lite carries 127 as written: one urgency on both. +const DEFAULT_PRIORITY: u8 = 127; + /// Maximum number of datagrams retained in the per-track send buffer. /// /// Datagrams are a best-effort send buffer, not a replay cache (unlike groups): only the last @@ -105,6 +109,7 @@ pub struct Info { pub max_age: Duration, /// The publisher's priority for this track, used only to break ties between /// subscriptions of equal subscriber priority. Reported in TRACK_INFO (Lite05+). + /// Higher is more urgent. Defaults to 127, the midpoint. pub priority: u8, } @@ -113,7 +118,7 @@ impl Default for Info { Self { timescale: Timescale::default(), max_age: DEFAULT_MAX_AGE, - priority: 0, + priority: DEFAULT_PRIORITY, } } } @@ -2187,7 +2192,11 @@ impl Demand { /// The publisher's tie-break priority, as set in [`Info::priority`]. pub(crate) fn priority(&self) -> u8 { // Always Some once the track exists; a closed one reads its last value. - self.state.read().info.as_ref().map_or(0, |info| info.priority) + self.state + .read() + .info + .as_ref() + .map_or(DEFAULT_PRIORITY, |info| info.priority) } /// Whether anyone is subscribed right now, without waiting. From 1c4fc1582b45b44109a42258385b002b45f33b60 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 26 Sep 2026 18:45:39 -0700 Subject: [PATCH 3/3] docs(go): a zero TrackInfo priority is the least urgent Co-Authored-By: Claude Opus 5.5 --- go/wrapper/types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/wrapper/types.go b/go/wrapper/types.go index 40bf4f53ea..5c21af7a4d 100644 --- a/go/wrapper/types.go +++ b/go/wrapper/types.go @@ -48,6 +48,7 @@ type ( // Subscription holds subscriber-side delivery preferences: priority, ordering, max age, and group range. Subscription = ffi.MoqSubscription // TrackInfo holds publisher-side track properties: priority, ordering, max age, and timescale. + // A zero Priority is the least urgent, not the default; set 127 for the midpoint a nil TrackInfo uses. TrackInfo = ffi.MoqTrackInfo // Video describes one catalog rendition, including whether the publisher recommends temporarily avoiding it. Video = ffi.MoqVideo