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
3 changes: 2 additions & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ jobs:
# viewer: diff-independent, since a break can arrive through the relay,
# js/net, or the wire between them.
# `rs quic-sim` sweeps moq-quic's simulated workloads from a fresh seed; PRs run a fixed 64.
# `test resume-model` sweeps resume against its model from a fresh seed; PRs run a fixed 64.
# It also runs the bandwidth probe's acceptance suite, of which PRs run the short cases.
# `test drill impaired` runs the relay drills over a freshly seeded shaper; PRs run loopback.
# `rs cli-export` runs moq-cli's export process tests, which take tens of seconds of real time.
Expand All @@ -364,7 +365,7 @@ jobs:
# `rs multicast` runs the mDNS tests, which loop multicast back on the runner.
# `rs render-sw` renders through wgpu on Mesa's software Vulkan driver.
# `test qmux` runs js/qmux against the in-tree Rust qmux, per draft.
recipe: ["gate", "rs doctest --workspace", "rs loom", "test drill-sensitivity", "rs uring", "test drain", "test json-properties", "rs quic-sim", "test drill impaired", "rs cli-export", "rs quic-stress", "rs audio-devices", "rs multicast", "rs render-sw", "test qmux"]
recipe: ["gate", "rs doctest --workspace", "rs loom", "test drill-sensitivity", "rs uring", "test drain", "test json-properties", "test resume-model", "rs quic-sim", "test drill impaired", "rs cli-export", "rs quic-stress", "rs audio-devices", "rs multicast", "rs render-sw", "test qmux"]
include:
- recipe: rs send-depth
nightly: true
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -559,8 +559,9 @@ jobs:
path: runner-upstream
persist-credentials: false

# Any failed pair or setup step fails the job. A complete run still writes
# its report first, since a failing run is the one worth looking at.
# Only moq-dev clients against this checkout's relay gate the job, plus any
# setup step; the other pairs are reported only. A complete run still
# writes its report first, since a failing run is the one worth looking at.
- name: Run upstream Docker matrices
run: just runner "$GITHUB_WORKSPACE/runner-upstream" "$GITHUB_WORKSPACE/interop-runner-report"

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ just fix # Auto-fix lint/formatting, same scope
These diff the branch against its base and only run the affected packages.

Quests: before starting work, run `quest guide` and follow it.
The `quest` binary comes from the kixelated/quest flake input and serves the quest skills; change them upstream and bump the input.
The `quest` binary comes from the moq-dev/quest flake input and serves the quest skills; change them upstream and bump the input.

# Cross-Package Sync

Expand Down
3 changes: 2 additions & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ process CPU (user + system, every thread) per MB into
budget (default 1); every received frame is checked byte for byte.

