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: 0 additions & 2 deletions dart/moq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ await for (final event in connection.announcements(
// Prefix stays origin-relative; captures reports wildcard matches.
print(event.announce.prefix);
print(event.announce.captures);
} else if (event is AnnounceEventLive) {
break; // every route live at subscribe time has been delivered
}
}
```
Expand Down
5 changes: 1 addition & 4 deletions dart/moq/lib/src/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ typedef AnnouncedBroadcast = MoqAnnouncedBroadcast;
typedef Announce = MoqAnnounce;

/// What an [AnnounceConsumer] yields: [AnnounceEventStart],
/// [AnnounceEventUpdate], [AnnounceEventEnd], or [AnnounceEventLive].
/// [AnnounceEventUpdate], or [AnnounceEventEnd].
typedef AnnounceEvent = MoqAnnounceEvent;

/// A route now covers the prefix; the stream had none there.
Expand All @@ -65,9 +65,6 @@ typedef AnnounceEventUpdate = UpdateMoqAnnounceEvent;
/// No route covers the prefix any more; carries its last route.
typedef AnnounceEventEnd = EndMoqAnnounceEvent;

/// Every route live at subscribe time has been delivered; what follows is live changes.
typedef AnnounceEventLive = LiveMoqAnnounceEvent;

/// The write side of a broadcast: publish tracks into it.
typedef BroadcastProducer = MoqBroadcastProducer;

Expand Down
3 changes: 0 additions & 3 deletions dart/moq/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,6 @@ final class Moq {
session.publish().createBroadcast(path: path);

/// Stream announce events matching [options]; prefixes stay relative to the origin.
///
/// A [AnnounceEventLive] follows the routes live at subscribe time, so a
/// listener can collect what is live and stop there.
Stream<AnnounceEvent> announcements({
AnnounceOptions options = const AnnounceOptions(),
}) async* {
Expand Down
32 changes: 4 additions & 28 deletions dart/moq/test/moq_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import 'package:test/test.dart';

const timeout = Duration(seconds: 10);

/// The next announce event that is not Live, which lands wherever the backlog ends.
/// The next announce event.
Future<AnnounceEvent> nextRoute(AnnounceConsumer announced) async {
while (true) {
final event = await announced.next().timeout(timeout);
if (event == null) throw StateError('announce stream ended');
if (event is! AnnounceEventLive) return event;
}
final event = await announced.next().timeout(timeout);
if (event == null) throw StateError('announce stream ended');
return event;
}

void main() {
Expand Down Expand Up @@ -214,28 +212,6 @@ void main() {
expect(update.announce.captures, ['alice']);
});

test('announced yields Live once caught up', () async {
final origin = MoqOriginProducer(config: MoqOriginConfig());
final consumer = origin.consume();

final empty = consumer.announced(config: MoqAnnounceConfig());
expect(await empty.next().timeout(timeout), isA<AnnounceEventLive>());
empty.cancel();
empty.dispose();

final broadcast = origin.createBroadcast(path: 'cam');
broadcast.announce(route: MoqRoute());
await consumer.announcedBroadcast(path: 'cam').available().timeout(timeout);

final announced = consumer.announced(config: MoqAnnounceConfig());
final first = await announced.next().timeout(timeout);
expect((first as AnnounceEventStart).announce.prefix, 'cam');
expect(await announced.next().timeout(timeout), isA<AnnounceEventLive>());
announced.cancel();
announced.dispose();
broadcast.close();
});

test('dynamic serves a request under a prefix', () async {
final origin = MoqOriginProducer(config: MoqOriginConfig());
final dynamic = origin.dynamic_(prefix: 'live', route: MoqRoute());
Expand Down
34 changes: 0 additions & 34 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3806,12 +3806,6 @@ class FfiConverterMoqAnnounceEvent {
lifted.value,
lifted.bytesRead - subview.offsetInBytes + 4,
);
case 4:
final lifted = LiveMoqAnnounceEvent.read(subview);
return LiftRetVal<MoqAnnounceEvent>(
lifted.value,
lifted.bytesRead - subview.offsetInBytes + 4,
);
default:
throw UniffiInternalError(
UniffiInternalError.unexpectedEnumCase,
Expand Down Expand Up @@ -3947,34 +3941,6 @@ class EndMoqAnnounceEvent extends MoqAnnounceEvent {
}
}

class LiveMoqAnnounceEvent extends MoqAnnounceEvent {
LiveMoqAnnounceEvent();
LiveMoqAnnounceEvent._();
static LiftRetVal<LiveMoqAnnounceEvent> read(Uint8List buf) {
int new_offset = buf.offsetInBytes;
return LiftRetVal(LiveMoqAnnounceEvent._(), new_offset);
}

@override
RustBuffer lower() {
final buf = Uint8List(allocationSize());
write(buf);
return toRustBuffer(buf);
}

@override
int allocationSize() {
return 4;
}

@override
int write(Uint8List buf) {
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, 4);
int new_offset = buf.offsetInBytes + 4;
return new_offset;
}
}

enum MoqTransport { quic, iroh, webSocket, tcp, unix, webTransport }

class FfiConverterMoqTransport {
Expand Down
1 change: 0 additions & 1 deletion demo/web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ discovery.run((effect) => {
for (;;) {
const entry = await effect.race(announced.next());
if (!entry) break;
if (entry.kind === "live") continue;
const path = entry.prefix;
// Only catalog-backed broadcasts are watchable streams; this skips the relay's
// `.stats` broadcast (see the stats dashboard demo for that one).
Expand Down
1 change: 0 additions & 1 deletion demo/web/src/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ discovery.run((effect) => {
for (;;) {
const entry = await effect.race(announced.next());
if (!entry) break;
if (entry.kind === "live") continue;
const path = entry.prefix;
const node = Net.Path.stripPrefix(prefix, path);
if (!node) continue;
Expand Down
12 changes: 4 additions & 8 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,10 @@ and TypeScript `Announce.Event`, whose `kind` is `"start"`, `"update"`, or
`"end"` alongside the same `Announce.Announce` fields. An update is a
reprice in place. Captures are present when the announced prefix pins every
wildcard in the most-specific matching scope member. The Rust consumer is a
`Stream` and the TypeScript one an async iterable. Both, and every binding over
moq-ffi or moq-c, also yield one `Live` marker (TypeScript `{ kind: "live" }`)
once the routes live at subscribe time have all been delivered, including those
a peer session was still sending: moq-lite-05+ counts them in `ANNOUNCE_OK`,
moq-lite-01/02 send them in `ANNOUNCE_INIT`, Rust IETF draft-16+ sessions count
them in `REQUEST_OK` when both sides speak
[active-count](/draft/moq-active-count), and anything else waits for the
stream to go quiet.
`Stream` and the TypeScript one an async iterable. Neither marks when the
routes live at subscribe time have all arrived: an origin merges many sessions
that connect and reconnect independently, so drive loading and offline UI from
the connection status instead.

Announcements are hints; requests are the authority. When a subscriber asks
for a covered path the advertiser will not serve, the advertiser refuses that
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 @@ -52,7 +52,7 @@ pkg-config file included.
- **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, so a cache linger never delays it. Only a relay keeps what it already delivered warm for 30 seconds, and a returning subscriber is served that cache only once the publisher confirms it is still current.
- **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 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), `moq_publish_close` (ends the broadcast for good and releases its handle), 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 `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, `_END`, or `_LIVE` once every route live when the listener started has been delivered, with no prefix or captures); `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); set `hidden` or 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), `moq_publish_close` (ends the broadcast for good and releases its handle), 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 `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, or `_END`); `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); set `hidden` or name the dot segment in `prefix` to list them.

```c
moq_client_config config;
Expand Down
5 changes: 1 addition & 4 deletions doc/lib/dart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ moq.announcements(
if (event is AnnounceEventStart) {
print(event.announce.prefix);
print(event.announce.captures);
} else if (event is AnnounceEventLive) {
print('caught up; what follows is live');
}
});
final broadcast = await moq.requestBroadcast('live/camera');
Expand Down Expand Up @@ -84,8 +82,7 @@ route is a capability, not an inventory. `announcements(options:)` takes a
literal prefix plus an optional relative pattern and yields `AnnounceEvent`s:
`AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd`
carrying an `Announce`, whose `prefix` stays origin-relative and whose
`captures` reports the wildcard matches, or `AnnounceEventLive` once every route
live at subscribe time has been delivered. Paths with
`captures` reports the wildcard matches. Paths with
a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless `hidden: true`.

