diff --git a/doc/lib/rs/moq-net.md b/doc/lib/rs/moq-net.md index 10b4b7f5b02..2856c9b74f6 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/README.md b/quest/m1/api-audit/README.md index 889e9fb6107..fccbae10733 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -64,7 +64,8 @@ Rejected in planning: ## Required - [Two broadcast resolutions](/quest/m1/api-audit/resolve.md) - Rust, JS, and the bindings resolve a broadcast with `request(path, epoch)` or `follow(path, epoch)`, and every racy two-step caller is gone -- [The origin announces](/quest/m1/api-audit/announce-owner.md) - a broadcast producer cannot announce; the origin hands out the announcement, so `Prepared` and standalone broadcasts lose methods that always fail +- [JS origin publications](/quest/m1/api-audit/js-publication.md) - `@moq/net` origins hand out a `Publication` that announces, so `Prepared` and standalone broadcasts lose methods that always throw +- [Binding publications](/quest/m1/api-audit/binding-publication.md) - moq-ffi, the C ABI, and every wrapper hand out a publication handle that announces, replacing the private adapters - [moq-net exports with a consumer](/quest/m1/api-audit/net-exports.md) - the dead and test-only moq-net, `@moq/net`, and moq-ffi items the follow-up audit found go private or away - [Bindings read gaps and ends](/quest/m1/api-audit/decode-read-bindings.md) - moq-ffi and every wrapper surface `Gap` and `End` instead of discarding them - [Pinned consumers adopt the CLI flag names](/quest/m1/api-audit/cli-flags-consumers.md) - pinned downstream deployments move past the flag rename and use its spellings diff --git a/quest/m1/api-audit/announce-owner.md b/quest/m1/api-audit/announce-owner.md deleted file mode 100644 index 5c4f27dad68..00000000000 --- a/quest/m1/api-audit/announce-owner.md +++ /dev/null @@ -1,27 +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. - -Public API: breaking in moq-net (and `@moq/net`/moq-ffi if they mirror the -trap). Wire: none. diff --git a/quest/m1/api-audit/binding-publication.md b/quest/m1/api-audit/binding-publication.md new file mode 100644 index 00000000000..f4692196473 --- /dev/null +++ b/quest/m1/api-audit/binding-publication.md @@ -0,0 +1,45 @@ +# [L] Binding publications + +## Goal + +In moq-ffi, the C ABI, and every wrapper, a broadcast producer fills tracks +and cannot announce. Creating a broadcast on an origin and publishing a +prepared answer return a publication handle that carries announce and +unannounce, mirroring Rust's `origin::Publication`, so a standalone or +prepared broadcast no longer exposes announcement calls that can only fail. + +## Plan + +Decided 2026-10-23 (maintainer, on +[moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ land the +announcement split in three PRs: native Rust in that PR, the +[JS mirror](/quest/m1/api-audit/js-publication.md), and this binding rollout +once the native PR merges. The native PR keeps private adapters that hold the +`Publication` beside each binding fill handle (`rs/moq-ffi/src/producer.rs` +`from_publication`, `rs/moq-c/src/publish.rs` `create_publication` and +`admit`); this quest replaces them with public handles. + +The shape is Rust's: an origin-owned record holding the fill broadcast and its +announcement capability, with the existing fill lifetime (dropping the +publication does not retract on its own). + +- moq-ffi: a `MoqPublication` object with `broadcast()`, `announce` + (taking `MoqAnnounceRoute`), and `unannounce`. + `MoqOriginProducer::create_broadcast` and `MoqBroadcastPrepared::publish` + return it; `MoqBroadcastProducer` loses `announce` and `unannounce`. +- C ABI, recommended shape: `moq_origin_create_broadcast(origin, path, + path_len, *broadcast_out)` returns a publication handle and writes the fill + handle to `broadcast_out`, as `moq_broadcast_request_prepare` already does. + `moq_broadcast_prepared_publish` returns a publication handle. + `moq_publish_announce` and `moq_publish_unannounce` become publication calls + taking that handle, with a call to release it. Admission keeps checking that + the prepared fill is the broadcast it prepared. +- Wrappers: py, swift, kt, dart, `go/wrapper`, `cpp/moq`, and `cpp/obs`, with + their tests, READMEs, `doc/lib/{py,swift,kt,go,dart,c,cpp}`, `doc/bin/obs.md`, + and a `MIGRATE.md` entry in each package. Regenerate bindings from source. + +Test in each layer: clone/drop, explicit unannounce, prepared +admission (including a mismatched fill), and a stale publication against a +replacement. Run `just test interop --all`. + +Public API: breaking in moq-ffi, the C ABI, and every wrapper. Wire: none. diff --git a/quest/m1/api-audit/js-publication.md b/quest/m1/api-audit/js-publication.md new file mode 100644 index 00000000000..93883fbf44e --- /dev/null +++ b/quest/m1/api-audit/js-publication.md @@ -0,0 +1,39 @@ +# [M] JS origin publications + +## Goal + +A `@moq/net` `broadcast.Producer` fills tracks; it cannot announce. Creating a +broadcast on an origin and publishing a `Prepared` answer return a +`Publication` that carries `announce`, `unannounce`, and `route`, so a +standalone broadcast and `prepared.broadcast` no longer expose announcement +calls that can only throw. Mirrors Rust's `origin::Publication`. + +## Plan + +Decided 2026-10-23 (maintainer, on +[moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ land the +announcement split in three PRs: native Rust in that PR, this `@moq/net` +mirror, and the [binding publications](/quest/m1/api-audit/binding-publication.md). +This one does not depend on the native PR and can start now. + +The shape is Rust's, approved on the same PR: an origin-owned record holding +the fill broadcast and its announcement capability. + +- `Producer.createBroadcast` (`js/net/src/origin.ts`) and `Prepared.publish` + return `Publication`. It exposes the fill producer as `broadcast`, plus + `announce(route)`, `unannounce()`, and `route`. No way to swap in another + producer, and no `finish`: a broadcast has no finished state. +- `broadcast.Producer` loses its public `announce`, `unannounce`, and `route` + (`js/net/src/broadcast.ts`); the announcer stays a module hook. +- Lifetime matches Rust: publication clones (or references) and fill handles + keep the existing broadcast lifetime; dropping a `Publication` does not + retract on its own. `unannounce` retracts sooner. +- Migrate every caller: `js/net` tests, benches, and examples, `js/publish`, + `js/moq-boy`, the `js/e2ee` docs, `js/watch` and `js/hang` tests, and + `demo/carla`. Add the `js/net/MIGRATE.md` entry and update + `doc/lib/js/net.md`. + +Test: clone/drop, explicit unannounce, Prepared admission, +and a stale publication against a replacement under a new epoch. + +Public API: breaking in `@moq/net`. Wire: none. diff --git a/quest/m1/broadcast-lifecycle/README.md b/quest/m1/broadcast-lifecycle/README.md index d9ae3b463ca..d9920582269 100644 --- a/quest/m1/broadcast-lifecycle/README.md +++ b/quest/m1/broadcast-lifecycle/README.md @@ -55,4 +55,4 @@ timer on main at 1582fdc07d: - [moq-transport finish interop](/quest/m2/ietf-finish.md) - the interop `finish` case, whose late readers rely on retention - [CLI and gst flags](https://github.com/moq-dev/moq/pull/6522) - removed the opt-in `linger` booleans, leaving completion to this epic -- [Publication owner](/quest/m1/api-audit/announce-owner.md) - adds `origin::Publication`; it must not forward a `finish` this epic deletes +- [JS origin publications](/quest/m1/api-audit/js-publication.md) and [binding publications](/quest/m1/api-audit/binding-publication.md) - mirror `origin::Publication`, which has no `finish` diff --git a/quest/m2/stats/preflight.md b/quest/m2/stats/preflight.md index c6119502caa..bde9d16cb78 100644 --- a/quest/m2/stats/preflight.md +++ b/quest/m2/stats/preflight.md @@ -9,7 +9,7 @@ rendition, sync point interval, audio/video skew, media timestamp continuity, the [health verdict](/doc/concept/stats.md#health) from the publisher's stats, and whether the broadcast ends with a clean unannounce (observed by the check; the publisher, which owns the -`broadcast::Producer`, does the unannounce). A failure names the broken layer +`origin::Publication`, does the unannounce). A failure names the broken layer and its evidence instead of one score, and the raw evidence is kept so two people reach the same diagnosis. 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-archive/README.md b/rs/moq-archive/README.md index c29d0f43833..b96bee472f7 100644 --- a/rs/moq-archive/README.md +++ b/rs/moq-archive/README.md @@ -31,7 +31,7 @@ use moq_archive::object_store::memory::InMemory; let store = moq_archive::Store::new(InMemory::new(), "recordings/demo"); moq_archive::Producer::new(store.clone(), broadcast, Default::default()).run().await?; -// `served` is a `moq_net::broadcast::Producer`, announced once `open` returns. +// `served` is an `origin::Publication`'s fill, announced once `open` returns. let consumer = moq_archive::Consumer::open(store, served, Default::default()).await?; consumer.run().await?; ``` diff --git a/rs/moq-archive/src/consumer/mod.rs b/rs/moq-archive/src/consumer/mod.rs index 832472b86e5..133eec41327 100644 --- a/rs/moq-archive/src/consumer/mod.rs +++ b/rs/moq-archive/src/consumer/mod.rs @@ -18,7 +18,7 @@ //! //! let store = moq_archive::Store::new(InMemory::new(), "recordings/demo"); //! let consumer = moq_archive::Consumer::open(store, broadcast, Default::default()).await?; -//! // Every stored track exists now: announce the broadcast, then serve it. +//! // Every stored track exists now: announce its publication, then serve it. //! consumer.run().await?; //! # Ok(()) //! # } diff --git a/rs/moq-boy/src/input.rs b/rs/moq-boy/src/input.rs index 8bcea7282e9..963d5a91105 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 08184dfd4a6..8c047c1dfc8 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 6df6a978883..cff5ec7c619 100644 --- a/rs/moq-c/src/api.rs +++ b/rs/moq-c/src/api.rs @@ -1762,7 +1762,7 @@ pub unsafe extern "C" fn moq_origin_create_broadcast(origin: u32, path: *const c let mut state = State::lock(); let broadcast = state.origin.create_broadcast(origin, path)?; - state.publish.create(broadcast) + state.publish.create_publication(broadcast) }) } @@ -1857,7 +1857,7 @@ pub unsafe extern "C" fn moq_broadcast_request_prepare( let (pending, driver) = state.origin.broadcast_request_take(request)?; let prepared = pending.prepare(epoch)?; let broadcast = state.publish.create((prepared.broadcast().clone(), driver.clone()))?; - let id = state.origin.prepared_insert(prepared, driver)?; + let id = state.origin.prepared_insert(prepared, driver, broadcast)?; *broadcast_out = broadcast.into(); Ok(id) }) @@ -1871,12 +1871,12 @@ pub unsafe extern "C" fn moq_broadcast_request_prepare( pub extern "C" fn moq_broadcast_prepared_publish(prepared: u32) -> i32 { ffi::enter(move || { let prepared = ffi::parse_id(prepared)?; - let (prepared, driver) = State::lock().origin.prepared_take(prepared)?; + let mut state = State::lock(); + let (prepared, driver, broadcast) = state.origin.prepared_take(prepared)?; driver.check()?; - let result = prepared.publish(); + let result = state.publish.admit(broadcast, prepared); driver.check()?; - result?; - Ok(()) + result }) } 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 aac4d2e2c0e..2b2fbe6de7d 100644 --- a/rs/moq-c/src/publish.rs +++ b/rs/moq-c/src/publish.rs @@ -56,6 +56,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, @@ -235,6 +236,7 @@ impl Publish { let id = self.broadcasts.insert(Broadcast { driver, producer: broadcast, + publication: None, catalog, video: BTreeMap::new(), audio: BTreeMap::new(), @@ -244,11 +246,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 fill leaves this registry only on close, which native admission refuses. + 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(()) @@ -257,7 +290,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(()) } @@ -1144,12 +1179,63 @@ 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(), + ended: false, + } + }; + 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( @@ -1173,7 +1259,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 731d4bc3920..5418a971f93 100644 --- a/rs/moq-c/src/test.rs +++ b/rs/moq-c/src/test.rs @@ -3704,14 +3704,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 22f3ae353d5..71f947dc80b 100644 --- a/rs/moq-cli/src/announced.rs +++ b/rs/moq-cli/src/announced.rs @@ -185,7 +185,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/archive.rs b/rs/moq-cli/src/archive.rs index bad6bd0b9c4..2946db80704 100644 --- a/rs/moq-cli/src/archive.rs +++ b/rs/moq-cli/src/archive.rs @@ -253,7 +253,7 @@ pub async fn import( config = config.with_refresh(refresh.into()); } let broadcast = origin.create_broadcast(&name).context("failed to create broadcast")?; - let consumer = moq_archive::Consumer::open(store, broadcast.clone(), config) + let consumer = moq_archive::Consumer::open(store, broadcast.broadcast().clone(), config) .await .with_context(|| format!("no readable recording at {}", args.store))?; broadcast diff --git a/rs/moq-cli/src/fetch.rs b/rs/moq-cli/src/fetch.rs index 3f3c6c8fa57..299bbcac48e 100644 --- a/rs/moq-cli/src/fetch.rs +++ b/rs/moq-cli/src/fetch.rs @@ -182,7 +182,7 @@ mod tests { struct Fixture { origin: moq_net::origin::Producer, _keep: ( - moq_net::broadcast::Producer, + moq_net::origin::Publication, Vec, moq_net::group::Producer, ), @@ -240,7 +240,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 bec2b7f792f..032785198df 100644 --- a/rs/moq-cli/src/hls.rs +++ b/rs/moq-cli/src/hls.rs @@ -159,12 +159,12 @@ pub async fn import(target: ImportTarget, args: ImportArgs, epoch: moq_net::Epoc linger, 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. @@ -172,14 +172,14 @@ 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"); let ran = match importer.init().await { Ok(()) => { notify_ready(); - run_announced(&mut importer, &mut listed, &mut producer, epoch).await + run_announced(&mut importer, &mut listed, &producer, epoch).await } Err(err) => Err(err.into()), }; @@ -193,7 +193,9 @@ pub async fn import(target: ImportTarget, args: ImportArgs, epoch: moq_net::Epoc // arrives after the end still gets the tail. An interrupt drops it at once. importer.finish()?; tracing::info!(?linger, "playlist ended; serving the finished broadcast"); - moq_mux::Linger::new([producer], importer, linger).wait().await; + moq_mux::Linger::new([producer.broadcast().clone()], importer, linger) + .wait() + .await; Ok(()) } @@ -203,7 +205,7 @@ pub async fn import(target: ImportTarget, args: ImportArgs, epoch: moq_net::Epoc async fn run_announced( importer: &mut moq_hls::import::Import, listed: &mut moq_mux::catalog::hang::Consumer, - producer: &mut moq_net::broadcast::Producer, + publication: &moq_net::origin::Publication, epoch: moq_net::Epoch, ) -> anyhow::Result<()> { let run = importer.run(); @@ -220,7 +222,7 @@ async fn run_announced( } }; anyhow::ensure!(catalog.is_some(), "the playlist ended before it declared a track"); - producer + publication .announce(moq_net::origin::Route::default().with_epoch(epoch)) .context("failed to announce broadcast")?; if !ended { @@ -325,7 +327,7 @@ mod test { tempfile::TempDir, moq_hls::import::Import, moq_mux::catalog::hang::Consumer, - moq_net::broadcast::Producer, + moq_net::origin::Publication, )> { let dir = tempfile::tempdir()?; write_playlist(dir.path(), segments, false); @@ -333,11 +335,15 @@ mod test { let (origin, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default()); tokio::spawn(moq_net::time::run(driver)); - let mut producer = origin.create_broadcast("hls")?; - let catalog = moq_mux::catalog::Producer::new(&mut producer, moq_mux::catalog::Config::default())?; + let producer = origin.create_broadcast("hls")?; + let catalog = + moq_mux::catalog::Producer::new(&mut producer.broadcast().clone(), moq_mux::catalog::Config::default())?; let listed = catalog.consume()?; - let mut importer = - moq_hls::import::Import::new(producer.clone(), catalog, moq_hls::import::Config::new(playlist))?; + let mut importer = moq_hls::import::Import::new( + producer.broadcast().clone(), + catalog, + moq_hls::import::Config::new(playlist), + )?; importer.init().await?; write_playlist(dir.path(), segments, true); @@ -350,8 +356,8 @@ mod test { #[tokio::test] async fn an_ended_playlist_announces_its_catalog() { for _ in 0..32 { - let (_dir, mut importer, mut listed, mut producer) = joined_then_ended(1).await.unwrap(); - run_announced(&mut importer, &mut listed, &mut producer, moq_net::Epoch::mint()) + let (_dir, mut importer, mut listed, producer) = joined_then_ended(1).await.unwrap(); + run_announced(&mut importer, &mut listed, &producer, moq_net::Epoch::mint()) .await .unwrap(); assert!(producer.route().is_some(), "the finished playlist was not announced"); @@ -371,8 +377,8 @@ mod test { /// A playlist that ends having declared nothing still fails, and announces nothing. #[tokio::test] async fn an_empty_playlist_fails_unannounced() { - let (_dir, mut importer, mut listed, mut producer) = joined_then_ended(0).await.unwrap(); - let result = run_announced(&mut importer, &mut listed, &mut producer, moq_net::Epoch::mint()).await; + let (_dir, mut importer, mut listed, producer) = joined_then_ended(0).await.unwrap(); + let result = run_announced(&mut importer, &mut listed, &producer, moq_net::Epoch::mint()).await; assert!(producer.route().is_none(), "an empty playlist was announced"); let err = result.expect_err("an empty playlist imports nothing"); assert!(format!("{err:#}").contains("before it declared a track"), "{err:#}"); diff --git a/rs/moq-cli/src/main.rs b/rs/moq-cli/src/main.rs index db216e4464a..7f655fd722a 100644 --- a/rs/moq-cli/src/main.rs +++ b/rs/moq-cli/src/main.rs @@ -747,7 +747,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)? } .with_linger(linger); publish.announce(epoch)?; @@ -1143,7 +1143,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 d99da3a67ab..b5eed18d985 100644 --- a/rs/moq-cli/src/play/media.rs +++ b/rs/moq-cli/src/play/media.rs @@ -1384,9 +1384,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()); @@ -1430,9 +1430,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()); @@ -1495,9 +1495,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 track = |name: &str| { let track = broadcast .create_track(name, hang::container::track_info(hang::catalog::PRIORITY.audio)) @@ -1583,12 +1583,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) @@ -1599,7 +1599,7 @@ mod tests { claim: &moq_net::origin::Dynamic, route: &moq_net::origin::Route, ) -> ( - moq_net::broadcast::Producer, + moq_net::origin::Publication, catalog::Producer, moq_mux::container::Producer, ) { @@ -1933,9 +1933,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 7068dc971ba..7c2eb061a04 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -303,10 +303,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 `--report` track, written beside the decode loop; see [`Self::with_report`]. @@ -321,13 +321,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, @@ -372,6 +372,7 @@ impl Publish { catalog: PublishCatalog::Ts(catalog), }, broadcast: Some(broadcast), + publication: None, route: None, report: None, linger: moq_mux::Linger::DEFAULT, @@ -420,12 +421,24 @@ impl Publish { catalog: PublishCatalog::Media(catalog), }, broadcast: Some(broadcast), + publication: None, route: None, report: None, linger: moq_mux::Linger::DEFAULT, }) } + /// 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`, ...). /// @@ -439,6 +452,7 @@ impl Publish { catalog: PublishCatalog::TsPrograms, }, broadcast: None, + publication: None, route: None, report: None, linger: moq_mux::Linger::DEFAULT, @@ -456,11 +470,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)?; @@ -480,6 +495,7 @@ impl Publish { audio, }, broadcast: Some(broadcast), + publication: Some(publication), route: None, report: None, linger: moq_mux::Linger::DEFAULT, @@ -534,7 +550,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")] @@ -547,7 +563,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, }; @@ -977,11 +993,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(); @@ -1040,7 +1056,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(); @@ -1093,7 +1109,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(); #[cfg_attr( not(feature = "capture"), expect( @@ -1287,7 +1304,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(), @@ -1732,7 +1749,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(); #[cfg_attr( not(feature = "capture"), expect( @@ -1846,7 +1863,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(); #[cfg_attr( not(feature = "capture"), expect( diff --git a/rs/moq-cli/src/report.rs b/rs/moq-cli/src/report.rs index e819594afc6..f04ae7a5145 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 f3a92f65867..311f6fdfb57 100644 --- a/rs/moq-cli/src/rtc.rs +++ b/rs/moq-cli/src/rtc.rs @@ -109,7 +109,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 532ce8e5ef6..2565277ca2f 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/src/transcode.rs b/rs/moq-cli/src/transcode.rs index a118cf40038..8a22ec0a890 100644 --- a/rs/moq-cli/src/transcode.rs +++ b/rs/moq-cli/src/transcode.rs @@ -176,7 +176,7 @@ impl Following { // Dropping the old ladder cancels its pending requests and rung tasks. drop(running.take()); let output = self.publish.create_broadcast(&self.output_path)?; - let transcoder = moq_transcode::Transcoder::new(source, output.clone(), self.config.clone())?; + let transcoder = moq_transcode::Transcoder::new(source, output.broadcast().clone(), self.config.clone())?; output.announce(moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()))?; tracing::info!(source = %self.source_path, output = %self.output_path, "transcoding"); running = Some(Box::pin(transcoder.run())); @@ -273,8 +273,8 @@ mod tests { let origin = moq_tokio::origin::spawn(); let publish = moq_tokio::origin::spawn(); let route = || moq_net::origin::Route::default().with_epoch(moq_net::Epoch::mint()); - let mut old = origin.publish("source", route()).unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut old, Default::default()).unwrap(); + let old = origin.publish("source", route()).unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut old.broadcast().clone(), Default::default()).unwrap(); let mut derivatives = moq_mux::follow::Instances::new(&publish.consume(), "derived", None).unwrap(); let following = Following { origin: origin.consume(), diff --git a/rs/moq-cli/tests/integration/epochs.rs b/rs/moq-cli/tests/integration/epochs.rs index e55907363ba..cabf046269c 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"; @@ -75,14 +75,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( @@ -91,7 +91,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); diff --git a/rs/moq-cli/tests/integration/export.rs b/rs/moq-cli/tests/integration/export.rs index 2e1400e4cb2..3cad64a896d 100644 --- a/rs/moq-cli/tests/integration/export.rs +++ b/rs/moq-cli/tests/integration/export.rs @@ -516,12 +516,12 @@ async fn the_same_instance_within_the_follow_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-e2ee/tests/transport.rs b/rs/moq-e2ee/tests/transport.rs index 6de56e3699e..c19bcdfad40 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 f25a243a251..959e35ee82d 100644 --- a/rs/moq-ffi/src/origin.rs +++ b/rs/moq-ffi/src/origin.rs @@ -431,7 +431,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(), )?)) @@ -600,9 +600,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 fill releases this state only on close, which native admission refuses. Ok(self.broadcast.clone()) } } diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index 1ee3187f961..450564cd76a 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, @@ -207,6 +208,7 @@ impl MoqBroadcastProducer { Ok(Self { state: std::sync::Mutex::new(Some(BroadcastProducer { broadcast, + publication: None, catalog, timebases: Default::default(), driver, @@ -216,6 +218,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())) @@ -290,7 +305,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(()) }) } @@ -303,7 +318,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 c0bede188d1..5ad87984d86 100644 --- a/rs/moq-ffi/src/test.rs +++ b/rs/moq-ffi/src/test.rs @@ -406,7 +406,16 @@ async fn announced_route_keeps_static_cost_on_reannounce() { epoch: route.epoch.clone(), }) .unwrap(); - let current = broadcast.state.lock().unwrap().as_ref().unwrap().broadcast.route(); + let current = broadcast + .state + .lock() + .unwrap() + .as_ref() + .unwrap() + .publication + .as_ref() + .unwrap() + .route(); assert_eq!(current.map(MoqRoute::from), Some(route)); broadcast.close().unwrap(); @@ -4180,7 +4189,8 @@ 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. @@ -4189,6 +4199,12 @@ async fn dynamic_broadcast_request() { Err(MoqError::Closed) )); prepared.publish().unwrap(); + served + .announce(MoqAnnounceRoute { + epoch: Some(epoch), + cost: 7, + }) + .unwrap(); assert!(matches!(request.path(), Err(MoqError::Closed))); let broadcast = tokio::time::timeout(TIMEOUT, request_broadcast) diff --git a/rs/moq-gst/src/sink/imp.rs b/rs/moq-gst/src/sink/imp.rs index e295cfb5fc3..1522246b98d 100644 --- a/rs/moq-gst/src/sink/imp.rs +++ b/rs/moq-gst/src/sink/imp.rs @@ -67,7 +67,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, /// How long a clean end keeps the finished broadcast up before its EOS. @@ -1051,7 +1051,8 @@ impl MoqSink { None if completion.finish() => { // The closed renditions and the catalog hold the finished tracks, so they go only // once the broadcast closes. - let finished = moq_mux::Linger::new([state.broadcast.clone()], (closed, catalog), state.linger); + let finished = + moq_mux::Linger::new([state.broadcast.broadcast().clone()], (closed, catalog), state.linger); self.linger_locked(control, finished) } None => Posted::Nothing, @@ -2041,7 +2042,7 @@ mod tests { let (broadcast, session) = { let control = sink.imp().control.lock().unwrap(); let state = control.live.as_ref().expect("live session"); - (state.broadcast.clone(), state.session.completion()) + (state.broadcast.broadcast().clone(), state.session.completion()) }; let consumer = broadcast.consume(); let track = broadcast.create_track("video", None).unwrap(); diff --git a/rs/moq-gst/src/sink/session.rs b/rs/moq-gst/src/sink/session.rs index a69fbae8435..52ad86aee4b 100644 --- a/rs/moq-gst/src/sink/session.rs +++ b/rs/moq-gst/src/sink/session.rs @@ -267,7 +267,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. @@ -278,8 +278,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(); @@ -533,7 +534,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(), @@ -552,7 +553,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 31307fbd704..8c38b8fb5d5 100644 --- a/rs/moq-gst/src/source/imp.rs +++ b/rs/moq-gst/src/source/imp.rs @@ -2255,13 +2255,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()); @@ -3056,6 +3057,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, @@ -3065,15 +3067,19 @@ 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 } /// The `cam` broadcast a rendition of `room` references as `./cam`, under `route`. fn sibling(origin: &moq_net::origin::Producer, route: moq_net::origin::Route) -> Self { - let publisher = Self::build(origin.create_broadcast("cam").unwrap(), video_rendition()); - publisher.broadcast.announce(route).unwrap(); + let publication = origin.create_broadcast("cam").unwrap(); + let mut publisher = Self::build(publication.broadcast().clone(), video_rendition()); + publication.announce(route).unwrap(); + publisher.publication = Some(publication); publisher } @@ -3092,6 +3098,7 @@ mod session_tests { moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Video), ); Self { + publication: None, broadcast, catalog, video, @@ -3647,9 +3654,9 @@ mod session_tests { first.video.finish().unwrap(); await_finished(&first); await_event(&mut follow, |event| matches!(event, moq_net::announce::Event::Start(_))); - first.broadcast.unannounce(); + first.publication.as_ref().unwrap().unannounce(); await_event(&mut follow, |event| matches!(event, moq_net::announce::Event::End(_))); - first.broadcast.announce(route).unwrap(); + first.publication.as_ref().unwrap().announce(route).unwrap(); await_event(&mut follow, |event| matches!(event, moq_net::announce::Event::Start(_))); let mut second = Publisher::sibling(&origin, epoch()); @@ -4285,7 +4292,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(_)) }); @@ -4424,7 +4436,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 3ddd9a405ef..83a7497de05 100644 --- a/rs/moq-hls/src/export/archive_tests.rs +++ b/rs/moq-hls/src/export/archive_tests.rs @@ -318,7 +318,7 @@ impl Replay { let driver = tokio::spawn(moq_net::time::run(driver)); let broadcast = origin.create_broadcast("rec").unwrap(); let serving = moq_archive::consumer::Config::default().with_cache(cache); - let consumer = moq_archive::Consumer::open(store.clone(), broadcast.clone(), serving) + let consumer = moq_archive::Consumer::open(store.clone(), broadcast.broadcast().clone(), serving) .await .unwrap(); let consumer = tokio::spawn(consumer.run()); diff --git a/rs/moq-hls/src/export/mod.rs b/rs/moq-hls/src/export/mod.rs index 97be0f11c38..472eb678d16 100644 --- a/rs/moq-hls/src/export/mod.rs +++ b/rs/moq-hls/src/export/mod.rs @@ -1084,10 +1084,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(); @@ -1146,10 +1148,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(); @@ -1218,10 +1222,10 @@ mod tests { #[tokio::test(start_paused = true)] async fn disabled_reference_handoff_continues_the_source_media_sequence() { 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 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 first = catalog.test_video("video0"); first.set(video_config()).unwrap(); @@ -1325,10 +1329,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"] { @@ -1456,10 +1461,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"] { @@ -1559,14 +1565,15 @@ mod tests { /// A broadcast advertising timelines for `video0` and `video1`. async fn timeline_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"] { @@ -1736,8 +1743,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(); @@ -1767,8 +1774,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(); @@ -1804,10 +1811,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; @@ -1849,10 +1856,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; @@ -1906,10 +1913,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(); @@ -1957,10 +1966,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(); @@ -2045,10 +2056,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(); @@ -2089,10 +2102,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(); @@ -2195,7 +2210,7 @@ mod tests { #[tokio::test] async fn root_clock_times_playlists_and_manifest() { 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; @@ -2203,7 +2218,7 @@ mod tests { let wall = moq_clock::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::Instant::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(); @@ -2332,11 +2347,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 @@ -2447,11 +2464,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 = @@ -2522,10 +2539,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(); @@ -2584,10 +2603,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(); @@ -2673,10 +2694,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(); @@ -2725,10 +2748,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(); @@ -2765,10 +2790,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(); @@ -2858,10 +2885,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(); @@ -2953,10 +2982,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 @@ -2964,7 +2994,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; @@ -3098,9 +3128,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(); @@ -3216,9 +3250,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)); @@ -3337,13 +3373,15 @@ 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 = origin.create_broadcast("media").unwrap(); - let _old_track = write_routed_media(&mut old_media, OLD, recorder, 0); + let old_media = origin.create_broadcast("media").unwrap(); + let _old_track = write_routed_media(&mut old_media.broadcast().clone(), OLD, recorder, 0); settle().await; let upstream = crate::resolved(&origin, "live").await; @@ -3388,12 +3426,12 @@ mod tests { } // A publisher serving the path afterwards, with rows recorded for it, is never bound. - let mut new_media = origin.create_broadcast("media").unwrap(); + let new_media = origin.create_broadcast("media").unwrap(); new_media.announce(sibling_route(11)).unwrap(); let _ = rendition.snapshot(); let recorder = catalog.enroll_test("video0").unwrap(); fanout.skip(None); - let _new_track = write_routed_media(&mut new_media, NEW, recorder, 6_000_000); + let _new_track = write_routed_media(&mut new_media.broadcast().clone(), NEW, recorder, 6_000_000); tokio::time::sleep(Duration::from_secs(1)).await; let snapshot = rendition.snapshot(); assert!(snapshot.segments.is_empty(), "nothing of the replacement is listed"); @@ -3411,13 +3449,15 @@ 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 = origin.create_broadcast("media").unwrap(); - let _track = write_routed_media(&mut media, KEYFRAME, recorder, 0); + let media = origin.create_broadcast("media").unwrap(); + let _track = write_routed_media(&mut media.broadcast().clone(), KEYFRAME, recorder, 0); settle().await; let upstream = crate::resolved(&origin, "live").await; @@ -3467,13 +3507,15 @@ 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 = origin.create_broadcast("media").unwrap(); - let _track = write_routed_media(&mut media, NEW, recorder, 0); + let media = origin.create_broadcast("media").unwrap(); + let _track = write_routed_media(&mut media.broadcast().clone(), NEW, recorder, 0); media.announce(sibling_route(11)).unwrap(); settle().await; @@ -3505,13 +3547,15 @@ 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 = origin.create_broadcast("media").unwrap(); - let _old_track = write_routed_media(&mut old_media, OLD, recorder, 0); + let old_media = origin.create_broadcast("media").unwrap(); + let _old_track = write_routed_media(&mut old_media.broadcast().clone(), OLD, recorder, 0); settle().await; let upstream = crate::resolved(&origin, "live").await; @@ -3536,9 +3580,9 @@ mod tests { .expect("the old retained segment drains"); assert!(contains(&served, OLD)); - let mut new_media = origin.create_broadcast("media").unwrap(); + let new_media = origin.create_broadcast("media").unwrap(); let recorder = catalog.enroll_test("video0").unwrap(); - let _new_track = write_routed_media(&mut new_media, NEW, recorder, 6_000_000); + let _new_track = write_routed_media(&mut new_media.broadcast().clone(), NEW, recorder, 6_000_000); new_media.announce(sibling_route(11)).unwrap(); let _ = rendition.snapshot(); tokio::time::sleep(Duration::from_secs(1)).await; @@ -3564,12 +3608,14 @@ 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 = origin.create_broadcast("media").unwrap(); - let track = write_routed_media(&mut media, OLD, recorder, 0); + let media = origin.create_broadcast("media").unwrap(); + let track = write_routed_media(&mut media.broadcast().clone(), OLD, recorder, 0); settle().await; let upstream = crate::resolved(&origin, "live").await; @@ -3612,12 +3658,14 @@ 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 = origin.create_broadcast("media").unwrap(); - let _track = write_routed_media(&mut media, OLD, recorder, 0); + let media = origin.create_broadcast("media").unwrap(); + let _track = write_routed_media(&mut media.broadcast().clone(), OLD, recorder, 0); settle().await; let upstream = crate::resolved(&origin, "live").await; @@ -3725,10 +3773,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(); @@ -3783,10 +3833,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(); @@ -4393,10 +4445,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 88190a2ec42..0543de5f44c 100644 --- a/rs/moq-hls/src/server/routes.rs +++ b/rs/moq-hls/src/server/routes.rs @@ -401,9 +401,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()); @@ -445,12 +446,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(); @@ -521,9 +522,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(run()).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); @@ -538,9 +540,10 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_restart_lists_the_replacement_under_its_own_urls() { let origin = moq_tokio::origin::spawn(); - let mut old = origin.create_broadcast("live").expect("publish"); + let old = origin.create_broadcast("live").expect("publish"); old.announce(run()).expect("announce"); - let (old_catalog, _, old_track, mut old_media) = publish_video(&mut old, video_config(), timed()); + let (old_catalog, _, old_track, mut old_media) = + publish_video(&mut old.broadcast().clone(), video_config(), timed()); write_three_gops(&mut old_media); let app = Server::new(origin.consume(), crate::export::Config::default()).router(); @@ -550,10 +553,11 @@ mod tests { wait_listed(&app, &old_playlist, "seg/656e3d1b.0.m4s").await; let next = moq_net::Epoch::mint(); - let mut new = origin.create_broadcast("live").expect("publish"); + let new = origin.create_broadcast("live").expect("publish"); new.announce(moq_net::origin::Route::default().with_epoch(next.clone())) .expect("announce"); - let (_new_catalog, _, _new_track, mut new_media) = publish_video(&mut new, video_config(), timed()); + let (_new_catalog, _, _new_track, mut new_media) = + publish_video(&mut new.broadcast().clone(), video_config(), timed()); write_three_gops(&mut new_media); let new_playlist = format!("/live/@{}/video/video0/media.m3u8", next.as_str()); wait_listed( @@ -584,9 +588,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(run()).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| { @@ -637,9 +642,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(run()).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()); @@ -712,9 +718,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 server = Server::new(pair.sub_origin.consume(), crate::export::Config::default()); @@ -737,9 +744,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 server = Server::new(pair.sub_origin.consume(), crate::export::Config::default()); @@ -836,13 +844,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(run()).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()); @@ -954,15 +962,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/benches/containers.rs b/rs/moq-mux/benches/containers.rs index d7137b0dde0..b2d13b25d39 100644 --- a/rs/moq-mux/benches/containers.rs +++ b/rs/moq-mux/benches/containers.rs @@ -56,8 +56,8 @@ async fn prepare(video: Video, payload: &Bytes) -> (moq_mux::Source, impl Stream } = video; let (origin, driver) = moq_net::origin::Producer::new(Default::default()); let driver = tokio::spawn(moq_net::time::run(driver)); - let mut broadcast = origin.create_broadcast("bench").unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let broadcast = origin.create_broadcast("bench").unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let mut publishers = Vec::new(); for index in 0..tracks { let track = broadcast diff --git a/rs/moq-mux/benches/export.rs b/rs/moq-mux/benches/export.rs index 7c6ad8d0097..dee27305ed1 100644 --- a/rs/moq-mux/benches/export.rs +++ b/rs/moq-mux/benches/export.rs @@ -26,8 +26,8 @@ impl Drop for Fixture { async fn prepare(tracks: usize, frames: usize) -> (moq_mux::Source, moq_mux::catalog::Consumer, Fixture) { let (origin, driver) = moq_net::origin::Producer::new(Default::default()); let driver = tokio::spawn(moq_net::time::run(driver)); - let mut broadcast = origin.create_broadcast("bench").unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let broadcast = origin.create_broadcast("bench").unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let stream = moq_mux::catalog::Consumer::from(catalog.consume().unwrap()); let mut publishers = Vec::new(); for index in 0..tracks { @@ -169,8 +169,8 @@ async fn prepare_video( ) -> (moq_mux::Source, moq_mux::catalog::Consumer, Fixture) { let (origin, driver) = moq_net::origin::Producer::new(Default::default()); let driver = tokio::spawn(moq_net::time::run(driver)); - let mut broadcast = origin.create_broadcast("bench").unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, Default::default()).unwrap(); + let broadcast = origin.create_broadcast("bench").unwrap(); + let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), Default::default()).unwrap(); let stream = moq_mux::catalog::Consumer::from(catalog.consume().unwrap()); let track = broadcast .create_track("video", catalog.track_info(hang::catalog::PRIORITY.video)) diff --git a/rs/moq-mux/src/catalog/producer.rs b/rs/moq-mux/src/catalog/producer.rs index d3e39bdd12f..6aad1714cfc 100644 --- a/rs/moq-mux/src/catalog/producer.rs +++ b/rs/moq-mux/src/catalog/producer.rs @@ -1456,7 +1456,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(); @@ -2065,11 +2065,11 @@ mod test { let wall = moq_clock::Wall::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/test_util.rs b/rs/moq-mux/src/container/test_util.rs index 48f0be9fd64..55ebf10e766 100644 --- a/rs/moq-mux/src/container/test_util.rs +++ b/rs/moq-mux/src/container/test_util.rs @@ -109,7 +109,7 @@ pub(crate) struct Moving { /// `other`'s track `a`. pub(crate) other: crate::container::Producer, origin: moq_net::origin::Producer, - _broadcasts: [moq_net::broadcast::Producer; 2], + _broadcasts: [moq_net::origin::Publication; 2], } impl Moving { @@ -119,9 +119,10 @@ impl Moving { pub(crate) fn new() -> Self { let origin = crate::source::produce_origin(); - let mut live = origin.publish("live", Default::default()).unwrap(); - let mut other = origin.publish("other", Default::default()).unwrap(); - let catalog = crate::catalog::Producer::new(&mut live, crate::catalog::Config::default()).unwrap(); + let live = origin.publish("live", Default::default()).unwrap(); + let other = origin.publish("other", Default::default()).unwrap(); + let catalog = + crate::catalog::Producer::new(&mut live.broadcast().clone(), crate::catalog::Config::default()).unwrap(); let track = |broadcast: &mut moq_net::broadcast::Producer, name: &str| { let info = hang::container::track_info(hang::catalog::PRIORITY.video); let track = broadcast.create_track(name, info).unwrap(); @@ -129,9 +130,9 @@ impl Moving { crate::container::Producer::new(track, wire) }; let mut moving = Self { - a: track(&mut live, "a"), - b: track(&mut live, "b"), - other: track(&mut other, "a"), + a: track(&mut live.broadcast().clone(), "a"), + b: track(&mut live.broadcast().clone(), "b"), + other: track(&mut other.broadcast().clone(), "a"), catalog, origin, _broadcasts: [live, other], diff --git a/rs/moq-mux/src/container/ts/export_test.rs b/rs/moq-mux/src/container/ts/export_test.rs index 4932fbf6dc8..9d95fe3e0cc 100644 --- a/rs/moq-mux/src/container/ts/export_test.rs +++ b/rs/moq-mux/src/container/ts/export_test.rs @@ -8293,10 +8293,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) } @@ -8390,8 +8390,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() @@ -8414,13 +8414,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); @@ -8464,8 +8464,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; @@ -8521,8 +8521,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() @@ -8545,8 +8545,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 @@ -8579,8 +8579,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() @@ -8595,9 +8595,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 @@ -8659,8 +8659,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, @@ -8715,8 +8715,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() @@ -8740,8 +8740,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(); @@ -8764,9 +8764,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)) @@ -8788,8 +8788,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(); @@ -8949,9 +8949,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(); @@ -9063,7 +9063,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, @@ -9078,7 +9078,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)); } @@ -9094,8 +9094,8 @@ async fn a_follower_fails_at_once_on_its_own_failure() { ]; for cause in causes { 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_follow(Duration::from_secs(10)); assert!( tokio::time::timeout(Duration::from_secs(1), follower.next()) @@ -9129,8 +9129,8 @@ async fn a_follower_waits_out_the_follow_on_a_lost_route() { ]; for cause in causes { 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 follow = Duration::from_secs(10); let mut follower = follower(&origin).await.with_follow(follow); assert!( @@ -9444,8 +9444,8 @@ async fn a_follower_keeps_a_return_awaiting_its_catalog_when_a_stitch_goes() { async fn a_follower_follows_a_return_after_a_lost_route() { 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_follow(Duration::from_secs(10)); assert!( tokio::time::timeout(Duration::from_secs(1), follower.next()) @@ -9457,8 +9457,8 @@ async fn a_follower_follows_a_return_after_a_lost_route() { track.abort(moq_net::Error::Unroutable); catalog.abort(moq_net::Error::Unroutable); drop(broadcast); - 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 @@ -9544,8 +9544,8 @@ async fn a_follower_takes_its_own_instance_winning_back_for_no_replacement() { }; // The export resolves its own instance. - let mut served = origin.create_broadcast("pool/job").unwrap(); - let mut catalog = crate::catalog::Producer::<()>::new(&mut served, Default::default()).unwrap(); + let served = origin.create_broadcast("pool/job").unwrap(); + let mut catalog = crate::catalog::Producer::<()>::new(&mut served.broadcast().clone(), Default::default()).unwrap(); served.announce(route(5)).unwrap(); let source = crate::source::resolved(&origin, "pool/job").await; let mut follower = super::Follower::new(open_export(source).await.unwrap()).unwrap(); @@ -9660,9 +9660,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 @@ -9685,9 +9685,9 @@ async fn a_follower_continues_through_a_same_epoch_handoff_after_a_gap() { catalog.abort(moq_net::Error::Unroutable); drop(exact); tokio::time::sleep(Duration::from_secs(1)).await; - let mut served = origin.create_broadcast("pool/job").unwrap(); - let mut catalog = crate::catalog::Producer::<()>::new(&mut served, Default::default()).unwrap(); - let mut track = aac_rendition(&mut served, &mut catalog, "a.aac"); + let served = origin.create_broadcast("pool/job").unwrap(); + let mut catalog = crate::catalog::Producer::<()>::new(&mut served.broadcast().clone(), Default::default()).unwrap(); + let mut track = aac_rendition(&mut served.broadcast().clone(), &mut catalog, "a.aac"); served.announce(route).unwrap(); let resumed = start.elapsed().as_millis() as u64; for ms in (resumed..resumed + 200).step_by(20) { @@ -9722,8 +9722,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); @@ -9779,8 +9779,8 @@ async fn a_midstream_stitch_awaits_the_replacement_catalog() { #[tokio::test(start_paused = true)] async fn a_midstream_stitch_keeps_the_old_instance_playing_until_it_switches() { let origin = crate::source::produce_origin(); - let (mut broadcast, mut catalog) = publish_bare(&origin, Some(&moq_net::Epoch::mint())); - let mut track = aac_rendition(&mut broadcast, &mut catalog, "a.aac"); + let (broadcast, mut catalog) = publish_bare(&origin, Some(&moq_net::Epoch::mint())); + let mut track = aac_rendition(&mut broadcast.broadcast().clone(), &mut catalog, "a.aac"); let export = open_export(crate::source::resolved(&origin, "live").await) .await .unwrap() @@ -9899,7 +9899,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() @@ -9908,13 +9908,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); } @@ -9945,10 +9945,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() @@ -9976,8 +9976,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() @@ -10014,16 +10014,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); } @@ -10166,10 +10166,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(); @@ -10701,8 +10701,8 @@ async fn a_track_renamed_on_its_pid_across_returns_retires_the_one_before() { track }; - let (mut broadcast, mut catalog) = publish_live::(&origin, route()); - let mut track = rendition(&mut broadcast, &mut catalog, "0.aac"); + let (broadcast, mut catalog) = publish_live::(&origin, route()); + let mut track = rendition(&mut broadcast.broadcast().clone(), &mut catalog, "0.aac"); let source = crate::source::resolved(&origin, "live").await; let mut export = open_ts(source, crate::catalog::CatalogFormat::Hang) .await @@ -10719,12 +10719,16 @@ async fn a_track_renamed_on_its_pid_across_returns_retires_the_one_before() { for round in 1..=4 { 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()); let back = origin.consume().routed_broadcast("live").await.unwrap(); let stream = crate::catalog::Consumer::new(&back, Default::default()).await.unwrap(); export = export.follow(back, stream).unwrap(); drain_frames(&mut export).await; - let mut track = rendition(&mut broadcast, &mut catalog, &format!("{round}.aac")); + let mut track = rendition( + &mut broadcast.broadcast().clone(), + &mut catalog, + &format!("{round}.aac"), + ); let resumed = start.elapsed().as_millis() as u64; for ms in (resumed..resumed + 200).step_by(20) { write_aac(&mut track, ms); @@ -10757,8 +10761,8 @@ async fn a_track_renamed_on_its_pid_across_a_return_is_quiet_from_its_listing() track }; - let (mut broadcast, mut catalog) = publish_live::(&origin, route()); - let mut track = rendition(&mut broadcast, &mut catalog, "0.aac"); + let (broadcast, mut catalog) = publish_live::(&origin, route()); + let mut track = rendition(&mut broadcast.broadcast().clone(), &mut catalog, "0.aac"); let source = crate::source::resolved(&origin, "live").await; let mut export = open_ts(source, crate::catalog::CatalogFormat::Hang) .await @@ -10775,12 +10779,12 @@ async fn a_track_renamed_on_its_pid_across_a_return_is_quiet_from_its_listing() let (before, _) = export.liveness(0x101); tokio::time::sleep(Duration::from_secs(10)).await; - let (mut broadcast, mut catalog) = publish_live::(&origin, route()); + let (broadcast, mut catalog) = publish_live::(&origin, route()); 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(); drain_frames(&mut export).await; - let mut track = rendition(&mut broadcast, &mut catalog, "1.aac"); + let mut track = rendition(&mut broadcast.broadcast().clone(), &mut catalog, "1.aac"); let resumed = start.elapsed().as_millis() as u64; let mut quiet = Vec::new(); for ms in (resumed..resumed + 4000).step_by(20) { @@ -10809,8 +10813,8 @@ async fn a_switch_follows_the_last_pmt_out() { 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 audio = aac_rendition(&mut old, &mut old_catalog, "a.aac"); + let (old, mut old_catalog) = publish_live::(&origin, route()); + let mut audio = aac_rendition(&mut old.broadcast().clone(), &mut old_catalog, "a.aac"); let source = crate::source::resolved(&origin, "live").await; let mut export = open_ts(source, crate::catalog::CatalogFormat::Hang) .await @@ -10827,8 +10831,8 @@ async fn a_switch_follows_the_last_pmt_out() { }]; frames.extend(drain_frames(&mut export).await); - let (mut new, mut new_catalog) = publish_live::(&origin, route()); - let mut audio = aac_rendition(&mut new, &mut new_catalog, "b.aac"); + let (new, mut new_catalog) = publish_live::(&origin, route()); + let mut 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 @@ -10864,8 +10868,8 @@ async fn a_switch_past_an_instance_that_laid_no_tables_follows_the_last_pmt_out( export.follow(replacement, stream).unwrap() }; - let (mut first, mut first_catalog) = publish_live::(&origin, route()); - let mut audio = aac_rendition(&mut first, &mut first_catalog, "a.aac"); + let (first, mut first_catalog) = publish_live::(&origin, route()); + let mut audio = aac_rendition(&mut first.broadcast().clone(), &mut first_catalog, "a.aac"); let source = crate::source::resolved(&origin, "live").await; let mut export = open_ts(source, crate::catalog::CatalogFormat::Hang) .await @@ -10878,16 +10882,16 @@ async fn a_switch_past_an_instance_that_laid_no_tables_follows_the_last_pmt_out( let mut frames = drain_frames(&mut export).await; // Built from what it holds, and replaced before its first slot. - let (mut second, mut second_catalog) = publish_live::(&origin, route()); - let mut audio = aac_rendition(&mut second, &mut second_catalog, "b.aac"); + let (second, mut second_catalog) = publish_live::(&origin, route()); + let mut audio = aac_rendition(&mut second.broadcast().clone(), &mut second_catalog, "b.aac"); for ms in (0..200).step_by(20) { write_aac(&mut audio, ms); } let mut export = follow(export).await; assert!(poll_frames(&mut export).is_empty()); - let (mut third, mut third_catalog) = publish_live::(&origin, route()); - let mut audio = aac_rendition(&mut third, &mut third_catalog, "c.aac"); + let (third, mut third_catalog) = publish_live::(&origin, route()); + let mut audio = aac_rendition(&mut third.broadcast().clone(), &mut third_catalog, "c.aac"); let mut export = follow(export).await; for ms in (0..200).step_by(20) { write_aac(&mut audio, ms); diff --git a/rs/moq-mux/src/container/ts/programs.rs b/rs/moq-mux/src/container/ts/programs.rs index 4ff1e9291b7..462550276c2 100644 --- a/rs/moq-mux/src/container/ts/programs.rs +++ b/rs/moq-mux/src/container/ts/programs.rs @@ -57,8 +57,8 @@ struct Waiting { 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 @@ -281,7 +281,7 @@ impl Programs { fn start(&mut self, number: u16, pmt_pid: u16, sections: SectionReassembler) -> anyhow::Result<()> { let pat = self.pat.as_ref().expect("a program waits on its PAT"); let name = program_broadcast(self.name.as_str(), number); - let mut broadcast = self + let broadcast = self .origin .create_broadcast(&name) .with_context(|| format!("failed to create broadcast {name}"))?; @@ -291,9 +291,9 @@ impl Programs { .config .clone() .with_catalog(catalog::hang::Catalog::::default()); - let catalog = catalog::Producer::new(&mut broadcast, config)?; + let catalog = catalog::Producer::new(&mut broadcast.broadcast().clone(), config)?; let pmt_pid = Pid::new(pmt_pid).expect("a packet's PID fits 13 bits"); - let mut import = Import::new(broadcast.clone(), catalog.reserve()) + let mut import = Import::new(broadcast.broadcast().clone(), catalog.reserve()) .with_program(number) .with_pmt(pmt_pid, sections); import.handle_pat(pat)?; @@ -368,7 +368,11 @@ impl Programs { program.catalog.finish()?; } anyhow::ensure!(announced, "input ended before any program declared a track"); - Ok(self.programs.iter().map(|program| program.broadcast.clone()).collect()) + Ok(self + .programs + .iter() + .map(|program| program.broadcast.broadcast().clone()) + .collect()) } /// Abort every program's catalog and media tracks with `err`. Consumes the importer. diff --git a/rs/moq-mux/src/linger.rs b/rs/moq-mux/src/linger.rs index c387650b7e0..804dc4f0ab6 100644 --- a/rs/moq-mux/src/linger.rs +++ b/rs/moq-mux/src/linger.rs @@ -91,7 +91,7 @@ mod tests { group.finish().unwrap(); } track.finish().unwrap(); - (broadcast, track) + (broadcast.broadcast().clone(), track) } /// Whether the origin has stopped routing [`PATH`]. diff --git a/rs/moq-net/benches/memory.rs b/rs/moq-net/benches/memory.rs index 84caeb0f528..06f159cadbd 100644 --- a/rs/moq-net/benches/memory.rs +++ b/rs/moq-net/benches/memory.rs @@ -24,7 +24,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"; @@ -90,7 +90,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 8691745f057..5f2e9135453 100644 --- a/rs/moq-net/benches/origin.rs +++ b/rs/moq-net/benches/origin.rs @@ -71,7 +71,7 @@ struct Fanout { producer: origin::Producer, consumer: origin::Consumer, driver: origin::Driver, - _publishers: Vec, + _publishers: Vec, subscribers: Vec, } @@ -1021,7 +1021,7 @@ struct Watching { pair: MockPair, tracks: Vec, subscriptions: Vec, - _broadcasts: Vec, + _broadcasts: Vec, _origins: [origin::Producer; 2], } @@ -1337,7 +1337,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, } @@ -1500,7 +1500,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 76f7ec95a94..440508101c8 100644 --- a/rs/moq-net/benches/session.rs +++ b/rs/moq-net/benches/session.rs @@ -327,7 +327,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, @@ -881,7 +881,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 8eef9575a41..70b93bf0bef 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -6994,7 +6994,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 cd38ab88291..268f1b85dda 100644 --- a/rs/moq-net/src/ietf/publisher/tracks.rs +++ b/rs/moq-net/src/ietf/publisher/tracks.rs @@ -1372,7 +1372,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 51a6ec08f26..09c0e321cc0 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -261,7 +261,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() } @@ -282,7 +282,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) @@ -324,7 +324,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(); } @@ -459,7 +459,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 @@ -651,9 +651,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 48101f0b509..216d464333d 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -49,8 +49,8 @@ pub use timed::Timed; pub mod origin { pub use super::follow::AnnouncedBroadcast; pub use super::origin_impl::{ - Config, Consumer, Cost, Driver, Dynamic, HopLimit, Link, Preference, Prepared, Producer, Request, Requesting, - Route, Routing, Source, + Config, Consumer, Cost, Driver, Dynamic, HopLimit, Link, 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 7bb068b1879..e293dbffcda 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -471,7 +471,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 @@ -2129,16 +2129,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 @@ -2160,10 +2161,12 @@ impl Producer { /// pattern can spell (`*` or `**`), [`Error::NavigationSegment`] for a literal `.` 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 @@ -2226,7 +2229,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) @@ -2259,7 +2262,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)] pub(crate) fn announce(&self, prefix: impl AsPath, route: Route) -> Result { @@ -2347,7 +2350,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 @@ -2610,9 +2613,50 @@ 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 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 + } + + /// 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. @@ -2763,7 +2807,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 { @@ -5610,9 +5654,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 } @@ -5622,7 +5664,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, @@ -5645,7 +5687,7 @@ impl Prepared { if let Ok(mut pending) = request.producer.write() { pending.resolved.get_or_insert(resolved); } - Ok(broadcast) + Ok(Publication { broadcast }) } } @@ -10552,6 +10594,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 91a4de62dc2..584efd62d19 100644 --- a/rs/moq-net/tests/integration/auth.rs +++ b/rs/moq-net/tests/integration/auth.rs @@ -1086,7 +1086,7 @@ async fn exact_and_subtree_grants_admit_their_broadcast(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 8f6b53cca01..666ce755162 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, } @@ -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, } 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/drained_claim.rs b/rs/moq-net/tests/integration/drained_claim.rs index de3ba3d33ee..ee13b3e47d1 100644 --- a/rs/moq-net/tests/integration/drained_claim.rs +++ b/rs/moq-net/tests/integration/drained_claim.rs @@ -13,7 +13,7 @@ use crate::support; use std::{cell::RefCell, rc::Rc, time::Duration}; -use moq_net::{Epoch, Hop, Timestamp, Version, broadcast, goaway::Goaway, origin, track}; +use moq_net::{Epoch, Hop, Timestamp, Version, goaway::Goaway, origin, track}; use support::harness::{MockConnectOptions, MockPair, connect_mock}; /// A millisecond track: the frames written here carry timestamps. @@ -47,8 +47,8 @@ async fn settle() { /// track carries the worker's name. struct Worker { _claim: Rc, - /// Each output with its track, which would end once its producer drops. - outputs: Rc>>, + /// Each output with its track, which would end once its publication drops. + outputs: Rc>>, origin: origin::Producer, } 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 42075bce0fb..73156f12f7c 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 c27e0cfb016..cb3a84b9e23 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| { @@ -359,7 +359,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; @@ -373,7 +373,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 9c65bc72952..81e15958e7c 100644 --- a/rs/moq-net/tests/integration/multi_cdn.rs +++ b/rs/moq-net/tests/integration/multi_cdn.rs @@ -17,7 +17,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. @@ -143,7 +143,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 dab2e9e4a32..8c7851b22f4 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 ca70c7b092b..2b05b770346 100644 --- a/rs/moq-net/tests/integration/request_caps.rs +++ b/rs/moq-net/tests/integration/request_caps.rs @@ -34,7 +34,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 cec0496f1a9..226d24dca35 100644 --- a/rs/moq-net/tests/integration/restart.rs +++ b/rs/moq-net/tests/integration/restart.rs @@ -605,7 +605,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 82787cf5888..71846dd48ec 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, @@ -769,7 +769,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 6b6cbe01cee..1a7c37a8d4a 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 10fbf478745..0c9570f3aa5 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_drain.rs b/rs/moq-relay/tests/integration/cluster_drain.rs index beeffa14cce..30c4c1fd54d 100644 --- a/rs/moq-relay/tests/integration/cluster_drain.rs +++ b/rs/moq-relay/tests/integration/cluster_drain.rs @@ -105,7 +105,7 @@ impl Forwarder { } /// A relay with a plaintext TCP listener and an open auth policy, publishing `name`. -async fn relay(name: &str) -> (Relay, SocketAddr, moq_net::broadcast::Producer) { +async fn relay(name: &str) -> (Relay, SocketAddr, moq_net::origin::Publication) { let mut config = moq_relay::Config::default(); config.listen.tcp.bind = Some("127.0.0.1:0".parse().unwrap()); config.auth.public = vec![moq_auth::Pattern::all()]; @@ -118,7 +118,7 @@ async fn relay(name: &str) -> (Relay, SocketAddr, moq_net::broadcast::Producer) } /// A peer relay publishing `name` and dialing B through `forwarder`. -async fn peer(name: &str, forwarder: &Forwarder) -> (cluster::Cluster, moq_net::broadcast::Producer) { +async fn peer(name: &str, forwarder: &Forwarder) -> (cluster::Cluster, moq_net::origin::Publication) { let mut config = cluster::Config::default(); config.connect = vec![Peer::new(format!("tcp://127.0.0.1:{}/", forwarder.port))]; let cluster = cluster::Cluster::new(cluster::Options::new(config)) diff --git a/rs/moq-relay/tests/integration/cluster_unknown.rs b/rs/moq-relay/tests/integration/cluster_unknown.rs index 267bc3471a8..2f390979db5 100644 --- a/rs/moq-relay/tests/integration/cluster_unknown.rs +++ b/rs/moq-relay/tests/integration/cluster_unknown.rs @@ -130,7 +130,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 66addbbd070..390d957c23b 100644 --- a/rs/moq-rtc/src/client/mod.rs +++ b/rs/moq-rtc/src/client/mod.rs @@ -107,7 +107,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/egress.rs b/rs/moq-rtc/src/egress.rs index 9b3fea6c687..30876d16aae 100644 --- a/rs/moq-rtc/src/egress.rs +++ b/rs/moq-rtc/src/egress.rs @@ -900,9 +900,11 @@ mod tests { #[tokio::test(start_paused = true)] async fn a_moved_rendition_follows_its_new_track() { let origin = produce_origin(); - let mut live = origin.publish("live", Default::default()).unwrap(); - let mut other = origin.publish("other", Default::default()).unwrap(); - let mut catalog = moq_mux::catalog::Producer::new(&mut live, moq_mux::catalog::Config::default()).unwrap(); + 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(); 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( @@ -910,9 +912,9 @@ mod tests { moq_mux::catalog::hang::Container::Legacy(moq_mux::container::Kind::Video), ) }; - let mut a = track(&mut live, "a"); - let mut b = track(&mut live, "b"); - let mut sibling = track(&mut other, "a"); + let mut a = track(&mut live.broadcast().clone(), "a"); + let mut b = track(&mut live.broadcast().clone(), "b"); + let mut sibling = track(&mut other.broadcast().clone(), "a"); let point = |catalog: &mut moq_mux::catalog::Producer, track: &str, broadcast: Option<&str>| { let mut config = VideoConfig::new(VideoCodec::VP8); config.container = hang::catalog::Container::Legacy; diff --git a/rs/moq-rtc/src/lib.rs b/rs/moq-rtc/src/lib.rs index e62d042d533..367f3bf4d67 100644 --- a/rs/moq-rtc/src/lib.rs +++ b/rs/moq-rtc/src/lib.rs @@ -153,14 +153,15 @@ mod tests { origin: &moq_net::origin::Producer, epoch: moq_net::Epoch, ) -> (Box, moq_mux::catalog::Producer) { - let mut broadcast = origin.create_broadcast("source").expect("create source broadcast"); + let broadcast = origin.create_broadcast("source").expect("create source broadcast"); broadcast .announce(moq_net::origin::Route::default().with_epoch(epoch)) .expect("announce source broadcast"); - let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()) - .expect("create source catalog"); - let bridge = - crate::codec::opus::Bridge::new(broadcast, catalog.clone(), 48_000, 2).expect("create Opus bridge"); + let catalog = + moq_mux::catalog::Producer::new(&mut broadcast.broadcast().clone(), moq_mux::catalog::Config::default()) + .expect("create source catalog"); + let bridge = crate::codec::opus::Bridge::new(broadcast.broadcast().clone(), catalog.clone(), 48_000, 2) + .expect("create Opus bridge"); (Box::new(bridge), catalog) } @@ -220,7 +221,8 @@ mod tests { let output_consumer = output.consume(); let client = Client::new(client::Config::default()); let url = format!("http://{address}/whep/source").parse().expect("WHEP URL"); - let viewer = tokio::spawn(async move { client.subscribe(url, output).await }); + let fill = output.broadcast().clone(); + let viewer = tokio::spawn(async move { client.subscribe(url, fill).await }); let session = tokio::time::timeout(TIMEOUT, sessions.recv()) .await .expect("WHEP negotiation timed out") @@ -310,15 +312,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 { @@ -405,7 +409,8 @@ mod tests { let whep = format!("http://user:pass@{address}/whep/ingested?jwt=secret") .parse() .expect("WHEP URL"); - let whep_client = tokio::spawn(async move { client.subscribe(whep, output).await }); + let fill = output.broadcast().clone(); + let whep_client = tokio::spawn(async move { client.subscribe(whep, fill).await }); let (whep_id, whep_task) = tokio::time::timeout(TIMEOUT, sources_rx.recv()) .await .expect("WHEP negotiation timed out") diff --git a/rs/moq-rtc/src/server/whep.rs b/rs/moq-rtc/src/server/whep.rs index b3584c4260b..e13472808f6 100644 --- a/rs/moq-rtc/src/server/whep.rs +++ b/rs/moq-rtc/src/server/whep.rs @@ -283,13 +283,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 9e8d6ad78e1..4bd691f66fb 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 03232084290..2296f8cf436 100644 --- a/rs/moq-rtmp/src/dial.rs +++ b/rs/moq-rtmp/src/dial.rs @@ -558,9 +558,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, } @@ -568,12 +567,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())?; @@ -600,7 +599,7 @@ impl Publisher { fn finish(mut self, linger: Duration) -> anyhow::Result> { self.importer.finish()?; self.catalog.finish()?; - Ok(moq_mux::Linger::new([self.broadcast.clone()], self, linger)) + Ok(moq_mux::Linger::new([self.broadcast.broadcast().clone()], self, linger)) } /// Flush any buffered media and finish the catalog, then end the broadcast so the origin unannounces it at once. @@ -689,17 +688,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(); @@ -786,12 +787,16 @@ mod tests { vseq.extend_from_slice(&[0x01, 0x00, 0x04, 0x68, 0xce, 0x3c, 0x80]); let mut vframe = vec![0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, idr.len() as u8]; vframe.extend_from_slice(idr); - let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); + let broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast .announce(origin::Route::default().with_epoch(epoch.clone())) .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(); @@ -877,10 +882,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 d30a6eb3e45..cd6df1cd260 100644 --- a/rs/moq-rtmp/src/server.rs +++ b/rs/moq-rtmp/src/server.rs @@ -1478,7 +1478,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 { @@ -1486,9 +1486,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, @@ -1499,10 +1498,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())?; @@ -1533,7 +1532,7 @@ impl Publisher { fn finish(mut self, linger: Duration) -> anyhow::Result> { self.importer.finish()?; self.catalog.finish()?; - Ok(moq_mux::Linger::new([self.broadcast.clone()], self, linger)) + Ok(moq_mux::Linger::new([self.broadcast.broadcast().clone()], self, linger)) } /// Flush any buffered media, finish the catalog, close out the broadcast's open groups, and end @@ -2117,10 +2116,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(); @@ -2173,10 +2174,12 @@ mod tests { let mut vframe = vec![0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, idr.len() as u8]; vframe.extend_from_slice(idr); - let mut broadcast = origin.create_broadcast("live/cam0").unwrap(); + let broadcast = origin.create_broadcast("live/cam0").unwrap(); broadcast.announce(origin::Route::default().with_epoch(epoch)).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(); @@ -2244,10 +2247,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. @@ -2521,10 +2526,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 b6d61705174..0156804389e 100644 --- a/rs/moq-rtsp/src/client.rs +++ b/rs/moq-rtsp/src/client.rs @@ -857,8 +857,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 8f04a759846..9e7f47c5471 100644 --- a/rs/moq-srt/src/server.rs +++ b/rs/moq-srt/src/server.rs @@ -913,10 +913,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(); @@ -1015,7 +1015,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, } @@ -1023,9 +1023,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 23fde702674..a67b30ce41a 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); } @@ -145,7 +144,7 @@ impl Publisher { } => { import.finish().map_err(moq_mux::Error::from)?; catalog.finish()?; - vec![broadcast.clone()] + vec![broadcast.broadcast().clone()] } Importer::All(programs) => programs.finish().map_err(moq_mux::Error::from)?, }; diff --git a/rs/moq-stats/benches/aggregate.rs b/rs/moq-stats/benches/aggregate.rs index 6ad8f6c9c3b..b75b9f30c2b 100644 --- a/rs/moq-stats/benches/aggregate.rs +++ b/rs/moq-stats/benches/aggregate.rs @@ -39,7 +39,7 @@ struct Body { /// A hand-published node stats broadcast with one group track carrying /// default-tier egress. struct Node { - source: moq_net::broadcast::Producer, + source: moq_net::origin::Publication, epoch: Epoch, known: bool, group: moq_json::snapshot::Producer, diff --git a/rs/moq-stats/benches/producer_tick.rs b/rs/moq-stats/benches/producer_tick.rs index 1c13e68389d..613cbc71295 100644 --- a/rs/moq-stats/benches/producer_tick.rs +++ b/rs/moq-stats/benches/producer_tick.rs @@ -35,7 +35,7 @@ use std::time::{Duration, Instant}; use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use futures::FutureExt; -use moq_net::{announce, broadcast, origin, stats}; +use moq_net::{announce, origin, stats}; use moq_stats::produce::bench::{Driver, FrameBytes}; struct Counter; @@ -93,7 +93,7 @@ struct Relay { driver: Driver, _origin: origin::Producer, _origin_driver: origin::Driver, - _publishers: Vec, + _publishers: Vec, _cursors: Vec, _sessions: Vec, _held: Vec, diff --git a/rs/moq-stats/src/aggregate.rs b/rs/moq-stats/src/aggregate.rs index 69316d90e61..4cb0c7c3a85 100644 --- a/rs/moq-stats/src/aggregate.rs +++ b/rs/moq-stats/src/aggregate.rs @@ -1297,7 +1297,7 @@ mod tests { #[allow(dead_code, reason = "held for their drop, never read")] struct Feed { announced: announce::Consumer, - source: broadcast::Producer, + source: origin::Publication, consumer: broadcast::Consumer, sub: track::Subscriber, ctx: moq_net::stats::Session, @@ -1377,7 +1377,7 @@ mod tests { /// Dropping it unannounces the node. #[allow(dead_code, reason = "held for their drop, never read")] struct NodeBroadcast { - source: broadcast::Producer, + source: origin::Publication, group: moq_json::snapshot::Producer, track: track::Producer, frame: TrafficFrame, @@ -2204,7 +2204,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 50d37bcf6e6..2e24af21abf 100644 --- a/rs/moq-stats/src/consume.rs +++ b/rs/moq-stats/src/consume.rs @@ -185,7 +185,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::{Encoding, Producer, Tier, produce}; @@ -207,7 +207,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 5e062239b8e..3ebca806820 100644 --- a/rs/moq-stats/src/produce.rs +++ b/rs/moq-stats/src/produce.rs @@ -607,7 +607,7 @@ impl PendingPair { /// The node's stats broadcast and its change-detection state. struct NodePublisher { - broadcast: broadcast::Producer, + broadcast: origin::Publication, /// Holds the broadcast's request queue open, so a subscriber asking for a /// presence or prefix track parks until the next drain serves it instead /// of being rejected `NotFound` on the spot. @@ -1694,7 +1694,7 @@ mod tests { #[allow(dead_code, reason = "held for their drop, never read")] 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 22a413e7fbd..29801575354 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 189387835fd..86201a3c7a4 100644 --- a/rs/moq-transcode/src/lib.rs +++ b/rs/moq-transcode/src/lib.rs @@ -814,7 +814,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 e04b8018d43..98c7b4350b7 100644 --- a/rs/moq-video/src/decode/decoder.rs +++ b/rs/moq-video/src/decode/decoder.rs @@ -611,9 +611,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 0ee58a5472e..6e6deb4712d 100644 --- a/test/interop/clients/rust/finish.rs +++ b/test/interop/clients/rust/finish.rs @@ -77,8 +77,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(), @@ -91,7 +92,7 @@ async fn main() -> anyhow::Result<()> { catalog.finish()?; broadcast.announce(Default::default())?; // Keep the finished broadcast up for the late readers, then take it offline. - let linger = moq_mux::Linger::new([broadcast], (media, catalog), LINGER); + let linger = moq_mux::Linger::new([broadcast.broadcast().clone()], (media, catalog), LINGER); println!("finish ready linger_us={}", LINGER.as_micros()); linger.wait().await; println!("finish ended");