Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
16d5bfd
chore(quest): claim quest/m1/api-audit/announce-owner.md
kixelated Oct 23, 2026
d10bbcb
feat(net): separate publication control from broadcast filling
kixelated Oct 23, 2026
1491fe4
Merge remote-tracking branch 'origin/main' into iterate-announce-owner
kixelated Oct 23, 2026
541f82f
quest: record the approved origin::Publication shape
kixelated Oct 23, 2026
a245bf4
quest: split the announce-owner rollout into JS and binding quests
kixelated Oct 23, 2026
da86a4f
Merge remote-tracking branch 'origin/main' into iterate-announce-owner
kixelated Oct 23, 2026
9c9c785
Merge remote-tracking branch 'origin/main' into iterate-6464
kixelated Oct 24, 2026
3f88345
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
e325c0f
quest: drop a link to the finished binding-state quest
kixelated Oct 24, 2026
3a4c639
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
b4e8925
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
4f5baf2
test(cli): hold epoch test publications as origin::Publication
kixelated Oct 24, 2026
ce87776
test(hls): borrow the publication's fill for main's sibling-denial test
kixelated Oct 24, 2026
318da14
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
dbfdb74
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
239aa1d
quest: drop the finished wrapper-config link from binding-publication
kixelated Oct 24, 2026
2d912f7
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
d62e12b
Merge remote-tracking branch 'origin/main' into rq-6464
kixelated Oct 24, 2026
c05ae79
Merge remote-tracking branch 'origin/main' into rq-6464
kixelated Oct 24, 2026
3f41216
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
4615438
Merge remote-tracking branch 'origin/main' into rq-6464
kixelated Oct 24, 2026
feaa504
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 24, 2026
383c2d2
test: format the Publication migrations of main's new tests
kixelated Oct 24, 2026
35c066d
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 24, 2026
b5d676a
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 25, 2026
c5d4dce
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 25, 2026
43fd621
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 25, 2026
b4e3830
test(net): the compress fixture holds the origin publication
kixelated Oct 25, 2026
22a8faf
test(hls): format the handoff test's catalog fill
kixelated Oct 25, 2026
cff9238
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 25, 2026
d4415df
Merge remote-tracking branch 'origin/main' into rqb-6464
kixelated Oct 25, 2026
1404928
Merge origin/main into quest/m1/api-audit/announce-owner
kixelated Oct 25, 2026
4768fe3
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/an…
kixelated Oct 25, 2026
6b1788d
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/an…
kixelated Oct 25, 2026
fa550a0
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/an…
kixelated Oct 25, 2026
f76ccc7
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/an…
kixelated Oct 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
2 changes: 1 addition & 1 deletion doc/lib/rs/moq-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ moq-transport at setup and presents one API either way. Media is a layer above

What you use it for, beyond what the concept page already describes:

