Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4c55710
feat(ffi): name the track an encoded audio or video publish writes (#…
kixelated Sep 25, 2026
1ca9283
fix(moq-gst): wait for the sink's reconnect loop to end on stop (#4074)
kixelated Sep 25, 2026
e0c3ef2
docs(quest): plan follow-ups from the next-16 quest run (#4105)
kixelated Sep 25, 2026
d78bf4e
fix(js): stop retaining a listener, reaction, or task per frame (#4085)
kixelated Sep 25, 2026
5dbfe60
feat(publish): advertise the page clock at the catalog root (#4082)
kixelated Sep 25, 2026
a342004
fix(net): end an IETF subscription from its PUBLISH_DONE (#4083)
kixelated Sep 25, 2026
aa2a47a
docs(quest): plan the VAAPI and PipeWire camera follow-ups (#4070)
kixelated Sep 25, 2026
a9fb0f5
feat(libmoq): advertise JSON tracks in the catalog, add binary data t…
bgreenway Sep 25, 2026
c15c3bf
quest(auth): claim moq-transport
kixelated Sep 25, 2026
0d167f5
test(moq-tokio): dial the WebSocket fallback on its own ephemeral por…
kixelated Sep 25, 2026
75d615d
docs(quest): plan an on-demand binary delta stats flavor (#4016)
kixelated Sep 25, 2026
d6f15f4
wip(net): moq-transport AUTH extension (Rust)
kixelated Sep 25, 2026
e385044
wip(net): moq-transport AUTH extension (JS)
kixelated Sep 25, 2026
7bb7546
quest(auth): complete moq-transport
kixelated Sep 25, 2026
35eb002
fix(net): reject trailing bytes after AUTH
kixelated Sep 25, 2026
f822924
test(signals): assert retention on bookkeeping, not a GC heap count (…
kixelated Sep 25, 2026
fd31194
test(moq-tokio): bind reconnect and worker tests to their own ports (…
kixelated Sep 25, 2026
9845b6b
fix(net): recheck grants after setup and size AUTH messages before th…
kixelated Sep 25, 2026
3333a5f
Merge remote-tracking branch 'origin/main' into land/auth-line
kixelated Sep 25, 2026
cf801e2
Merge quest/m1/auth/README (with main) into quest/m1/auth/moq-transport
kixelated Sep 25, 2026
43fc828
docs(net): document MoQ Auth on moq-transport sessions
kixelated Sep 25, 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 40 additions & 4 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,13 @@ class MoqAudioInit {
final MoqAudioFormat format;
final Uint8List data;
final String? label;
MoqAudioInit({required this.format, required this.data, this.label = null});
final String? track;
MoqAudioInit({
required this.format,
required this.data,
this.label = null,
this.track = null,
});
}

class FfiConverterMoqAudioInit {
Expand All @@ -525,8 +531,13 @@ class FfiConverterMoqAudioInit {
);
final label = label_lifted.value;
new_offset += label_lifted.bytesRead;
final track_lifted = FfiConverterOptionalString.read(
Uint8List.view(buf.buffer, new_offset),
);
final track = track_lifted.value;
new_offset += track_lifted.bytesRead;
return LiftRetVal(
MoqAudioInit(format: format, data: data, label: label),
MoqAudioInit(format: format, data: data, label: label, track: track),
new_offset - buf.offsetInBytes,
);
}
Expand All @@ -536,6 +547,7 @@ class FfiConverterMoqAudioInit {
FfiConverterMoqAudioFormat.allocationSize(value.format) +
FfiConverterUint8List.allocationSize(value.data) +
FfiConverterOptionalString.allocationSize(value.label) +
FfiConverterOptionalString.allocationSize(value.track) +
0;
final buf = Uint8List(total_length);
write(value, buf);
Expand All @@ -556,13 +568,18 @@ class FfiConverterMoqAudioInit {
value.label,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterOptionalString.write(
value.track,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset - buf.offsetInBytes;
}

static int allocationSize(MoqAudioInit value) {
return FfiConverterMoqAudioFormat.allocationSize(value.format) +
FfiConverterUint8List.allocationSize(value.data) +
FfiConverterOptionalString.allocationSize(value.label) +
FfiConverterOptionalString.allocationSize(value.track) +
0;
}
}
Expand Down Expand Up @@ -1308,11 +1325,13 @@ class MoqVideoInit {
final Uint8List data;
final String? label;
final MoqVideoHint? hint;
final String? track;
MoqVideoInit({
required this.format,
required this.data,
this.label = null,
this.hint = null,
this.track = null,
});
}

Expand Down Expand Up @@ -1343,8 +1362,19 @@ class FfiConverterMoqVideoInit {
);
final hint = hint_lifted.value;
new_offset += hint_lifted.bytesRead;
final track_lifted = FfiConverterOptionalString.read(
Uint8List.view(buf.buffer, new_offset),
);
final track = track_lifted.value;
new_offset += track_lifted.bytesRead;
return LiftRetVal(
MoqVideoInit(format: format, data: data, label: label, hint: hint),
MoqVideoInit(
format: format,
data: data,
label: label,
hint: hint,
track: track,
),
new_offset - buf.offsetInBytes,
);
}
Expand All @@ -1355,6 +1385,7 @@ class FfiConverterMoqVideoInit {
FfiConverterUint8List.allocationSize(value.data) +
FfiConverterOptionalString.allocationSize(value.label) +
FfiConverterOptionalMoqVideoHint.allocationSize(value.hint) +
FfiConverterOptionalString.allocationSize(value.track) +
0;
final buf = Uint8List(total_length);
write(value, buf);
Expand All @@ -1379,6 +1410,10 @@ class FfiConverterMoqVideoInit {
value.hint,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterOptionalString.write(
value.track,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset - buf.offsetInBytes;
}

Expand All @@ -1387,6 +1422,7 @@ class FfiConverterMoqVideoInit {
FfiConverterUint8List.allocationSize(value.data) +
FfiConverterOptionalString.allocationSize(value.label) +
FfiConverterOptionalMoqVideoHint.allocationSize(value.hint) +
FfiConverterOptionalString.allocationSize(value.track) +
0;
}
}
Expand Down Expand Up @@ -12373,7 +12409,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio() !=
47444) {
31691) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqbroadcastproducer_publish_audio_on_track() !=
Expand Down
3 changes: 2 additions & 1 deletion doc/concept/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ In Rust the catalog owns the lifetime: `catalog.json_stream(track, config)` (or
`json_snapshot` / `binary_snapshot` / `binary_stream`) writes the entry and
retracts it when the producer drops. Read the config from `catalog.json.tracks`
or `catalog.binary.tracks`, then pair its name and config with
`moq_mux::catalog::Entry::new` to subscribe. In the browser, read the same map,
`moq_mux::catalog::Entry::new` to subscribe. In C, `moq_publish_json_*` and
`moq_publish_binary_*` do the same, retracting on `_finish`. In the browser, read the same map,
subscribe by name, and hand the track to `@moq/json` or `@moq/binary`.

## Container
Expand Down
4 changes: 2 additions & 2 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ A client that publishes a broadcast outside its grant closes the session with
`UNAUTHORIZED` and names the path in the close reason, rather than waiting
forever for a subscriber the relay will never let through. A grant is advice
for the side that holds it; the side that issued it still enforces its own
scope. Older versions and moq-transport have no grant; the token in the URL
keeps working everywhere.
scope. Older versions, and moq-transport peers that do not negotiate the MoQ Auth
extension, have no grant; the token in the URL keeps working everywhere.

## Discovery

Expand Down
5 changes: 3 additions & 2 deletions doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ tracks waiting for a group that never arrives.
Several project drafts extend the IETF wire without breaking it, since `SETUP`
ignores unknown parameters: [cluster](/draft/moq-cluster) routing hop lists,
[solicit](/draft/moq-solicit) to make announcements opt-in,
[hidden](/draft/moq-hidden) to keep `.`-named namespaces out of discovery, and
[probe](/draft/moq-probe) for bandwidth estimation.
[hidden](/draft/moq-hidden) to keep `.`-named namespaces out of discovery,
[auth](/draft/moq-auth) to tell each peer what it may publish and subscribe to,
and [probe](/draft/moq-probe) for bandwidth estimation.
[moq-e2ee](/draft/moq-e2ee) is not a transport extension: it encrypts application
payloads so relays still forward named tracks they cannot read.

Expand Down
2 changes: 1 addition & 1 deletion doc/lib/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ and `target/include/moq.h`.
- **Server.** `moq_server_listen` binds before it returns (a bad address or certificate fails there) and hands each incoming session to `on_request` as a request handle. Read `moq_session_request_path` and `_query` to route and authenticate, then `moq_session_request_accept` (a session handle, with origins like `moq_session_connect`) or `moq_session_request_reject` with an HTTP-style code (401 and 403 become the protocol's unauthorized close). An accepted session reports `1` once SETUP completes and never reconnects. `moq_server_addr` reports an ephemeral port and `moq_server_fingerprints` the hashes a client pins for a `tls_generate` certificate. `moq_server_close` stops listening; its terminal callback fires once the sockets are released.
- **Demand.** A watcher on a published track (`moq_publish_track_demand`, `moq_publish_media_demand`, `moq_encode_video_demand`, `moq_encode_audio_demand`) calls `on_demand` with `MOQ_DEMAND_USED` or `MOQ_DEMAND_UNUSED` right away and again on every change, so an encoder on a battery-powered device runs only while someone is watching. The first call is the current state, so a track that went unused before the watcher existed still reports it. `moq_publish_demand_cancel` stops it; the terminal callback still fires. A container has no single demand and is refused. Demand follows the last real subscriber: an origin that served the track drops its source copy on the unused edge and keeps only the finished groups it already cached warm for 30 seconds, so the cache linger does not delay the unused edge.
- **Requests.** `moq_publish_dynamic` serves subscriptions to tracks the broadcast never declared: each arrives as a request handle, read its name with `moq_track_request_name`, then `moq_track_request_accept` (a raw track handle), `moq_track_request_video` / `_audio` (the media handle `moq_publish_video` / `_audio` return), or `moq_track_request_abort` with an application code the subscriber sees. Without a live handler an unknown name is refused. `moq_publish_track_dynamic` does the same for fetches of groups a track no longer has cached, delivered as `moq_group_request_*` (`sequence`, `priority`, `frame_start`); `moq_group_request_accept` starts the producer at `frame_start` so written frames keep their group indices. Register it with `moq_track_request_dynamic` before accepting a track that was itself requested by a fetch, so that pending group survives the transition. Both handlers stop with `moq_publish_dynamic_cancel`.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON snapshot and stream tracks, group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a literal prefix and an optional relative pattern filter; `moq_announce_update.prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); name the dot segment in `prefix` to list them.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a literal prefix and an optional relative pattern filter; `moq_announce_update.prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); name the dot segment in `prefix` to list them.

```c
moq_client_config config;
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/js/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ for (;;) {
- **Discovery** by any pattern scope (`origin.announced(scope)`, such as `room/*/chat`; default everything). Each event's `prefix` is the covered prefix relative to the origin, `captures` reports what the scope's wildcards matched when the prefix pins them, and `kind` says whether it was announced, updated, or retracted. The consumer is an async iterable. `origin.broadcasts(scope)` is a live `Getter<ReadonlyMap<Path.Valid, Route>>` of the same covered prefixes for UIs that need the current set. A borrowed `Connection.origin` also exposes `dynamic(prefix, route)` for serving paths on demand.
- **Subscriptions** carry a priority, a `Time.Milli` max age, and optional `groups` bounds. Groups arrive out of order and are read frame by frame, with `Error.TooFarBehind` when a reader asks for a frame the group never held and `Error.GroupTooLarge` when a write exceeds the cache budget and aborts the group.
- **Datagrams** on moq-lite 05+ and fetch-by-sequence for history.
- **Authorization** on moq-lite 06: an established session's `auth.grant` is a `Getter<Auth.Grant | undefined>` with what the relay lets this side publish and subscribe to, learned right after setup. `auth.add(token)` presents another token without reconnecting and resolves with an `Auth.Token` to `close()` later; it rejects with `Auth.Unsupported` when the peer takes no tokens in band. A connection that publishes a broadcast outside its grant closes with `SessionCode.Unauthorized`, naming the path in the reason.
- **Authorization** on moq-lite 06, and on moq-transport draft-17+ when both sides negotiate the [MoQ Auth extension](/draft/moq-auth): an established session's `auth.grant` is a `Getter<Auth.Grant | undefined>` with what the relay lets this side publish and subscribe to, learned right after setup. `auth.add(token)` presents another token without reconnecting and resolves with an `Auth.Token` to `close()` later; it rejects with `Auth.Unsupported` when the peer takes no tokens in band. A connection that publishes a broadcast outside its grant closes with `SessionCode.Unauthorized`, naming the path in the reason.
- **Errors** live under one namespace: a stream reset throws `Error.Stream` with a `StreamCode`, while a session close gives `Error.Session` with a `SessionCode`. The registries are disjoint, so the same number means different things in each, and 64+ is yours. Named conditions such as `Error.TooFarBehind`, `Error.FrameTooLarge`, and `Error.GroupTooLarge` subclass `Error.Stream`, so one `code` check handles a condition raised here or reported by the peer. IETF streams use their own mapping: cancellation sends CANCELLED, other local failures send INTERNAL\_ERROR, and received codes remain opaque.
- **Paths** with `Path.relative` for the cross-broadcast catalog references hang uses. Path patterns (`Path.Pattern`, `Path.Patterns`) are re-exported from [`@moq/pattern`](https://www.npmjs.com/package/@moq/pattern). Literal `Path` stays a coordinate.

Expand Down
10 changes: 10 additions & 0 deletions doc/lib/js/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ clock when they flush frames. Catalog jitter is the spread above each
rendition's own recent minimum lateness, so a constant encoder delay is not jitter.
The advertised value only rises; frame duration alone does not set it.

## Clock

Every timestamp the publisher writes is `performance.now()` in microseconds,
so camera, microphone, screen, and file sources share one timeline. The catalog
advertises that mapping as its root `clock` from the first snapshot, with PTS
zero at `performance.timeOrigin`, so a viewer or an HLS export can name any
frame's wall time. The mapping is fixed for the page: a system-clock
adjustment never retimes the broadcast. Stamp your own tracks (e.g. text cues)
on the same timeline to stay in sync.

## Custom tracks

`broadcast.net` is the underlying `Moq.Broadcast.Producer`, so an application
Expand Down
3 changes: 2 additions & 1 deletion doc/lib/js/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ The rules that differ from other signal libraries:

- **Nothing is tracked implicitly.** `effect.get(signal)` subscribes; `signal.peek()` doesn't.
- **Writes coalesce per microtask** and only notify on a real change (deep for plain objects, identity for class instances).
- **Effects own their resources.** `effect.timer`, `interval`, `animate`, `event`, `spawn`, and `run` (a nested effect) all clean up on rerun or close, so never call `setTimeout` or `addEventListener` inside one directly. A rerun waits for the previous run's `spawn` tasks to settle, and `effect.abort`/`effect.cancel` tell them to stop.
- **Effects own their resources.** `effect.timer`, `interval`, `animate`, `event`, `spawn`, and `run` (a nested effect) all clean up on rerun or close, so never call `setTimeout` or `addEventListener` inside one directly. A rerun waits for the previous run's `spawn` tasks to settle, and `effect.abort`/`effect.race` tell them to stop.
- **Race with `race`, not `Promise.race`.** `Promise.race` leaves a listener on every value that loses, so racing a long-lived one (a `closed`, a run's teardown) once per frame grows the heap. `race([...])` accepts promises and `Once` values and drops its listeners when it settles; `effect.race(promise)` also resolves `undefined` once the run is torn down.
- **Dev builds warn** about effects that tracked nothing, effects garbage-collected without `close()`, and signals leaking subscribers.

Components follow one shape: `in` (wired inputs), `out` (read-only derived
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/js/watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const dispose = el.signals.run((effect) => {
const consumer = new Json.Snapshot.Consumer<unknown>({ track });
effect.spawn(async () => {
for (;;) {
const value = await Promise.race([effect.cancel, consumer.next()]);
const value = await effect.race(consumer.next());
if (value === undefined) break;
console.log("metadata", value);
}
Expand Down
14 changes: 8 additions & 6 deletions doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ Expiry is therefore approximate; a late `gc` extends retention.

## Authorization

`session.auth()` is the session's `auth::Handle`. On moq-lite 06 each side
presents its connection's credential right after setup, and `grant()` watches
the union of every grant this side holds: `None` until the peer answers,
forever on other versions. `add(token)` presents another token and resolves
`session.auth()` is the session's `auth::Handle`. On moq-lite 06, and on
moq-transport draft-17+ when both sides negotiate the
[MoQ Auth extension](/draft/moq-auth), each side presents its connection's
credential right after setup, and `grant()` watches the union of every grant
this side holds: `None` until the peer answers, forever otherwise. `add(token)` presents another token and resolves
once the peer answers; drop the returned `auth::Token` to withdraw it.

```rust
Expand All @@ -91,8 +92,9 @@ origin handles allow and refuses any other token as unsupported. To verify
tokens yourself, take `handshake.auth().requests()` on the `server::Handshake`
before `ok()` (or `session.auth().requests()` before first polling the driver)
and answer every `auth::Request` with `accept(grant)`, which returns an
`auth::Issued` you can `update` or `revoke`, or `reject`. The wire carries
prefix grants for now, so a grant that is not a union of subtrees is refused.
`auth::Issued` you can `update` or `revoke`, or `reject`. Both wires carry
prefix grants for now, so a grant that is not a union of subtrees is refused
and the presenter sees `Unsupported`; after a grant, such an update revokes it.

A client whose origin publishes a broadcast outside its grant closes the
session with `Unauthorized`, naming the path in the close reason. A grant that
Expand Down
Loading
Loading