moq-net: auto-create Origin on connect/accept, expose via Session#1536
Merged
Conversation
Make explicitly constructing an Origin optional at the Rust layer to
match the FFI redesign that follows. When neither publish nor consume
is set on the Client builder, connect() now creates a fresh Origin and
wires it as both sides; the returned ClientSession exposes the producer
and consumer so the caller can publish broadcasts and read announcements
without ever touching Origin::random() themselves.
Client::connect's return type changes from Session to ClientSession:
pub struct ClientSession {
pub session: Session,
pub publisher: Option<OriginProducer>,
pub consumer: Option<OriginConsumer>,
}
The Option fields are populated only on the no-config path so callers
that wire their own origin(s) (relay, cli, gst, boy, hang, ffi) keep
managing them as before. They just access cs.session instead of the
session directly.
Downstream call-site updates spread across moq-native (client,
reconnect, examples, tests), moq-relay (cluster, smoke tests), moq-gst,
libmoq, and moq-ffi. Each one renames its local binding from `session`
to `cs` and threads `cs.session` where it used to use `session`. None
of them currently exercise the auto-origin path (they all pre-set at
least one side), so the new fields are always None for them.
One internal moq-net test (no_alpn_falls_back_to_draft14_and_switches_
version_post_setup) depended on "no origin → trivial close with code 0
(Cancel)" to validate SETUP framing. With the auto-origin attached the
Lite01 session now closes with RequiredExtension (code 1) since Lite01
can't satisfy the new origin. Loosened the assertion to "any non-Version
error" since the test's purpose is the framing path, not the close code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace MoqClient::connect's return type with a richer MoqClientSession
that exposes the auto-created origin sides via accessor methods, mirroring
the moq-net change in the prior commit.
pub struct MoqClientSession {
session: Arc<MoqSession>,
publisher: Option<Arc<MoqOriginProducer>>,
consumer: Option<Arc<MoqOriginConsumer>>,
}
#[uniffi::export]
impl MoqClientSession {
pub fn session(&self) -> Arc<MoqSession>;
pub fn publisher(&self) -> Option<Arc<MoqOriginProducer>>;
pub fn consumer(&self) -> Option<Arc<MoqOriginConsumer>>;
}
publisher/consumer are Some only on the no-config path (caller didn't call
setPublish or setConsume). Callers that wire their own origin keep doing
so and ignore the new accessors.
Rename `MoqOriginProducer::publish` to `add_broadcast` per the redesign
plan: `cs.publisher().add_broadcast(...)` reads cleanly where
`cs.publisher().publish(...)` would stutter. The internal Python wrapper
keeps its own `publish` method name (no public Python API churn) but now
calls `add_broadcast` on the FFI underneath.
Cross-package sync per CLAUDE.md:
- py/moq-rs/moq/client.py: extract `.session()` from the new return type
so the public `client.session` property still hands back a MoqSession.
- py/moq-rs/moq/origin.py: wrapper.publish calls inner.add_broadcast.
- swift/README.md, doc/lib/{swift,kt}/{index,moq}.md: show the new
pattern (no MoqOriginProducer() construction needed in the happy path;
cs.publisher() / cs.consumer() return the auto-created sides).
New test `server_client_roundtrip_auto_origin` validates the no-config
path end-to-end. Existing 17 tests still pass after migrating each
`origin.publish(...)` and `session.shutdown()` call to `add_broadcast`
and `cs.session().shutdown()` respectively.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Session now carries the auto-created origin sides directly:
pub struct Session {
/* existing fields */
publisher: Option<OriginProducer>,
consumer: Option<OriginConsumer>,
}
impl Session {
pub fn publisher(&self) -> Option<&OriginProducer> { ... }
pub fn consumer(&self) -> Option<&OriginConsumer> { ... }
}
The ClientSession wrapper from the previous commit was client-only, but
the auto-origin pattern is symmetric: a server with no publish/consume
configured can also have one wired in. Server::accept now auto-creates
identically to Client::connect, and both surface the result via the
same Session accessors. One type instead of two; no name to bikeshed.
Downstream callers unwind: `let cs = client.connect(...)` → `let session
= client.connect(...)`, `cs.session.closed()` → `session.closed()`, etc.
The unwind is mostly mechanical (sed `cs.session.` → `cs.`) across
moq-native, moq-relay, moq-cli, moq-gst, libmoq, moq-ffi.
FFI mirror in the same shape: MoqClientSession deleted, publisher() and
consumer() accessors added directly to MoqSession. MoqClient::connect
goes back to returning Arc<MoqSession>. Python wrapper drops the
.session() extraction. Swift README + doc/lib/{swift,kt}/{index,moq}.md
updated to call `cs.shutdown()` / `cs.publisher()` / `cs.consumer()`
directly on the returned session.
The existing FFI test server_client_roundtrip_auto_origin still
exercises the no-config path end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kes Producer Drop the Option<> around publisher/consumer on Session and at the FFI MoqSession level. Each accessor is now infallibly Some: whatever the caller wired via with_publish/with_consume, or a fresh auto-created Origin for any side they left unset. The duplex no-config case still defaults to one shared Origin (the typical client). Unifying on always-Some required picking a consistent type for the publish-side wiring. with_publish (on both Client and Server) now takes an OriginProducer instead of an OriginConsumer; internally moq-net calls .consume() on it just like every caller used to do at the call site. with_consume already took a Producer, so that side is unchanged. Cascading updates: - Drop trailing `.consume()` from every `with_publish(x.consume())` call across moq-cli, moq-gst, moq-boy, moq-native tests/examples, moq-relay, libmoq, moq-ffi. - Relay's `Cluster::subscriber()` returns Option<OriginProducer> now (the underlying `.with_root(...).scope(...)` already yields a Producer; we just stop calling `.consume()` at the end). - relay/web.rs `serve_announced` / `serve_fetch` derive their read handle via `origin.consume()` since they now hold a Producer. - FFI MoqSession.publisher() / .consumer() return Arc<...> directly (no Option). The auto-origin smoke test drops its .expect(). - Swift README + per-language docs no longer mention nil/null fallback for the accessors. Behavioral note: a client/server that wires only one side now also gets a fresh auto-created Origin on the other side, where previously the unset side was simply absent. Allocations are cheap; the trade is "always have something to call .publisher() / .consumer() on" for "slightly more state per one-sided session". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picked up #1528 (moq-rtc) and #1531 (compress field on Track) by merging origin/dev. The new moq-rtc binary still passes publisher.consume() to with_publish, which used to take an OriginConsumer but now takes an OriginProducer. Drop the .consume(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-origin work removed `Origin` from server.rs's imports because the construction logic moved into the shared `client::resolve_origins` helper. The accept() docstring still referenced [`Origin`] as an intra-doc link, which now resolves to nothing. cargo doc with -D warnings (CI) flags this; locally it only shows up when running `just check` which has the same flag. Switch to the fully-qualified [`Origin`](crate::Origin) form so the link works without depending on what's currently in scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kixelated
commented
May 30, 2026
…unce Per inline review on rs/hang/examples/video.rs and rs/libmoq/src/session.rs: with_publish (and the matching libmoq Session::connect publish param) takes an OriginConsumer again. The user's downstream code was already written against that shape and the asymmetry actually fits the relay's scoped-subscriber use case better than the symmetric Producer form. Session.publisher and Session.consumer stay always-Some. When the caller overrides either side via with_publish / with_consume, the session's accessor for that side becomes a standalone auto-created no-op (the caller drives publishing/consuming through whatever they passed in). The duplex no-config path is unchanged: one shared origin powers Session.publisher, Session.consumer, and both wire directions. resolve_origins picks the four values (Session.publisher, Session.consumer, wire-publish, wire-consume) from the user's overrides in one place, so both Client::connect and Server::accept funnel through the same logic. Renamed MoqOriginProducer::add_broadcast to announce per the inline review on doc/lib/swift/moq.md (cleaner: cs.publisher().announce(...) vs publisher().add_broadcast). Python's OriginProducer.publish wrapper keeps its name and now calls inner.announce underneath. Doc nits per review: - doc/lib/swift/index.md: inline `let consumer = cs.consumer()` into the same line as `.announced(prefix:)`. - doc/lib/swift/moq.md + doc/lib/swift/index.md: use cdn.moq.dev/anon as the example URL. Cascading caller-side cleanup: re-add .consume() to every with_publish(producer) call across moq-cli, moq-native (tests + examples), moq-boy, moq-gst, moq-relay, moq-rtc, libmoq, FFI, and the relay's cluster.subscriber() goes back to returning OriginConsumer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
resolve_origins collapses from a 4-tuple to a 2-tuple. The asymmetric Consumer-vs-Producer API was forcing Session.publisher / Session.consumer to decouple from the wire whenever the caller overrode either side (no-op fallback). Unifying with_publish on OriginProducer removes the asymmetry: the same origin powers Session.publisher AND the wire's read side (via .consume() internally). Inverse of the previous commit: - with_publish (moq-net Client + Server + Request, moq-native, libmoq, FFI) takes OriginProducer again. - resolve_origins picks (publisher, consumer) with at most two fresh origins and the duplex sharing for the no-config case. No more 4-tuple, no no-op fallback. - All `with_publish(x.consume())` callers across moq-cli, moq-native (tests + examples), moq-boy, moq-gst, moq-rtc, libmoq, FFI, and moq-relay/tests/smoke.rs drop the .consume() and pass the Producer directly. - moq-relay's cluster.subscriber() returns Option<OriginProducer> again (the underlying .scope() yields a Producer; we just stop calling .consume() at the end). - moq-relay/src/web.rs serve_announced / serve_fetch derive the consumer locally via origin.consume() since they now hold a Producer. - chat.rs and video.rs examples flip their run_session signature back to OriginProducer. This is the API kixelated@ ended up endorsing on the PR: I had thrashed between the two shapes; the 4-tuple was the artifact of trying to satisfy "always-Some + with_publish(Consumer)" simultaneously. With with_publish(Producer), the always-Some property falls out naturally and the function is two lines per arm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the accessor names on Session (publisher / consumer) and reads better with the new symmetric Producer-taking shape: with_publisher(p) sets the publisher that Session::publisher returns. Rename is purely mechanical across the workspace; no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kixelated
commented
May 30, 2026
…n from inner libs
Two structural simplifications:
1) Client and Server fields go from Option<OriginProducer> to plain
OriginProducer. Default::default() initializes both sides with one
shared Origin::random().produce() — the typical full-duplex client
needs no setup. with_publisher/with_consumer overwrite the defaults.
resolve_origins is gone; connect/accept just clone the fields.
2) lite::start, ietf::start, PublisherConfig, SubscriberConfig, and
the ietf Subscriber field all drop Option. There's always an origin
now, so the no-op fallbacks (Origin::random().produce().consume(),
the has_origin/is_none guards, the .as_ref().expect()/.unwrap()
chains) disappear.
Callers that hold an Option (FFI MoqClient, MoqServer, libmoq's
Session::connect, moq-relay's connection/websocket handlers) move
their None-handling to the call site — `if let Some(p) = ... { client
= client.with_publisher(p) }`. moq-net itself defaults the unset side
to a fresh no-op origin, which matches the previous behavior for
publish-only or subscribe-only configurations.
doc/lib/rs/env/native.md publishing+subscribing examples updated to
the new pattern (no manual Origin::new().produce(); use
session.publisher() / session.consumer() directly).
doc/lib/{swift,kt}/{index,moq}.md drop the `let consumer = cs.consumer()`
temporary per review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes explicitly constructing an Origin optional on both client and server in
moq-netandmoq-ffi. The common-case setup collapses from this:to this:
with_publish/with_consume/with_originstay for callers that need a custom topology (relay, gst, cli, ffi); when any of those is set,session.publisher()andsession.consumer()returnNoneand the caller drives things through the origin it already holds.Targeting
devper CLAUDE.md (breaking change to public APIs inrs/moq-net,rs/moq-ffi, and the language wrappers).Rust layer (
rs/moq-net)Sessionnow carries the auto-created origin sides directly:Client::connectandServer::acceptboth auto-create an Origin when neither publish nor consume was set, then surface it through these accessors. One type instead of a separateClientSessionwrapper, symmetric across the client and server paths.Downstream call sites in
moq-native,moq-relay,moq-cli,moq-gst,libmoq, andmoq-ffithreadsession.publisher()/session.consumer()where they need the auto-created sides; everything else just continues to usesessionas before.One internal moq-net test was depending on "no origin → trivial close with code 0 (Cancel)" to validate SETUP framing. With auto-origin attached the Lite01 session now closes with
RequiredExtension(code 1) since Lite01 can't satisfy the new origin. Loosened the assertion to "any non-Version error" since the test's purpose is the framing path, not the close code.FFI layer (
rs/moq-ffi)Mirror at the FFI level:
MoqSessiongainspublisher()andconsumer()accessor methods (uniffi Objects, returnOption<Arc<MoqOriginProducer>>/Option<Arc<MoqOriginConsumer>>).MoqClient::connectcontinues to returnArc<MoqSession>.Renamed
MoqOriginProducer::publish→add_broadcastsosession.publisher().unwrap().add_broadcast(path, broadcast)reads cleanly instead of stuttering…publisher().publish(...).New test
server_client_roundtrip_auto_originvalidates the no-config path end-to-end (client doesn't callset_publish/set_consume;session.publisher()andsession.consumer()areSomeand drive a server → client broadcast).Per-language wrapper sync (CLAUDE.md cross-package table)
py/moq-rs/moq/client.py: no extraction step needed, just stores the returnedMoqSessiondirectly.py/moq-rs/moq/origin.py: PythonOriginProducer.publishwrapper kept (no Python API churn); now callsadd_broadcaston the FFI underneath.swift/README.md,doc/lib/{swift,kt}/{index,moq}.md: show the new shape — noMoqOriginProducer()construction in the happy path;session.publisher()/session.consumer()return the auto-created sides.go/: pure binding shim, no Go-side wrapper to update; consumers get the regeneratedMoqClient.Connect()returning*MoqSessionwith the new accessors.Moq.swift/ KotlinMoq.kt: already deleted in the prior PR swift+kt: re-export FFI, session.shutdown(); explicit Origin wiring #1526, so no entry-point shims to update here.Why fold into
Sessioninstead of keepingClientSessionThe earlier version of this PR added a separate
ClientSession { session, publisher, consumer }wrapper. Two reasons to fold:ClientSessionwould force either a parallelServerSession(more types) or asymmetric APIs. Folding the fields ontoSessionletsServer::acceptuse the same path with zero new naming.EndpointandConnectionare overloaded), andSessionalready represents "the connected thing." Adding optional fields fits cleanly without inventing a new concept.Test plan
cargo test --workspace --testspasses (all suites, including the newserver_client_roundtrip_auto_origin).cargo fmt --checkclean;cargo clippy --workspace --all-targetsclean.just swift checkpasses (2/2 smoke tests).just kt checkonly ran the binding regeneration step. CI exercises the full Kotlin build.just py check.(Written by Claude)