The binding benchmark itself lives in
[rust2](https://github.com/kixelated/rust2/blob/main/bench/ffi/README.md), with
[rust2](https://github.com/moq-dev/rust2/blob/main/bench/ffi/README.md), with
the UniFFI fork whose changes it measures: its `just bench-ffi` runs these
runners nightly at a pinned moq revision, against the fork, and reports each
wrapper's ratio to Rust. So that they keep working here, moq's Nightly runs every
Expand Down
1 change: 1 addition & 0 deletions bun.lock

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

5 changes: 5 additions & 0 deletions c/moq/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ How to update your code for each breaking change, newest first.

The hand-written crate never published as moq-c: a libmoq 0.6 user applies these entries before moving to the generated API.

### A subscription without `max_delay_present` has no limit

A `moq_subscription` with `max_delay_present` false sets no subscriber limit, leaving the publisher's max age alone to bound it, instead of moq-net's default of zero.
Start from `moq_subscription_defaults()`, or set `max_delay_present`, where you relied on a zeroed struct; a NULL pointer still means the defaults.

### Paths with an empty segment are refused ([#6648](https://github.com/moq-dev/moq/pull/6648))

`moq_origin_create_broadcast`, `moq_origin_requests`, `moq_origin_announced_broadcast`, and `moq_origin_request` return an error for a path with a leading, trailing, or doubled `/` instead of collapsing it. Pass the canonical path, such as `room/cam`. The generated API refuses the same paths.
Expand Down
7 changes: 7 additions & 0 deletions c/moq/test/probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ int main(void) {
moq_dispatcher dispatcher = {loop_dispatch, loop_shutdown, NULL};
moq_set_dispatcher(&dispatcher);

/* Only the defaults carry moq-net's zero max delay; a bare record sets no subscriber limit. */
moq_subscription *defaults = moq_subscription_defaults();
CHECK(defaults->max_delay.has_value && defaults->max_delay.value == 0);
moq_subscription_free(defaults);
moq_subscription bare = moq_subscription_default();
CHECK(!bare.max_delay.has_value);

/* A synchronous error is a returned value with Rust's Display message. */
moq_server_config unbound_config = moq_server_config_default();
moq_server *unbound = NULL;
Expand Down
7 changes: 7 additions & 0 deletions cpp/moq/test/probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ Coroutine read_one(std::shared_ptr<moq::TrackConsumer> consumer, std::optional<m
}
#endif

// Only the defaults carry moq-net's zero max delay; a bare record sets no subscriber limit.
void subscription_defaults() {
CHECK(moq::subscription_defaults().max_delay == std::chrono::nanoseconds::zero());
CHECK(!moq::Subscription{}.max_delay.has_value());
}

// Exercise the installed audio publisher and consumer rather than only generated signatures.
void audio_roundtrip() {
auto broadcast = ok(moq::BroadcastProducer::init(), "audio broadcast");
Expand Down Expand Up @@ -172,6 +178,7 @@ void audio_roundtrip() {
} // namespace

int main() {
subscription_defaults();
audio_roundtrip();
auto timebase_owner = ok(moq::BroadcastProducer::init(), "timebase owner");
auto timebase_other = ok(moq::BroadcastProducer::init(), "timebase other");
Expand Down
2 changes: 1 addition & 1 deletion dart/moq/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Their handles `OriginDynamic`, `BroadcastDynamic`, and `TrackDynamic` are `Origi

The records carry durations as `Duration` fields instead of `*Us` microsecond integers, so the duration extension getters are gone: read `backoff.initial`, `stats.rtt`, `grant.expires`, `info.maxAge`, `fill.maxDelay()`, and `consumer.delay()` directly.
`audio.EncoderOutput.frameDuration` is nullable: pass null where you passed `frameDurationUs: 0` for the codec's own frame.
`Subscription.maxDelay` has no default, so a `MoqSubscription()` that omits it gets no staleness limit instead of moq-net's 0: pass `Duration.zero` for the old default.
`Subscription.maxDelay` defaults to null, so a `Subscription()` that omits it gets no staleness limit instead of moq-net's 0: start from `defaultSubscription()`, or pass `maxDelay: Duration.zero`, for the old default.
A negative `Duration` throws `ArgumentError` wherever it crosses into the native library.

### Connection state
Expand Down
6 changes: 6 additions & 0 deletions dart/moq/lib/src/aliases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ typedef Frame = MoqFrame;
/// Tunes how a track subscription is delivered: priority, latency budget, ranges, and group ordering.
typedef Subscription = MoqSubscription;

/// moq-net's defaults, whose `maxDelay` of zero skips a superseded group at once.
///
/// A null subscription means the same. A `Subscription()` leaves `maxDelay` null, which
/// sets no subscriber limit, so pass `maxDelay: Duration.zero` when building one to match.
Subscription defaultSubscription() => moqSubscriptionDefaults();

/// A span of a track to deliver: from `start` up to `end`, or onward when `end` is null.
typedef Range = MoqRange;

Expand Down
5 changes: 5 additions & 0 deletions dart/moq/test/moq_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ void main() {
broadcast.close();
});

test('a default subscription skips superseded groups at once', () {
expect(defaultSubscription().maxDelay, Duration.zero);
expect(Subscription().maxDelay, isNull);
});

test('a negative duration in a raw record is refused', () {
final broadcast = BroadcastProducer();
final track = broadcast.publishTrack(name: 'negative', info: null);
Expand Down
21 changes: 20 additions & 1 deletion dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ class MoqSubscription {
final MoqOrder? order;
MoqSubscription({
this.priority = 0,
this.maxDelay,
this.maxDelay = null,
this.ranges = null,
this.order = null,
});
Expand Down Expand Up @@ -16909,6 +16909,14 @@ class FfiConverterUint8List {
}

const _uniffiAssetId = "package:moq_ffi/uniffi:moq_ffi";
MoqSubscription moqSubscriptionDefaults() {
return rustCallWithLifter(
(status) => uniffi_moq_ffi_fn_func_moq_subscription_defaults(status),
FfiConverterMoqSubscription.lift,
null,
);
}

void moqLogLevel({required String level}) {
return rustCall((status) {
uniffi_moq_ffi_fn_func_moq_log_level(
Expand Down Expand Up @@ -19525,6 +19533,11 @@ external RustBuffer uniffi_moq_ffi_fn_method_moqerror_uniffi_trait_display(
Pointer<RustCallStatus> uniffiStatus,
);

@Native<RustBuffer Function(Pointer<RustCallStatus>)>(assetId: _uniffiAssetId)
external RustBuffer uniffi_moq_ffi_fn_func_moq_subscription_defaults(
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Void Function(RustBuffer, Pointer<RustCallStatus>)>(
assetId: _uniffiAssetId,
)
Expand Down Expand Up @@ -19877,6 +19890,9 @@ external void ffi_moq_ffi_rust_future_complete_void(
Pointer<RustCallStatus> uniffiStatus,
);

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_func_moq_subscription_defaults();

@Native<Uint16 Function()>(assetId: _uniffiAssetId)
external int uniffi_moq_ffi_checksum_func_moq_log_level();

Expand Down Expand Up @@ -20569,6 +20585,9 @@ void _checkApiVersion() {
}

void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_func_moq_subscription_defaults() != 37074) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_func_moq_log_level() != 24625) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
Expand Down
2 changes: 2 additions & 0 deletions doc/lib/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ inside. The header is the reference: each function carries a doc comment.
- **Tokens.** Set `moq_client_config.tokens` instead of putting the credential in the URL. The first is the connection credential, sent in SETUP and, while an offered version can't carry it there (lite before 07), as the URL's `?jwt=`; each other token widens the grant over AUTH. A `?jwt=` already in the URL stays the connection credential, which is how to reach a relay older than this release. To replace expiring tokens, close the session and connect again.
- **Threading.** Call any function from any thread. Callbacks run on moq-c's runtime thread; the callback and its `user_data` must be safe to use there. Synchronize access to data shared with other threads, and dispatch UI work to your UI thread. Raw publish calls block until the codec takes the frame, which paces a publisher.
- **Zeroed config is the default.** A zeroed `moq_client_config` means the default for every field, so new fields don't break callers. A field whose default isn't zero has a `has_*` flag: `backoff_timeout_us = 0` means "retry forever" only with `has_backoff_timeout = true`.
- **Subscription defaults.** `moq_subscription_defaults()` returns moq-net's defaults, whose zero `max_delay_us` skips a superseded group at once; a NULL `moq_subscription` pointer means the same. Change its fields rather than zeroing one: a zeroed `moq_subscription` leaves `max_delay_present` false, which sets no subscriber limit.
- **Compressed tracks.** `moq_track_info.compress` on a raw track sends each group DEFLATE-compressed to opt-in `moq-lite-07-wip` peers; other peers, and every reader here, get the original frames.
- **fMP4 import.** `MOQ_CONTAINER_FORMAT_FMP4` splits each fragment into one frame per sample.
- **Audio needs cuts.** Video groups at its keyframes, but audio forms a group only where you call `moq_publish_media_cut`: after every frame, or at a segment cadence to align with video. Pass that cadence as `moq_audio_init.hint.group_duration_us`.
- **Catalog hints.** Give the catalog what the encoder knows instead of having it estimated: `moq_video_init.hint` (coded size, bitrate, frame rate, group duration) and `moq_audio_init.hint` (bitrate, group duration). A supplied bitrate is never re-estimated. `group_duration_us` is the longest span between two group starts: the GOP, or your cut cadence. Leave a field's `has_*` flag false to have it measured. A bitrate or `group_duration_us` flag set with a 0 value returns `MOQ_ERROR_INVALID_CONFIG`.
- **Ending cleanly.** When the input ends, finish every track, then call `moq_publish_catalog_finish()` to finish the catalog, which ends the content. The broadcast stays announced and keeps serving the finished tracks, so a subscriber still catching up can drain them, until `moq_publish_close()` takes it offline. How long to keep it up is yours to decide. A track still open at the close ends with `UNROUTABLE`, which readers take as the publisher leaving, and its handle is released.
- **Timeline breaks.** On a seek or pause, call `moq_publish_media_discontinuity`, then keep timestamps moving forward and resume video on a keyframe.
- **JSON values.** `moq_consume_json_value` fills `moq_json_value` with the document and its frame's `timestamp_us`, rounded down to a whole microsecond; `timestamp_present` is false on an untimed track. A snapshot consumer's callback fires for each state in order, but a group superseded before it is read is skipped.
- **JSON windows.** `moq_publish_json_window` publishes a sliding run of records: `moq_publish_json_window_push` appends at the back, `moq_publish_json_window_pop` drops from the front, and `moq_publish_json_window_cut` closes the open group at a natural boundary. The catalog has no window mode, so a reader names the track. `moq_consume_json_window_event` fills `moq_json_window_event` with a push (the record at `start`, in `json`), a pop of `[start, end)`, or a skip of `[start, end)` that this reader joined too late to receive. Set `checkpoint_records` to bound what each new group restates for a window you never pop.
- **Decoded reads.** `moq_decode_audio` and `moq_decode_video` deliver read ids in order: a frame, a gap where media was lost, or an end where the publisher stopped its encoder. Check `kind` on what `moq_decode_audio_read` or `moq_decode_video_read` writes. Free each id with `moq_decode_audio_read_free` or `moq_decode_video_read_free` promptly: a held video frame holds a decoder buffer, and the decoder stalls. A decoder reads everything the track still holds unless a live player sets `start` to `MOQ_DECODE_START_LATEST`, which joins at the newest group and skips the cached backlog.
- **Stats.** `moq_session_stats()` fills `moq_connection_stats` with `rtt_us`, `estimated_send_rate_bps`, and the byte and packet counters (`bytes_sent`, `bytes_received`, `bytes_lost`, `packets_sent`, `packets_received`, `packets_lost`). Each has a `<field>_valid` flag, false when the transport does not report it, which is not the same as zero. `moq_session_snapshot()` adds the negotiated `protocol`. A failed dial or dropped connection reaches the session's `on_state` with its `failure` code, and `moq_error()` gives the reason during that callback. Hold the handle returned by `moq_session_recv_bandwidth()` to request receive estimates. `moq_bandwidth_consumer_peek()` reads the estimate and `moq_bandwidth_consumer_changed()` waits for a change. `moq_bandwidth_consumer_close()` releases demand; the last consumer stops PROBE. Stats snapshots do not request it.
- **Demand.** A watcher from `moq_publish_track_demand()` (or the media, video, and audio variants) calls back `MOQ_DEMAND_USED` or `MOQ_DEMAND_UNUSED`, again whenever the wanted group moves. `moq_publish_demand_wanted()` reads that group: start a new one once it passes your largest. Set `refresh` in `moq_track_info` to have a joining subscriber ask for a new group once the newest has grown past `refresh` times its first frame.
Expand Down
4 changes: 4 additions & 0 deletions doc/lib/cpp/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ Hold the owned `BandwidthConsumer` returned by `session->recv_bandwidth()` to re

`moq::TrackInfo::compress` on a raw track sends each group DEFLATE-compressed to opt-in `moq-lite-07-wip` peers; other peers, and every reader here, get the original frames.

## Subscription defaults

`moq::subscription_defaults()` returns moq-net's defaults, whose zero `max_delay` skips a superseded group at once; passing `std::nullopt` for a subscription means the same. Change its fields rather than starting from `moq::Subscription{}`, whose unset `max_delay` sets no subscriber limit.

## Track demand

`demand()->wanted()` is the group subscribers want, or `std::nullopt` while nobody subscribes, and `wanted_changed()` waits for it to move: start a new group once it passes your largest. Set `TrackInfo::refresh` to have a joining subscriber ask for a new group once the newest has grown past `refresh` times its first frame.
Expand Down
1 change: 1 addition & 0 deletions doc/lib/dart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ shows which module holds each.
- **Tokens.** Pass `ConnectOptions(tokens: [...])` instead of putting the credential in the URL. The first is the connection credential, sent in SETUP and, while an offered version can't carry it there (lite before 07), as the URL's `?jwt=`; each other token widens the grant over AUTH. `moq.tokens().set_(tokens: [...])` replaces them before they expire: the next dial presents them, a live AUTH session at once, and after an authorization refusal the session redials only if they changed. `await tokens().answered()` returns once the relay accepts them and throws its refusal with the relay's code. A `?jwt=` already in the URL stays the connection credential, which is how to reach a relay older than this release.
- **Errors.** Read diagnostic payloads from `reason` and structured protocol failures from `details`.
- **Disabled renditions.** A rendition with `enabled: false` has no frames coming until its publisher re-enables it. Select an enabled rendition from the latest catalog before opening a decoder. Keep one already playing while every rendition of its kind is disabled: the convenience decoders accept a disabled configuration, and a re-enable, such as an unmute, then plays without a resubscribe. Among the video renditions you can decode, prefer the highest `preference`; `0` is the default and `-1` marks a compatibility transcode.
- **Subscription defaults.** `defaultSubscription()` returns moq-net's defaults, whose zero `maxDelay` skips a superseded group at once; passing no subscription means the same. A `Subscription()` leaves `maxDelay` null, which sets no subscriber limit, so pass `maxDelay: Duration.zero` when building one to match.
- **Track demand.** Obtain a dynamic track request’s `demand()` before accepting it to watch subscribers through the handoff to its producer. The watch does not keep the track alive and ends when the track closes or the request is rejected.

## Runtime
Expand Down
Loading
Loading