Skip to content

feat(moq-net)!: add a Latency type and standardize on latency_max - #2688

Merged
kixelated merged 7 commits into
devfrom
claude/latency-api-naming-f87982
Aug 6, 2026
Merged

feat(moq-net)!: add a Latency type and standardize on latency_max#2688
kixelated merged 7 commits into
devfrom
claude/latency-api-naming-f87982

Conversation

@kixelated

@kixelated kixelated commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • with_latency_max was awkward English, and the _max suffix existed only to leave room for a future latency_min floor. A type carries that better than a suffix: adding min later becomes an additive field rather than a second setter callers must keep consistent with the first.
  • Adds moq_net::Latency, built via Latency::max(d) or Latency::REAL_TIME (the default, which skips aggressively). moq-mux re-exports it, so call sites there still read moq_mux::Latency.
  • No impl From<Duration>: a bare scalar collapses to a fixed jitter buffer in the JS Latency union (floor included), while here it is a ceiling only, so the conversion would silently mean two different things across the two languages. The explicit constructor keeps that unrepresentable.
  • container::Producer::with_latency becomes with_buffer. It is the one knob that genuinely adds delay (publisher-side frame packing) and sat confusingly next to the consumer ceiling under the same name.
  • Standardizes the spelling on latency_max. It was already the 227-to-45 majority in Rust and matches the existing latency_default / latency_bound / --latency-max / latency-max= names; there is no min_latency or default_latency anywhere, so max_latency was an outlier rather than a competing convention.

Why moq-net and not moq-mux

The concept is densest in moq-net, which already owns all three pieces: Subscription carries this budget on the wire as Subscriber Max Latency, the publisher's cache enforces it in evict_expired, and Subscription's own doc comment already described the local receiver-side bound as the same budget, just implemented a crate up. The reordering consumer that enforces it locally is expected to move moq-net-ward now that tracks are timestamp-aware (frame::Frame.timestamp, Info.timescale), so landing the type in moq-mux would have meant moving it twice, breaking the same six crates each time.

Drift vs retention

Two things share the latency_max spelling and only one is this type:

Concept Type
Subscription.latency Drift budget: how stale before a group is skipped Latency
moq-mux Consumer ceiling Same budget, enforced locally while reordering Latency
track::Info.latency_max Retention bound: how long the publisher keeps a group Duration

The draft separated these deliberately (it moved Publisher Max Latency to TRACK_INFO and redefined it as a retention bound, "the inverse of an HTTP Cache-Control: max-age", while Subscriber Max Latency stayed a delivery-time preference). Typing both alike would re-conflate them. Being on the wire is not the criterion; which concept it is, is. moq-srt's latency is likewise the SRT receive latency reused as a skip threshold, so it stays a Duration and wraps only at the moq-mux boundary.

Latency::min is not added: nothing implements a floor yet, and shipping a field the consumer ignores would be dead config. Worth noting for later that a floor means holding frames that have arrived, which is playout pacing. If that turns out to be a player-only concern, this type stays ceiling-only and js/watch's {min, max} union is a genuinely different type that shares a name.

Public API changes

