Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4464b26
chore(quest): claim quest/m1/datagram-tracks/rust.md
kixelated Nov 3, 2026
bf57029
wip: datagram tracks in the moq-net model and lite wire
kixelated Nov 3, 2026
ee34845
wip: remove NOT_FETCHABLE and IETF stream-track datagrams
kixelated Nov 3, 2026
76658b8
wip: adapt tests, e2ee, ffi, c, archive to datagram tracks
kixelated Nov 3, 2026
2a32965
wip: lite-05/06 fallback after the stream lookup resolves; keep the s…
kixelated Nov 3, 2026
bc4f65e
wip: draft, docs, stats for datagram tracks
kixelated Nov 3, 2026
884cdc3
wip: quest tree, interop lanes, stale docs
kixelated Nov 3, 2026
87af2c6
wip: datagram serve rechecks demand atomically
kixelated Nov 3, 2026
10267a8
wip: migration notes
kixelated Nov 3, 2026
7e5ba21
wip: ffi local readers open datagrams eagerly; binding tests and docs
kixelated Nov 3, 2026
6df93de
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 3, 2026
66fb494
fix: formatting and retarget the leaked-broadcasts drill mutation
kixelated Nov 3, 2026
cce3c04
fix: biome format
kixelated Nov 3, 2026
1c87b8d
fix(net): gate, demand, and work guard for upstream datagram subscrip…
kixelated Nov 3, 2026
c249d09
fix(net): check datagram demand before the auth wait
kixelated Nov 3, 2026
4f2bdc2
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 3, 2026
3714dda
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 4, 2026
930ce53
fix(net): drain datagrams on finish, end lite-05/06 fallback with SUB…
kixelated Nov 4, 2026
2d5b74e
fix(net): a datagram consumer created after a finish starts past the …
kixelated Nov 4, 2026
e45a8e3
feat(net): carry both kinds of a name on lite-05/06 subscriptions
kixelated Nov 4, 2026
c47d94c
fix(net): share old-version datagrams without adding demand; withdraw…
kixelated Nov 4, 2026
0f24893
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 4, 2026
98984a7
fix(net): end shared old-version datagrams as their subscription ends
kixelated Nov 4, 2026
30adfe4
fix(c): read a datagram-only track without a stream track
kixelated Nov 4, 2026
f6aead3
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 4, 2026
0a15a9c
style: format the interop tail usage error
pullfrog[bot] Nov 4, 2026
5b130db
fix: review findings on the datagram tracks rework
kixelated Nov 4, 2026
adceb8f
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 4, 2026
2c15645
Merge remote-tracking branch 'origin/quest/m1/datagram-tracks/rust' i…
kixelated Nov 4, 2026
7591c00
fix(net): release a finished datagram track's buffer once nobody read…
kixelated Nov 4, 2026
ad87d30
Merge remote-tracking branch 'origin/main' into quest/m1/datagram-tra…
kixelated Nov 4, 2026
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
8 changes: 4 additions & 4 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22124,7 +22124,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackconsumer_recv_datagram() !=
17412) {
32344) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackconsumer_recv_group() != 40781) {
Expand Down Expand Up @@ -22437,7 +22437,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_append_datagram() !=
8934) {
38326) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_append_group() != 45225) {
Expand All @@ -22459,10 +22459,10 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_insert_datagram() !=
29400) {
30484) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_largest() != 3968) {
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_largest() != 24089) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_requests() != 60252) {
Expand Down
2 changes: 1 addition & 1 deletion demo/web/src/stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ <h1 class="text-2xl font-bold leading-tight">MoQ Relay Stats</h1>
<div id="kpi-fetches" class="text-2xl font-semibold tabular-nums mt-0.5">0</div>
</div>
<div class="rounded-lg border border-neutral-800 bg-neutral-900/50 p-3">
<div class="text-[11px] uppercase tracking-wide text-neutral-500" title="Single-frame groups carried over unreliable QUIC datagrams">Datagrams</div>
<div class="text-[11px] uppercase tracking-wide text-neutral-500" title="Datagrams of datagram tracks, carried over unreliable QUIC datagrams and counted apart from groups">Datagrams</div>
<div id="kpi-datagrams" class="text-2xl font-semibold tabular-nums mt-0.5">0</div>
</div>
<div class="rounded-lg border border-neutral-800 bg-neutral-900/50 p-3">
Expand Down
6 changes: 3 additions & 3 deletions demo/web/src/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ interface Snapshot {
bytes?: number;
frames?: number;
groups?: number;
// Single-frame groups carried over unreliable QUIC datagrams: a subset of `groups`,
// with their payload already in `frames` / `bytes`.
// Datagrams of datagram tracks: they belong to no group, so they count in neither
// `groups` nor `frames`, while their payload is in `bytes`.
datagrams?: number;
}
type BroadcastFrame = Record<string, Snapshot>;
Expand Down Expand Up @@ -264,7 +264,7 @@ interface Sample {
ingress: number; // cumulative ingress bytes
announce: number; // cumulative announce-control bytes (both directions)
fetches: number; // cumulative group fetches requested
datagrams: number; // cumulative single-frame groups sent as QUIC datagrams
datagrams: number; // cumulative datagrams of datagram tracks, apart from groups
broadcasters: number;
viewers: number;
tracks: number;
Expand Down
18 changes: 14 additions & 4 deletions doc/.vitepress/drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,30 @@ describe("tables render as tables", () => {

const rendered = html(page as DraftPage);

// The Bidirectional Streams table: one header, eight body rows, each
// The Bidirectional Streams table: one header, nine body rows, each
// separated by a kramdown rule in the source. Cells carry the source's
// alignment, so match around the style attribute.
const header = /<th[^>]*>Stream<\/th>/.exec(rendered);
expect(header).not.toBeNull();

for (const stream of ["Announce", "Subscribe", "Fetch", "Probe", "Goaway", "Track", "Auth", "Tracks"]) {
for (const stream of [
"Announce",
"Subscribe",
"Fetch",
"Probe",
"Goaway",
"Track",
"Auth",
"Tracks",
"Datagrams",
]) {
expect(rendered).toMatch(new RegExp(`<td[^>]*>${stream}</td>`));
}

// One header row plus eight body rows, in a single table.
// One header row plus nine body rows, in a single table.
const start = rendered.lastIndexOf("<table", header?.index);
const rows = rendered.slice(start).split("</table>")[0];
expect(rows.match(/<tr>/g) ?? []).toHaveLength(9);
expect(rows.match(/<tr>/g) ?? []).toHaveLength(10);
});
});

Expand Down
17 changes: 8 additions & 9 deletions doc/concept/archives.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ cannot negotiate moq-lite 07.

Recording starts with content the subscription can still receive. A missing
group is given up after the publisher's max age capped by the producer's max
delay, so a gap cannot stall later groups. Datagrams are uncached and can only
be recorded from when the subscription takes effect. An archive cannot recover
content already lost or evicted before it subscribed. The same max delay
delay, so a gap cannot stall later groups. Only stream tracks are recorded: a
datagram track is live-only, so an archive's playback never includes it. An
archive cannot recover content already lost or evicted before it subscribed. The same max delay
(`producer::Config::max_delay`, 10 s by default and in `moq export archive`)
bounds the backlog: content further behind the live edge is skipped, even when
the publisher still caches it.
Expand All @@ -74,7 +74,7 @@ segment. Within one broadcast prefix:
Objects coalesce content by duration, using media time for timed tracks and
wall time for untimed ones. A flush deadline also stores idle tracks, splitting
long groups between frames when needed. Groups, frame positions, timestamps,
payloads, and the distinction between groups and datagrams survive storage.
and payloads survive storage.
With DVR pruning enabled, old superseded objects are deleted after the retention
window and grace; the newest object of each track stays. Pruning runs only while
the recorder runs: once a recording completes or its recorder stops, objects not
Expand All @@ -101,11 +101,10 @@ periodically listed for new tracks and objects. Once `.end` appears, refreshing
stops and tracks end as recorded; without it, including when a recorder stopped
early, the archive stays live. Empty or unrecognized prefixes are refused.

Serving history is not paced playback from the beginning. Datagrams remain
lossy datagrams, never fetchable groups. Time ranges currently resolve against
the served track's cache, not the archive's stored keys, so they cannot reliably
seek old stored content. There is no automatic switch from a live broadcast to
its archive at the original path.
Serving history is not paced playback from the beginning. Time ranges currently
resolve against the served track's cache, not the archive's stored keys, so they
cannot reliably seek old stored content. There is no automatic switch from a
live broadcast to its archive at the original path.

The archived catalog is an ordinary track. [HLS](/bin/hls) can serve a recording
whose renditions include their `hls` record tracks, just as it serves a live
Expand Down
2 changes: 1 addition & 1 deletion doc/concept/compression.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ends list `moq-lite-07-wip`. Sessions on the default versions, and relay links
among them, carry the inflated frames. Only Rust offers the moq-transport
extension, so a JavaScript or third-party moq-transport peer reads the
inflated copy. A received track's `compress` flag says which form arrived. A
datagram compresses alone, with a fresh window.
datagram track is never compressed.

## Cost on the relay

Expand Down
4 changes: 3 additions & 1 deletion doc/concept/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ what lets it forward media it can't parse.
A **broadcast** is a named, discoverable collection of **tracks** from one
publisher. A **track** is a live sequence of **groups**, and each group is a
sequence of **frames** delivered reliably and in order over its own QUIC
stream (or, for a tiny single frame, as one unreliable datagram). Groups are independent: they can arrive out of order, be prioritized
stream. Groups are independent: they can arrive out of order, be prioritized
against each other, and be dropped under congestion without corrupting the
rest. For video a group is a GoP starting at a keyframe; for audio it's a
second or so of samples; for data it's whatever unit can be skipped as a whole.
A track can instead be a [datagram track](/concept/moq-lite#datagrams): small
unreliable payloads with no groups.

Subscribers pull. Nothing is transmitted until someone subscribes, duplicate
subscriptions are merged at every relay on the way upstream, and a publisher
Expand Down
43 changes: 25 additions & 18 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ carries [publisher instances](#publisher-instances).
| **Track** | A live sequence of groups, delivered out of order until closed. | Track |
| **Group** | A sequence of frames delivered reliably and in order, on its own QUIC stream. | Group |
| **Frame** | A sized chunk of bytes. | Object |
| **Datagram** | One unreliable frame sent as a QUIC datagram instead of a group. | Datagram |
| **Datagram** | One unreliable payload of a datagram track, sent as a QUIC datagram. | Datagram |

A broadcast is named by a path: non-empty segments joined by `/`, such as
`room/alice`. A path never starts or ends with `/` or doubles one, and a `\`
Expand Down Expand Up @@ -322,7 +322,7 @@ than the live and default starts. An untimed track refuses a time anchor. At the
edge, a time end
may let one group through before its first frame shows it starts past the end:
the first group that has yet to start is handed out without waiting for that
frame, and the next one waits on it. A datagram is judged by its own timestamp.
frame, and the next one waits on it.

Order only decides what goes first, never what is skipped, so catching up oldest
first needs a max delay that covers the backlog. When a relay serves subscribers of
Expand Down Expand Up @@ -430,22 +430,29 @@ keyframes do not use this mechanism.

## Datagrams

Since moq-lite 05, a publisher can send a tiny single-frame group as a QUIC
datagram: unreliable, unordered, under about 1200 bytes, and never
retransmitted. It suits real-time audio and sensor data. There is no stream
fallback, so a datagram that doesn't fit isn't delivered that way.

A datagram shares its subscription's priority and group order with streams,
so a low-priority datagram waits behind higher-priority groups instead of
jumping the queue. From a native Rust publisher over QUIC or WebTransport,
a full send buffer drops the lowest-priority subscription's
oldest datagram.

Nothing caches a datagram. A subscription gets the ones inside its group
range, and a new one may get the few still in the publisher's short send
buffer, but `FETCH` never returns one. A publisher that knows the sequence was
a datagram refuses with `NOT_FETCHABLE` (moq-lite-07), otherwise with
`NOT_FOUND`. Use a group for anything a late joiner needs.
A track is either a stream track, made of groups, or a datagram track, decided
when the publisher creates it. A datagram track is a series of QUIC datagrams:
unreliable, unordered, under about 1200 bytes, and never retransmitted. Each
has the datagram track's own sequence number and a timestamp, and no group. It
suits real-time audio and sensor data. There is no stream fallback, so a
datagram that doesn't fit isn't delivered.

Datagram tracks have a namespace apart from stream tracks, so `audio` as
datagrams and `audio` as streams are two independent tracks. Nothing on the
wire says which kind a name is: the application knows from its catalog or by
convention, so a broadcast's track listing never includes a datagram track. In
Rust, `broadcast.create_datagrams(name, info)` publishes one and
`broadcast.datagrams(name)` reads it.

A datagram subscription is live from when it starts: it has no ranges, order,
or max delay, only a priority, which a subscriber can change. A datagram still
waits behind higher-priority traffic instead of jumping the queue. Nothing
caches a datagram, and `FETCH` never returns one, so use a stream track for
anything a late joiner needs.

Datagram tracks need a transport with datagrams, so not WebSocket or TCP. The
draft's [Datagrams](/draft/moq-lite#datagrams) section specifies them, and each
library's API docs say which versions carry them.

## What moq-lite leaves out

Expand Down
Loading
Loading