Sessions reconnect with backoff when the transport drops and re-announce local
Expand Down
6 changes: 2 additions & 4 deletions doc/lib/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ for event, err := range announced.All(ctx) {
}
ann, ok := event.(moq.AnnounceEventStart)
if !ok {
continue // AnnounceEventUpdate, AnnounceEventEnd, or AnnounceEventLive
continue // AnnounceEventUpdate or AnnounceEventEnd
}
// Prefix stays origin-relative; Captures reports what each wildcard matched.
fmt.Printf("captures: %v\n", ann.Announce.Captures)
Expand Down Expand Up @@ -92,9 +92,7 @@ serve. A route is a capability, not an inventory. `Announced(options)` combines
a literal prefix with an optional relative pattern and yields an `AnnounceEvent`:
`AnnounceEventStart`, `AnnounceEventUpdate`, or `AnnounceEventEnd`
carrying an `Announce`, whose `Prefix` stays relative to the origin and whose
`Captures` reports the wildcard matches, or `AnnounceEventLive` once every route
live at subscribe time has been delivered. Break on `AnnounceEventLive` to list
what is live and stop.
`Captures` reports the wildcard matches.
Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts) unless
`Hidden: true`.

Expand Down
8 changes: 3 additions & 5 deletions doc/lib/js/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ for (;;) {
- **Origins** hold the broadcasts, not the connection: closing a session unannounces them but leaves them created for the next one. `origin.request(path)` resolves an announced local broadcast with no round trip, so a page that watches what it publishes reads its own copy, unless a cheaper route announces the same path. A refusal from the most specific route ends the request: `request.closed` settles with the handler's error and no broader route is asked. Create, populate, then `announce()` for an exact path; use `dynamic(prefix, route)` when the set of paths is not known: an exact-path subscribe before the tracks exist is refused, and nobody, local or remote, can see or reach a broadcast until it announces.
- **Connections** race WebTransport against WebSocket. `new Connection({ url })` pools one connection per relay URL and reconnects with backoff, which the elements use. Supplying WebTransport/WebSocket options, discovery, delay, `goaway`, or a caller-owned origin selects a private loop; explicit `share: true` refuses those options. On a relay's GOAWAY the connection dials the replacement at once while the old session keeps serving its groups until it closes or the `goaway.handover` cap (default 10s, lowered to the relay's deadline) passes; the handle and its origin stay the same. An empty GOAWAY redials the same URL through a fresh DNS resolve. A redirect is followed onto the same host by default (`goaway.redirect: "follow"` lets the relay pick the host, `"ignore"` never moves), and moves the pooled entry to the new URL; a malformed or refused one, including a host change under a pinned certificate, ends the connection with `Error.RefusedRedirect` instead of reconnecting. `closed` settles when the handle is released (`null` on a clean close); the failure that stopped retrying the current URL is `error`, and a new URL recovers the same handle. A connection owns one send-rate sampler and one `Bandwidth.Allocator`; publishers reserve against it so their encoder targets sum to the estimate instead of each matching it.
- **Bandwidth** (`Bandwidth.Allocator`) divides the connection's send-rate estimate by track priority, max-min fair within a tier. An idle track claims nothing. The receive side is untouched.
- **Discovery** by any pattern scope (`origin.announced(scope)`, such as `room/*/chat`; default everything). A route event's `kind` says whether the route started, updated, or ended; its `prefix` is the covered prefix relative to the origin, and `captures` reports what the scope's wildcards matched when the prefix pins them. One `{ kind: "live" }` marker, with no prefix, follows the routes live at subscribe time. 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.
- **Discovery** by any pattern scope (`origin.announced(scope)`, such as `room/*/chat`; default everything). A route event's `kind` says whether the route started, updated, or ended; its `prefix` is the covered prefix relative to the origin, and `captures` reports what the scope's wildcards matched when the prefix pins them. 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.
- **Publisher retention** is optional `Track.Info.maxAge`, measured against media timestamps: omission sets no limit, while zero serves only the live edge. It clamps every subscriber's max age, and stalled media does not age out merely because wall time passes. Separately, cached groups nobody writes for 30 seconds are reclaimed, except a live track's newest group, which stays replayable until the track closes.
- **Subscriptions** carry a priority, a `Time.Milli` max age measured against media timestamps, 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.
- **Track ends**: `close()` ends a track at its live edge, while `finishAt(n)` declares the exclusive end ahead of it and still accepts the groups below. A subscriber reads the end with `final()` or awaits `finished()`. Group and datagram readers end once the live edge reaches the declared boundary, even while missing lower groups wait out the grace. The `closed` signal settles separately once every group below the end has arrived or was dropped; one reset before its header arrived is skipped after the subscription's max age on moq-lite (one second without one), or after one second on IETF.
Expand Down Expand Up @@ -109,10 +109,8 @@ A route is a capability, not an inventory. `origin.announced(scope)` yields
place), or `"end"`, each also carrying an `Announce.Announce`: `prefix`
is the covered prefix relative to the origin, `captures` is one pattern per
scope wildcard when the prefix pins a complete match (otherwise `undefined`),
and `route` carries hops and cost (on a retraction, its last values). A single
`{ kind: "live" }` follows the routes live at subscribe time, including every
route a connected peer was still sending, so a caller listing what is live
stops there. The consumer is an async iterable. A prefix is
and `route` carries hops and cost (on a retraction, its last values). The
consumer is an async iterable. A prefix is
not a broadcast name; the scope filters locally while sessions request its
literal head on the wire. Paths with a `.`-prefixed segment below that head
are [hidden](/concept/moq-lite#hidden-broadcasts) unless `announced(scope, { hidden: true })` opts in;
Expand Down
Loading
Loading