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
2 changes: 1 addition & 1 deletion dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
4 changes: 3 additions & 1 deletion doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go/wrapper/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions js/net/src/ietf/priority.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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);
});
2 changes: 1 addition & 1 deletion js/net/src/lite/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
);
});

Expand Down
4 changes: 2 additions & 2 deletions js/net/src/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
12 changes: 8 additions & 4 deletions js/net/src/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -100,7 +104,7 @@ export function infoDefaults(info: Partial<Info> = {}): 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),
};
}

Expand Down Expand Up @@ -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;
}

/**
Expand Down
1 change: 0 additions & 1 deletion quest/m1/moxygen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 0 additions & 28 deletions quest/m1/moxygen/priority.md

This file was deleted.

4 changes: 2 additions & 2 deletions rs/libmoq/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions rs/moq-ffi/src/producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

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 Preserve the midpoint default in Go

When a Go caller passes &TrackInfo{} or sets only max age/timescale, the generated struct still supplies priority: 0 because UniFFI defaults do not reach Go. The wrapper directly aliases ffi.MoqTrackInfo in go/wrapper/types.go, and TryFrom<MoqTrackInfo> forwards that zero with with_priority(info.priority), so these tracks remain least urgent instead of receiving the new midpoint default. Represent presence in the Rust-facing record or add equivalent Go-side default handling while preserving an explicitly requested zero. (Written by GPT-5.6 Sol)

AGENTS.md reference: rs/moq-ffi/AGENTS.md:L17-L17

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.

Agreed it is a gap, same class as libmoq's moq_track_info: a zero Go struct sends priority 0. Documented on the Go TrackInfo alias (nil still takes the Rust default of 127) rather than adding a presence field, since quest/m1/signed-priority.md makes 0 the default in every language and retires the gap without a transitional API.

(Written by Claude Opus 5.5)

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
Expand Down
34 changes: 23 additions & 11 deletions rs/moq-net/src/ietf/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-net/src/lite/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
11 changes: 5 additions & 6 deletions rs/moq-net/src/model/bandwidth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 11 additions & 2 deletions rs/moq-net/src/model/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}

Expand All @@ -113,7 +118,7 @@ impl Default for Info {
Self {
timescale: Timescale::default(),
max_age: DEFAULT_MAX_AGE,
priority: 0,
priority: DEFAULT_PRIORITY,
}
}
}
Expand Down Expand Up @@ -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.
Expand Down
Loading