- **An origin outlives the session.** Broadcasts are created on the origin, and a reconnect announces them again. Closing the session does not delete them.
- **An origin outlives the session.** Creating a broadcast returns an origin publication that controls its announcement and exposes the broadcast to fill. Standalone and Prepared fill producers cannot announce; only publishing the Prepared answer admits its publication. A reconnect announces existing publications again, and closing the session does not delete them.
- **Requests can pin an epoch.** `consumer.request_broadcast(path, Some(epoch))` resolves only through a route announcing that [publisher epoch](/concept/moq-lite#publisher-epochs); `None` takes whichever route wins. The resolved consumer names its epoch in `info().epoch`, pinned or not.
- **Rank several CDNs.** Dial each with its own subscribe handle, `origin.clone().with_preference(origin::Preference::new(rank))` (higher wins; 0 is neutral and a negative rank demotes), all on one origin. Requests use the preferred link that has a route and fail over to the next; see [multiple CDNs](/concept/moq-lite#multiple-cdns).
- **Follow restarts.** `consumer.announced_broadcast(path, None)` hands out each publisher instance serving a path from `available()`: the first once announced, then each one that takes over or returns after an end, like JS `request(path, { announced: true })`. A broadcast already resolved stays on the old instance until dropped or its route goes. `consumer.follow(path)` yields the underlying start, update, restart, and end of the route a request resolves.
Expand Down
3 changes: 2 additions & 1 deletion quest/m1/api-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ Rejected in planning:
## Required

- [Two broadcast resolutions](/quest/m1/api-audit/resolve.md) - Rust, JS, and the bindings resolve a broadcast with `request(path, epoch)` or `follow(path, epoch)`, and every racy two-step caller is gone
- [The origin announces](/quest/m1/api-audit/announce-owner.md) - a broadcast producer cannot announce; the origin hands out the announcement, so `Prepared` and standalone broadcasts lose methods that always fail
- [JS origin publications](/quest/m1/api-audit/js-publication.md) - `@moq/net` origins hand out a `Publication` that announces, so `Prepared` and standalone broadcasts lose methods that always throw
- [Binding publications](/quest/m1/api-audit/binding-publication.md) - moq-ffi, the C ABI, and every wrapper hand out a publication handle that announces, replacing the private adapters
- [moq-net exports with a consumer](/quest/m1/api-audit/net-exports.md) - the dead and test-only moq-net, `@moq/net`, and moq-ffi items the follow-up audit found go private or away
- [Bindings read gaps and ends](/quest/m1/api-audit/decode-read-bindings.md) - moq-ffi and every wrapper surface `Gap` and `End` instead of discarding them
- [Pinned consumers adopt the CLI flag names](/quest/m1/api-audit/cli-flags-consumers.md) - pinned downstream deployments move past the flag rename and use its spellings
Expand Down
27 changes: 0 additions & 27 deletions quest/m1/api-audit/announce-owner.md

This file was deleted.

45 changes: 45 additions & 0 deletions quest/m1/api-audit/binding-publication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# [L] Binding publications

## Goal

In moq-ffi, the C ABI, and every wrapper, a broadcast producer fills tracks
and cannot announce. Creating a broadcast on an origin and publishing a
prepared answer return a publication handle that carries announce and
unannounce, mirroring Rust's `origin::Publication`, so a standalone or
prepared broadcast no longer exposes announcement calls that can only fail.

## Plan

Decided 2026-10-23 (maintainer, on
[moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ land the
announcement split in three PRs: native Rust in that PR, the
[JS mirror](/quest/m1/api-audit/js-publication.md), and this binding rollout
once the native PR merges. The native PR keeps private adapters that hold the
`Publication` beside each binding fill handle (`rs/moq-ffi/src/producer.rs`
`from_publication`, `rs/moq-c/src/publish.rs` `create_publication` and
`admit`); this quest replaces them with public handles.

The shape is Rust's: an origin-owned record holding the fill broadcast and its
announcement capability, with the existing fill lifetime (dropping the
publication does not retract on its own).

- moq-ffi: a `MoqPublication` object with `broadcast()`, `announce`
(taking `MoqAnnounceRoute`), and `unannounce`.
`MoqOriginProducer::create_broadcast` and `MoqBroadcastPrepared::publish`
return it; `MoqBroadcastProducer` loses `announce` and `unannounce`.
- C ABI, recommended shape: `moq_origin_create_broadcast(origin, path,
path_len, *broadcast_out)` returns a publication handle and writes the fill
handle to `broadcast_out`, as `moq_broadcast_request_prepare` already does.
`moq_broadcast_prepared_publish` returns a publication handle.
`moq_publish_announce` and `moq_publish_unannounce` become publication calls
taking that handle, with a call to release it. Admission keeps checking that
the prepared fill is the broadcast it prepared.
- Wrappers: py, swift, kt, dart, `go/wrapper`, `cpp/moq`, and `cpp/obs`, with
their tests, READMEs, `doc/lib/{py,swift,kt,go,dart,c,cpp}`, `doc/bin/obs.md`,
and a `MIGRATE.md` entry in each package. Regenerate bindings from source.

Test in each layer: clone/drop, explicit unannounce, prepared
admission (including a mismatched fill), and a stale publication against a
replacement. Run `just test interop --all`.

Public API: breaking in moq-ffi, the C ABI, and every wrapper. Wire: none.
39 changes: 39 additions & 0 deletions quest/m1/api-audit/js-publication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [M] JS origin publications

## Goal

A `@moq/net` `broadcast.Producer` fills tracks; it cannot announce. Creating a
broadcast on an origin and publishing a `Prepared` answer return a
`Publication` that carries `announce`, `unannounce`, and `route`, so a
standalone broadcast and `prepared.broadcast` no longer expose announcement
calls that can only throw. Mirrors Rust's `origin::Publication`.

## Plan

Decided 2026-10-23 (maintainer, on
[moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ land the
announcement split in three PRs: native Rust in that PR, this `@moq/net`
mirror, and the [binding publications](/quest/m1/api-audit/binding-publication.md).
This one does not depend on the native PR and can start now.

The shape is Rust's, approved on the same PR: an origin-owned record holding
the fill broadcast and its announcement capability.

- `Producer.createBroadcast` (`js/net/src/origin.ts`) and `Prepared.publish`
return `Publication`. It exposes the fill producer as `broadcast`, plus
`announce(route)`, `unannounce()`, and `route`. No way to swap in another
producer, and no `finish`: a broadcast has no finished state.
- `broadcast.Producer` loses its public `announce`, `unannounce`, and `route`
(`js/net/src/broadcast.ts`); the announcer stays a module hook.
- Lifetime matches Rust: publication clones (or references) and fill handles
keep the existing broadcast lifetime; dropping a `Publication` does not
retract on its own. `unannounce` retracts sooner.
- Migrate every caller: `js/net` tests, benches, and examples, `js/publish`,
`js/moq-boy`, the `js/e2ee` docs, `js/watch` and `js/hang` tests, and
`demo/carla`. Add the `js/net/MIGRATE.md` entry and update
`doc/lib/js/net.md`.

Test: clone/drop, explicit unannounce, Prepared admission,
and a stale publication against a replacement under a new epoch.

Public API: breaking in `@moq/net`. Wire: none.
2 changes: 1 addition & 1 deletion quest/m1/broadcast-lifecycle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ timer on main at 1582fdc07d:

- [moq-transport finish interop](/quest/m2/ietf-finish.md) - the interop `finish` case, whose late readers rely on retention
- [CLI and gst flags](https://github.com/moq-dev/moq/pull/6522) - removed the opt-in `linger` booleans, leaving completion to this epic
- [Publication owner](/quest/m1/api-audit/announce-owner.md) - adds `origin::Publication`; it must not forward a `finish` this epic deletes
- [JS origin publications](/quest/m1/api-audit/js-publication.md) and [binding publications](/quest/m1/api-audit/binding-publication.md) - mirror `origin::Publication`, which has no `finish`
2 changes: 1 addition & 1 deletion quest/m2/stats/preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ rendition, sync point interval, audio/video skew,
media timestamp continuity, the [health verdict](/doc/concept/stats.md#health)
from the publisher's stats, and whether the broadcast ends with a clean
unannounce (observed by the check; the publisher, which owns the
`broadcast::Producer`, does the unannounce). A failure names the broken layer
`origin::Publication`, does the unannounce). A failure names the broken layer
and its evidence instead of one score, and the raw evidence is kept so two
people reach the same diagnosis.

Expand Down
4 changes: 2 additions & 2 deletions rs/hang/examples/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ fn create_track(broadcast: &mut moq_net::broadcast::Producer) -> anyhow::Result<
async fn run_broadcast(origin: moq_net::origin::Producer) -> anyhow::Result<()> {
// Create a broadcast on the origin and announce its path.
// NOTE: The path is empty because we're using the URL to scope the broadcast.
let mut broadcast = origin.create_broadcast("").context("failed to create broadcast")?;
let broadcast = origin.create_broadcast("").context("failed to create broadcast")?;
broadcast
.announce(Default::default())
.context("failed to announce broadcast")?;
let track = create_track(&mut broadcast)?;
let track = create_track(&mut broadcast.broadcast().clone())?;

// Wrap in a Producer for keyframe-based group management.
let mut producer = moq_mux::container::Producer::new(
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-archive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use moq_archive::object_store::memory::InMemory;
let store = moq_archive::Store::new(InMemory::new(), "recordings/demo");
moq_archive::Producer::new(store.clone(), broadcast, Default::default()).run().await?;

// `served` is a `moq_net::broadcast::Producer`, announced once `open` returns.
// `served` is an `origin::Publication`'s fill, announced once `open` returns.
let consumer = moq_archive::Consumer::open(store, served, Default::default()).await?;
consumer.run().await?;
```
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-archive/src/consumer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
//!
//! let store = moq_archive::Store::new(InMemory::new(), "recordings/demo");
//! let consumer = moq_archive::Consumer::open(store, broadcast, Default::default()).await?;
//! // Every stored track exists now: announce the broadcast, then serve it.
//! // Every stored track exists now: announce its publication, then serve it.
//! consumer.run().await?;
//! # Ok(())
//! # }
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-boy/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ mod tests {
}

/// Publish a viewer with a live command track.
fn viewer(origin: &moq_net::origin::Producer) -> (moq_net::broadcast::Producer, moq_net::track::Producer) {
fn viewer(origin: &moq_net::origin::Producer) -> (moq_net::origin::Publication, moq_net::track::Producer) {
let broadcast = origin.create_broadcast("alice").unwrap();
let track = broadcast
.create_track(
Expand Down
11 changes: 6 additions & 5 deletions rs/moq-boy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async fn run(config: &Config) -> Result<()> {

// Create the broadcast on the publish origin and announce its path.
let broadcast_path = format!("{game_prefix}/{name}");
let mut broadcast = publish_origin
let broadcast = publish_origin
.create_broadcast(&broadcast_path)
.context("failed to create broadcast")?;
let epoch = moq_net::Epoch::mint();
Expand All @@ -270,15 +270,16 @@ async fn run(config: &Config) -> Result<()> {
.connect(url);

// Set up catalog and encoders.
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default())?;
let video_encoder = video::VideoEncoder::spawn(broadcast.clone(), catalog.clone()).await;
let catalog =
moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default())?;
let video_encoder = video::VideoEncoder::spawn(broadcast.broadcast().clone(), catalog.clone()).await;

let audio_encoder = audio::AudioEncoder::new(broadcast.clone(), catalog.clone(), 44100)?;
let audio_encoder = audio::AudioEncoder::new(broadcast.broadcast().clone(), catalog.clone(), 44100)?;

let video_track = video_encoder.demand.clone();
let audio_track = audio_encoder.demand();

let status_publisher = status::StatusPublisher::new(&mut broadcast)?;
let status_publisher = status::StatusPublisher::new(&mut broadcast.broadcast().clone())?;

let session = Arc::new(Session {
video_encoder,
Expand Down
12 changes: 6 additions & 6 deletions rs/moq-c/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,7 +1762,7 @@ pub unsafe extern "C" fn moq_origin_create_broadcast(origin: u32, path: *const c

let mut state = State::lock();
let broadcast = state.origin.create_broadcast(origin, path)?;
state.publish.create(broadcast)
state.publish.create_publication(broadcast)
})
}

Expand Down Expand Up @@ -1857,7 +1857,7 @@ pub unsafe extern "C" fn moq_broadcast_request_prepare(
let (pending, driver) = state.origin.broadcast_request_take(request)?;
let prepared = pending.prepare(epoch)?;
let broadcast = state.publish.create((prepared.broadcast().clone(), driver.clone()))?;
let id = state.origin.prepared_insert(prepared, driver)?;
let id = state.origin.prepared_insert(prepared, driver, broadcast)?;
*broadcast_out = broadcast.into();
Ok(id)
})
Expand All @@ -1871,12 +1871,12 @@ pub unsafe extern "C" fn moq_broadcast_request_prepare(
pub extern "C" fn moq_broadcast_prepared_publish(prepared: u32) -> i32 {
ffi::enter(move || {
let prepared = ffi::parse_id(prepared)?;
let (prepared, driver) = State::lock().origin.prepared_take(prepared)?;
let mut state = State::lock();
let (prepared, driver, broadcast) = state.origin.prepared_take(prepared)?;
driver.check()?;
let result = prepared.publish();
let result = state.publish.admit(broadcast, prepared);
driver.check()?;
result?;
Ok(())
result
})
}

Expand Down
13 changes: 7 additions & 6 deletions rs/moq-c/src/origin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub struct Origin {
broadcast_request: NonZeroSlab<(moq_net::origin::Request, crate::ffi::Background)>,

/// Claim answers being filled before their exact route is announced.
prepared: NonZeroSlab<(moq_net::origin::Prepared, crate::ffi::Background)>,
prepared: NonZeroSlab<(moq_net::origin::Prepared, crate::ffi::Background, Id)>,
}

#[derive(Clone)]
Expand Down Expand Up @@ -325,7 +325,7 @@ impl Origin {
&self,
origin: Id,
path: P,
) -> Result<(moq_net::broadcast::Producer, crate::ffi::Background), Error> {
) -> Result<(moq_net::origin::Publication, crate::ffi::Background), Error> {
let origin = self.get(origin)?;
let result = origin.producer.create_broadcast(path);
origin.driver.check()?;
Expand Down Expand Up @@ -422,14 +422,15 @@ impl Origin {
&mut self,
prepared: moq_net::origin::Prepared,
driver: crate::ffi::Background,
broadcast: Id,
) -> Result<Id, Error> {
self.prepared.insert((prepared, driver))
self.prepared.insert((prepared, driver, broadcast))
}

pub fn prepared_take(
&mut self,
prepared: Id,
) -> Result<(moq_net::origin::Prepared, crate::ffi::Background), Error> {
) -> Result<(moq_net::origin::Prepared, crate::ffi::Background, Id), Error> {
self.prepared.remove(prepared).ok_or(Error::NotFound)
}

Expand All @@ -456,7 +457,7 @@ mod supervision_tests {
.unwrap();
let mut publish = crate::publish::Publish::default();
let broadcast = publish
.create(origins.create_broadcast(origin, "retained").unwrap())
.create_publication(origins.create_broadcast(origin, "retained").unwrap())
.unwrap();
origins.close(origin).unwrap();
*trigger
Expand All @@ -470,7 +471,7 @@ mod supervision_tests {
));
let healthy = origins.create().unwrap();
let broadcast = publish
.create(origins.create_broadcast(healthy, "healthy").unwrap())
.create_publication(origins.create_broadcast(healthy, "healthy").unwrap())
.unwrap();
publish.announce(broadcast, Default::default()).unwrap();
}
Expand Down
Loading
Loading