Breaking (hence dev):

  • moq-net: new Latency (REAL_TIME, max(), merge(), pub max field, #[non_exhaustive]). track::Subscription::latency_max: Duration -> latency: Latency; Subscription::with_latency_max -> with_latency. track::Info::latency_max and Info::with_latency_max are unchanged.
  • moq-mux: container::Consumer::{with_latency_max, set_latency_max} -> {with_latency, set_latency}, taking Latency. container::{fmp4, mkv, ts, flv}::Export::with_latency and codec::{h264, h265}::Export::with_latency change parameter type. container::Producer::with_latency -> with_buffer. Re-exports moq_net::Latency.
  • moq-audio / moq-video: decode::Config::latency_max: Option<Duration> -> latency: Latency (Latency::default() already means what None did).
  • moq-rtmp: DEFAULT_LATENCY, Config::latency, Play::with_latency, Client::with_latency change to Latency.

Internal only (not public surface): moq_net::lite is a private module (mod lite;), and js/net exports only ./src/index.ts + ./src/zod.ts with no lite re-export, so the max_latency -> latency_max / maxLatency -> latencyMax renames on the SUBSCRIBE messages break nothing downstream. Likewise moq-cli's SubscribeArgs and libmoq's moq_consume_{video,audio} parameter names (C has no named arguments).

No wire format, no CLI flag, and no FFI record field changed. --latency-max was already the flag name, and moq_subscription.latency_max_ms / moq_track_info.latency_max_ms keep their flat _ms shape (a tagged enum through uniffi and the C ABI is noise for a two-field bound).

Cross-package sync

  • No wire format change, so no drafts/ update. The rename is identifier-only; the spec's prose field names are untouched.
  • No moq-ffi record change, so py/, swift/, kt/, go/, and doc/lib/{py,swift,kt,go,c} need nothing. Verified by grep: zero stale references under doc/, py/, swift/, kt/, go/, cpp/, test/, demo/.
  • rs/moq-net <-> js/net: the message-level renames are mirrored. Subscription.latency has no JS counterpart to update, because js/net's track.Subscription exposes only priority and has never carried a drift budget; adding one is new feature work, not a mirror of this change.

Rebase note

Rebased onto origin/dev after #2630 landed. ExportSource::for_stream conflicted: #2630 switched it to request_catalog() while this branch changed its latency parameter. Both are preserved. #2630 also added two tests in the same file passing Duration::ZERO into for_video / for_video_raw / for_audio; since there is deliberately no From<Duration>, those are migrated to Latency::REAL_TIME.

Test plan

  • just fix (no changes) and just check clean, including clippy and rustdoc at -D warnings.
  • cargo clippy -p libmoq -p moq-ffi -p moq-gst --all-targets -- -D warnings clean (just check does not select those).
  • cargo nextest run over moq-net, moq-mux, moq-audio, moq-video, moq-cli, moq-rtmp, moq-srt, libmoq, moq-ffi, hang, moq-native: 1961 tests, all passing.
  • @moq/net: 354 tests passing, bun check clean.
  • Not run: just test smoke-full. No wire, FFI, or gateway behavior changed, only identifiers and types.

🤖 Generated with Claude Code

(written by Opus 5)

`with_latency_max` was awkward English, and the `_max` suffix existed to leave
room for a future `latency_min` floor. A type carries that better than a suffix:
adding `min` later becomes an additive field rather than a second setter the
caller has to keep consistent with the first.

`moq_net::Latency` is that type, built via `Latency::max(d)` or
`Latency::REAL_TIME` (the default, which skips aggressively). It lives in moq-net
because that is where the concept is densest: `Subscription` carries the same
budget on the wire as `Subscriber Max Latency`, the publisher's cache already
enforces it in `evict_expired`, and `Subscription`'s own docs already described
the local receiver-side bound as the same budget. The reordering consumer that
enforces it locally is expected to follow moq-net-ward now that tracks are
timestamp-aware, so putting the type anywhere else meant moving it twice.

Two concepts share the `latency_max` spelling today and only one of them is this
type. `Subscription` is a *drift* budget (how stale before a group is skipped)
and becomes `Latency`; `track::Info::latency_max` is a *retention* bound (how
long the publisher keeps a group, the inverse of `Cache-Control: max-age`) and
stays a `Duration`. The draft separated these deliberately, so typing both alike
would re-conflate them. Being on the wire is not the criterion; which concept it
is, is. moq-srt's `latency` is likewise the SRT receive latency reused as a skip
threshold, so it stays a `Duration` and wraps at the boundary.

Everything consumer-side in moq-mux now takes `Latency`:
`container::Consumer::{with_latency, set_latency}`, the fmp4/mkv/ts/flv/h264/h265
exporters, `moq_{audio,video}::decode::Config`, and moq-rtmp, whose `latency` is
purely the moq-level skip ceiling. moq-mux re-exports the type.

No `impl From<Duration>`: a bare scalar collapses to a fixed jitter buffer in the
JS `Latency` union (floor included), while here it is a ceiling only, so the
conversion would mean two different things across the two languages.

`container::Producer::with_latency` is renamed `with_buffer`, since it is the one
knob that genuinely *adds* delay (publisher-side frame packing) and sat
confusingly next to the consumer ceiling under the same name.

Also standardizes the spelling on `latency_max`, which was already the 227-to-45
majority in Rust and matches the existing `latency_default` / `latency_bound` /
`--latency-max` / `latency-max=` names. The holdouts were the lite SUBSCRIBE
messages (and their js/net mirrors) plus a few libmoq and moq-cli internals; both
of those modules are private, so those renames are internal. No wire format, CLI
flag, or FFI record field changes, so no draft or binding updates are needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the claude/latency-api-naming-f87982 branch from e838de7 to ea30bc3 Compare August 6, 2026 05:34
@kixelated kixelated changed the title feat(moq-mux)!: add a Latency type and standardize on latency_max feat(moq-net)!: add a Latency type and standardize on latency_max Aug 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea30bc3bb5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/lite/subscribe.ts
@kixelated
kixelated force-pushed the claude/latency-api-naming-f87982 branch from 5484b88 to de220b9 Compare August 6, 2026 19:57
@kixelated kixelated closed this Aug 6, 2026
@kixelated kixelated reopened this Aug 6, 2026
@kixelated
kixelated merged commit 98730d5 into dev Aug 6, 2026
@kixelated
kixelated deleted the claude/latency-api-naming-f87982 branch August 6, 2026 20:11
kixelated added a commit that referenced this pull request Aug 12, 2026
`moq play` sets a `latency_max` field that no longer exists: #2688
renamed it to `latency` and retyped it as `Latency`, while #2697 added
this call site against the old shape. Neither PR could see the other, and
`dev` never runs the `--all-features` build outside a PR, so it landed
red and now fails Check on every PR that targets it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant