From 16d5bfd2dabe29291e7a9bf91409f66d55340119 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 20:55:12 -0700 Subject: [PATCH 01/11] chore(quest): claim quest/m1/api-audit/announce-owner.md Quest-Claim: 62eb0760a5d0da71 From d10bbcb7b79540f8f8bf0722353b82e46148ba40 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 23:37:19 -0700 Subject: [PATCH 02/11] feat(net): separate publication control from broadcast filling Propose an origin-owned Publication with immutable fill access and migrate current Rust consumers. Preserve finished Prepared publication and keep public JS/binding rollout open for the return/lifetime decision. Checkpoint validation: 8289 native tests and 513 capture/playback tests passed before the final CLI play fixture migration. The current full CI rerun and all-language interop are still pending. Co-Authored-By: GPT-6 --- doc/lib/rs/moq-net.md | 2 +- quest/m1/api-audit/announce-owner.md | 24 ++ rs/hang/examples/video.rs | 4 +- rs/moq-boy/src/input.rs | 2 +- rs/moq-boy/src/main.rs | 11 +- rs/moq-c/src/api.rs | 12 +- rs/moq-c/src/origin.rs | 13 +- rs/moq-c/src/publish.rs | 93 +++++++- rs/moq-c/src/test.rs | 38 +++- rs/moq-cli/src/announced.rs | 2 +- rs/moq-cli/src/fetch.rs | 4 +- rs/moq-cli/src/hls.rs | 6 +- rs/moq-cli/src/main.rs | 4 +- rs/moq-cli/src/play/media.rs | 18 +- rs/moq-cli/src/publish.rs | 47 ++-- rs/moq-cli/src/report.rs | 6 +- rs/moq-cli/src/rtc.rs | 2 +- rs/moq-cli/src/rtsp.rs | 4 +- rs/moq-cli/tests/integration/export.rs | 6 +- rs/moq-cli/tests/integration/stats.rs | 6 +- rs/moq-e2ee/tests/transport.rs | 4 +- rs/moq-ffi/src/origin.rs | 9 +- rs/moq-ffi/src/producer.rs | 21 +- rs/moq-ffi/src/test.rs | 37 ++- rs/moq-gst/src/sink/imp.rs | 2 +- rs/moq-gst/src/sink/session.rs | 11 +- rs/moq-gst/src/source/imp.rs | 22 +- rs/moq-hls/src/export/archive_tests.rs | 2 +- rs/moq-hls/src/export/mod.rs | 210 +++++++++++------- rs/moq-hls/src/server/routes.rs | 43 ++-- rs/moq-mux/src/catalog/producer.rs | 10 +- rs/moq-mux/src/container/ts/export_test.rs | 118 +++++----- rs/moq-mux/src/container/ts/programs.rs | 10 +- rs/moq-net/MIGRATE.md | 13 +- rs/moq-net/benches/memory.rs | 4 +- rs/moq-net/benches/origin.rs | 8 +- rs/moq-net/benches/session.rs | 4 +- rs/moq-net/benches/stats.rs | 4 +- rs/moq-net/benches/viewers.rs | 4 +- rs/moq-net/src/ietf/publisher.rs | 2 +- rs/moq-net/src/ietf/publisher/tracks.rs | 2 +- rs/moq-net/src/model/broadcast.rs | 13 +- rs/moq-net/src/model/mod.rs | 4 +- rs/moq-net/src/model/origin.rs | 131 +++++++++-- rs/moq-net/tests/integration/auth.rs | 2 +- .../tests/integration/catalog_handover.rs | 4 +- rs/moq-net/tests/integration/compress.rs | 2 +- rs/moq-net/tests/integration/datagram.rs | 2 +- .../tests/integration/fetch_from_feed.rs | 2 +- rs/moq-net/tests/integration/fetch_only.rs | 2 +- .../tests/integration/lazy_discovery.rs | 2 +- rs/moq-net/tests/integration/mesh_withdraw.rs | 8 +- rs/moq-net/tests/integration/multi_cdn.rs | 4 +- rs/moq-net/tests/integration/quiet_catalog.rs | 2 +- rs/moq-net/tests/integration/request_caps.rs | 2 +- rs/moq-net/tests/integration/restart.rs | 2 +- rs/moq-net/tests/integration/route_change.rs | 6 +- rs/moq-net/tests/integration/session_close.rs | 2 +- rs/moq-net/tests/integration/session_panic.rs | 2 +- rs/moq-net/tests/integration/source_errors.rs | 2 +- .../tests/integration/subscribe_tracks.rs | 2 +- rs/moq-net/tests/integration/track_tail.rs | 2 +- .../tests/integration/cluster_unknown.rs | 2 +- rs/moq-relay/tests/integration/connection.rs | 2 +- .../tests/integration/upstream_links.rs | 2 +- rs/moq-room/src/room.rs | 2 +- rs/moq-rtc/src/client/mod.rs | 2 +- rs/moq-rtc/src/lib.rs | 12 +- rs/moq-rtc/src/server/whep.rs | 8 +- rs/moq-rtc/src/server/whip.rs | 2 +- rs/moq-rtmp/src/dial.rs | 29 +-- rs/moq-rtmp/src/server.rs | 37 +-- rs/moq-rtsp/src/client.rs | 6 +- rs/moq-srt/src/server.rs | 10 +- rs/moq-srt/src/ts.rs | 11 +- rs/moq-stats/benches/aggregate.rs | 2 +- rs/moq-stats/src/aggregate.rs | 6 +- rs/moq-stats/src/consume.rs | 4 +- rs/moq-stats/src/produce.rs | 4 +- .../tests/integration/receive_credit.rs | 4 +- rs/moq-transcode/examples/transcode.rs | 2 +- rs/moq-transcode/src/lib.rs | 2 +- rs/moq-video/src/decode/decoder.rs | 5 +- test/interop/clients/rust/finish.rs | 5 +- 84 files changed, 807 insertions(+), 394 deletions(-) diff --git a/doc/lib/rs/moq-net.md b/doc/lib/rs/moq-net.md index d9295f9ad4a..e79943b6547 100644 --- a/doc/lib/rs/moq-net.md +++ b/doc/lib/rs/moq-net.md @@ -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. diff --git a/quest/m1/api-audit/announce-owner.md b/quest/m1/api-audit/announce-owner.md index 8b06e16b98f..08530c5d039 100644 --- a/quest/m1/api-audit/announce-owner.md +++ b/quest/m1/api-audit/announce-owner.md @@ -23,6 +23,30 @@ error. - Mirror the shape in `@moq/net` if its producer has the same trap, and in moq-ffi where the binding exposes announce on the broadcast. +The draft proposes an `origin::Publication` owner that combines the fill +broadcast and its real announcement capability. `create_broadcast`, `publish`, +and an admitted `Prepared::publish` return it; `Prepared::broadcast` and +standalone producers remain fill-only. The proposed record forwards fill +operations and exposes the borrowed producer through `broadcast()`. + +The return/lifetime shape still needs review. The current proposal preserves +last-fill and `Finish` retention rather than making the last announcement +capability clone retract independently. A literal producer/announcement pair +would make that transfer explicit but requires media holders and finish/linger +paths to retain both. Settle this choice before mirroring the record in JS, +moq-ffi, C, and every wrapper; do not export an optional capability on a fill +producer to avoid the migration. + +The native proposal migrates current Rust holders, examples, benchmarks, and +private C/UniFFI adapters. Remaining work is accepting the return/lifetime shape, +mirroring the public role in JS and all bindings, regenerating outputs from +source, and updating those consumers and package migrations. Exercise clone/drop, +explicit unannounce, finished tails, Prepared admission, and stale owners +against a replacement publication. A finished-but-open Prepared answer remains +publishable and serveable through its retained `Finish`; a missing binding fill +registry/state after finish does not mean the native broadcast is closed. Keep +deferred validity and request role redesigns separate. + Public API: breaking in moq-net (and `@moq/net`/moq-ffi if they mirror the trap). Wire: none. diff --git a/rs/hang/examples/video.rs b/rs/hang/examples/video.rs index 40fc8f57511..d61cfc54022 100644 --- a/rs/hang/examples/video.rs +++ b/rs/hang/examples/video.rs @@ -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( diff --git a/rs/moq-boy/src/input.rs b/rs/moq-boy/src/input.rs index 4099443eac5..ead472d9807 100644 --- a/rs/moq-boy/src/input.rs +++ b/rs/moq-boy/src/input.rs @@ -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( diff --git a/rs/moq-boy/src/main.rs b/rs/moq-boy/src/main.rs index bf19324aa5e..ddd6346c390 100644 --- a/rs/moq-boy/src/main.rs +++ b/rs/moq-boy/src/main.rs @@ -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(); @@ -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, diff --git a/rs/moq-c/src/api.rs b/rs/moq-c/src/api.rs index aa7170afaa1..6e5c44fe510 100644 --- a/rs/moq-c/src/api.rs +++ b/rs/moq-c/src/api.rs @@ -1788,7 +1788,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) }) } @@ -1883,7 +1883,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) }) @@ -1897,12 +1897,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 }) } diff --git a/rs/moq-c/src/origin.rs b/rs/moq-c/src/origin.rs index 7e7eaf7296f..e58dd2a1e63 100644 --- a/rs/moq-c/src/origin.rs +++ b/rs/moq-c/src/origin.rs @@ -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)] @@ -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()?; @@ -422,14 +422,15 @@ impl Origin { &mut self, prepared: moq_net::origin::Prepared, driver: crate::ffi::Background, + broadcast: Id, ) -> Result { - 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) } @@ -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 @@ -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(); } diff --git a/rs/moq-c/src/publish.rs b/rs/moq-c/src/publish.rs index b91a258f62a..b4464fd474e 100644 --- a/rs/moq-c/src/publish.rs +++ b/rs/moq-c/src/publish.rs @@ -46,6 +46,7 @@ enum Dynamic { struct Broadcast { driver: crate::ffi::Background, producer: moq_net::broadcast::Producer, + publication: Option, catalog: moq_mux::catalog::Producer, video: BTreeMap, audio: BTreeMap, @@ -226,6 +227,7 @@ impl Publish { let id = self.broadcasts.insert(Broadcast { driver, producer: broadcast, + publication: None, catalog, video: BTreeMap::new(), audio: BTreeMap::new(), @@ -234,11 +236,42 @@ impl Publish { Ok(id) } + /// Retain a native publication alongside its C fill handle and catalog. + pub fn create_publication( + &mut self, + (publication, driver): (moq_net::origin::Publication, crate::ffi::Background), + ) -> Result { + let id = self.create((publication.broadcast().clone(), driver))?; + self.broadcasts.get_mut(id).expect("just-created broadcast").publication = Some(publication); + Ok(id) + } + + /// Publish a prepared answer and retain control beside its live fill handle. + pub fn admit(&mut self, id: Id, prepared: moq_net::origin::Prepared) -> Result<(), Error> { + let broadcast = self.broadcasts.get_mut(id); + if let Some(broadcast) = &broadcast { + broadcast.driver.check()?; + if !broadcast.producer.is_clone(prepared.broadcast()) { + return Err(Error::BroadcastNotFound); + } + } + let publication = prepared.publish()?; + if let Some(broadcast) = broadcast { + broadcast.publication = Some(publication); + } + // A finished fill has left this registry; its Finish retains the publication. + Ok(()) + } + /// Advertise the broadcast's exact path as a route. Announcing again re-prices /// in place. Until announced, the broadcast is invisible and unroutable. pub fn announce(&mut self, broadcast: Id, route: moq_net::origin::Route) -> Result<(), Error> { let broadcast = self.broadcast_mut(broadcast)?; - let result = broadcast.producer.announce(route); + let result = broadcast + .publication + .as_ref() + .ok_or(Error::Moq(moq_net::Error::Closed))? + .announce(route); broadcast.driver.check()?; result?; Ok(()) @@ -247,7 +280,9 @@ impl Publish { /// Retract the broadcast's exact-path advertisement, if any. pub fn unannounce(&mut self, broadcast: Id) -> Result<(), Error> { let broadcast = self.broadcast_mut(broadcast)?; - broadcast.producer.unannounce(); + if let Some(publication) = &broadcast.publication { + publication.unannounce(); + } Ok(()) } @@ -1117,12 +1152,62 @@ impl Publish { mod supervision_tests { use super::*; + #[tokio::test(start_paused = true)] + async fn admission_must_match_the_broadcast_it_filled() { + let origin = moq_tokio::origin::spawn(); + let claim = origin.dynamic("room").unwrap(); + let pending = origin.consume().request_broadcast("room/first", None); + let prepared = claim + .requested_broadcast() + .await + .unwrap() + .prepare(moq_net::Epoch::mint()) + .unwrap(); + let mut publish = Publish::default(); + let first = publish + .create((prepared.broadcast().clone(), Default::default())) + .unwrap(); + let alias = { + let original = publish.broadcast(first).unwrap(); + Broadcast { + driver: original.driver.clone(), + producer: original.producer.clone(), + publication: None, + catalog: original.catalog.clone(), + video: Default::default(), + audio: Default::default(), + finished: Default::default(), + } + }; + let clone = publish.broadcasts.insert(alias).unwrap(); + assert_ne!(first, clone); + publish.admit(clone, prepared).unwrap(); + pending.await.unwrap(); + + let pending = origin.consume().request_broadcast("room/second", None); + let prepared = claim + .requested_broadcast() + .await + .unwrap() + .prepare(moq_net::Epoch::mint()) + .unwrap(); + let other = publish + .create((prepared.broadcast().clone(), Default::default())) + .unwrap(); + assert!(matches!(publish.admit(first, prepared), Err(Error::BroadcastNotFound))); + assert!(pending.await.is_err()); + assert!(matches!( + publish.announce(other, Default::default()), + Err(Error::Moq(moq_net::Error::Closed)) + )); + } + #[tokio::test] async fn a_container_importer_keeps_the_origin_failure_after_other_owners_are_released() { let (origin, completion, trigger) = crate::ffi::panicking_origin(); let mut publish = Publish::default(); let broadcast = publish - .create((origin.create_broadcast("retained").unwrap(), completion.clone())) + .create_publication((origin.create_broadcast("retained").unwrap(), completion.clone())) .unwrap(); let container = publish .container( @@ -1146,7 +1231,7 @@ mod supervision_tests { let (origin, completion, trigger) = crate::ffi::panicking_origin(); let mut publish = Publish::default(); let broadcast = publish - .create((origin.create_broadcast("retained").unwrap(), completion.clone())) + .create_publication((origin.create_broadcast("retained").unwrap(), completion.clone())) .unwrap(); let producer = &publish.broadcast(broadcast).unwrap().producer; // A dynamic handle outlives the consumed broadcast, so only an explicit close ends it. diff --git a/rs/moq-c/src/test.rs b/rs/moq-c/src/test.rs index d98cc3fe827..0a49637e6da 100644 --- a/rs/moq-c/src/test.rs +++ b/rs/moq-c/src/test.rs @@ -2685,6 +2685,40 @@ fn dynamic_serves_a_request_under_a_prefix() { assert_eq!(moq_origin_close(origin), 0); } +#[test] +fn a_finished_prepared_fill_can_still_be_published() { + let origin = id(moq_origin_create()); + let cb = Callback::new(); + let dynamic = id(unsafe { moq_origin_dynamic(origin, std::ptr::null(), 0, Some(channel_callback), cb.ptr) }); + let path = b"finished/answer"; + let pending = Callback::new(); + id(unsafe { + moq_origin_request( + origin, + path.as_ptr().cast(), + path.len(), + Some(channel_callback), + pending.ptr, + ) + }); + let request = id(cb.recv()); + let epoch = moq_net::Epoch::mint().to_string(); + let mut fill = 0; + let prepared = id(unsafe { moq_broadcast_request_prepare(request, epoch.as_ptr().cast(), epoch.len(), &mut fill) }); + let mut linger = 0; + let finished = id(unsafe { moq_publish_finish(fill, &mut linger) }); + assert_eq!(moq_broadcast_prepared_publish(prepared), 0); + let current = id(pending.recv()); + assert_eq!(pending.recv_terminal(), 0); + let late = id(request_once(origin, path)); + assert_eq!(moq_consume_close(current), 0); + assert_eq!(moq_consume_close(late), 0); + assert_eq!(moq_publish_finish_close(finished), 0); + assert_eq!(moq_origin_dynamic_cancel(dynamic), 0); + assert_eq!(cb.recv_terminal(), 0); + assert_eq!(moq_origin_close(origin), 0); +} + #[test] fn dynamic_refuses_a_missing_callback() { let origin = id(moq_origin_create()); @@ -3606,14 +3640,14 @@ fn consume_announced_close_cancels() { } /// Publish `path` on `origin` through `route`, which the C ABI cannot set. -fn publish(origin: u32, path: &str, route: moq_net::origin::Route) -> moq_net::broadcast::Producer { +fn publish(origin: u32, path: &str, route: moq_net::origin::Route) -> moq_net::origin::Publication { let _runtime = crate::ffi::RUNTIME.enter(); let origin = crate::Id::try_from(origin).unwrap(); State::lock().origin.get(origin).unwrap().publish(path, route).unwrap() } /// Publish `path` on `origin` under a fresh epoch, as each run of a publisher does. -fn publish_run(origin: u32, path: &str) -> moq_net::broadcast::Producer { +fn publish_run(origin: u32, path: &str) -> moq_net::origin::Publication { publish( origin, path, diff --git a/rs/moq-cli/src/announced.rs b/rs/moq-cli/src/announced.rs index ff0ae698bae..dc3b8f40e79 100644 --- a/rs/moq-cli/src/announced.rs +++ b/rs/moq-cli/src/announced.rs @@ -186,7 +186,7 @@ mod tests { /// Stops the relay, which ends every session on it. shutdown: moq_relay::shutdown::Trigger, /// The published broadcasts by path; dropping one retracts it. - broadcasts: Vec<(&'static str, moq_net::broadcast::Producer)>, + broadcasts: Vec<(&'static str, moq_net::origin::Publication)>, _publisher: moq_tokio::Connection, } diff --git a/rs/moq-cli/src/fetch.rs b/rs/moq-cli/src/fetch.rs index a91824ce382..c10b8cadf50 100644 --- a/rs/moq-cli/src/fetch.rs +++ b/rs/moq-cli/src/fetch.rs @@ -183,7 +183,7 @@ mod tests { struct Fixture { origin: moq_net::origin::Producer, _keep: ( - moq_net::broadcast::Producer, + moq_net::origin::Publication, Vec, moq_net::group::Producer, ), @@ -241,7 +241,7 @@ mod tests { /// Kept so the published broadcast, its tracks, and the open group stay live. _publisher: (moq_tokio::Connection, Fixture), /// Kept so the relay's hidden broadcast and its track stay live. - _hidden: (moq_net::broadcast::Producer, moq_net::track::Producer), + _hidden: (moq_net::origin::Publication, moq_net::track::Producer), } impl NetworkFixture { diff --git a/rs/moq-cli/src/hls.rs b/rs/moq-cli/src/hls.rs index b2425a1a7a3..864dff81b64 100644 --- a/rs/moq-cli/src/hls.rs +++ b/rs/moq-cli/src/hls.rs @@ -58,12 +58,12 @@ pub async fn import(target: ImportTarget, args: ImportArgs, epoch: moq_net::Epoc max_age, bandwidth, } = target; - let mut producer = origin.create_broadcast(&name).context("failed to create broadcast")?; + let producer = origin.create_broadcast(&name).context("failed to create broadcast")?; let config = moq_mux::catalog::Config::default() .with_max_age(max_age) .with_bandwidth(bandwidth); - let catalog = moq_mux::catalog::Producer::new(&mut producer, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut producer.broadcast().clone(), config)?; // Announced once the catalog first goes out, which waits for every rendition's init: a // relay queries a broadcast's tracks the moment it is announced, and keeps the refusals of a // publisher that cannot serve its declared tracks yet. @@ -71,7 +71,7 @@ pub async fn import(target: ImportTarget, args: ImportArgs, epoch: moq_net::Epoc let mut config = moq_hls::import::Config::new(playlist_url(&args.playlist)?); config.passthrough = args.passthrough; - let mut importer = moq_hls::import::Import::new(producer.clone(), catalog, config)?; + let mut importer = moq_hls::import::Import::new(producer.broadcast().clone(), catalog, config)?; tracing::info!(%name, "importing HLS"); diff --git a/rs/moq-cli/src/main.rs b/rs/moq-cli/src/main.rs index aa28cdd09ce..6ee61fc1bb6 100644 --- a/rs/moq-cli/src/main.rs +++ b/rs/moq-cli/src/main.rs @@ -740,7 +740,7 @@ fn spawn_import( Publish::ts_programs(origin.clone(), name, config) } else { let broadcast = origin.create_broadcast(&name).context("failed to create broadcast")?; - Publish::new(broadcast, &format, config)? + Publish::from_publication(broadcast, &format, config)? }; publish.announce(epoch)?; local = Some(publish); @@ -1136,7 +1136,7 @@ mod tests { connection: moq_tokio::Connection, consumer: moq_net::origin::Consumer, _shutdown: Shutdown, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _track: moq_net::track::Producer, } diff --git a/rs/moq-cli/src/play/media.rs b/rs/moq-cli/src/play/media.rs index f20cc5c0298..63a83293c38 100644 --- a/rs/moq-cli/src/play/media.rs +++ b/rs/moq-cli/src/play/media.rs @@ -1290,9 +1290,9 @@ mod tests { async fn disabling_a_live_high_rendition_plays_the_lower_one() { tokio::time::pause(); let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("room").unwrap(); + let broadcast = origin.create_broadcast("room").unwrap(); broadcast.announce(Default::default()).unwrap(); - let mut catalog = catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let mut high = rendition(&broadcast, &catalog, "high"); let recorder = Recorder::default(); let player = tokio::spawn(media(&origin, Duration::from_millis(50), recorder.clone()).run()); @@ -1336,9 +1336,9 @@ mod tests { let delay = Duration::from_millis(500); let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("room").unwrap(); + let broadcast = origin.create_broadcast("room").unwrap(); broadcast.announce(Default::default()).unwrap(); - let mut catalog = catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let recorder = Recorder::default(); let player = tokio::spawn(media(&origin, delay, recorder.clone()).run()); @@ -1391,12 +1391,12 @@ mod tests { origin: &moq_net::origin::Producer, route: moq_net::origin::Route, ) -> ( - moq_net::broadcast::Producer, + moq_net::origin::Publication, catalog::Producer, moq_mux::container::Producer, ) { - let mut broadcast = origin.create_broadcast("room").unwrap(); - let catalog = catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let broadcast = origin.create_broadcast("room").unwrap(); + let catalog = catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let audio = rendition(&broadcast, &catalog, "audio"); broadcast.announce(route).unwrap(); (broadcast, catalog, audio) @@ -1718,9 +1718,9 @@ mod tests { async fn a_finite_audio_track_plays_its_final_samples() { tokio::time::pause(); let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("room").unwrap(); + let broadcast = origin.create_broadcast("room").unwrap(); broadcast.announce(Default::default()).unwrap(); - let mut catalog = catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let recorder = Recorder::default(); let player = tokio::spawn(media(&origin, Duration::from_millis(50), recorder.clone()).run()); let mut audio = rendition(&broadcast, &catalog, "audio"); diff --git a/rs/moq-cli/src/publish.rs b/rs/moq-cli/src/publish.rs index f3b1fe85c40..4f3fdbdb33a 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -298,10 +298,10 @@ enum Source { /// a broadcast that the MoQ side announces. pub struct Publish { source: Source, - // Keeps the origin-created broadcast alive for the publisher's lifetime; - // tracks and importers don't. `None` for `import ts --program all`, whose - // programs each keep and announce their own. + // Keeps the fill surface until finish. `None` for `import ts --program all`, + // whose programs each own their fill and publication control. broadcast: Option, + publication: Option, // A stdin import's route, announced once its catalog lists the tracks; see [`Self::announce`]. route: Option, // The `--stats` report, written beside the decode loop; see [`Self::with_stats`]. @@ -314,13 +314,13 @@ pub struct Publish { /// relay queries a broadcast's tracks the moment it is announced, so a standby /// announced any sooner can receive requests for tracks it cannot serve yet. struct Pending { - broadcast: moq_net::broadcast::Producer, + broadcast: moq_net::origin::Publication, catalog: moq_net::track::Consumer, route: moq_net::origin::Route, } impl Pending { - fn new(broadcast: moq_net::broadcast::Producer, route: moq_net::origin::Route) -> anyhow::Result { + fn new(broadcast: moq_net::origin::Publication, route: moq_net::origin::Route) -> anyhow::Result { let catalog = broadcast.consume().track(hang::Catalog::DEFAULT_NAME)?; Ok(Self { broadcast, @@ -365,6 +365,7 @@ impl Publish { catalog: PublishCatalog::Ts(catalog), }, broadcast: Some(broadcast), + publication: None, route: None, stats: None, }); @@ -412,11 +413,23 @@ impl Publish { catalog: PublishCatalog::Media(catalog), }, broadcast: Some(broadcast), + publication: None, route: None, stats: None, }) } + /// Retain origin announcement control alongside the stdin decoder's fill handle. + pub(super) fn from_publication( + publication: moq_net::origin::Publication, + format: &PublishFormat, + config: moq_mux::catalog::Config, + ) -> anyhow::Result { + let mut result = Self::new(publication.broadcast().clone(), format, config)?; + result.publication = Some(publication); + Ok(result) + } + /// Build a publisher decoding a multi-program TS from stdin into one broadcast per program, /// named after `name` (`event.hang` becomes `event/1.hang`, `event/2.hang`, ...). /// @@ -430,6 +443,7 @@ impl Publish { catalog: PublishCatalog::TsPrograms, }, broadcast: None, + publication: None, route: None, stats: None, } @@ -446,11 +460,12 @@ impl Publish { /// left after audio's reservation rather than the whole uplink. #[cfg(feature = "capture")] pub fn capture( - mut broadcast: moq_net::broadcast::Producer, + publication: moq_net::origin::Publication, args: &CaptureArgs, bandwidth: moq_net::bandwidth::Allocator, max_age: Option, ) -> anyhow::Result { + let mut broadcast = publication.broadcast().clone(); let config = moq_mux::catalog::Config::default().with_max_age(max_age); let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; @@ -470,6 +485,7 @@ impl Publish { audio, }, broadcast: Some(broadcast), + publication: Some(publication), route: None, stats: None, }) @@ -498,7 +514,7 @@ impl Publish { /// has declared its tracks: see [`Pending`]. pub fn announce(&mut self, epoch: moq_net::Epoch) -> anyhow::Result<()> { let route = moq_net::origin::Route::default().with_epoch(epoch); - match (&self.source, &self.broadcast) { + match (&self.source, &self.publication) { (_, None) => {} (Source::Stream { .. }, Some(_)) => self.route = Some(route), #[cfg(feature = "capture")] @@ -511,7 +527,7 @@ impl Publish { pub async fn run(self) -> anyhow::Result<()> { match self.source { Source::Stream { decoder, catalog } => { - let pending = match (&self.broadcast, self.route) { + let pending = match (&self.publication, self.route) { (Some(broadcast), Some(route)) => Some(Pending::new(broadcast.clone(), route)?), _ => None, }; @@ -920,11 +936,11 @@ pub(crate) mod tests { async fn manufacture_input() -> Vec { // Create the broadcast on a throwaway origin so the exporter can resolve it by path. let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("cli").unwrap(); + let broadcast = origin.create_broadcast("cli").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; let config = moq_mux::catalog::Config::default().with_catalog(Catalog::::default()); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config).unwrap(); // Reserve before the hand-written tracks below, so their catalog edits are withheld and // the import still places the clock, keeping bbb's PTS those tracks are stamped against. let reserved = catalog.reserve(); @@ -983,7 +999,7 @@ pub(crate) mod tests { pes_producer.finish().unwrap(); // Add the real video/audio (moves `broadcast` into the importer). - let mut import = Import::new(broadcast, reserved); + let mut import = Import::new(broadcast.broadcast().clone(), reserved); import.decode(&BytesMut::from(BBB)).unwrap(); import.finish().unwrap(); @@ -1036,7 +1052,8 @@ pub(crate) mod tests { let broadcast = origin.create_broadcast("cli").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut publish = Publish::new(broadcast, &PublishFormat::Ts { program: None }, Default::default()).unwrap(); + let mut publish = + Publish::from_publication(broadcast, &PublishFormat::Ts { program: None }, Default::default()).unwrap(); #[allow(irrefutable_let_patterns)] let Source::Stream { decoder, .. } = &mut publish.source else { panic!("expected a stream source"); @@ -1216,7 +1233,7 @@ pub(crate) mod tests { let origin = moq_tokio::origin::spawn(); let broadcast = origin.create_broadcast("pt.hang").unwrap(); broadcast.announce(Default::default()).unwrap(); - let mut publish = Publish::new( + let mut publish = Publish::from_publication( broadcast, &PublishFormat::TsPassthrough { pcr_pid: None }, Default::default(), @@ -1574,7 +1591,7 @@ pub(crate) mod tests { let consumer = origin.consume(); let mut announced = consumer.announced(); let broadcast = origin.create_broadcast("demo").unwrap(); - let publish = Publish::new(broadcast.clone(), &format, Default::default()).unwrap(); + let publish = Publish::from_publication(broadcast.clone(), &format, Default::default()).unwrap(); #[allow(irrefutable_let_patterns)] let Source::Stream { decoder, catalog } = publish.source else { panic!("expected a stream source"); @@ -1670,7 +1687,7 @@ pub(crate) mod tests { let origin = moq_tokio::origin::spawn(); let mut announced = origin.consume().announced(); let broadcast = origin.create_broadcast("demo").unwrap(); - let publish = Publish::new(broadcast.clone(), &format, Default::default()).unwrap(); + let publish = Publish::from_publication(broadcast.clone(), &format, Default::default()).unwrap(); #[allow(irrefutable_let_patterns)] let Source::Stream { decoder, catalog } = publish.source else { panic!("expected a stream source"); diff --git a/rs/moq-cli/src/report.rs b/rs/moq-cli/src/report.rs index fdbcf7be822..d54690a69bf 100644 --- a/rs/moq-cli/src/report.rs +++ b/rs/moq-cli/src/report.rs @@ -265,7 +265,7 @@ pub struct EchoTarget { struct Feedback { path: moq_net::PathOwned, // Held for the route: dropping it retracts the announcement. - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, tracks: Pair>, } @@ -354,8 +354,8 @@ impl Echo { } fn publish(&self, path: &moq_net::PathOwned) -> anyhow::Result { - let mut broadcast = self.target.origin.create_broadcast(path)?; - let tracks = Pair::create(&mut broadcast, hang::echo::TRACK)?; + let broadcast = self.target.origin.create_broadcast(path)?; + let tracks = Pair::create(&mut broadcast.broadcast().clone(), hang::echo::TRACK)?; // A fresh instance, so a reader sees a replaced feedback broadcast as a restart. broadcast.announce(moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()))?; Ok(Feedback { diff --git a/rs/moq-cli/src/rtc.rs b/rs/moq-cli/src/rtc.rs index 269944fefcb..67c539779b6 100644 --- a/rs/moq-cli/src/rtc.rs +++ b/rs/moq-cli/src/rtc.rs @@ -108,7 +108,7 @@ pub async fn connect_import(target: ImportTarget, url: Url, epoch: moq_net::Epoc config.max_age = target.max_age; config.bandwidth = target.bandwidth; let client = moq_rtc::Client::new(config); - client.subscribe(url, producer).await?; + client.subscribe(url, producer.broadcast().clone()).await?; Ok(()) } diff --git a/rs/moq-cli/src/rtsp.rs b/rs/moq-cli/src/rtsp.rs index d0d9326991c..2b4498d5310 100644 --- a/rs/moq-cli/src/rtsp.rs +++ b/rs/moq-cli/src/rtsp.rs @@ -69,14 +69,14 @@ fn is_fatal(err: &anyhow::Error) -> bool { /// Publish one session as one broadcast, announced once its catalog names a track, setting /// `published` then. async fn run(target: &ImportTarget, client: &moq_rtsp::Client, published: &mut bool) -> anyhow::Result { - let mut broadcast = target + let broadcast = target .origin .create_broadcast(&target.name) .context("failed to create broadcast")?; let config = moq_mux::catalog::Config::default() .with_max_age(target.max_age) .with_bandwidth(target.bandwidth.clone()); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config)?; // A relay asks for a broadcast's tracks the moment it is announced, so wait for the // first keyframe to put them in the catalog. diff --git a/rs/moq-cli/tests/integration/export.rs b/rs/moq-cli/tests/integration/export.rs index 96e22b0a86f..f6db6188fd5 100644 --- a/rs/moq-cli/tests/integration/export.rs +++ b/rs/moq-cli/tests/integration/export.rs @@ -510,12 +510,12 @@ async fn the_same_instance_within_the_linger_continues() { let epoch = moq_net::Epoch::mint(); let lite07 = ["--connect-version", LITE_07]; let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("demo").unwrap(); + let broadcast = origin.create_broadcast("demo").unwrap(); let config = moq_mux::catalog::Config::default() .with_max_age(Some(Duration::from_secs(1))) .with_catalog(moq_mux::catalog::hang::Catalog::::default()); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); - let mut import = ts::Import::new(broadcast.clone(), catalog.reserve()); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config).unwrap(); + let mut import = ts::Import::new(broadcast.broadcast().clone(), catalog.reserve()); broadcast .announce(moq_net::origin::Route::default().with_epoch(epoch.clone())) .unwrap(); diff --git a/rs/moq-cli/tests/integration/stats.rs b/rs/moq-cli/tests/integration/stats.rs index a2ef7bdce31..7976899579b 100644 --- a/rs/moq-cli/tests/integration/stats.rs +++ b/rs/moq-cli/tests/integration/stats.rs @@ -388,12 +388,12 @@ async fn export_echo_reports_what_the_export_reads() { let (expected, _) = written(CLIP); // A publisher soliciting feedback under `viewers`, a sibling of its broadcast. - let mut broadcast = relay.origin.create_broadcast("demo").expect("create the broadcast"); + let broadcast = relay.origin.create_broadcast("demo").expect("create the broadcast"); let config = moq_mux::catalog::Config::default() .with_max_age(Some(Duration::from_secs(1))) .with_catalog(moq_mux::catalog::hang::Catalog::::default()); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); - let mut import = ts::Import::new(broadcast.clone(), catalog.reserve()); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config).unwrap(); + let mut import = ts::Import::new(broadcast.broadcast().clone(), catalog.reserve()); let mut solicit = catalog.modify().unwrap(); solicit.echo = Some(hang::catalog::Echo::new("viewers".to_string())); solicit.commit().unwrap(); diff --git a/rs/moq-e2ee/tests/transport.rs b/rs/moq-e2ee/tests/transport.rs index 68665cffdc4..6d9343ac67c 100644 --- a/rs/moq-e2ee/tests/transport.rs +++ b/rs/moq-e2ee/tests/transport.rs @@ -26,7 +26,7 @@ fn produce_origin(hop: u64) -> moq_net::origin::Producer { struct Fixture { producer: moq_e2ee::track::Producer, consumer: moq_e2ee::track::Consumer, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _pair: MockPair, } @@ -44,7 +44,7 @@ async fn connect_protected(version: Version, track: &str) -> Fixture { let consumer_origin = produce_origin(2); let broadcast = publisher.create_broadcast(&path).unwrap(); - let protected = moq_e2ee::broadcast::Producer::new(broadcast.clone(), generation.clone()); + let protected = moq_e2ee::broadcast::Producer::new(broadcast.broadcast().clone(), generation.clone()); let producer = protected.create_track(track, timed()).unwrap(); broadcast .announce(moq_net::origin::Route::default().with_epoch(generation.epoch().clone())) diff --git a/rs/moq-ffi/src/origin.rs b/rs/moq-ffi/src/origin.rs index 6bbc6af4af4..dd051e349df 100644 --- a/rs/moq-ffi/src/origin.rs +++ b/rs/moq-ffi/src/origin.rs @@ -435,7 +435,7 @@ impl MoqOriginProducer { let result = self.inner.create_broadcast(path.as_str()); self.inner.driver.check()?; let broadcast = result?; - Ok(Arc::new(MoqBroadcastProducer::from_inner( + Ok(Arc::new(MoqBroadcastProducer::from_publication( broadcast, self.inner.driver.clone(), )?)) @@ -604,9 +604,14 @@ impl MoqBroadcastPrepared { let _guard = crate::ffi::enter(); self.driver.check()?; let prepared = self.inner.lock().unwrap().take().ok_or(MoqError::Closed)?; + let mut state = self.broadcast.state.lock().unwrap(); let result = prepared.publish(); self.driver.check()?; - result?; + let publication = result?; + if let Some(state) = state.as_mut() { + state.publication = Some(publication); + } + // A finished fill has released this state; its Finish retains the publication. Ok(self.broadcast.clone()) } } diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index cc78d7529fd..58cc95a1f37 100644 --- a/rs/moq-ffi/src/producer.rs +++ b/rs/moq-ffi/src/producer.rs @@ -90,6 +90,7 @@ impl TryFrom<&moq_net::track::Info> for MoqTrackInfo { pub(crate) struct BroadcastProducer { pub(crate) broadcast: moq_net::broadcast::Producer, + pub(crate) publication: Option, // Carries the untyped `Extra` extension so callers can attach application catalog // sections by name (the only extension shape that crosses the FFI boundary). pub(crate) catalog: moq_mux::catalog::Producer, @@ -204,6 +205,7 @@ impl MoqBroadcastProducer { Ok(Self { state: std::sync::Mutex::new(Some(BroadcastProducer { broadcast, + publication: None, catalog, timebases: Default::default(), driver, @@ -212,6 +214,19 @@ impl MoqBroadcastProducer { }) } + /// Retain the native publication capability alongside the binding's fill handle. + pub(crate) fn from_publication( + publication: moq_net::origin::Publication, + driver: Background, + ) -> Result { + let result = Self::from_inner(publication.broadcast().clone(), driver)?; + result.with_state(|state| { + state.publication = Some(publication); + Ok(()) + })?; + Ok(result) + } + #[cfg(test)] pub(crate) fn consume_inner(&self) -> Result { self.with_state(|state| Ok(state.broadcast.consume())) @@ -286,7 +301,7 @@ impl MoqBroadcastProducer { let _guard = crate::ffi::enter(); let route: moq_net::origin::Route = route.try_into()?; self.with_state(|state| { - state.broadcast.announce(route)?; + state.publication.as_ref().ok_or(MoqError::Closed)?.announce(route)?; Ok(()) }) } @@ -299,7 +314,9 @@ impl MoqBroadcastProducer { pub fn unannounce(&self) -> Result<(), MoqError> { let _guard = crate::ffi::enter(); self.with_state(|state| { - state.broadcast.unannounce(); + if let Some(publication) = &state.publication { + publication.unannounce(); + } Ok(()) }) } diff --git a/rs/moq-ffi/src/test.rs b/rs/moq-ffi/src/test.rs index 67c72461817..3ecba897532 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -3867,12 +3867,20 @@ async fn dynamic_broadcast_request() { .unwrap(); assert_eq!(request.path().unwrap(), "dynamic/broadcast"); - let prepared = request.prepare(moq_net::Epoch::mint().to_string()).unwrap(); + let epoch = moq_net::Epoch::mint().to_string(); + let prepared = request.prepare(epoch.clone()).unwrap(); let served = prepared.broadcast(); let track = served.publish_track("status".into(), Some(timed_info())).unwrap(); // Only publish announces the answer, under its epoch and after admission. assert!(matches!(served.announce(MoqRoute::default()), Err(MoqError::Closed))); prepared.publish().unwrap(); + served + .announce(MoqRoute { + epoch: Some(epoch), + cost: 7, + ..Default::default() + }) + .unwrap(); assert!(matches!(request.path(), Err(MoqError::Closed))); let broadcast = tokio::time::timeout(TIMEOUT, request_broadcast) @@ -3902,6 +3910,33 @@ async fn dynamic_broadcast_request() { served.close().unwrap(); } +/// A finished prepared answer remains publishable through its retained tail. +#[tokio::test(start_paused = true)] +async fn a_finished_prepared_fill_can_still_be_published() { + let origin = MoqOriginProducer::new(MoqOriginConfig::default()); + let dynamic = serve(&origin, ""); + let consumer = origin.consume(); + let pending = tokio::spawn(async move { consumer.request_broadcast("finished/answer".into()).await }); + let request = dynamic.requested_broadcast().await.unwrap(); + let prepared = request.prepare(moq_net::Epoch::mint().to_string()).unwrap(); + let fill = prepared.broadcast(); + let track = fill.publish_track("tail".into(), Some(timed_info())).unwrap(); + track + .write_frame(MoqFrame { + payload: b"tail".to_vec(), + timestamp_us: Some(0), + }) + .unwrap(); + track.finish().unwrap(); + drop(track); + let finished = fill.finish().unwrap(); + prepared.publish().unwrap(); + let resolved = pending.await.unwrap().unwrap(); + let track = resolved.subscribe_track("tail".into(), None).await.unwrap(); + assert_eq!(track.read_frame().await.unwrap().unwrap().payload, b"tail"); + finished.close(); +} + /// A prefix serves requests beneath it; cancelling the handle /// rejects what it parked and later requests are unroutable. #[tokio::test] diff --git a/rs/moq-gst/src/sink/imp.rs b/rs/moq-gst/src/sink/imp.rs index 2ecda4e23c3..e9fa225dec9 100644 --- a/rs/moq-gst/src/sink/imp.rs +++ b/rs/moq-gst/src/sink/imp.rs @@ -64,7 +64,7 @@ impl TryFrom for ResolvedSettings { /// Live state for one READY to PAUSED run. struct State { session: Session, - broadcast: moq_net::broadcast::Producer, + broadcast: moq_net::origin::Publication, catalog: Option, wall: std::sync::Arc, /// Whether a clean end keeps serving the finished broadcast until its media is stale. diff --git a/rs/moq-gst/src/sink/session.rs b/rs/moq-gst/src/sink/session.rs index 7b07596b63c..16dcb3d3005 100644 --- a/rs/moq-gst/src/sink/session.rs +++ b/rs/moq-gst/src/sink/session.rs @@ -284,7 +284,7 @@ impl Session { ) -> Result<( Self, SessionRegistration, - moq_net::broadcast::Producer, + moq_net::origin::Publication, moq_mux::catalog::Producer, )> { // Producer setup may touch tokio time (group eviction), so run it inside the runtime context. @@ -295,8 +295,9 @@ impl Session { // lingers, rather than resuming viewers into the old run's group sequence. Reconnects keep it. let epoch = moq_net::Epoch::mint(); gst::info!(CAT, "publishing {} under epoch {epoch}", settings.broadcast); - let mut broadcast = origin.publish(&settings.broadcast, moq_net::origin::Route::default().with_epoch(epoch))?; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default())?; + let broadcast = origin.publish(&settings.broadcast, moq_net::origin::Route::default().with_epoch(epoch))?; + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default())?; let status = Arc::new(Status::default()); let completion = CompletionState::new(); @@ -550,7 +551,7 @@ mod tests { assert_eq!(structure.get::("ended"), Ok(2)); } - fn started_with_broadcast() -> (Session, moq_net::broadcast::Producer) { + fn started_with_broadcast() -> (Session, moq_net::origin::Publication) { gst::init().unwrap(); let settings = ResolvedSettings { url: "https://127.0.0.1:1".parse().unwrap(), @@ -571,7 +572,7 @@ mod tests { (session, connection) } - fn announced_epoch(broadcast: &moq_net::broadcast::Producer) -> moq_net::Epoch { + fn announced_epoch(broadcast: &moq_net::origin::Publication) -> moq_net::Epoch { broadcast.route().expect("announced").epoch.expect("an epoch") } diff --git a/rs/moq-gst/src/source/imp.rs b/rs/moq-gst/src/source/imp.rs index 9316007c684..293cb0c7efd 100644 --- a/rs/moq-gst/src/source/imp.rs +++ b/rs/moq-gst/src/source/imp.rs @@ -1915,13 +1915,14 @@ mod session_tests { let _pad_ids = pad_ids(); let element = element(); let origin = origin(); - let mut catalog_broadcast = origin.create_broadcast("room/catalog").unwrap(); + let catalog_broadcast = origin.create_broadcast("room/catalog").unwrap(); let mut local = catalog_broadcast.dynamic(); let left = origin.create_broadcast("room/left").unwrap(); let mut remote = left.dynamic(); let right = origin.create_broadcast("room/right").unwrap(); let mut replacement = right.dynamic(); - let mut catalog = moq_mux::catalog::Producer::new(&mut catalog_broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut catalog_broadcast.broadcast().clone(), Default::default()).unwrap(); let mut config = video_rendition(); config.broadcast = Some("./left".into()); config.track = Some("encoded".into()); @@ -2694,6 +2695,7 @@ mod session_tests { /// A broadcast with one video rendition, and what feeds it. struct Publisher { + publication: Option, broadcast: moq_net::broadcast::Producer, catalog: moq_mux::catalog::Producer, video: moq_mux::container::Producer, @@ -2703,8 +2705,10 @@ mod session_tests { impl Publisher { /// Publish at `room` on `origin` under `route`. fn new(origin: &moq_net::origin::Producer, route: moq_net::origin::Route, config: VideoConfig) -> Self { - let publisher = Self::build(origin.create_broadcast("room").unwrap(), config); - publisher.broadcast.announce(route).unwrap(); + let publication = origin.create_broadcast("room").unwrap(); + let mut publisher = Self::build(publication.broadcast().clone(), config); + publication.announce(route).unwrap(); + publisher.publication = Some(publication); publisher } @@ -2728,6 +2732,7 @@ mod session_tests { moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Video), ); Self { + publication: None, broadcast, catalog, video, @@ -3131,7 +3136,12 @@ mod session_tests { let session = follow(&element, &origin); recorder.written(&mut publisher, 1); - publisher.broadcast.announce(route.with_cost(5)).unwrap(); + publisher + .publication + .as_ref() + .unwrap() + .announce(route.with_cost(5)) + .unwrap(); await_event(&mut announced, |event| { matches!(event, moq_net::announce::Event::Update(_)) }); @@ -3270,7 +3280,7 @@ mod session_tests { let session = follow(&element, &origin); let pad = recorder.written(&mut old, 1); - old.broadcast.unannounce(); + old.publication.as_ref().unwrap().unannounce(); await_event(&mut announced, |event| { matches!(event, moq_net::announce::Event::End(_)) }); diff --git a/rs/moq-hls/src/export/archive_tests.rs b/rs/moq-hls/src/export/archive_tests.rs index 8025a454d6c..148e5a7c866 100644 --- a/rs/moq-hls/src/export/archive_tests.rs +++ b/rs/moq-hls/src/export/archive_tests.rs @@ -287,7 +287,7 @@ struct Replay { reader: Option>, broadcaster: Arc, _catalog: moq_json::snapshot::Producer, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _origin: moq_net::origin::Producer, } diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index 2eed7f670e4..0d75f59f6de 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -1118,10 +1118,12 @@ mod tests { #[tokio::test] async fn master_lists_only_playable_renditions() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -1180,10 +1182,12 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_disabled_rendition_leaves_the_master_and_keeps_its_playlist() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -1261,10 +1265,11 @@ mod tests { async fn handoff(history: bool) { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let reserved = catalog.reserve(); let mut first = catalog.test_video("video0"); first.set(video_config()).unwrap(); @@ -1376,10 +1381,11 @@ mod tests { #[tokio::test(start_paused = true)] async fn unaligned_reference_handoff_marks_the_gap_without_reusing_urls() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); catalog .mutate(|catalog| { for name in ["video0", "video1"] { @@ -1507,10 +1513,11 @@ mod tests { /// The old reference cuts 2s records through 6s; the replacement cuts `span_ms` records. async fn edges_around_a_handoff(span_ms: u64) { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); catalog .mutate(|catalog| { for name in ["video0", "video1"] { @@ -1610,10 +1617,11 @@ mod tests { #[tokio::test(start_paused = true)] async fn history_handoff_keeps_following_the_original_sources_retention() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); catalog .mutate(|catalog| { for name in ["video0", "video1"] { @@ -1726,14 +1734,15 @@ mod tests { /// A durable broadcast advertising `video0` and `video1`, so history mode follows retention. async fn durable_pair() -> ( moq_net::origin::Producer, - moq_net::broadcast::Producer, + moq_net::origin::Publication, moq_mux::catalog::Producer, ) { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); catalog .mutate(|catalog| { for name in ["video0", "video1"] { @@ -1981,8 +1990,8 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_transmuxed_segment_keeps_its_init_with_an_unfinished_tail() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); - let (mut catalog, mut registration, _media) = publish_vp8(&mut broadcast); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); + let (mut catalog, mut registration, _media) = publish_vp8(&mut broadcast.broadcast().clone()); registration .set(hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8)) .unwrap(); @@ -2012,8 +2021,8 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_blocked_init_bootstrap_does_not_hide_a_cached_init() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); - let (mut catalog, mut registration, _media) = publish_vp8(&mut broadcast); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); + let (mut catalog, mut registration, _media) = publish_vp8(&mut broadcast.broadcast().clone()); registration .set(hang::catalog::VideoConfig::new(hang::catalog::VideoCodec::VP8)) .unwrap(); @@ -2049,10 +2058,10 @@ mod tests { #[tokio::test] async fn a_reconfigure_versions_the_init_url() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let (_catalog, mut registration, _media) = publish_vp8(&mut broadcast); + let (_catalog, mut registration, _media) = publish_vp8(&mut broadcast.broadcast().clone()); let broadcaster = Broadcaster::new(crate::resolved(&origin, "live").await, Config::default()); let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await; @@ -2094,10 +2103,10 @@ mod tests { #[tokio::test] async fn a_generation_versions_segment_urls_and_a_new_one_resets_the_window() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let (_catalog, _registration, mut media) = publish_vp8(&mut broadcast); + let (_catalog, _registration, mut media) = publish_vp8(&mut broadcast.broadcast().clone()); let broadcaster = Broadcaster::new(crate::resolved(&origin, "live").await, Config::default()); let _ = tokio::time::timeout(Duration::from_secs(5), broadcaster.ready()).await; @@ -2150,10 +2159,12 @@ mod tests { #[tokio::test] async fn a_new_generation_rebuilds_an_inline_init() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); let mut config = video_config(); @@ -2201,10 +2212,12 @@ mod tests { #[tokio::test] async fn serves_playlist_and_segments_from_the_timeline() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -2289,10 +2302,12 @@ mod tests { #[tokio::test] async fn a_duration_marker_times_the_hls_trailing_sample() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -2333,10 +2348,12 @@ mod tests { #[tokio::test] async fn serves_dash_manifest_and_time_addressed_segments() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut video_registration = catalog.test_video("video0").unwrap(); @@ -2434,7 +2451,7 @@ mod tests { use std::time::{SystemTime, UNIX_EPOCH}; let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; @@ -2442,7 +2459,7 @@ mod tests { let wall = UNIX_EPOCH + Duration::from_millis(hang::catalog::MOQ_EPOCH_UNIX_MILLIS); let config = moq_mux::catalog::Config::default() .with_clock(moq_mux::Clock::at(moq_net::time::now(), wall).expect("a representable wall")); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config).unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -2571,11 +2588,13 @@ mod tests { } let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let mut import = moq_mux::container::fmp4::Import::new(broadcast, catalog.reserve()); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); + let mut import = moq_mux::container::fmp4::Import::new(broadcast.broadcast().clone(), catalog.reserve()); import.decode(&init).unwrap(); // Watch before the first fragment, as a live viewer would, so a catalog the moov @@ -2677,11 +2696,11 @@ mod tests { } else { None }; - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); - let mut import = moq_mux::container::fmp4::Import::new(broadcast, catalog.reserve()); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); + let mut import = moq_mux::container::fmp4::Import::new(broadcast.broadcast().clone(), catalog.reserve()); import.decode(&init).unwrap(); let snapshot = catalog.snapshot(); let video_muxer = @@ -2752,10 +2771,12 @@ mod tests { #[tokio::test] async fn dash_manifest_turns_static_when_finished() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -2814,10 +2835,12 @@ mod tests { #[tokio::test] async fn renditions_snap_to_their_own_keyframes() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registrations = Vec::new(); @@ -2903,10 +2926,12 @@ mod tests { #[tokio::test] async fn an_audio_only_broadcast_segments_on_its_audio() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_audio("audio0").unwrap(); @@ -2955,10 +2980,12 @@ mod tests { #[tokio::test] async fn target_duration_follows_the_observed_segments() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -2995,10 +3022,12 @@ mod tests { #[tokio::test] async fn audio_and_video_segments_are_aligned() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut video_registration = catalog.test_video("video0").unwrap(); @@ -3088,10 +3117,12 @@ mod tests { #[tokio::test] async fn dropping_the_broadcaster_keeps_a_cursor_drainable() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -3184,10 +3215,11 @@ mod tests { async fn ended_rendition(sibling: bool) -> Ended { const KEYFRAME: &[u8] = &[0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00]; let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").unwrap(); + let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); - let mut target = if sibling { + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); + let target = if sibling { let target = origin.create_broadcast("media").unwrap(); target.announce(Default::default()).unwrap(); target @@ -3195,7 +3227,7 @@ mod tests { broadcast.clone() }; let recorder = catalog.enroll_test("video0").unwrap(); - let mut media = write_routed_media(&mut target, KEYFRAME, recorder, 0); + let mut media = write_routed_media(&mut target.broadcast().clone(), KEYFRAME, recorder, 0); media.finish().unwrap(); catalog.finish().unwrap(); settle().await; @@ -3329,9 +3361,13 @@ mod tests { origin: &moq_net::origin::Producer, payload: &'static [u8], ) -> (Box, hang::catalog::VideoConfig) { - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = moq_mux::catalog::Producer::new( + &mut broadcast.broadcast().clone(), + moq_mux::catalog::Config::default(), + ) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -3447,9 +3483,11 @@ mod tests { let origin = produce_origin(); // The catalog broadcast carries the catalog and the timeline; the media lives next door. - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let old_media = publish_media(&origin, OLD, Some(recorder)); @@ -3577,9 +3615,11 @@ mod tests { const NEW: &[u8] = b"NEWNEWNEWNEWNEWN"; let origin = produce_origin(); - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let mut old_media = moq_net::broadcast::Info::new().produce(); @@ -3664,9 +3704,11 @@ mod tests { const KEYFRAME: &[u8] = &[0x10, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00]; let origin = produce_origin(); - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let mut media = moq_net::broadcast::Info::new().produce(); @@ -3727,9 +3769,11 @@ mod tests { const NEW: &[u8] = b"NEWNEWNEWNEWNEWN"; let origin = produce_origin(); - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let mut media = moq_net::broadcast::Info::new().produce(); @@ -3766,9 +3810,11 @@ mod tests { const NEW: &[u8] = b"NEWNEWNEWNEWNEWN"; let origin = produce_origin(); - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let mut old_media = moq_net::broadcast::Info::new().produce(); @@ -3831,9 +3877,11 @@ mod tests { const OLD: &[u8] = b"OLDOLDOLDOLDOLDO"; let origin = produce_origin(); - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let mut media = moq_net::broadcast::Info::new().produce(); let track = write_routed_media(&mut media, OLD, recorder, 0); @@ -3880,9 +3928,11 @@ mod tests { const OLD: &[u8] = b"OLDOLDOLDOLDOLDO"; let origin = produce_origin(); - let mut live = origin.create_broadcast("live").expect("publish allowed"); + let live = origin.create_broadcast("live").expect("publish allowed"); live.announce(Default::default()).expect("publish allowed"); - let catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); let mut media = moq_net::broadcast::Info::new().produce(); let _track = write_routed_media(&mut media, OLD, recorder, 0); @@ -3994,10 +4044,12 @@ mod tests { #[tokio::test] async fn dropping_the_broadcaster_releases_its_renditions() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -4052,10 +4104,12 @@ mod tests { #[tokio::test] async fn record_cursors_yield_renditions_and_segments() { let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut registration = catalog.test_video("video0").unwrap(); @@ -4490,10 +4544,12 @@ mod tests { const NEW: &[u8] = b"NEWNEWNEWNEWNEWN"; let origin = produce_origin(); - let mut broadcast = origin.create_broadcast("live").expect("publish allowed"); + let broadcast = origin.create_broadcast("live").expect("publish allowed"); broadcast.announce(Default::default()).expect("publish allowed"); settle().await; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let reserved = catalog.reserve(); let mut video0 = catalog.test_video("video0").unwrap(); diff --git a/rs/moq-hls/src/server/routes.rs b/rs/moq-hls/src/server/routes.rs index 08b28075ccd..1c0802a2849 100644 --- a/rs/moq-hls/src/server/routes.rs +++ b/rs/moq-hls/src/server/routes.rs @@ -744,9 +744,10 @@ mod tests { #[tokio::test] async fn respond_serves_a_route_rewritten_into_scope() { let pair = lite_pair().await; - let mut broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); + let broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); - let (_catalog, _registration, _track, mut media) = publish_video(&mut broadcast, video_config(), timed()); + let (_catalog, _registration, _track, mut media) = + publish_video(&mut broadcast.broadcast().clone(), video_config(), timed()); write_three_gops(&mut media); let server = Server::new(pair.sub_origin.consume(), crate::export::Config::default()); @@ -787,12 +788,12 @@ mod tests { "moq-lite-05", ] { let pair = lite_pair_on(version, moq_net::Hop::random()).await; - let mut broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); + let broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); // A hinted timeline is declared before any media, so the rendition is listed early. let hinted = moq_mux::timeline::Config::default().with_duration_hint(Duration::from_secs(2)); let catalog = moq_mux::catalog::Producer::new( - &mut broadcast, + &mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default().with_timeline(hinted), ) .unwrap(); @@ -863,9 +864,10 @@ mod tests { #[tokio::test(start_paused = true)] async fn master_answers_404_until_a_rendition_can_start() { let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("live").expect("publish"); + let broadcast = origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); - let (_catalog, _registration, _track, mut media) = publish_video(&mut broadcast, video_config(), timed()); + let (_catalog, _registration, _track, mut media) = + publish_video(&mut broadcast.broadcast().clone(), video_config(), timed()); let app = Server::new(origin.consume(), crate::export::Config::default()).router(); assert_eq!(status(&app, "/live/master.m3u8").await, StatusCode::NOT_FOUND); @@ -879,9 +881,10 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_failed_timeline_answers_an_uncacheable_error() { let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("live").expect("publish"); + let broadcast = origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let name = hang::timeline::default_name("video0"); catalog .mutate(|catalog| { @@ -939,9 +942,10 @@ mod tests { #[tokio::test(start_paused = true)] async fn serves_only_versioned_media_urls() { let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("live").expect("publish"); + let broadcast = origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); - let (_catalog, _registration, _track, mut media) = publish_video(&mut broadcast, video_config(), timed()); + let (_catalog, _registration, _track, mut media) = + publish_video(&mut broadcast.broadcast().clone(), video_config(), timed()); write_three_gops(&mut media); let server = Server::new(origin.consume(), crate::export::Config::default()); @@ -1006,9 +1010,10 @@ mod tests { config }) .await; - let mut broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); + let broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); - let (_catalog, _registration, _track, mut media) = publish_video(&mut broadcast, video_config(), timed()); + let (_catalog, _registration, _track, mut media) = + publish_video(&mut broadcast.broadcast().clone(), video_config(), timed()); write_three_gops(&mut media); let app = Server::new(pair.sub_origin.consume(), crate::export::Config::default()).router(); @@ -1024,9 +1029,10 @@ mod tests { #[tokio::test] async fn a_disconnected_publisher_on_the_catalog_broadcast_answers_404() { let pair = lite_pair().await; - let mut broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); + let broadcast = pair.pub_origin.create_broadcast("live").expect("publish"); broadcast.announce(Default::default()).expect("announce"); - let (catalog, registration, track, mut media) = publish_video(&mut broadcast, video_config(), timed()); + let (catalog, registration, track, mut media) = + publish_video(&mut broadcast.broadcast().clone(), video_config(), timed()); write_three_gops(&mut media); let app = Server::new(pair.sub_origin.consume(), crate::export::Config::default()).router(); @@ -1116,15 +1122,18 @@ mod tests { #[tokio::test] async fn a_disconnected_sibling_publisher_answers_404() { let pair = lite_pair().await; - let mut catalog_broadcast = pair.pub_origin.create_broadcast("room/live").expect("catalog"); + let catalog_broadcast = pair.pub_origin.create_broadcast("room/live").expect("catalog"); catalog_broadcast .announce(Default::default()) .expect("announce catalog"); let media_broadcast = pair.pub_origin.create_broadcast("room/source").expect("media"); media_broadcast.announce(Default::default()).expect("announce media"); - let catalog = - moq_mux::catalog::Producer::new(&mut catalog_broadcast, moq_mux::catalog::Config::default()).unwrap(); + let catalog = moq_mux::catalog::Producer::new( + &mut catalog_broadcast.broadcast().clone(), + moq_mux::catalog::Config::default(), + ) + .unwrap(); let reserved = catalog.reserve(); let mut config = video_config(); config.broadcast = Some(moq_net::path::Relative::new("../source").to_owned()); diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index dd22ffde43e..a4b6f6ca7fb 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -1455,7 +1455,7 @@ mod test { let (origin, driver) = moq_net::origin::Producer::new(Default::default()); let driver = tokio::spawn(moq_net::time::run(driver)); let mut broadcast = match path { - Some(path) => origin.create_broadcast(path).unwrap(), + Some(path) => origin.create_broadcast(path).unwrap().broadcast().clone(), None => moq_net::broadcast::Info::new().produce(), }; let mut producer = Producer::new(&mut broadcast, Config::default()).unwrap(); @@ -2061,11 +2061,11 @@ mod test { let wall = SystemTime::UNIX_EPOCH + Duration::from_millis(hang::catalog::MOQ_EPOCH_UNIX_MILLIS + 60_000); let clock = crate::Clock::at(captured, wall).unwrap(); let (origin, _driver) = moq_net::origin::Producer::new(Default::default()); - let mut front = origin.create_broadcast("robot/front.hang").unwrap(); - let mut rear = origin.create_broadcast("robot/rear.hang").unwrap(); + let front = origin.create_broadcast("robot/front.hang").unwrap(); + let rear = origin.create_broadcast("robot/rear.hang").unwrap(); let catalogs = [ - Producer::new(&mut front, Config::default().with_clock(clock)).unwrap(), - Producer::new(&mut rear, Config::default().with_clock(clock)).unwrap(), + Producer::new(&mut front.broadcast().clone(), Config::default().with_clock(clock)).unwrap(), + Producer::new(&mut rear.broadcast().clone(), Config::default().with_clock(clock)).unwrap(), ]; let mut broadcasts = [front, rear]; for (catalog, broadcast) in catalogs.iter().zip(&mut broadcasts) { diff --git a/rs/moq-mux/src/container/ts/export_test.rs b/rs/moq-mux/src/container/ts/export_test.rs index 895d4b725ce..fc0c222a084 100644 --- a/rs/moq-mux/src/container/ts/export_test.rs +++ b/rs/moq-mux/src/container/ts/export_test.rs @@ -7977,10 +7977,10 @@ async fn a_track_leaving_the_catalog_is_read_to_its_end() { fn publish_live( origin: &moq_net::origin::Producer, route: moq_net::origin::Route, -) -> (moq_net::broadcast::Producer, crate::catalog::Producer) { - let mut broadcast = origin.publish("live", route).unwrap(); +) -> (moq_net::origin::Publication, crate::catalog::Producer) { + let broadcast = origin.publish("live", route).unwrap(); let config = crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()); - let catalog = crate::catalog::Producer::new(&mut broadcast, config).unwrap(); + let catalog = crate::catalog::Producer::new(&mut broadcast.broadcast().clone(), config).unwrap(); (broadcast, catalog) } @@ -8074,8 +8074,8 @@ async fn same_instance_returns(finish: bool) { let epoch = moq_net::Epoch::mint(); let route = || moq_net::origin::Route::default().with_epoch(epoch.clone()); - let (mut broadcast, mut catalog) = publish_live::<()>(&origin, route()); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_live::<()>(&origin, route()); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let mut export = open_export(crate::source::resolved(&origin, "live").await) .await .unwrap() @@ -8098,13 +8098,13 @@ async fn same_instance_returns(finish: bool) { // Back under the same epoch a second later, its media as far on as the wall clock, its // catalog listing the track only in its second snapshot. tokio::time::sleep(Duration::from_secs(1)).await; - let (mut broadcast, mut catalog) = publish_live::<()>(&origin, route()); + let (broadcast, mut catalog) = publish_live::<()>(&origin, route()); std::ops::DerefMut::deref_mut(&mut catalog.modify().unwrap()); let back = origin.consume().routed_broadcast("live").await.unwrap(); let stream = crate::catalog::Consumer::new(&back, Default::default()).await.unwrap(); let mut export = export.follow(back, stream).unwrap(); frames.extend(drain_frames(&mut export).await); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let resumed = start.elapsed().as_millis() as u64; for ms in (resumed..resumed + 200).step_by(20) { write_aac(&mut track, ms); @@ -8149,8 +8149,8 @@ async fn a_return_keeps_catalog_and_group_history() { let origin = crate::source::produce_origin(); let epoch = moq_net::Epoch::mint(); let route = moq_net::origin::Route::default().with_epoch(epoch.clone()); - let (mut broadcast, mut catalog) = publish_live::<()>(&origin, route.clone()); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_live::<()>(&origin, route.clone()); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let mut groups = track.consume(); let clock = catalog.snapshot().clock; let source = crate::source::resolved(&origin, "live").await; @@ -8206,8 +8206,8 @@ async fn a_switch_carries_through_an_instance_that_never_built_its_tables() { let origin = crate::source::produce_origin(); let route = || moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()); - let (mut old, mut old_catalog) = publish_live::<()>(&origin, route()); - let mut old_audio = aac_rendition(&mut old, &mut old_catalog, "a.aac"); + let (old, mut old_catalog) = publish_live::<()>(&origin, route()); + let mut old_audio = aac_rendition(&mut old.broadcast().clone(), &mut old_catalog, "a.aac"); let mut export = open_export(crate::source::resolved(&origin, "live").await) .await .unwrap() @@ -8230,8 +8230,8 @@ async fn a_switch_carries_through_an_instance_that_never_built_its_tables() { ); // ...and then to another. - let (mut new, mut new_catalog) = publish_live::<()>(&origin, route()); - let mut new_audio = aac_rendition(&mut new, &mut new_catalog, "b.aac"); + let (new, mut new_catalog) = publish_live::<()>(&origin, route()); + let mut new_audio = aac_rendition(&mut new.broadcast().clone(), &mut new_catalog, "b.aac"); let replacement = origin.consume().request_broadcast("live", None).await.unwrap(); let stream = crate::catalog::Consumer::new(&replacement, Default::default()) .await @@ -8264,8 +8264,8 @@ async fn another_instance_is_a_full_program_switch() { let origin = crate::source::produce_origin(); let route = || moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()); - let (mut old, mut old_catalog) = publish_live::<()>(&origin, route()); - let mut old_audio = aac_rendition(&mut old, &mut old_catalog, "a.aac"); + let (old, mut old_catalog) = publish_live::<()>(&origin, route()); + let mut old_audio = aac_rendition(&mut old.broadcast().clone(), &mut old_catalog, "a.aac"); let mut export = open_export(crate::source::resolved(&origin, "live").await) .await .unwrap() @@ -8280,9 +8280,9 @@ async fn another_instance_is_a_full_program_switch() { Some(vec![StreamType::AdtsAac]) ); - let (mut new, mut new_catalog) = publish_live::<()>(&origin, route()); - let mut video = h264_rendition(&mut new, &mut new_catalog, "video.avc1"); - let mut new_audio = aac_rendition(&mut new, &mut new_catalog, "b.aac"); + let (new, mut new_catalog) = publish_live::<()>(&origin, route()); + let mut video = h264_rendition(&mut new.broadcast().clone(), &mut new_catalog, "video.avc1"); + let mut new_audio = aac_rendition(&mut new.broadcast().clone(), &mut new_catalog, "b.aac"); let replacement = origin.consume().request_broadcast("live", None).await.unwrap(); let stream = crate::catalog::Consumer::new(&replacement, Default::default()) .await @@ -8344,8 +8344,8 @@ async fn a_switch_moving_the_pmt_advances_the_pat() { let origin = crate::source::produce_origin(); let route = || moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()); let publish = |pmt_pid: u16| { - let (mut broadcast, mut catalog) = publish_live::(&origin, route()); - let track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_live::(&origin, route()); + let track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); catalog.modify().unwrap().ext.mpegts.program = Some(tscat::Program { transport_stream_id: 7, program_number: 1, @@ -8400,8 +8400,8 @@ async fn export_stats_count_only_returned_output() { let route = || moq_net::origin::Route::default().with_epoch(epoch.clone()); let units = |stats: stats::Export| stats.streams.values().map(|row| row.units).sum::() as usize; - let (mut broadcast, mut catalog) = publish_live::<()>(&origin, route()); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_live::<()>(&origin, route()); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let mut export = open_export(crate::source::resolved(&origin, "live").await) .await .unwrap() @@ -8425,8 +8425,8 @@ async fn export_stats_count_only_returned_output() { assert_eq!(units(export.stats()), pes_count(&frames)); drop((broadcast, catalog, track)); - let (mut broadcast, mut catalog) = publish_live::<()>(&origin, route()); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_live::<()>(&origin, route()); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let back = origin.consume().routed_broadcast("live").await.unwrap(); let stream = crate::catalog::Consumer::new(&back, Default::default()).await.unwrap(); let mut export = export.follow(back, stream).unwrap(); @@ -8449,9 +8449,9 @@ async fn export_stats_count_only_returned_output() { /// Drained after every write, like [`export_twice`], so the export reads every frame and a row /// stops only because its track did. async fn export_liveness(sample: u64, stop: u64) -> (stats::Export, stats::Export, Vec) { - let mut broadcast = moq_net::broadcast::Info::new().produce(); + let broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); - let mut catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap(); + let mut catalog = crate::catalog::Producer::new(&mut broadcast.clone(), crate::catalog::Config::default()).unwrap(); let track = broadcast .create_track("video.avc1", hang::container::track_info(hang::catalog::PRIORITY.video)) @@ -8473,8 +8473,8 @@ async fn export_liveness(sample: u64, stop: u64) -> (stats::Export, stats::Expor .insert(track.name().to_string(), cfg); let mut video = Producer::new(track, HangContainer::Legacy(crate::container::Kind::Data)); let mut audio = [ - aac_rendition(&mut broadcast, &mut catalog, "primary.aac"), - aac_rendition(&mut broadcast, &mut catalog, "secondary.aac"), + aac_rendition(&mut broadcast.clone(), &mut catalog, "primary.aac"), + aac_rendition(&mut broadcast.clone(), &mut catalog, "secondary.aac"), ]; let mut export = open_export(crate::source::announced(&consumer).await).await.unwrap(); @@ -8634,9 +8634,9 @@ async fn a_large_pmt_keeps_within_the_system_buffer() { /// Export `streams` language-tagged AAC streams, check the PMT reassembles across its packets /// and keeps within TBsys and Bsys at `mux_rate`, and return how many packets it spans. async fn large_pmt(streams: u16, mux_rate: Option) -> usize { - let mut broadcast = moq_net::broadcast::Info::new().produce(); + let broadcast = moq_net::broadcast::Info::new().produce(); let mut catalog = crate::catalog::Producer::new( - &mut broadcast, + &mut broadcast.clone(), crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), ) .unwrap(); @@ -8748,7 +8748,7 @@ async fn large_pmt(streams: u16, mux_rate: Option) -> usize { fn publish_bare( origin: &moq_net::origin::Producer, epoch: Option<&moq_net::Epoch>, -) -> (moq_net::broadcast::Producer, crate::catalog::Producer<()>) { +) -> (moq_net::origin::Publication, crate::catalog::Producer<()>) { let route = moq_net::origin::Route::default(); publish_live::<()>( origin, @@ -8763,7 +8763,7 @@ async fn follower(origin: &moq_net::origin::Producer) -> super::Follower { } /// End a catalog-only broadcast cleanly. -fn finish((broadcast, mut catalog): (moq_net::broadcast::Producer, crate::catalog::Producer<()>)) { +fn finish((broadcast, mut catalog): (moq_net::origin::Publication, crate::catalog::Producer<()>)) { catalog.finish().unwrap(); drop((broadcast, catalog)); } @@ -8773,8 +8773,8 @@ fn finish((broadcast, mut catalog): (moq_net::broadcast::Producer, crate::catalo #[tokio::test(start_paused = true)] async fn a_follower_fails_with_the_broadcast_up_after_the_grace() { let origin = crate::source::produce_origin(); - let (mut broadcast, mut catalog) = publish_bare(&origin, Some(&moq_net::Epoch::mint())); - let track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_bare(&origin, Some(&moq_net::Epoch::mint())); + let track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let mut follower = follower(&origin).await.with_linger(Duration::from_secs(10)); assert!( tokio::time::timeout(Duration::from_secs(1), follower.next()) @@ -9070,8 +9070,8 @@ async fn a_follower_keeps_a_return_awaiting_its_catalog_when_a_stitch_goes() { async fn a_follower_follows_a_return_that_came_back_during_the_grace() { let origin = crate::source::produce_origin(); let epoch = moq_net::Epoch::mint(); - let (mut broadcast, mut catalog) = publish_bare(&origin, Some(&epoch)); - let track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_bare(&origin, Some(&epoch)); + let track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let mut follower = follower(&origin).await.with_linger(Duration::from_secs(10)); assert!( tokio::time::timeout(Duration::from_secs(1), follower.next()) @@ -9089,8 +9089,8 @@ async fn a_follower_follows_a_return_that_came_back_during_the_grace() { ); // The end and the return both arrive before the follower looks again. drop((broadcast, catalog)); - let (mut broadcast, mut catalog) = publish_bare(&origin, Some(&epoch)); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_bare(&origin, Some(&epoch)); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); assert!( tokio::time::timeout(Duration::from_secs(3), follower.next()) .await @@ -9292,9 +9292,9 @@ async fn a_follower_continues_through_a_same_epoch_handoff_after_a_gap() { let origin = crate::source::produce_origin(); let epoch = moq_net::Epoch::mint(); let route = moq_net::origin::Route::default().with_epoch(epoch.clone()); - let mut exact = origin.publish("pool/job", route.clone()).unwrap(); - let mut catalog = crate::catalog::Producer::<()>::new(&mut exact, Default::default()).unwrap(); - let mut track = aac_rendition(&mut exact, &mut catalog, "a.aac"); + let exact = origin.publish("pool/job", route.clone()).unwrap(); + let mut catalog = crate::catalog::Producer::<()>::new(&mut exact.broadcast().clone(), Default::default()).unwrap(); + let mut track = aac_rendition(&mut exact.broadcast().clone(), &mut catalog, "a.aac"); let source = crate::source::resolved(&origin, "pool/job").await; let export = open_export(source) .await @@ -9360,8 +9360,8 @@ async fn a_follower_continues_through_a_same_epoch_handoff_after_a_gap() { async fn a_follower_ignores_a_covering_prefix_while_the_exact_route_serves() { let origin = crate::source::produce_origin(); let route = |epoch| moq_net::origin::Route::default().with_epoch(epoch); - let mut exact = origin.publish("pool/job", route(moq_net::Epoch::mint())).unwrap(); - let catalog = crate::catalog::Producer::<()>::new(&mut exact, Default::default()).unwrap(); + let exact = origin.publish("pool/job", route(moq_net::Epoch::mint())).unwrap(); + let catalog = crate::catalog::Producer::<()>::new(&mut exact.broadcast().clone(), Default::default()).unwrap(); let source = crate::source::resolved(&origin, "pool/job").await; let mut follower = super::Follower::new(open_export(source).await.unwrap()).unwrap(); let quiet = Duration::from_secs(1); @@ -9654,7 +9654,7 @@ fn audio_pts(frames: &[Frame]) -> Vec { #[tokio::test(start_paused = true)] async fn a_late_rendition_never_reads_a_replacement() { let origin = crate::source::produce_origin(); - let (mut old, mut old_catalog) = publish_live::<()>(&origin, Default::default()); + let (old, mut old_catalog) = publish_live::<()>(&origin, Default::default()); let mut export = open_export(crate::source::resolved(&origin, "live").await) .await .unwrap() @@ -9663,13 +9663,13 @@ async fn a_late_rendition_never_reads_a_replacement() { assert!(drain_frames(&mut export).await.is_empty(), "nothing listed yet"); // Another instance takes the path, its audio ten seconds ahead. - let (mut new, mut new_catalog) = publish_live::<()>(&origin, Default::default()); - let mut new_audio = aac_rendition(&mut new, &mut new_catalog, "a.aac"); + let (new, mut new_catalog) = publish_live::<()>(&origin, Default::default()); + let mut new_audio = aac_rendition(&mut new.broadcast().clone(), &mut new_catalog, "a.aac"); for ms in (10_000..10_200).step_by(20) { write_aac(&mut new_audio, ms); } - let mut old_audio = aac_rendition(&mut old, &mut old_catalog, "a.aac"); + let mut old_audio = aac_rendition(&mut old.broadcast().clone(), &mut old_catalog, "a.aac"); for ms in (0..200).step_by(20) { write_aac(&mut old_audio, ms); } @@ -9700,10 +9700,10 @@ async fn a_repointed_si_entry_never_reads_a_replacement() { }; let origin = crate::source::produce_origin(); - let (mut old, mut catalog) = publish_live::(&origin, Default::default()); - let mut video = h264_rendition(&mut old, &mut catalog, "video.avc1"); - let _a = si_track(&mut old, "a.si", &sdt_a); - let _b = si_track(&mut old, "b.si", &sdt_b); + let (old, mut catalog) = publish_live::(&origin, Default::default()); + let mut video = h264_rendition(&mut old.broadcast().clone(), &mut catalog, "video.avc1"); + let _a = si_track(&mut old.broadcast().clone(), "a.si", &sdt_a); + let _b = si_track(&mut old.broadcast().clone(), "b.si", &sdt_b); catalog .modify() .unwrap() @@ -9731,8 +9731,8 @@ async fn a_repointed_si_entry_never_reads_a_replacement() { .collect(); // Another instance takes the path with its own `b.si`, and only then is the entry repointed. - let mut new = origin.publish("live", Default::default()).unwrap(); - let _new_b = si_track(&mut new, "b.si", &sdt_new); + let new = origin.publish("live", Default::default()).unwrap(); + let _new_b = si_track(&mut new.broadcast().clone(), "b.si", &sdt_new); catalog .modify() .unwrap() @@ -9769,16 +9769,16 @@ async fn a_switch_reads_the_instance_it_followed() { .with_delay(RECORDING_MAX_AGE) .with_replay(); - let (mut followed, mut followed_catalog) = publish_live::<()>(&origin, Default::default()); - let mut followed_audio = aac_rendition(&mut followed, &mut followed_catalog, "a.aac"); + let (followed, mut followed_catalog) = publish_live::<()>(&origin, Default::default()); + let mut followed_audio = aac_rendition(&mut followed.broadcast().clone(), &mut followed_catalog, "a.aac"); for ms in (0..200).step_by(20) { write_aac(&mut followed_audio, ms); } let broadcast = origin.consume().request_broadcast("live", None).await.unwrap(); // Replaced again, ten seconds ahead, before the switched export requests its track. - let (mut newer, mut newer_catalog) = publish_live::<()>(&origin, Default::default()); - let mut newer_audio = aac_rendition(&mut newer, &mut newer_catalog, "a.aac"); + let (newer, mut newer_catalog) = publish_live::<()>(&origin, Default::default()); + let mut newer_audio = aac_rendition(&mut newer.broadcast().clone(), &mut newer_catalog, "a.aac"); for ms in (10_000..10_200).step_by(20) { write_aac(&mut newer_audio, ms); } @@ -9897,10 +9897,10 @@ impl Placed { /// Export `placed` beside an AAC rendition running past its last PES, returning every slice. async fn export_placed(placed: Placed) -> crate::Result> { - let mut broadcast = moq_net::broadcast::Info::new().produce(); + let broadcast = moq_net::broadcast::Info::new().produce(); let consumer = broadcast.consume(); let mut catalog = crate::catalog::Producer::new( - &mut broadcast, + &mut broadcast.clone(), crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::::default()), ) .unwrap(); diff --git a/rs/moq-mux/src/container/ts/programs.rs b/rs/moq-mux/src/container/ts/programs.rs index e48dd5780ce..435df148866 100644 --- a/rs/moq-mux/src/container/ts/programs.rs +++ b/rs/moq-mux/src/container/ts/programs.rs @@ -32,8 +32,8 @@ pub struct Programs { struct Program { /// The PAT's program number. number: u16, - /// Keeps the origin-created broadcast alive; the importer's clone doesn't. - broadcast: moq_net::broadcast::Producer, + /// Owns announcement control while the importer holds a fill clone. + broadcast: moq_net::origin::Publication, import: Import, catalog: catalog::Producer, /// The route and catalog track until the catalog first names a servable track: announced any @@ -85,7 +85,7 @@ impl Programs { }; for program in pat.program_numbers() { let name = program_broadcast(self.name.as_str(), program); - let mut broadcast = self + let broadcast = self .origin .create_broadcast(&name) .with_context(|| format!("failed to create broadcast {name}"))?; @@ -95,8 +95,8 @@ impl Programs { .config .clone() .with_catalog(catalog::hang::Catalog::::default()); - let catalog = catalog::Producer::new(&mut broadcast, config)?; - let mut import = Import::new(broadcast.clone(), catalog.reserve()).with_program(program); + let catalog = catalog::Producer::new(&mut broadcast.broadcast().clone(), config)?; + let mut import = Import::new(broadcast.broadcast().clone(), catalog.reserve()).with_program(program); // Seeded with the scanner's PAT rather than replaying its packets, so every section // reaches the importer and nothing the scanner counted is counted again. import.handle_pat(&pat)?; diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index e16d361e179..fe520cb50e1 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,15 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Announcement control belongs to a publication + +`origin::Producer::create_broadcast`, `publish`, and `Prepared::publish` return `origin::Publication`. +Use its `announce`, `unannounce`, and `route` methods to control the origin advertisement. +Use `publication.broadcast()` for a borrowed `broadcast::Producer` fill surface; `Publication` also forwards fill operations. +A standalone producer and `Prepared::broadcast()` expose no public announcement methods. +Cloning a publication shares its broadcast and route. A cloned fill producer or `broadcast::Finish` keeps the publication alive after the control handle drops; call `unannounce` to retract sooner. +Use `publication.finish()` to preserve finished tracks and their existing linger. + ### IETF whole-group FETCH answers name the requested group ([#6199](https://github.com/moq-dev/moq/issues/6199)) On moq-transport drafts 14 through 19, a whole-group `FETCH_OK` now names `{g, 0}` instead of `{g+1, 0}`. @@ -125,8 +134,8 @@ Treat a failed group read as the loss of that group and keep reading the track, ### A claim answers by publishing (#5678) `origin::Producer::dynamic(prefix)` takes no route: an application claim has no epoch, cost, or hops. -Answer each `origin::Request` with `request.prepare(epoch)?` (a fresh `Epoch::mint()` per output run), fill `prepared.broadcast()`, then `prepared.publish()?`, which announces the broadcast at the exact path under that epoch and returns its producer. -Only `publish` announces it: `prepared.broadcast().announce(..)` fails with `Error::Closed`. +Answer each `origin::Request` with `request.prepare(epoch)?` (a fresh `Epoch::mint()` per output run), fill `prepared.broadcast()`, then `prepared.publish()?`, which announces the broadcast at the exact path under that epoch and returns its `origin::Publication`. +Only `publish` announces the answer: `prepared.broadcast()` exposes fill operations without announcement control. `Dynamic::update` is gone: drop the claim and claim again to change it, and close an answer's producer to end that output. `Request::accept` is no longer part of the public API and panics on a request from `dynamic`. Dropping a `Prepared` unpublished refuses the request, as dropping a `Request` does, and `publish` fails with `Error::Unroutable` once the claim went away. diff --git a/rs/moq-net/benches/memory.rs b/rs/moq-net/benches/memory.rs index 59e4fa58c7e..999b2d19167 100644 --- a/rs/moq-net/benches/memory.rs +++ b/rs/moq-net/benches/memory.rs @@ -22,7 +22,7 @@ use std::time::{Duration, Instant}; use bytes::Bytes; use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use moq_net::{Client, Hop, Server, Timestamp, Version, broadcast, cache, origin, track, transport::memory}; +use moq_net::{Client, Hop, Server, Timestamp, Version, cache, origin, track, transport::memory}; const VERSION: &str = "moq-lite-06"; @@ -88,7 +88,7 @@ struct Link { tracks: Vec, subscribers: Vec, payload: Bytes, - _broadcast: broadcast::Producer, + _broadcast: origin::Publication, _sessions: (moq_net::Session, moq_net::Session), _origins: (origin::Producer, origin::Producer), } diff --git a/rs/moq-net/benches/origin.rs b/rs/moq-net/benches/origin.rs index 5decd12b2fd..405135065d9 100644 --- a/rs/moq-net/benches/origin.rs +++ b/rs/moq-net/benches/origin.rs @@ -60,7 +60,7 @@ struct Fanout { producer: origin::Producer, consumer: origin::Consumer, driver: origin::Driver, - _publishers: Vec, + _publishers: Vec, subscribers: Vec, } @@ -915,7 +915,7 @@ struct Watching { pair: MockPair, tracks: Vec, subscriptions: Vec, - _broadcasts: Vec, + _broadcasts: Vec, _origins: [origin::Producer; 2], } @@ -1172,7 +1172,7 @@ const COPY_WAIT: Duration = Duration::from_secs(5); /// One generation at [`COPY_PATH`], held so its copies stay subscribed. struct Generation { - broadcast: broadcast::Producer, + broadcast: origin::Publication, tracks: Vec, } @@ -1335,7 +1335,7 @@ const RESUME_OVERLAP: [(usize, usize, usize); 17] = [ /// groups, each replacement in reverse arrival order. struct ResumeOverlap { _producer: origin::Producer, - _generations: Vec<(broadcast::Producer, track::Producer)>, + _generations: Vec<(origin::Publication, track::Producer)>, readers: Vec, } diff --git a/rs/moq-net/benches/session.rs b/rs/moq-net/benches/session.rs index c8baa670ceb..0d0b32e1061 100644 --- a/rs/moq-net/benches/session.rs +++ b/rs/moq-net/benches/session.rs @@ -325,7 +325,7 @@ struct Cluster { relays: Vec, /// One track per broadcast, indexed like [`path`]. tracks: Vec, - _broadcasts: Vec, + _broadcasts: Vec, _origins: Vec, _pairs: Vec, next_hop: u64, @@ -879,7 +879,7 @@ impl Dash { struct DashRoom { _cluster: Cluster, tracks: Vec, - _broadcasts: Vec, + _broadcasts: Vec, dash: Viewer, payload: Bytes, } diff --git a/rs/moq-net/benches/stats.rs b/rs/moq-net/benches/stats.rs index 255078ef208..cc6e1300db5 100644 --- a/rs/moq-net/benches/stats.rs +++ b/rs/moq-net/benches/stats.rs @@ -6,7 +6,7 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use futures::FutureExt; -use moq_net::{announce, broadcast, fuzz, origin, stats, transport}; +use moq_net::{announce, fuzz, origin, stats, transport}; /// `(broadcasts, tiers)` shapes; every broadcast records under every tier. const SHAPES: [(usize, usize); 4] = [(100, 1), (1_000, 1), (100, 10), (1_000, 10)]; @@ -16,7 +16,7 @@ const SHAPES: [(usize, usize); 4] = [(100, 1), (1_000, 1), (100, 10), (1_000, 10 struct Registry { registry: stats::Registry, _driver: origin::Driver, - _publishers: Vec, + _publishers: Vec, _cursors: Vec, } diff --git a/rs/moq-net/benches/viewers.rs b/rs/moq-net/benches/viewers.rs index fcd617a9646..48b1744a7b4 100644 --- a/rs/moq-net/benches/viewers.rs +++ b/rs/moq-net/benches/viewers.rs @@ -14,7 +14,7 @@ use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering}; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use futures::FutureExt; -use moq_net::{Hop, Timestamp, broadcast, kio, origin}; +use moq_net::{Hop, Timestamp, kio, origin}; /// A millisecond track: the frames written here carry timestamps. fn timed() -> moq_net::track::Info { @@ -67,7 +67,7 @@ struct Room { producer: origin::Producer, driver: origin::Driver, broadcasts: usize, - _sources: Vec<(broadcast::Producer, moq_net::track::Producer)>, + _sources: Vec<(origin::Publication, moq_net::track::Producer)>, } impl Room { diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index 4e37dc9ecf7..2498038f3f7 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -6678,7 +6678,7 @@ mod serve_tests { log: Log, track: track::Producer, _origin: origin::Producer, - _broadcast: crate::broadcast::Producer, + _broadcast: crate::origin::Publication, } fn serve(version: Version) -> Serve { diff --git a/rs/moq-net/src/ietf/publisher/tracks.rs b/rs/moq-net/src/ietf/publisher/tracks.rs index e3a6a4144c3..ed669a3bd5e 100644 --- a/rs/moq-net/src/ietf/publisher/tracks.rs +++ b/rs/moq-net/src/ietf/publisher/tracks.rs @@ -1243,7 +1243,7 @@ mod tests { publisher: Publisher, session: memory::Session, peer: memory::Session, - broadcast: crate::broadcast::Producer, + broadcast: crate::origin::Publication, tracks: HashMap<&'static str, track::Producer>, _origin: origin::Producer, } diff --git a/rs/moq-net/src/model/broadcast.rs b/rs/moq-net/src/model/broadcast.rs index f4b0af63950..8394243a2d7 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -273,7 +273,7 @@ impl Producer { } /// The route this broadcast is announced with, or `None` while it is not. - pub fn route(&self) -> Option { + pub(super) fn route(&self) -> Option { self.alive.announcer.lock().as_ref()?.route() } @@ -294,7 +294,7 @@ impl Producer { /// through an origin, so there is nothing to announce into), on a claim answer /// still being [prepared](super::origin::Request::prepare), once the broadcast /// has closed, or once the origin's driver has been dropped. - pub fn announce(&self, route: Route) -> Result<(), Error> { + pub(super) fn announce(&self, route: Route) -> Result<(), Error> { let mut announcer = self.alive.announcer.lock(); let announcer = announcer.as_mut().ok_or(Error::Closed)?; announcer.announce(route) @@ -336,7 +336,7 @@ impl Producer { /// the broadcast the origin served from it ends, while tracks already in /// flight carry on to their own end. [`announce`](Self::announce) brings it /// back. - pub fn unannounce(&self) { + pub(super) fn unannounce(&self) { self.alive.unannounce(); } @@ -476,7 +476,7 @@ impl Producer { /// End the broadcast for good, whether or not other clones are still alive. /// - /// Retracts its announcement and local discovery; a later [`Self::announce`] fails + /// Retracts its announcement and local discovery; a later [`super::origin::Publication::announce`] fails /// with [`Error::Closed`]. Tracks already handed out carry on and end with their /// own finish or abort. Every later [`Consumer::track`], and every name reserved or /// still queued for a handler, answers [`Error::Unroutable`]: the same answer an @@ -785,9 +785,8 @@ impl Producer { } } -/// A session-owned handle to a source broadcast created via -/// [`crate::origin::Producer::create_broadcast`], closing it on drop even while the -/// session's serve machines still hold its [`Dynamic`]. +/// A session-owned source broadcast, closed on drop even while the session's +/// serve machines still hold its [`Dynamic`]. /// /// A peer's retraction and a dead session end the source the same way: a broadcast /// carries no end cause past this hop. Shared by the lite and IETF subscribers. diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index 3c443bc5c77..60bfa8eb504 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -53,8 +53,8 @@ pub use timed::Timed; pub mod origin { pub use super::follow::AnnouncedBroadcast; pub use super::origin_impl::{ - Config, Consumer, Cost, DEFAULT_UPDATE_HOLD, Driver, Dynamic, Preference, Prepared, Producer, Request, - Requesting, Route, Routing, Source, + Config, Consumer, Cost, DEFAULT_UPDATE_HOLD, Driver, Dynamic, Preference, Prepared, Producer, Publication, + Request, Requesting, Route, Routing, Source, }; } diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 710ff840845..37828cc95ef 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -435,7 +435,7 @@ impl From for Preference { /// The path a route took through the mesh and what using it costs. /// /// The metadata half of an advertisement: [`Producer::dynamic`] pairs it with -/// the prefix it covers, [`broadcast::Producer::announce`] with the +/// the prefix it covers, [`Publication::announce`] with the /// broadcast's exact path, and [`Consumer::announced`] yields both. A route /// claims capability, not inventory: it says paths under its prefix are /// servable, never that any specific broadcast exists. The common convention is @@ -1930,16 +1930,17 @@ impl Producer { } } - /// Create a broadcast at `path`, fed through the returned producer. + /// Create an unannounced publication at `path` and its broadcast fill surface. /// - /// This is how local content enters an origin. The returned - /// [`broadcast::Producer`] is a source. Announce it with a [`Route::epoch`] + /// This is how local content enters an origin. The returned [`Publication`] + /// controls its advertisement; [`Publication::broadcast`] borrows the fill producer. + /// Announce it with a [`Route::epoch`] /// ([`Epoch::mint`](crate::Epoch::mint) per run, or a replica's shared one) so a /// restart replaces it rather than resuming into different content, and so /// routes with that epoch resume across each other. Without one it never /// resumes elsewhere. /// - /// The broadcast exists for nobody until [`broadcast::Producer::announce`]: + /// The broadcast exists for nobody until [`Publication::announce`]: /// until then no announce cursor lists it and a request for its path fails /// with [`Error::Unroutable`], for a consumer of this origin exactly as for a /// peer. Announce once the tracks a subscriber needs first exist. To serve @@ -1960,10 +1961,12 @@ impl Producer { /// [`Path::MAX_PARTS`], [`Error::InvalidPath`] if it holds a segment no /// pattern can spell (`*` or `**`), or [`Error::Closed`] once the origin's /// [`Driver`] has been dropped. - pub fn create_broadcast(&self, path: impl AsPath) -> Result { + pub fn create_broadcast(&self, path: impl AsPath) -> Result { let full = self.root.join(path.as_path()).to_owned(); let (broadcast, announcer) = self.create_withheld(full)?; - Ok(broadcast.with_announcer(announcer)) + Ok(Publication { + broadcast: broadcast.with_announcer(announcer), + }) } /// A new broadcast at `full` with its announcer withheld, so it cannot announce @@ -2025,7 +2028,7 @@ impl Producer { } /// Create and advertise a broadcast in one call. - pub fn publish(&self, path: impl AsPath, route: Route) -> Result { + pub fn publish(&self, path: impl AsPath, route: Route) -> Result { let broadcast = self.create_broadcast(path)?; broadcast.announce(route)?; Ok(broadcast) @@ -2059,7 +2062,7 @@ impl Producer { /// A request under an advertise-only route resolves [`Error::Unroutable`] /// unless an announced broadcast or a served route ([`Self::dynamic`]) covers /// the path too. Tests use it to shape the route table; everything else - /// advertises through a broadcast ([`broadcast::Producer::announce`]) or a + /// advertises through a broadcast ([`Publication::announce`]) or a /// [`Dynamic`] handler, which serve what they claim. #[cfg(test)] // private: test @@ -2151,7 +2154,7 @@ impl Producer { /// splices into a copy of the last. This is how a service answers a whole /// subtree without publishing each path ahead of time; a publisher that /// knows its broadcasts advertises each one's exact path with - /// [`broadcast::Producer::announce`] instead, so subscribers can enumerate + /// [`Publication::announce`] instead, so subscribers can enumerate /// them. /// /// The prefix must overlap this producer's pattern scope. Individual requests @@ -2418,9 +2421,55 @@ struct Serving { advertised: bool, } +/// An origin-owned broadcast publication, with a fill surface and its announcement capability. +/// +/// Clones share one broadcast and route. Closing the fill broadcast retracts the +/// route for every clone. Dropping the last fill handle does the same; a cloned +/// fill handle or a [`broadcast::Finish`] retains the publication even after the +/// last `Publication` handle drops. Use [`Self::unannounce`] to withdraw earlier. +#[derive(Clone)] +pub struct Publication { + broadcast: broadcast::Producer, +} + +impl Publication { + /// The broadcast to fill, without announcement control. + pub fn broadcast(&self) -> &broadcast::Producer { + &self.broadcast + } + + /// Finish filling and keep the publication available for its media linger. + pub fn finish(self) -> broadcast::Finish { + self.broadcast.finish() + } + + /// Advertise this publication's path or replace its route. + pub fn announce(&self, route: Route) -> Result<(), Error> { + self.broadcast.announce(route) + } + + /// Withdraw this publication while keeping its filled tracks alive. + pub fn unannounce(&self) { + self.broadcast.unannounce(); + } + + /// The route this publication currently advertises, if any. + pub fn route(&self) -> Option { + self.broadcast.route() + } +} + +impl std::ops::Deref for Publication { + type Target = broadcast::Producer; + + fn deref(&self) -> &Self::Target { + &self.broadcast + } +} + /// The table entry a broadcast owns: its exact path, advertised and withdrawn -/// through [`broadcast::Producer::announce`] and -/// [`broadcast::Producer::unannounce`], and removed when the broadcast ends. +/// through [`Publication::announce`] and +/// [`Publication::unannounce`], and removed when the broadcast ends. /// /// Handed to the broadcast by [`Producer::create_broadcast`], so a standalone /// broadcast has none and cannot announce. @@ -2595,7 +2644,7 @@ impl AnnounceProducer { /// Hide the entries from everyone, local and remote alike: cursors see a /// retraction and requests stop resolving through them. The entries stay, /// so announcing again restores the same route. What - /// [`broadcast::Producer::unannounce`] does. + /// [`Publication::unannounce`] does. fn withdraw(&self) { let mut shared = self.shared.lock(); for (prefix, id) in &self.entries { @@ -5488,9 +5537,7 @@ pub struct Prepared { } impl Prepared { - /// The unannounced broadcast to fill before publishing. Its own - /// [`announce`](broadcast::Producer::announce) fails with [`Error::Closed`]: only - /// [`Self::publish`] announces it. + /// The unannounced fill surface; only [`Self::publish`] can announce the answer. pub fn broadcast(&self) -> &broadcast::Producer { &self.broadcast } @@ -5500,7 +5547,7 @@ impl Prepared { /// Fails with [`Error::Unroutable`], announcing nothing, once the request was /// released or its claim retracted, or with [`Error::Closed`] once the broadcast /// closed. A failed answer can never announce. - pub fn publish(self) -> Result { + pub fn publish(self) -> Result { let Self { request, broadcast, @@ -5523,7 +5570,7 @@ impl Prepared { if let Ok(mut pending) = request.producer.write() { pending.resolved.get_or_insert(resolved); } - Ok(broadcast) + Ok(Publication { broadcast }) } } @@ -10325,6 +10372,54 @@ mod tests { assert_eq!(pending.await.unwrap().info().epoch.as_ref(), Some(&epoch)); } + /// Announcement control follows the origin role while fill clones retain publication lifetime. + #[moq_clock::test] + #[cfg(feature = "async")] + async fn publication_clones_share_control_and_fill_retention() { + let producer = origin(1).produce(); + let consumer = producer.consume(); + let mut events = consumer.announced(); + let publication = producer.create_broadcast("room/alice").unwrap(); + let fill = publication.broadcast().clone(); + let control = publication.clone(); + publication.announce(Route::default().with_epoch(epoch())).unwrap(); + events.expect_next_active("room/alice").await; + control.unannounce(); + events.expect_next_ended("room/alice").await; + assert!(publication.route().is_none()); + publication.announce(Route::default().with_epoch(epoch())).unwrap(); + events.expect_next_active("room/alice").await; + drop(control); + drop(publication); + events.assert_next_wait(); + consumer.request_broadcast("room/alice", None).await.unwrap(); + drop(fill); + events.expect_next_ended("room/alice").await; + } + + /// A withdrawn stale publication cannot remove a replacement instance's route. + #[moq_clock::test] + #[cfg(feature = "async")] + async fn stale_publication_control_cannot_withdraw_a_replacement() { + let producer = origin(1).produce(); + let consumer = producer.consume(); + let old = producer + .publish("room/alice", Route::default().with_epoch(epoch())) + .unwrap(); + let replacement_epoch: crate::Epoch = "01900000-0000-7000-8000-000000000002".parse().unwrap(); + let replacement = producer + .publish("room/alice", Route::default().with_epoch(replacement_epoch.clone())) + .unwrap(); + old.unannounce(); + old.close(); + assert_eq!(replacement.route().unwrap().epoch, Some(replacement_epoch.clone())); + let resolved = consumer + .request_broadcast("room/alice", replacement_epoch.clone()) + .await + .unwrap(); + assert_eq!(resolved.info().epoch.as_ref(), Some(&replacement_epoch)); + } + /// A refused answer keeps its announcer withheld, so a clone cannot announce it later. #[moq_clock::test] #[cfg(feature = "async")] diff --git a/rs/moq-net/tests/integration/auth.rs b/rs/moq-net/tests/integration/auth.rs index 08930c16b0f..86cb7432a36 100644 --- a/rs/moq-net/tests/integration/auth.rs +++ b/rs/moq-net/tests/integration/auth.rs @@ -988,7 +988,7 @@ async fn older_versions_have_no_grant(version: &'static str) { /// Connect on `version` and wait for a client announcement to reach the server. Each /// driver presents its setup token before it handles any other stream, so by then a /// version with AUTH has opened its Auth Stream on both sides. -async fn connect_announced(version: &'static str) -> (Pair, moq_net::broadcast::Producer) { +async fn connect_announced(version: &'static str) -> (Pair, moq_net::origin::Publication) { let client_origin = produce_origin(2); let broadcast = client_origin.create_broadcast("room/x").unwrap(); broadcast.announce(Default::default()).unwrap(); diff --git a/rs/moq-net/tests/integration/catalog_handover.rs b/rs/moq-net/tests/integration/catalog_handover.rs index 8039b5eb11f..39cfbc80ae4 100644 --- a/rs/moq-net/tests/integration/catalog_handover.rs +++ b/rs/moq-net/tests/integration/catalog_handover.rs @@ -16,7 +16,7 @@ use crate::support; use std::time::Duration; use futures::{StreamExt, channel::mpsc}; -use moq_net::{Error, Hop, Timestamp, Version, broadcast, origin, track}; +use moq_net::{Error, Hop, Timestamp, Version, origin, track}; use support::harness::{MockConnectOptions, MockPair, connect_mock}; const TEST_TIMEOUT: Duration = Duration::from_secs(300); @@ -91,7 +91,7 @@ struct Topology { p_to_a: Option, a_to_r: Option, links: Vec, - _broadcast: broadcast::Producer, + _broadcast: moq_net::origin::Publication, _track: track::Producer, /// Every payload written to the open group so far. written: Vec, diff --git a/rs/moq-net/tests/integration/compress.rs b/rs/moq-net/tests/integration/compress.rs index a846b1efac3..e7f9b66acf1 100644 --- a/rs/moq-net/tests/integration/compress.rs +++ b/rs/moq-net/tests/integration/compress.rs @@ -47,7 +47,7 @@ fn payload(frame: u64) -> Vec { struct Fixture { track: moq_net::track::Producer, subscriber: moq_net::track::Subscriber, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _hops: Vec, _origins: Vec, } diff --git a/rs/moq-net/tests/integration/datagram.rs b/rs/moq-net/tests/integration/datagram.rs index 2b373c9c6f6..c9206ddad55 100644 --- a/rs/moq-net/tests/integration/datagram.rs +++ b/rs/moq-net/tests/integration/datagram.rs @@ -34,7 +34,7 @@ fn produce_origin(hop: u64) -> moq_net::origin::Producer { struct Fixture { producer: moq_net::track::Producer, subscriber: moq_net::track::Subscriber, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _pair: MockPair, } diff --git a/rs/moq-net/tests/integration/fetch_from_feed.rs b/rs/moq-net/tests/integration/fetch_from_feed.rs index 66f33efce1c..2431a93f096 100644 --- a/rs/moq-net/tests/integration/fetch_from_feed.rs +++ b/rs/moq-net/tests/integration/fetch_from_feed.rs @@ -24,7 +24,7 @@ fn produce_origin(hop: u64) -> moq_net::origin::Producer { struct Relay { track: moq_net::track::Producer, remote: moq_net::broadcast::Consumer, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _pair: support::harness::MockPair, _origins: (moq_net::origin::Producer, moq_net::origin::Producer), } diff --git a/rs/moq-net/tests/integration/fetch_only.rs b/rs/moq-net/tests/integration/fetch_only.rs index 70d7bd7fffb..7fae98e0f52 100644 --- a/rs/moq-net/tests/integration/fetch_only.rs +++ b/rs/moq-net/tests/integration/fetch_only.rs @@ -41,7 +41,7 @@ async fn resolve(origin: &moq_net::origin::Producer) -> moq_net::broadcast::Cons } /// Publish a finished track of three groups, and flag any subscription it ever gets. -fn publish_finished(origin: &moq_net::origin::Producer) -> (moq_net::broadcast::Producer, Arc) { +fn publish_finished(origin: &moq_net::origin::Producer) -> (moq_net::origin::Publication, Arc) { let broadcast = origin.create_broadcast("bcast").unwrap(); let mut track = broadcast.create_track("video", timed()).unwrap(); broadcast.announce(Default::default()).unwrap(); diff --git a/rs/moq-net/tests/integration/lazy_discovery.rs b/rs/moq-net/tests/integration/lazy_discovery.rs index 92cbc1ca2e4..c75d59a6346 100644 --- a/rs/moq-net/tests/integration/lazy_discovery.rs +++ b/rs/moq-net/tests/integration/lazy_discovery.rs @@ -34,7 +34,7 @@ fn produce_origin(hop: u64) -> origin::Producer { struct Lazy { subscriber: origin::Producer, pair: MockPair, - _broadcasts: Vec, + _broadcasts: Vec, } impl Lazy { diff --git a/rs/moq-net/tests/integration/mesh_withdraw.rs b/rs/moq-net/tests/integration/mesh_withdraw.rs index e993176f4c9..ca2c10cf594 100644 --- a/rs/moq-net/tests/integration/mesh_withdraw.rs +++ b/rs/moq-net/tests/integration/mesh_withdraw.rs @@ -13,7 +13,7 @@ use std::{collections::HashMap, time::Duration}; use futures::channel::mpsc; -use moq_net::{Hop, Version, announce, broadcast, cache, origin}; +use moq_net::{Hop, Version, announce, cache, origin}; use support::harness::{MockConnectOptions, MockPair, connect_mock, peer, peer_with_latency}; fn produce_origin(hop: u64) -> origin::Producer { @@ -97,7 +97,7 @@ impl Mesh { /// Publish `count` broadcasts spread over every relay but the watcher, starting /// at relay `offset`, and require the watcher to see each announced. - async fn publish(&mut self, count: usize, offset: usize) -> Vec { + async fn publish(&mut self, count: usize, offset: usize) -> Vec { let relays = self.nodes.len() - 1; let broadcasts = (0..count) .map(|i| { @@ -350,7 +350,7 @@ impl Cluster { /// Publish `path` from the client and require everyone to see it. Returns /// what the watching client saw. - async fn publish(&mut self, path: &str) -> (broadcast::Producer, Vec) { + async fn publish(&mut self, path: &str) -> (moq_net::origin::Publication, Vec) { let broadcast = self.publisher.create_broadcast(path).unwrap(); broadcast.announce(Default::default()).unwrap(); moq_clock::sim::idle().await; @@ -364,7 +364,7 @@ impl Cluster { } /// Close `broadcast` cleanly: what the watching client, then each relay, saw. - async fn close(&mut self, path: &str, broadcast: broadcast::Producer) -> (Vec, Vec>) { + async fn close(&mut self, path: &str, broadcast: moq_net::origin::Publication) -> (Vec, Vec>) { drop(broadcast); moq_clock::sim::idle().await; let client = drain(&mut self.client).remove(path).unwrap(); diff --git a/rs/moq-net/tests/integration/multi_cdn.rs b/rs/moq-net/tests/integration/multi_cdn.rs index 6fb67494522..e56b6f10b2b 100644 --- a/rs/moq-net/tests/integration/multi_cdn.rs +++ b/rs/moq-net/tests/integration/multi_cdn.rs @@ -16,7 +16,7 @@ use crate::support; use std::time::Duration; use futures::{StreamExt, channel::mpsc}; -use moq_net::{Error, Hop, Timestamp, Version, announce, broadcast, origin, track}; +use moq_net::{Error, Hop, Timestamp, Version, announce, origin, track}; use support::harness::{MockConnectOptions, MockPair, connect_mock}; /// A millisecond track: the frames written here carry timestamps. @@ -142,7 +142,7 @@ struct Topology { cdn_b: origin::Producer, subscriber: origin::Producer, track: track::Producer, - _broadcast: broadcast::Producer, + _broadcast: moq_net::origin::Publication, _pushes: Vec, s_to_a: Option, s_to_b: MockPair, diff --git a/rs/moq-net/tests/integration/quiet_catalog.rs b/rs/moq-net/tests/integration/quiet_catalog.rs index b5254bc2740..16c64485607 100644 --- a/rs/moq-net/tests/integration/quiet_catalog.rs +++ b/rs/moq-net/tests/integration/quiet_catalog.rs @@ -47,7 +47,7 @@ async fn settle() { } /// Announce `live/catalog.json` holding one finished snapshot group. -fn publish(origin: &origin::Producer) -> (broadcast::Producer, track::Producer) { +fn publish(origin: &origin::Producer) -> (moq_net::origin::Publication, track::Producer) { let broadcast = origin.create_broadcast("live").unwrap(); let track = broadcast.create_track("catalog.json", timed()).unwrap(); broadcast.announce(Default::default()).unwrap(); diff --git a/rs/moq-net/tests/integration/request_caps.rs b/rs/moq-net/tests/integration/request_caps.rs index 7f003f97234..7c90df61e07 100644 --- a/rs/moq-net/tests/integration/request_caps.rs +++ b/rs/moq-net/tests/integration/request_caps.rs @@ -33,7 +33,7 @@ fn produce_origin(hop: u64) -> moq_net::origin::Producer { struct Setup { client: moq_net::origin::Producer, _server: moq_net::origin::Producer, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, tracks: Vec, _pair: MockPair, } diff --git a/rs/moq-net/tests/integration/restart.rs b/rs/moq-net/tests/integration/restart.rs index c5b24d1d1a2..a67dc664263 100644 --- a/rs/moq-net/tests/integration/restart.rs +++ b/rs/moq-net/tests/integration/restart.rs @@ -601,7 +601,7 @@ async fn a_more_specific_source_cannot_fill_a_held_covering_track() { /// fresh epoch and published at its exact path, and `outputs[path]` holds the latest. struct Pool { _claim: Rc, - outputs: Rc>>, + outputs: Rc>>, answered: Rc>, } diff --git a/rs/moq-net/tests/integration/route_change.rs b/rs/moq-net/tests/integration/route_change.rs index 2d8f607f9c2..13312b2e89a 100644 --- a/rs/moq-net/tests/integration/route_change.rs +++ b/rs/moq-net/tests/integration/route_change.rs @@ -22,7 +22,7 @@ use std::collections::BTreeMap; use std::time::Duration; use futures::{StreamExt, channel::mpsc}; -use moq_net::{Error, Hop, Timestamp, Version, announce, broadcast, origin, track}; +use moq_net::{Error, Hop, Timestamp, Version, announce, origin, track}; use support::harness::{MockConnectOptions, MockPair, connect_mock}; /// A millisecond track: the frames written here carry timestamps. @@ -96,7 +96,7 @@ struct Topology { publisher: origin::Producer, relay_b: origin::Producer, subscriber: origin::Producer, - _broadcast: broadcast::Producer, + _broadcast: moq_net::origin::Publication, track: track::Producer, p_to_a: Option, a_to_r: Option, @@ -750,7 +750,7 @@ struct Replica { /// Tags its payloads, so the reader shows which replica served each frame. name: &'static str, origin: origin::Producer, - broadcast: broadcast::Producer, + broadcast: moq_net::origin::Publication, track: track::Producer, group: Option, link: Option, diff --git a/rs/moq-net/tests/integration/session_close.rs b/rs/moq-net/tests/integration/session_close.rs index 1c2aedd57b8..f83c2c70cf4 100644 --- a/rs/moq-net/tests/integration/session_close.rs +++ b/rs/moq-net/tests/integration/session_close.rs @@ -33,7 +33,7 @@ struct Setup { pair: MockPair, track: moq_net::track::Producer, reader: moq_clock::sim::JoinHandle, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, } async fn setup(version: &str) -> Setup { diff --git a/rs/moq-net/tests/integration/session_panic.rs b/rs/moq-net/tests/integration/session_panic.rs index 14c36d3156e..daf222ae954 100644 --- a/rs/moq-net/tests/integration/session_panic.rs +++ b/rs/moq-net/tests/integration/session_panic.rs @@ -34,7 +34,7 @@ struct Publisher { server_driver: moq_clock::sim::JoinHandle, /// The client's origin, which it publishes from. origin: origin::Producer, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, } async fn publish(version: Version, relay: &origin::Producer, hop: u64, name: &str) -> Publisher { diff --git a/rs/moq-net/tests/integration/source_errors.rs b/rs/moq-net/tests/integration/source_errors.rs index 92b81051d75..6e2f5a6c93c 100644 --- a/rs/moq-net/tests/integration/source_errors.rs +++ b/rs/moq-net/tests/integration/source_errors.rs @@ -31,7 +31,7 @@ fn produce_origin(hop: u64) -> moq_net::origin::Producer { /// to the relay, with `bcast` resolved at the relay and the far end. struct Relayed { _publisher: moq_net::origin::Producer, - broadcast: moq_net::broadcast::Producer, + broadcast: moq_net::origin::Publication, upstream: MockPair, _downstream: MockPair, relayed: moq_net::broadcast::Consumer, diff --git a/rs/moq-net/tests/integration/subscribe_tracks.rs b/rs/moq-net/tests/integration/subscribe_tracks.rs index abda5ccd5b4..907ea902925 100644 --- a/rs/moq-net/tests/integration/subscribe_tracks.rs +++ b/rs/moq-net/tests/integration/subscribe_tracks.rs @@ -45,7 +45,7 @@ struct Setup { pair: MockPair, track: moq_net::track::Producer, frames: futures::channel::mpsc::UnboundedReceiver>, - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, } async fn setup(version: Version) -> Setup { diff --git a/rs/moq-net/tests/integration/track_tail.rs b/rs/moq-net/tests/integration/track_tail.rs index 22733723167..2f9a979954d 100644 --- a/rs/moq-net/tests/integration/track_tail.rs +++ b/rs/moq-net/tests/integration/track_tail.rs @@ -69,7 +69,7 @@ struct Pair { track: moq_net::track::Producer, remote: moq_net::broadcast::Consumer, _keep: ( - moq_net::broadcast::Producer, + moq_net::origin::Publication, moq_net::origin::Producer, moq_net::origin::Producer, ), diff --git a/rs/moq-relay/tests/integration/cluster_unknown.rs b/rs/moq-relay/tests/integration/cluster_unknown.rs index 581ec2260a2..f55adae01ff 100644 --- a/rs/moq-relay/tests/integration/cluster_unknown.rs +++ b/rs/moq-relay/tests/integration/cluster_unknown.rs @@ -133,7 +133,7 @@ fn client(version: Option) -> moq_tokio::Client { } struct Publisher { - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, _session: moq_tokio::Connection, streamer: tokio::task::AbortHandle, } diff --git a/rs/moq-relay/tests/integration/connection.rs b/rs/moq-relay/tests/integration/connection.rs index c815699e013..e67d1776347 100644 --- a/rs/moq-relay/tests/integration/connection.rs +++ b/rs/moq-relay/tests/integration/connection.rs @@ -72,7 +72,7 @@ fn publish( name: &str, ) -> ( moq_net::origin::Producer, - moq_net::broadcast::Producer, + moq_net::origin::Publication, moq_net::track::Producer, ) { let origin = moq_tokio::origin::spawn(); diff --git a/rs/moq-relay/tests/integration/upstream_links.rs b/rs/moq-relay/tests/integration/upstream_links.rs index bef034a7554..88000ca2ac2 100644 --- a/rs/moq-relay/tests/integration/upstream_links.rs +++ b/rs/moq-relay/tests/integration/upstream_links.rs @@ -140,7 +140,7 @@ async fn node(id: u64, connect: Vec) -> Node { /// A broadcast writing a group every 50ms on `origin`, so a subscriber attaching /// at any point receives one. Dropping it ends the broadcast. struct Publisher { - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, writer: tokio::task::AbortHandle, } diff --git a/rs/moq-room/src/room.rs b/rs/moq-room/src/room.rs index 65c6d54b903..41217cee014 100644 --- a/rs/moq-room/src/room.rs +++ b/rs/moq-room/src/room.rs @@ -162,7 +162,7 @@ mod tests { moq_tokio::origin::spawn() } - fn publish(origin: &origin::Producer, path: &str) -> broadcast::Producer { + fn publish(origin: &origin::Producer, path: &str) -> origin::Publication { let broadcast = origin.create_broadcast(path).expect(path); broadcast.announce(Route::default()).expect("announce"); broadcast diff --git a/rs/moq-rtc/src/client/mod.rs b/rs/moq-rtc/src/client/mod.rs index 00790466e99..72c2f80ed08 100644 --- a/rs/moq-rtc/src/client/mod.rs +++ b/rs/moq-rtc/src/client/mod.rs @@ -109,7 +109,7 @@ mod tests { .parse() .expect("WHEP URL"); let err = Client::new(Config::default()) - .subscribe(url, broadcast) + .subscribe(url, broadcast.broadcast().clone()) .await .expect_err("dial a closed port"); diff --git a/rs/moq-rtc/src/lib.rs b/rs/moq-rtc/src/lib.rs index ae1087ccd33..0cc98491d5c 100644 --- a/rs/moq-rtc/src/lib.rs +++ b/rs/moq-rtc/src/lib.rs @@ -106,15 +106,17 @@ mod tests { let source_origin = moq_tokio::origin::spawn(); let source_consumer = source_origin.consume(); let mut announcements = source_consumer.announced(); - let mut source = source_origin + let source = source_origin .create_broadcast("source") .expect("create source broadcast"); source .announce(moq_net::origin::Route::default()) .expect("announce source broadcast"); - let catalog = moq_mux::catalog::Producer::new(&mut source, moq_mux::catalog::Config::default()) - .expect("create source catalog"); - let mut opus = crate::codec::opus::Bridge::new(source, catalog, 48_000, 2).expect("create Opus bridge"); + let catalog = + moq_mux::catalog::Producer::new(&mut source.broadcast().clone(), moq_mux::catalog::Config::default()) + .expect("create source catalog"); + let mut opus = crate::codec::opus::Bridge::new(source.broadcast().clone(), catalog, 48_000, 2) + .expect("create Opus bridge"); Bridge::push( &mut opus, Frame { @@ -202,7 +204,7 @@ mod tests { let whep = format!("http://user:pass@{address}/whep/ingested?jwt=secret") .parse() .expect("WHEP URL"); - let whep_wire = tokio::time::timeout(TIMEOUT, client.subscribe(whep, output)) + let whep_wire = tokio::time::timeout(TIMEOUT, client.subscribe(whep, output.broadcast().clone())) .await .expect("WHEP negotiation timed out") .expect("WHEP negotiation failed"); diff --git a/rs/moq-rtc/src/server/whep.rs b/rs/moq-rtc/src/server/whep.rs index 46006d4cf5a..04ff2ebb4a8 100644 --- a/rs/moq-rtc/src/server/whep.rs +++ b/rs/moq-rtc/src/server/whep.rs @@ -289,13 +289,15 @@ mod tests { async fn a_broadcast_resolves_through_a_lazy_client_session() { let pub_origin = moq_tokio::origin::spawn(); - let mut source = pub_origin.create_broadcast("cam").expect("create broadcast"); + let source = pub_origin.create_broadcast("cam").expect("create broadcast"); source .announce(moq_net::origin::Route::default()) .expect("announce broadcast"); let catalog = - moq_mux::catalog::Producer::new(&mut source, moq_mux::catalog::Config::default()).expect("create catalog"); - let mut opus = crate::codec::opus::Bridge::new(source, catalog, 48_000, 2).expect("create Opus bridge"); + moq_mux::catalog::Producer::new(&mut source.broadcast().clone(), moq_mux::catalog::Config::default()) + .expect("create catalog"); + let mut opus = crate::codec::opus::Bridge::new(source.broadcast().clone(), catalog, 48_000, 2) + .expect("create Opus bridge"); opus.push(Frame { timestamp_us: 20_000, payload: Bytes::from_static(&[0xfc, 0xff, 0xfe]), diff --git a/rs/moq-rtc/src/server/whip.rs b/rs/moq-rtc/src/server/whip.rs index b8c73285d7a..2513d8d44f1 100644 --- a/rs/moq-rtc/src/server/whip.rs +++ b/rs/moq-rtc/src/server/whip.rs @@ -127,7 +127,7 @@ pub async fn accept( let config = moq_mux::catalog::Config::default() .with_max_age(server.config().max_age) .with_bandwidth(server.config().bandwidth.clone()); - let sink = Box::new(IngestSink::new(producer, config)?); + let sink = Box::new(IngestSink::new(producer.broadcast().clone(), config)?); // Register a session on the shared media mux: known ICE credentials (so the // demux routes this peer's STUN by ufrag), an inbox to read datagrams from, diff --git a/rs/moq-rtmp/src/dial.rs b/rs/moq-rtmp/src/dial.rs index 5d4cb59d271..4a2ece78613 100644 --- a/rs/moq-rtmp/src/dial.rs +++ b/rs/moq-rtmp/src/dial.rs @@ -543,9 +543,8 @@ struct Publisher { importer: FlvImport, // Finished at a clean end, so the finished broadcast keeps serving the catalog. catalog: moq_mux::catalog::Producer, - // A clone of the importer's producer, so an end can close the broadcast - // (prompt unannounce) even though the importer owns it. - broadcast: moq_net::broadcast::Producer, + // Controls publication so an end can retract before the importer's fill clone drops. + broadcast: moq_net::origin::Publication, // This pull's publisher instance, so pulling again replaces it rather than resuming into it. epoch: moq_net::Epoch, } @@ -553,12 +552,12 @@ struct Publisher { impl Publisher { fn new(origin: &origin::Producer, path: &str, config: moq_mux::catalog::Config) -> anyhow::Result { let epoch = moq_net::Epoch::mint(); - let mut broadcast = origin + let broadcast = origin .publish(path, moq_net::origin::Route::default().with_epoch(epoch.clone())) .map_err(|err| anyhow::anyhow!("broadcast '{path}' could not be published: {err}"))?; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config)?; let handle = broadcast.clone(); - let mut importer = FlvImport::new(broadcast, catalog.reserve()); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); // Feed the FLV file header once up front; media tags follow per message. importer.decode(&flv::file_header())?; @@ -668,17 +667,19 @@ mod tests { /// A broadcast with one AVC keyframe at `live/cam0` and its tracks finished, with the /// importer that keeps them listed and the catalog to finish it with. - fn source(origin: &origin::Producer) -> (moq_net::broadcast::Producer, moq_mux::catalog::Producer, FlvImport) { + fn source(origin: &origin::Producer) -> (moq_net::origin::Publication, moq_mux::catalog::Producer, FlvImport) { let mut vseq = vec![0x17, 0x00, 0x00, 0x00, 0x00]; vseq.extend_from_slice(&[0x01, 0x42, 0xc0, 0x1f, 0xff, 0xe1, 0x00, 0x04, 0x67, 0x42, 0xc0, 0x1f]); vseq.extend_from_slice(&[0x01, 0x00, 0x04, 0x68, 0xce, 0x3c, 0x80]); let mut vframe = vec![0x17, 0x01, 0x00, 0x00, 0x00]; vframe.extend_from_slice(&[0, 0, 0, 5, 0x65, 0x88, 0x84, 0x21, 0x00]); - let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); + let broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let mut importer = FlvImport::new(broadcast.clone(), catalog.reserve()); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vseq)).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vframe)).unwrap(); @@ -768,10 +769,12 @@ mod tests { vframe.extend_from_slice(&[0, 0, 0, 5, 0x65, 0x88, 0x84, 0x21, 0x00]); let server_origin = moq_tokio::origin::spawn(); - let mut broadcast = server_origin.create_broadcast("live/cam0").unwrap(); + let broadcast = server_origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let mut importer = FlvImport::new(broadcast, catalog.reserve()); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vseq)).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vframe)).unwrap(); diff --git a/rs/moq-rtmp/src/server.rs b/rs/moq-rtmp/src/server.rs index df78fac6a90..c602ed7bfe5 100644 --- a/rs/moq-rtmp/src/server.rs +++ b/rs/moq-rtmp/src/server.rs @@ -1470,7 +1470,7 @@ async fn run_handshake(stream: &mut S, peer: SocketAddr) -> anyhow::R } /// An active publish: the moq-mux FLV importer, which owns the origin-created -/// [`BroadcastProducer`](moq_net::broadcast::Producer) it publishes into. +/// [`BroadcastProducer`](moq_net::origin::Publication) it publishes into. /// [`Self::close`] or dropping it closes the broadcast and unannounces the path; /// [`Self::finish`] hands back the finished broadcast to linger on. struct Publisher { @@ -1478,9 +1478,8 @@ struct Publisher { // Finished at a clean end, so the finished broadcast keeps serving the catalog. catalog: moq_mux::catalog::Producer, metadata: crate::metadata::Dropped, - // A clone of the importer's producer, so an end can close the broadcast - // (prompt unannounce) even though the importer owns it. - broadcast: moq_net::broadcast::Producer, + // Controls publication so an end can retract before the importer's fill clone drops. + broadcast: moq_net::origin::Publication, // This connection's publisher instance, so an encoder reconnecting under the // same key replaces this broadcast at once instead of resuming into it. epoch: moq_net::Epoch, @@ -1491,10 +1490,10 @@ impl Publisher { /// the FLV file header, so subsequent tags decode against an initialized demuxer. fn new(origin: &origin::Producer, path: &str, config: moq_mux::catalog::Config) -> anyhow::Result { let epoch = moq_net::Epoch::mint(); - let mut broadcast = origin.publish(path, moq_net::origin::Route::default().with_epoch(epoch.clone()))?; - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; + let broadcast = origin.publish(path, moq_net::origin::Route::default().with_epoch(epoch.clone()))?; + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config)?; let handle = broadcast.clone(); - let mut importer = FlvImport::new(broadcast, catalog.reserve()); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); // Feed the FLV file header once up front; media tags follow per message. importer.decode(&flv::file_header())?; @@ -2059,10 +2058,12 @@ mod tests { // Publish the broadcast at `live/cam0` by feeding synthetic FLV to the importer. let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); + let broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let mut importer = FlvImport::new(broadcast, catalog.reserve()); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vseq)).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vframe)).unwrap(); @@ -2107,10 +2108,12 @@ mod tests { const VP9_KEYFRAME_320X240: &[u8] = &[0x82, 0x49, 0x83, 0x42, 0x20, 0x13, 0xf0, 0x0e, 0xf0, 0x00]; let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); + let broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let mut importer = FlvImport::new(broadcast, catalog.reserve()); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); // Enhanced video CodedFrames keyframe: ex-header, keyframe, packet type 1. @@ -2384,10 +2387,12 @@ mod tests { let frames = multitrack_body(CODED_FRAMES, &[(0, nalu(0x88)), (1, nalu(0x99))]); let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); + let broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap(); - let mut importer = FlvImport::new(broadcast, catalog.reserve()); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); + let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &seq)).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &frames)).unwrap(); diff --git a/rs/moq-rtsp/src/client.rs b/rs/moq-rtsp/src/client.rs index 76e505d7940..3c2c2b5ceeb 100644 --- a/rs/moq-rtsp/src/client.rs +++ b/rs/moq-rtsp/src/client.rs @@ -853,8 +853,10 @@ mod tests { for _ in 0..2 { let epoch = moq_net::Epoch::mint(); let route = moq_net::origin::Route::default().with_epoch(epoch.clone()); - let mut broadcast = origin.publish("camera", route).unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let broadcast = origin.publish("camera", route).unwrap(); + let mut catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()) + .unwrap(); published_tx.send((epoch, broadcast.clone(), catalog.clone())).unwrap(); // The first session to return ends the run; dropping the other's pull // finishes its tracks. diff --git a/rs/moq-srt/src/server.rs b/rs/moq-srt/src/server.rs index 41511572626..8350d42711b 100644 --- a/rs/moq-srt/src/server.rs +++ b/rs/moq-srt/src/server.rs @@ -891,10 +891,10 @@ mod tests { let (origin, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); tokio::spawn(moq_net::time::run(driver)); - let mut broadcast = origin.create_broadcast("marker").unwrap(); + let broadcast = origin.create_broadcast("marker").unwrap(); broadcast.announce(moq_net::origin::Route::default()).unwrap(); let mut catalog = moq_mux::catalog::Producer::new( - &mut broadcast, + &mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default().with_catalog(moq_mux::catalog::hang::Catalog::::default()), ) .unwrap(); @@ -993,7 +993,7 @@ mod tests { /// One publisher instance of `live`: an announced broadcast under its own epoch with one /// Legacy AAC track, for the egress tests. struct Instance { - _broadcast: moq_net::broadcast::Producer, + _broadcast: moq_net::origin::Publication, catalog: moq_mux::catalog::Producer, audio: moq_mux::container::Producer, } @@ -1001,9 +1001,9 @@ mod tests { impl Instance { fn publish(origin: &moq_net::origin::Producer, track: &str) -> Self { let route = moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()); - let mut broadcast = origin.publish("live", route).unwrap(); + let broadcast = origin.publish("live", route).unwrap(); let mut catalog = moq_mux::catalog::Producer::new( - &mut broadcast, + &mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default() .with_catalog(moq_mux::catalog::hang::Catalog::::default()), ) diff --git a/rs/moq-srt/src/ts.rs b/rs/moq-srt/src/ts.rs index cbee735bb79..d3cdfecdcae 100644 --- a/rs/moq-srt/src/ts.rs +++ b/rs/moq-srt/src/ts.rs @@ -53,9 +53,8 @@ enum Importer { import: Box>, // Finished at a clean end, so the finished broadcast keeps serving the catalog. catalog: Box>, - // A clone of the importer's producer, so an end can finish or close the broadcast - // even though the importer owns it. - broadcast: moq_net::broadcast::Producer, + // Controls publication while the importer fills it, including finish or close at EOF. + broadcast: moq_net::origin::Publication, }, /// Each program's broadcast is created once the first PAT names it, and announced once its PMT /// declares its tracks. @@ -83,10 +82,10 @@ impl Publisher { Some(Program::All) => Importer::All(Box::new(ts::Programs::new(origin.clone(), path, config))), Some(Program::One(_)) | None => { let route = moq_net::origin::Route::default().with_epoch(epoch.insert(moq_net::Epoch::mint()).clone()); - let mut broadcast = origin.publish(path, route)?; + let broadcast = origin.publish(path, route)?; let config = config.with_catalog(moq_mux::catalog::hang::Catalog::::default()); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config)?; - let mut import = ts::Import::new(broadcast.clone(), catalog.reserve()); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), config)?; + let mut import = ts::Import::new(broadcast.broadcast().clone(), catalog.reserve()); if let Some(Program::One(program)) = program { import = import.with_program(program); } diff --git a/rs/moq-stats/benches/aggregate.rs b/rs/moq-stats/benches/aggregate.rs index b96a78bf363..efcfb9e7fbc 100644 --- a/rs/moq-stats/benches/aggregate.rs +++ b/rs/moq-stats/benches/aggregate.rs @@ -28,7 +28,7 @@ const GRACE: Duration = Duration::from_secs(1); /// A hand-published node stats broadcast with a default-tier publisher traffic track. struct Node { - source: moq_net::broadcast::Producer, + source: moq_net::origin::Publication, epoch: Epoch, known: bool, traffic: moq_json::snapshot::Producer, diff --git a/rs/moq-stats/src/aggregate.rs b/rs/moq-stats/src/aggregate.rs index 1e234c37147..8bfa09fdf0b 100644 --- a/rs/moq-stats/src/aggregate.rs +++ b/rs/moq-stats/src/aggregate.rs @@ -1022,7 +1022,7 @@ mod tests { #[allow(dead_code)] struct Feed { announced: announce::Consumer, - source: broadcast::Producer, + source: origin::Publication, consumer: broadcast::Consumer, sub: track::Subscriber, ctx: moq_net::stats::Session, @@ -1101,7 +1101,7 @@ mod tests { /// the same path is that node returning. Dropping it unannounces the node. #[allow(dead_code)] struct NodeBroadcast { - source: broadcast::Producer, + source: origin::Publication, traffic: moq_json::snapshot::Producer, track: track::Producer, frame: TrafficFrame, @@ -1879,7 +1879,7 @@ mod tests { /// totals track and no epoch, so a new one at the same path is that node /// restarting without telling the reader. struct TotalsNode { - _source: broadcast::Producer, + _source: origin::Publication, totals: moq_json::snapshot::Producer, } diff --git a/rs/moq-stats/src/consume.rs b/rs/moq-stats/src/consume.rs index fa515e62dbd..d3111d794b8 100644 --- a/rs/moq-stats/src/consume.rs +++ b/rs/moq-stats/src/consume.rs @@ -206,7 +206,7 @@ mod tests { use std::time::Duration; - use moq_net::{Consume, PathOwned, Timestamp, announce, broadcast, origin, track}; + use moq_net::{Consume, PathOwned, Timestamp, announce, origin, track}; use crate::{Producer, Tier, produce}; @@ -228,7 +228,7 @@ mod tests { track: track::Producer, sub: track::Subscriber, _announced: announce::Consumer, - _source: broadcast::Producer, + _source: origin::Publication, _ctx: moq_net::stats::Session, } diff --git a/rs/moq-stats/src/produce.rs b/rs/moq-stats/src/produce.rs index 4f8d113c3c2..f03d1f87425 100644 --- a/rs/moq-stats/src/produce.rs +++ b/rs/moq-stats/src/produce.rs @@ -880,7 +880,7 @@ impl TrackFamily { struct GroupPublisher { /// The group's key: the leading segments every member path and label shares. key: PathOwned, - broadcast: broadcast::Producer, + broadcast: origin::Publication, /// Holds the broadcast's request queue open, so a subscriber asking for a /// tier track no drain has created yet parks (served next tick) instead of /// being rejected `NotFound` on the spot. @@ -1956,7 +1956,7 @@ mod tests { #[allow(dead_code)] struct Feed { announced: announce::Consumer, - source: broadcast::Producer, + source: origin::Publication, consumer: broadcast::Consumer, sub: Option, } diff --git a/rs/moq-tokio/tests/integration/receive_credit.rs b/rs/moq-tokio/tests/integration/receive_credit.rs index 2a058a4a0ac..4034f40a131 100644 --- a/rs/moq-tokio/tests/integration/receive_credit.rs +++ b/rs/moq-tokio/tests/integration/receive_credit.rs @@ -53,7 +53,7 @@ fn bufferbloat(bits_per_second: u64) -> moq_shaper::Profile { /// A server publishing broadcast `room`, and a client subscribed to its origin over a /// connection with the given transport settings. struct Setup { - broadcast: moq_net::broadcast::Producer, + broadcast: moq_net::origin::Publication, remote: moq_net::broadcast::Consumer, /// The client's connection, for its credit stats. conn: driver::Connection, @@ -382,7 +382,7 @@ async fn lite_frame_past_the_window_gives_way() { /// A subscriber served on the raw driver with a connection window of [`WINDOW`], and a /// publisher dialing it whose connection can also send raw streams. struct Reversed { - broadcast: moq_net::broadcast::Producer, + broadcast: moq_net::origin::Publication, remote: moq_net::broadcast::Consumer, /// The publisher's connection, for streams no session would send. conn: driver::Connection, diff --git a/rs/moq-transcode/examples/transcode.rs b/rs/moq-transcode/examples/transcode.rs index 2546c1d2d87..38733e0fbbc 100644 --- a/rs/moq-transcode/examples/transcode.rs +++ b/rs/moq-transcode/examples/transcode.rs @@ -91,7 +91,7 @@ async fn main() -> anyhow::Result<()> { tracing::info!(source = %source_path, output = %output_path, "transcoding"); tokio::select! { - res = moq_transcode::run(source, output, config) => Ok(res?), + res = moq_transcode::run(source, output.broadcast().clone(), config) => Ok(res?), res = session.closed() => Ok(res?), } } diff --git a/rs/moq-transcode/src/lib.rs b/rs/moq-transcode/src/lib.rs index 6456a4a0b80..94df72c8d84 100644 --- a/rs/moq-transcode/src/lib.rs +++ b/rs/moq-transcode/src/lib.rs @@ -784,7 +784,7 @@ mod tests { let origin = moq_tokio::origin::spawn(); let output = origin.create_broadcast("room/transcode").unwrap(); let consumer = output.consume(); - let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config)); + let transcoder = tokio::spawn(run(source.broadcast.consume(), output.broadcast().clone(), config)); // The derivative catalog appears before anything is encoded, with the // rung sized against the source and the passthrough reference. Yield diff --git a/rs/moq-video/src/decode/decoder.rs b/rs/moq-video/src/decode/decoder.rs index 05bd8187743..2dfe3bc60a5 100644 --- a/rs/moq-video/src/decode/decoder.rs +++ b/rs/moq-video/src/decode/decoder.rs @@ -608,9 +608,10 @@ mod tests { let (origin, driver) = moq_net::origin::Producer::new(Default::default()); tokio::spawn(moq_net::time::run(driver)); - let mut source = origin.publish("test", Default::default()).unwrap(); + let source = origin.publish("test", Default::default()).unwrap(); let mut source_catalog = - moq_mux::catalog::Producer::new(&mut source, moq_mux::catalog::Config::default()).unwrap(); + moq_mux::catalog::Producer::new(&mut source.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let track = source .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video)) .unwrap(); diff --git a/test/interop/clients/rust/finish.rs b/test/interop/clients/rust/finish.rs index 4fdd6b69ff6..0d77dfb6871 100644 --- a/test/interop/clients/rust/finish.rs +++ b/test/interop/clients/rust/finish.rs @@ -74,8 +74,9 @@ async fn main() -> anyhow::Result<()> { .await?; let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()); if args.role == "publish" { - let mut broadcast = origin.create_broadcast(&args.broadcast)?; - let mut catalog = moq_mux::catalog::Producer::<()>::new(&mut broadcast, Default::default())?; + let broadcast = origin.create_broadcast(&args.broadcast)?; + let mut catalog = + moq_mux::catalog::Producer::<()>::new(&mut broadcast.broadcast().clone(), Default::default())?; let mut media = moq_mux::import::TrackStream::video( broadcast.reserve_track("video")?, catalog.reserve(), From 541f82ff1d53bcfb855e50f0d0c32c4d3f0319fc Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 13:04:35 -0700 Subject: [PATCH 03/11] quest: record the approved origin::Publication shape The maintainer approved origin::Publication as proposed: an origin-owned record holding the fill broadcast and its announcement capability, with immutable fill access and the existing fill/Finish lifetime. Co-Authored-By: Claude Opus 5.5 --- quest/m1/api-audit/announce-owner.md | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/quest/m1/api-audit/announce-owner.md b/quest/m1/api-audit/announce-owner.md index 08530c5d039..4f75a8f0312 100644 --- a/quest/m1/api-audit/announce-owner.md +++ b/quest/m1/api-audit/announce-owner.md @@ -23,29 +23,29 @@ error. - Mirror the shape in `@moq/net` if its producer has the same trap, and in moq-ffi where the binding exposes announce on the broadcast. -The draft proposes an `origin::Publication` owner that combines the fill -broadcast and its real announcement capability. `create_broadcast`, `publish`, +Decided 2026-10-23 (maintainer, on +[moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ the +announcement handle is `origin::Publication`, an origin-owned record holding the +fill broadcast and its announcement capability. `create_broadcast`, `publish`, and an admitted `Prepared::publish` return it; `Prepared::broadcast` and -standalone producers remain fill-only. The proposed record forwards fill -operations and exposes the borrowed producer through `broadcast()`. - -The return/lifetime shape still needs review. The current proposal preserves -last-fill and `Finish` retention rather than making the last announcement -capability clone retract independently. A literal producer/announcement pair -would make that transfer explicit but requires media holders and finish/linger -paths to retain both. Settle this choice before mirroring the record in JS, -moq-ffi, C, and every wrapper; do not export an optional capability on a fill -producer to avoid the migration. - -The native proposal migrates current Rust holders, examples, benchmarks, and -private C/UniFFI adapters. Remaining work is accepting the return/lifetime shape, -mirroring the public role in JS and all bindings, regenerating outputs from -source, and updating those consumers and package migrations. Exercise clone/drop, -explicit unannounce, finished tails, Prepared admission, and stale owners -against a replacement publication. A finished-but-open Prepared answer remains -publishable and serveable through its retained `Finish`; a missing binding fill -registry/state after finish does not mean the native broadcast is closed. Keep -deferred validity and request role redesigns separate. +standalone producers stay fill-only. It borrows its fill through `broadcast()`, +forwards immutable fill operations (no `DerefMut`, so a caller cannot swap in an +unrelated producer), and `finish()` hands off to the existing `Finish`. Clones, +fill clones, and a `Finish` keep the existing broadcast lifetime; the last +`Publication` dropping does not retract on its own. Reason: it keeps today's +linger and fill retention without a second route mechanism, while a literal +producer/announcer pair would make every media holder and finish path carry both. +Do not export an optional capability on a fill producer to avoid the migration. + +The native side (moq-net, its Rust holders, examples, benchmarks, and private +C/UniFFI adapters) is migrated. Remaining: mirror `Publication` in `@moq/net`, +moq-ffi, the C ABI, and every wrapper (py, swift, kt, dart, go, cpp, obs), +regenerate outputs, and update their consumers, docs, and `MIGRATE.md` entries. +Exercise clone/drop, explicit unannounce, finished tails, Prepared admission, and +stale owners against a replacement publication in each. A finished-but-open +Prepared answer remains publishable and serveable through its retained `Finish`; +a missing binding fill registry/state after finish does not mean the native +broadcast is closed. Keep deferred validity and request role redesigns separate. Public API: breaking in moq-net (and `@moq/net`/moq-ffi if they mirror the trap). Wire: none. From a245bf44dac14ba4ba4e642d27526d9c52ecaf28 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 13:54:34 -0700 Subject: [PATCH 04/11] quest: split the announce-owner rollout into JS and binding quests The maintainer chose three PRs: this one lands the native origin::Publication and finishes the announce-owner quest, the JS mirror can start now, and the binding rollout follows once this merges. Co-Authored-By: Claude Opus 5.5 --- quest/m1/api-audit/README.md | 5 +- quest/m1/api-audit/announce-owner.md | 56 ----------------------- quest/m1/api-audit/binding-publication.md | 53 +++++++++++++++++++++ quest/m1/api-audit/js-publication.md | 44 ++++++++++++++++++ 4 files changed, 100 insertions(+), 58 deletions(-) delete mode 100644 quest/m1/api-audit/announce-owner.md create mode 100644 quest/m1/api-audit/binding-publication.md create mode 100644 quest/m1/api-audit/js-publication.md diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 8888fc0cabf..d353a14d33f 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -70,7 +70,8 @@ Rejected in planning: - [MIGRATE matches release](/quest/m1/api-audit/migrate.md) - every break already on main has a correct entry measured against release, and entries for unreleased packages are gone - [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 - [JS prepared answers](/quest/m1/api-audit/js-prepared.md) - a leaked or abandoned `Prepared` refuses its request instead of blocking the path -- [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 - [Unpublish what nothing uses](/quest/m1/api-audit/unpublish.md) - packages, features, and listeners with no consumer stop shipping or stop being on by default - [Container formats name inputs](/quest/m1/api-audit/container-format.md) - `ContainerFormat` names what is parsed, never how it is packaged; `ContainerStream` and TS passthrough internals are gone - [Binding connection state](/quest/m1/api-audit/binding-state.md) - one `state()` snapshot and `state_changed()` replace `status`, `connects`, and both `failure` polls @@ -90,7 +91,7 @@ Rejected in planning: ## Related -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - removes `accept` from application requests; [the origin announces](/quest/m1/api-audit/announce-owner.md) narrows `Prepared` beside it +- [Claim request type](/quest/m1/public-model/claim-request-type.md) - removes `accept` from application requests; [JS origin publications](/quest/m1/api-audit/js-publication.md) narrows `Prepared` beside it - [Readers follow restarts](/quest/m1/sink-restart-follow.md) - moves readers onto the follow operation that [two broadcast resolutions](/quest/m1/api-audit/resolve.md) names - [API-diff check on release PRs](/quest/m1/api-diff.md) - the backstop that catches breaks this epic misses - [Main is no slower than release](/quest/m1/release-perf/README.md) - the other release-readiness check diff --git a/quest/m1/api-audit/announce-owner.md b/quest/m1/api-audit/announce-owner.md deleted file mode 100644 index 4f75a8f0312..00000000000 --- a/quest/m1/api-audit/announce-owner.md +++ /dev/null @@ -1,56 +0,0 @@ -# [M] The origin announces - -## Goal - -A `broadcast::Producer` fills tracks; it cannot announce. Announcing is a -handle the origin hands out, so a `Prepared` answer and a standalone -`Info::produce()` broadcast no longer expose `announce`, `unannounce`, or -`route` calls that can only fail. - -## Plan - -Decided 2026-10-22 (API audit). Today `Prepared::broadcast()` -(`rs/moq-net/src/model/origin.rs`) returns a cloneable producer whose -`announce` fails with `Error::Closed` (documented in `MIGRATE.md`), and a -standalone broadcast's `announce`, `unannounce`, and `route` can never work -(inherited). The prior audit called the `Prepared` case a panic; it is an -error. - -- `origin::Producer::create_broadcast` (and `publish`) return the broadcast - plus an origin-owned announcement handle that carries `route`/`unannounce`. -- `Prepared` exposes only the fill surface; `publish` stays the one way to - answer. -- Mirror the shape in `@moq/net` if its producer has the same trap, and in - moq-ffi where the binding exposes announce on the broadcast. - -Decided 2026-10-23 (maintainer, on -[moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ the -announcement handle is `origin::Publication`, an origin-owned record holding the -fill broadcast and its announcement capability. `create_broadcast`, `publish`, -and an admitted `Prepared::publish` return it; `Prepared::broadcast` and -standalone producers stay fill-only. It borrows its fill through `broadcast()`, -forwards immutable fill operations (no `DerefMut`, so a caller cannot swap in an -unrelated producer), and `finish()` hands off to the existing `Finish`. Clones, -fill clones, and a `Finish` keep the existing broadcast lifetime; the last -`Publication` dropping does not retract on its own. Reason: it keeps today's -linger and fill retention without a second route mechanism, while a literal -producer/announcer pair would make every media holder and finish path carry both. -Do not export an optional capability on a fill producer to avoid the migration. - -The native side (moq-net, its Rust holders, examples, benchmarks, and private -C/UniFFI adapters) is migrated. Remaining: mirror `Publication` in `@moq/net`, -moq-ffi, the C ABI, and every wrapper (py, swift, kt, dart, go, cpp, obs), -regenerate outputs, and update their consumers, docs, and `MIGRATE.md` entries. -Exercise clone/drop, explicit unannounce, finished tails, Prepared admission, and -stale owners against a replacement publication in each. A finished-but-open -Prepared answer remains publishable and serveable through its retained `Finish`; -a missing binding fill registry/state after finish does not mean the native -broadcast is closed. Keep deferred validity and request role redesigns separate. - -Public API: breaking in moq-net (and `@moq/net`/moq-ffi if they mirror the -trap). Wire: none. - -## Related - -- [Claim request type](/quest/m1/public-model/claim-request-type.md) - splits `accept` off application requests in the same file; coordinate the order -- [Public routes and answering](/quest/m1/public-model/answer.md) - later makes the forwarding claim public diff --git a/quest/m1/api-audit/binding-publication.md b/quest/m1/api-audit/binding-publication.md new file mode 100644 index 00000000000..bff6e151801 --- /dev/null +++ b/quest/m1/api-audit/binding-publication.md @@ -0,0 +1,53 @@ +# [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 and finish lifetime (dropping +the publication does not retract on its own). + +- moq-ffi: a `MoqPublication` object with `broadcast()`, `announce` + (taking `MoqAnnounceRoute`), `unannounce`, and `finish`. + `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. +- A finished-but-open prepared answer stays publishable and serveable through + its retained finish; a missing binding fill registry after finish does not + mean the native broadcast closed. + +Test in each layer: clone/drop, explicit unannounce, a finished tail, 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. + +## Related + +- [Binding connection state](/quest/m1/api-audit/binding-state.md) - another binding surface reshape across the same wrappers +- [Wrapper config names](/quest/m1/api-audit/wrapper-config.md) - touches the same wrapper packages diff --git a/quest/m1/api-audit/js-publication.md b/quest/m1/api-audit/js-publication.md new file mode 100644 index 00000000000..31961152eaf --- /dev/null +++ b/quest/m1/api-audit/js-publication.md @@ -0,0 +1,44 @@ +# [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()`, `route`, and `finish()` handing off to + the existing `broadcast.Finish`. No way to swap in another producer. +- `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), fill handles, and + a `Finish` 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, a finished tail, Prepared admission, +and a stale publication against a replacement under a new epoch. + +Public API: breaking in `@moq/net`. Wire: none. + +## Related + +- [JS prepared answers](/quest/m1/api-audit/js-prepared.md) - reshapes `Prepared` ownership in the same file; coordinate the order +- [Claim request type](/quest/m1/public-model/claim-request-type.md) - splits `accept` off application requests beside `Prepared` From e325c0fbb3760d8176bd2558d16696a46c8c62bc Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 00:02:57 -0700 Subject: [PATCH 05/11] quest: drop a link to the finished binding-state quest Co-Authored-By: Claude Opus 5.5 --- quest/m1/api-audit/binding-publication.md | 1 - 1 file changed, 1 deletion(-) diff --git a/quest/m1/api-audit/binding-publication.md b/quest/m1/api-audit/binding-publication.md index bff6e151801..e3061db9e45 100644 --- a/quest/m1/api-audit/binding-publication.md +++ b/quest/m1/api-audit/binding-publication.md @@ -49,5 +49,4 @@ Public API: breaking in moq-ffi, the C ABI, and every wrapper. Wire: none. ## Related -- [Binding connection state](/quest/m1/api-audit/binding-state.md) - another binding surface reshape across the same wrappers - [Wrapper config names](/quest/m1/api-audit/wrapper-config.md) - touches the same wrapper packages From 4f5baf2c28861042df26cc07f370352f01fb9e1b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 02:14:07 -0700 Subject: [PATCH 06/11] test(cli): hold epoch test publications as origin::Publication Co-Authored-By: Claude Opus 5.5 --- rs/moq-cli/tests/integration/epochs.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/moq-cli/tests/integration/epochs.rs b/rs/moq-cli/tests/integration/epochs.rs index 992d27bae11..a8947b08a98 100644 --- a/rs/moq-cli/tests/integration/epochs.rs +++ b/rs/moq-cli/tests/integration/epochs.rs @@ -2,7 +2,7 @@ use std::time::{Duration, Instant}; -use hang::moq_net::{self, Epoch, Timestamp, announce, broadcast, origin, track}; +use hang::moq_net::{self, Epoch, Timestamp, announce, origin, track}; use moq_mux::catalog::Stream; const LITE_07: &str = "moq-lite-07-wip"; @@ -71,14 +71,14 @@ async fn read(subscription: &mut track::Subscriber) -> String { struct Published { catalog: moq_mux::catalog::Producer, - _catalog_broadcast: broadcast::Producer, - sibling: broadcast::Producer, + _catalog_broadcast: origin::Publication, + sibling: origin::Publication, media: track::Producer, } fn publish(origin: &origin::Producer, epoch: &Epoch, tag: &str) -> Published { let route = origin::Route::default().with_epoch(epoch.clone()); - let mut broadcast = origin.publish("live/catalog.hang", route.clone()).unwrap(); + let broadcast = origin.publish("live/catalog.hang", route.clone()).unwrap(); let sibling = origin.publish("live/media", route).unwrap(); let media = sibling .create_track( @@ -87,7 +87,7 @@ fn publish(origin: &origin::Producer, epoch: &Epoch, tag: &str) -> Published { ) .unwrap(); write(&media, &format!("{tag}:0")); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); catalog .mutate(|catalog| { let mut audio = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2); From ce877760e7be9ba8b0e415920d17943a1a00a2c4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 02:20:29 -0700 Subject: [PATCH 07/11] test(hls): borrow the publication's fill for main's sibling-denial test Co-Authored-By: Claude Opus 5.5 --- rs/moq-hls/src/server/routes.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/moq-hls/src/server/routes.rs b/rs/moq-hls/src/server/routes.rs index be63989dc00..c7a4124fffe 100644 --- a/rs/moq-hls/src/server/routes.rs +++ b/rs/moq-hls/src/server/routes.rs @@ -1131,13 +1131,13 @@ mod tests { #[tokio::test(start_paused = true)] async fn denied_sibling_media_answers_403_without_poisoning_allowed_siblings() { let origin = moq_tokio::origin::spawn(); - let mut broadcast = origin.create_broadcast("room/live").unwrap(); + let broadcast = origin.create_broadcast("room/live").unwrap(); broadcast.announce(Default::default()).unwrap(); let source = origin.create_broadcast("room/source").unwrap(); source.announce(Default::default()).unwrap(); let secret = origin.create_broadcast("room/secret").unwrap(); secret.announce(Default::default()).unwrap(); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let reserved = catalog.reserve(); let mut config = video_config(); config.broadcast = Some(moq_net::path::Relative::new("./source").to_owned()); From 239aa1d63e4e302c8ebb3960bb4bf9b015d1e875 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 05:44:14 -0700 Subject: [PATCH 08/11] quest: drop the finished wrapper-config link from binding-publication Co-Authored-By: Claude Opus 5.5 --- quest/m1/api-audit/binding-publication.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/quest/m1/api-audit/binding-publication.md b/quest/m1/api-audit/binding-publication.md index e3061db9e45..eaf52fcc0c7 100644 --- a/quest/m1/api-audit/binding-publication.md +++ b/quest/m1/api-audit/binding-publication.md @@ -46,7 +46,3 @@ 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. - -## Related - -- [Wrapper config names](/quest/m1/api-audit/wrapper-config.md) - touches the same wrapper packages From 383c2d238027bcd13972b0252370e363162f01c2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 15:30:33 -0700 Subject: [PATCH 09/11] test: format the Publication migrations of main's new tests Co-Authored-By: Claude Opus 5.5 --- rs/moq-rtc/src/egress.rs | 3 ++- rs/moq-rtmp/src/dial.rs | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rs/moq-rtc/src/egress.rs b/rs/moq-rtc/src/egress.rs index 2a2a258aba3..30876d16aae 100644 --- a/rs/moq-rtc/src/egress.rs +++ b/rs/moq-rtc/src/egress.rs @@ -903,7 +903,8 @@ mod tests { let live = origin.publish("live", Default::default()).unwrap(); let other = origin.publish("other", Default::default()).unwrap(); let mut catalog = - moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()).unwrap(); + moq_mux::catalog::Producer::new(&mut live.broadcast().clone(), moq_mux::catalog::Config::default()) + .unwrap(); let track = |broadcast: &mut moq_net::broadcast::Producer, name: &str| { let info = moq_net::track::Info::default().with_timescale(moq_net::Timescale::MILLI); moq_mux::container::Producer::new( diff --git a/rs/moq-rtmp/src/dial.rs b/rs/moq-rtmp/src/dial.rs index da41fcfaff1..1ca58041fc5 100644 --- a/rs/moq-rtmp/src/dial.rs +++ b/rs/moq-rtmp/src/dial.rs @@ -789,9 +789,11 @@ mod tests { broadcast .announce(origin::Route::default().with_epoch(epoch.clone())) .unwrap(); - let catalog = - moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) - .unwrap(); + let catalog = moq_mux::catalog::Producer::new( + &mut broadcast.broadcast().clone(), + moq_mux::catalog::Config::default(), + ) + .unwrap(); let mut importer = FlvImport::new(broadcast.broadcast().clone(), catalog.reserve()); importer.decode(&flv::file_header()).unwrap(); importer.decode(&flv::tag(flv::TAG_VIDEO, 0, &vseq)).unwrap(); From b4e38308f255c7e173d1ce6990cc0e1d2a498d71 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 17:53:48 -0700 Subject: [PATCH 10/11] test(net): the compress fixture holds the origin publication Co-Authored-By: Claude Opus 5.5 --- rs/moq-net/tests/integration/compress.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-net/tests/integration/compress.rs b/rs/moq-net/tests/integration/compress.rs index aec258fbc69..666ce755162 100644 --- a/rs/moq-net/tests/integration/compress.rs +++ b/rs/moq-net/tests/integration/compress.rs @@ -55,7 +55,7 @@ struct Fixture { /// A track announced by the original publisher, before any relay carries it. struct Published { publisher: moq_net::origin::Producer, - broadcast: moq_net::broadcast::Producer, + broadcast: moq_net::origin::Publication, track: moq_net::track::Producer, } From 22a8faf1d3d7095704cc7b58a282a6f113ae13f7 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 17:54:10 -0700 Subject: [PATCH 11/11] test(hls): format the handoff test's catalog fill Co-Authored-By: Claude Opus 5.5 --- rs/moq-hls/src/export/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index 5ea7147d7f5..51245d86c09 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -1221,8 +1221,7 @@ mod tests { let broadcast = origin.create_broadcast("live").unwrap(); broadcast.announce(Default::default()).unwrap(); settle().await; - let catalog = - moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); + let catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let reserved = catalog.reserve(); let mut first = catalog.test_video("video0"); first.set(video_config()).unwrap();