diff --git a/rs/hang/examples/subscribe.rs b/rs/hang/examples/subscribe.rs index 85f2b7b886..15ef57a5f5 100644 --- a/rs/hang/examples/subscribe.rs +++ b/rs/hang/examples/subscribe.rs @@ -52,8 +52,8 @@ async fn run_subscribe(consumer: moq_net::OriginConsumer) -> anyhow::Result<()> // Read the catalog to discover available tracks. let catalog_track = broadcast - .consume_track(hang::Catalog::DEFAULT_NAME) - .subscribe(hang::Catalog::default_subscription()) + .track(hang::Catalog::DEFAULT_NAME)? + .subscribe(hang::Catalog::default_subscription())? .await?; let mut catalog = moq_mux::catalog::hang::Consumer::new(catalog_track); @@ -77,11 +77,11 @@ async fn run_subscribe(consumer: moq_net::OriginConsumer) -> anyhow::Result<()> // Subscribe to the video track. let track_consumer = broadcast - .consume_track(name) + .track(name)? .subscribe(moq_net::Subscription { priority: 1, ..Default::default() - }) + })? .await?; let mut ordered = moq_mux::container::Consumer::new(track_consumer, moq_mux::catalog::hang::Container::Legacy) .with_latency(Duration::from_millis(500)); diff --git a/rs/hang/examples/video.rs b/rs/hang/examples/video.rs index d84a2db89e..247259ff2c 100644 --- a/rs/hang/examples/video.rs +++ b/rs/hang/examples/video.rs @@ -42,7 +42,7 @@ async fn run_session(origin: moq_net::OriginProducer) -> anyhow::Result<()> { // The catalog can contain multiple tracks, used by the viewer to choose the best track. fn create_track(broadcast: &mut moq_net::BroadcastProducer) -> anyhow::Result { // Basic information about the video track. - let video_track = moq_net::Track::new("video"); + let video_track = "video"; // Example video configuration // In a real application, you would get this from the encoder @@ -61,7 +61,7 @@ fn create_track(broadcast: &mut moq_net::BroadcastProducer) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result<()> { // Create and publish a broadcast to the origin. - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let track = create_track(&mut broadcast)?; // NOTE: The path is empty because we're using the URL to scope the broadcast. diff --git a/rs/hang/src/catalog/root.rs b/rs/hang/src/catalog/root.rs index 4d64ee2b32..7a4d5bce97 100644 --- a/rs/hang/src/catalog/root.rs +++ b/rs/hang/src/catalog/root.rs @@ -64,9 +64,12 @@ impl Catalog { Ok(serde_json::to_writer(writer, self)?) } - pub fn default_track() -> moq_net::Track { - // The catalog is JSON and re-sent on every change, so it pays to compress. - moq_net::Track::new(Catalog::DEFAULT_NAME).with_compress(true) + /// Track properties for creating the catalog track via + /// [`create_track`](moq_net::BroadcastProducer::create_track) at + /// [`DEFAULT_NAME`](Self::DEFAULT_NAME). The catalog is JSON and re-sent on + /// every change, so it pays to compress. + pub fn default_track_info() -> moq_net::TrackInfo { + moq_net::TrackInfo::default().with_compress(true) } /// The subscription preferences used for the catalog track (high priority so diff --git a/rs/kio/src/consumer.rs b/rs/kio/src/consumer.rs index d820bda6ed..c91041b5a5 100644 --- a/rs/kio/src/consumer.rs +++ b/rs/kio/src/consumer.rs @@ -4,7 +4,7 @@ use std::{ }; use crate::{ - Counts, State, Weak, + Counts, Mut, State, Weak, lock::*, producer::{Producer, Ref}, waiter::*, @@ -106,6 +106,24 @@ impl Consumer { } } + /// Acquire mutable access to the shared state. + /// + /// Returns `Ok(Mut)` if the channel is open, or `Err(Ref)` with + /// read-only access if closed. Only locks once. + pub fn write(&self) -> Result, Ref<'_, T>> { + let state = self.state.lock(); + if state.closed { + Err(Ref { state }) + } else { + Ok(Mut::new(state)) + } + } + + /// Returns `true` if the channel has been closed by the producer. + pub fn is_closed(&self) -> bool { + self.state.lock().closed + } + /// Returns `true` if both consumers share the same underlying state. pub fn same_channel(&self, other: &Self) -> bool { self.state.is_clone(&other.state) diff --git a/rs/libmoq/src/consume.rs b/rs/libmoq/src/consume.rs index 6cbe3f5fcd..50f35441e4 100644 --- a/rs/libmoq/src/consume.rs +++ b/rs/libmoq/src/consume.rs @@ -69,8 +69,8 @@ impl Consume { tokio::spawn(async move { let res = async move { let catalog = broadcast - .consume_track(hang::catalog::Catalog::DEFAULT_NAME) - .subscribe(hang::catalog::Catalog::default_subscription()) + .track(hang::catalog::Catalog::DEFAULT_NAME)? + .subscribe(hang::catalog::Catalog::default_subscription())? .await?; Self::run_catalog(on_catalog, broadcast.clone(), catalog.into(), channel.1).await } @@ -250,11 +250,11 @@ impl Consume { tokio::spawn(async move { let res = async move { let track = broadcast - .consume_track(&name) + .track(&name)? .subscribe(moq_net::Subscription { priority: 1, ..Default::default() - }) + })? .await?; let track = moq_mux::container::Consumer::new(track, moq_mux::catalog::hang::Container::Legacy) .with_latency(latency); @@ -302,11 +302,11 @@ impl Consume { tokio::spawn(async move { let res = async move { let track = broadcast - .consume_track(&name) + .track(&name)? .subscribe(moq_net::Subscription { priority: 2, ..Default::default() - }) + })? .await?; let track = moq_mux::container::Consumer::new(track, moq_mux::catalog::hang::Container::Legacy) .with_latency(latency); @@ -407,7 +407,7 @@ impl Consume { // `subscribe` blocks on SUBSCRIBE_OK, so run it inside the task. tokio::spawn(async move { let res = async move { - let track = broadcast.consume_track(&name).subscribe(None).await?; + let track = broadcast.track(&name)?.subscribe(None)?.await?; Self::run_raw(on_frame, track, channel.1).await } .await; diff --git a/rs/libmoq/src/publish.rs b/rs/libmoq/src/publish.rs index 14fd67f9ea..c64f57590f 100644 --- a/rs/libmoq/src/publish.rs +++ b/rs/libmoq/src/publish.rs @@ -22,7 +22,7 @@ pub struct Publish { impl Publish { pub fn create(&mut self) -> Result { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast)?; let id = self.broadcasts.insert((broadcast, catalog))?; @@ -128,7 +128,7 @@ impl Publish { /// if you want to describe the track in the catalog as well. pub fn track(&mut self, broadcast: Id, name: &str) -> Result { let (broadcast, _) = self.broadcasts.get_mut(broadcast).ok_or(Error::BroadcastNotFound)?; - let track = broadcast.create_track(moq_net::Track::new(name))?; + let track = broadcast.create_track(name, None)?; self.tracks.insert(track) } diff --git a/rs/moq-audio/src/consumer.rs b/rs/moq-audio/src/consumer.rs index 09523c9e9b..e575e716d2 100644 --- a/rs/moq-audio/src/consumer.rs +++ b/rs/moq-audio/src/consumer.rs @@ -53,7 +53,7 @@ impl AudioConsumer { }; let name = name.into(); - let track = broadcast.consume_track(&name).subscribe(None).await?; + let track = broadcast.track(&name)?.subscribe(None)?.await?; let mut track = moq_mux::container::Consumer::new(track, moq_mux::container::legacy::Wire); if let Some(latency) = output.latency_max { track = track.with_latency(latency); diff --git a/rs/moq-audio/src/producer.rs b/rs/moq-audio/src/producer.rs index 66a94652a6..135c40488c 100644 --- a/rs/moq-audio/src/producer.rs +++ b/rs/moq-audio/src/producer.rs @@ -61,8 +61,10 @@ impl AudioProducer { // Audio hang frames carry microsecond timestamps; advertise that on the // track so Lite05 subscribers know what scale to expect and the model // layer accepts Frame::timestamp on append. - let track = - broadcast.create_track(moq_net::Track::new(name.clone()).with_timescale(hang::container::TIMESCALE))?; + let track = broadcast.create_track( + name.clone(), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + )?; let track = moq_mux::container::Producer::new(track, moq_mux::container::legacy::Wire); let mut catalog_mut = catalog.clone(); diff --git a/rs/moq-audio/tests/roundtrip.rs b/rs/moq-audio/tests/roundtrip.rs index 5f3da37e3c..45d198c4dd 100644 --- a/rs/moq-audio/tests/roundtrip.rs +++ b/rs/moq-audio/tests/roundtrip.rs @@ -30,7 +30,7 @@ fn f32_bytes(samples: &[f32]) -> Bytes { #[tokio::test] async fn opus_round_trip_48k_stereo() { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut catalog_consumer = catalog.consume().unwrap(); let broadcast_consumer = broadcast.consume(); @@ -110,7 +110,7 @@ async fn opus_round_trip_48k_stereo() { #[tokio::test] async fn opus_round_trip_44100_s16_resampled() { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut catalog_consumer = catalog.consume().unwrap(); let broadcast_consumer = broadcast.consume(); diff --git a/rs/moq-boy/src/input.rs b/rs/moq-boy/src/input.rs index 2784ac9b4a..2a0eb1c9b1 100644 --- a/rs/moq-boy/src/input.rs +++ b/rs/moq-boy/src/input.rs @@ -99,7 +99,7 @@ async fn handle_viewer_commands( broadcast: moq_net::BroadcastConsumer, cmd_tx: &tokio::sync::mpsc::Sender, ) -> anyhow::Result<()> { - let mut track = broadcast.consume_track("command").subscribe(None).await?; + let mut track = broadcast.track("command")?.subscribe(None)?.await?; while let Some(mut group) = track.recv_group().await? { while let Some(frame) = group.read_frame().await? { diff --git a/rs/moq-boy/src/main.rs b/rs/moq-boy/src/main.rs index 339669a1b6..4cdebd4d8d 100644 --- a/rs/moq-boy/src/main.rs +++ b/rs/moq-boy/src/main.rs @@ -209,7 +209,7 @@ async fn run(config: &Config) -> Result<()> { let client = config.client.clone().init()?; // Create the broadcast producer. - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); // Publish origin: the game session broadcast. let publish_origin = moq_net::Origin::random().produce(); diff --git a/rs/moq-boy/src/status.rs b/rs/moq-boy/src/status.rs index 425a65a846..579b4cfb75 100644 --- a/rs/moq-boy/src/status.rs +++ b/rs/moq-boy/src/status.rs @@ -40,7 +40,7 @@ pub struct StatusPublisher { impl StatusPublisher { pub fn new(broadcast: &mut moq_net::BroadcastProducer) -> anyhow::Result { - let producer = broadcast.create_track(moq_net::Track::new("status"))?; + let producer = broadcast.create_track("status", None)?; Ok(Self { producer, diff --git a/rs/moq-cli/src/publish.rs b/rs/moq-cli/src/publish.rs index 34d8e776bf..d3bdd1169a 100644 --- a/rs/moq-cli/src/publish.rs +++ b/rs/moq-cli/src/publish.rs @@ -42,7 +42,7 @@ pub struct Publish { impl Publish { pub fn new(format: &PublishFormat) -> anyhow::Result { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast)?; let decoder = match format { diff --git a/rs/moq-ffi/src/consumer.rs b/rs/moq-ffi/src/consumer.rs index cbeb127fc9..a26baa771b 100644 --- a/rs/moq-ffi/src/consumer.rs +++ b/rs/moq-ffi/src/consumer.rs @@ -84,8 +84,8 @@ impl MoqBroadcastConsumer { pub async fn subscribe_catalog(&self) -> Result, MoqError> { let track = self .inner - .consume_track(hang::catalog::Catalog::DEFAULT_NAME) - .subscribe(hang::catalog::Catalog::default_subscription()) + .track(hang::catalog::Catalog::DEFAULT_NAME)? + .subscribe(hang::catalog::Catalog::default_subscription())? .await?; let consumer = moq_mux::catalog::hang::Consumer::from(track); Ok(Arc::new(MoqCatalogConsumer { @@ -97,7 +97,7 @@ impl MoqBroadcastConsumer { /// /// Frames are returned as plain byte payloads with no codec or container parsing. pub async fn subscribe_track(&self, name: String) -> Result, MoqError> { - let track = self.inner.consume_track(&name).subscribe(None).await?; + let track = self.inner.track(&name)?.subscribe(None)?.await?; Ok(Arc::new(MoqTrackConsumer::new(track))) } @@ -117,7 +117,7 @@ impl MoqBroadcastConsumer { let media: moq_mux::catalog::hang::Container = (&container) .try_into() .map_err(|e| MoqError::Codec(format!("invalid container: {e}")))?; - let track = self.inner.consume_track(&name).subscribe(None).await?; + let track = self.inner.track(&name)?.subscribe(None)?.await?; let latency = std::time::Duration::from_millis(max_latency_ms); let consumer = moq_mux::container::Consumer::new(track, media).with_latency(latency); Ok(Arc::new(MoqMediaConsumer { diff --git a/rs/moq-ffi/src/producer.rs b/rs/moq-ffi/src/producer.rs index e3bf590dc0..528f638bdf 100644 --- a/rs/moq-ffi/src/producer.rs +++ b/rs/moq-ffi/src/producer.rs @@ -74,7 +74,7 @@ impl MoqBroadcastProducer { #[uniffi::constructor] pub fn new() -> Result, MoqError> { let _guard = crate::ffi::RUNTIME.enter(); - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast)?; Ok(Arc::new(Self { state: std::sync::Mutex::new(Some(BroadcastProducer { broadcast, catalog })), @@ -143,7 +143,7 @@ impl MoqBroadcastProducer { let state = guard.as_ref().ok_or_else(|| MoqError::Closed)?; // Clone the broadcast handle (shared Arc internally) to get &mut access. let mut broadcast = state.broadcast.clone(); - let producer = broadcast.create_track(moq_net::Track::new(name))?; + let producer = broadcast.create_track(name, None)?; Ok(Arc::new(MoqTrackProducer { inner: std::sync::Mutex::new(Some(producer)), })) @@ -173,7 +173,7 @@ impl MoqTrackProducer { let _guard = crate::ffi::RUNTIME.enter(); let guard = self.inner.lock().unwrap(); let track = guard.as_ref().ok_or_else(|| MoqError::Closed)?; - Ok(track.name.clone()) + Ok(track.name().to_string()) } /// Wait until this track has at least one active consumer. @@ -286,7 +286,7 @@ impl MoqMediaProducer { let _guard = crate::ffi::RUNTIME.enter(); let guard = self.inner.lock().unwrap(); let media = guard.as_ref().ok_or_else(|| MoqError::Closed)?; - Ok(media.track.name.clone()) + Ok(media.track.name().to_string()) } /// Wait until this media track has at least one active consumer. diff --git a/rs/moq-gst/src/sink/imp.rs b/rs/moq-gst/src/sink/imp.rs index e372a341b4..6f92e68430 100644 --- a/rs/moq-gst/src/sink/imp.rs +++ b/rs/moq-gst/src/sink/imp.rs @@ -388,7 +388,7 @@ async fn run_session( let client = client_config.init()?; let origin = moq_net::Origin::random().produce(); - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let broadcast_consumer = broadcast.consume(); let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast)?; diff --git a/rs/moq-gst/src/source/imp.rs b/rs/moq-gst/src/source/imp.rs index b02e3122c7..6fe53a252d 100644 --- a/rs/moq-gst/src/source/imp.rs +++ b/rs/moq-gst/src/source/imp.rs @@ -433,8 +433,8 @@ async fn run_session( }; let catalog_track = broadcast - .consume_track(hang::catalog::Catalog::DEFAULT_NAME) - .subscribe(hang::catalog::Catalog::default_subscription()) + .track(hang::catalog::Catalog::DEFAULT_NAME)? + .subscribe(hang::catalog::Catalog::default_subscription())? .await?; let mut catalog = moq_mux::catalog::hang::Consumer::new(catalog_track); let catalog = catalog.next().await?.context("catalog missing")?.clone(); @@ -448,7 +448,7 @@ async fn run_session( }; let caps = video_caps(&config)?; let endpoint = request_pad(&control_tx, descriptor.clone(), caps).await?; - let track_consumer = broadcast.consume_track(&track_name).subscribe(None).await?; + let track_consumer = broadcast.track(&track_name)?.subscribe(None)?.await?; let track = moq_mux::container::Consumer::new(track_consumer, moq_mux::catalog::hang::Container::Legacy) .with_latency(Duration::from_secs(1)); tasks.push(spawn_track_pump(track, descriptor, endpoint, shutdown.clone())); @@ -461,7 +461,7 @@ async fn run_session( }; let caps = audio_caps(&config)?; let endpoint = request_pad(&control_tx, descriptor.clone(), caps).await?; - let track_consumer = broadcast.consume_track(&track_name).subscribe(None).await?; + let track_consumer = broadcast.track(&track_name)?.subscribe(None)?.await?; let track = moq_mux::container::Consumer::new(track_consumer, moq_mux::catalog::hang::Container::Legacy) .with_latency(Duration::from_secs(1)); tasks.push(spawn_track_pump(track, descriptor, endpoint, shutdown.clone())); diff --git a/rs/moq-mux/src/catalog/consumer.rs b/rs/moq-mux/src/catalog/consumer.rs index af23f9010e..3919b027f5 100644 --- a/rs/moq-mux/src/catalog/consumer.rs +++ b/rs/moq-mux/src/catalog/consumer.rs @@ -26,13 +26,13 @@ impl Consumer { Ok(match format { CatalogFormat::Hang => { let track = broadcast - .consume_track(hang::Catalog::DEFAULT_NAME) - .subscribe(hang::Catalog::default_subscription()) + .track(hang::Catalog::DEFAULT_NAME)? + .subscribe(hang::Catalog::default_subscription())? .await?; Self::Hang(super::hang::Consumer::new(track)) } CatalogFormat::Msf => { - let track = broadcast.consume_track(moq_msf::DEFAULT_NAME).subscribe(None).await?; + let track = broadcast.track(moq_msf::DEFAULT_NAME)?.subscribe(None)?.await?; Self::Msf(super::msf::Consumer::new(track)) } }) diff --git a/rs/moq-mux/src/catalog/hang/consumer.rs b/rs/moq-mux/src/catalog/hang/consumer.rs index 4cbec1ec0e..5ea75b0979 100644 --- a/rs/moq-mux/src/catalog/hang/consumer.rs +++ b/rs/moq-mux/src/catalog/hang/consumer.rs @@ -8,7 +8,6 @@ use crate::Result; /// /// This wraps a `moq_net::TrackSubscriber` and automatically deserializes JSON /// catalog data to discover available audio and video tracks in a broadcast. -#[derive(Clone)] pub struct Consumer { /// Access to the underlying track consumer. pub track: moq_net::TrackSubscriber, @@ -93,7 +92,7 @@ mod test { #[test] fn waits_for_pending_catalog_group_payload() { - let mut track = Catalog::default_track().produce(); + let mut track = moq_net::TrackProducer::new(Catalog::DEFAULT_NAME, Catalog::default_track_info()); let mut consumer = Consumer::new(track.subscribe(None)); let mut group = track.append_group().expect("catalog group should append"); @@ -109,7 +108,7 @@ mod test { #[test] fn waits_for_pending_catalog_group_payload_after_track_finish() { - let mut track = Catalog::default_track().produce(); + let mut track = moq_net::TrackProducer::new(Catalog::DEFAULT_NAME, Catalog::default_track_info()); let mut consumer = Consumer::new(track.subscribe(None)); let mut group = track.append_group().expect("catalog group should append"); @@ -127,7 +126,7 @@ mod test { #[test] fn returns_latest_complete_catalog_group() { - let mut track = Catalog::default_track().produce(); + let mut track = moq_net::TrackProducer::new(Catalog::DEFAULT_NAME, Catalog::default_track_info()); let mut consumer = Consumer::new(track.subscribe(None)); let waiter = kio::Waiter::noop(); @@ -153,7 +152,7 @@ mod test { #[test] fn waits_for_newer_pending_group_instead_of_returning_older_ready_group() { - let mut track = Catalog::default_track().produce(); + let mut track = moq_net::TrackProducer::new(Catalog::DEFAULT_NAME, Catalog::default_track_info()); let mut consumer = Consumer::new(track.subscribe(None)); let waiter = kio::Waiter::noop(); @@ -180,7 +179,7 @@ mod test { #[test] fn retained_pending_group_is_superseded_by_newer_group() { - let mut track = Catalog::default_track().produce(); + let mut track = moq_net::TrackProducer::new(Catalog::DEFAULT_NAME, Catalog::default_track_info()); let mut consumer = Consumer::new(track.subscribe(None)); let waiter = kio::Waiter::noop(); @@ -210,7 +209,7 @@ mod test { #[test] fn returns_none_when_empty_track_finishes() { - let mut track = Catalog::default_track().produce(); + let mut track = moq_net::TrackProducer::new(Catalog::DEFAULT_NAME, Catalog::default_track_info()); let mut consumer = Consumer::new(track.subscribe(None)); let waiter = kio::Waiter::noop(); diff --git a/rs/moq-mux/src/catalog/hang/producer.rs b/rs/moq-mux/src/catalog/hang/producer.rs index 2b1b4fd4ed..6dead75c65 100644 --- a/rs/moq-mux/src/catalog/hang/producer.rs +++ b/rs/moq-mux/src/catalog/hang/producer.rs @@ -27,8 +27,8 @@ impl Producer { broadcast: &mut moq_net::BroadcastProducer, catalog: hang::Catalog, ) -> Result { - let hang_track = broadcast.create_track(hang::Catalog::default_track())?; - let msf_track = broadcast.create_track(moq_net::Track::new(moq_msf::DEFAULT_NAME))?; + let hang_track = broadcast.create_track(hang::Catalog::DEFAULT_NAME, hang::Catalog::default_track_info())?; + let msf_track = broadcast.create_track(moq_msf::DEFAULT_NAME, None)?; Ok(Self { hang_track, diff --git a/rs/moq-mux/src/codec/aac/import.rs b/rs/moq-mux/src/codec/aac/import.rs index f519fab6ce..f4cd79805f 100644 --- a/rs/moq-mux/src/codec/aac/import.rs +++ b/rs/moq-mux/src/codec/aac/import.rs @@ -21,7 +21,8 @@ impl Import { config: Config, ) -> crate::Result { let track = broadcast.create_track( - moq_net::Track::new(broadcast.unique_name(".aac")).with_timescale(hang::container::TIMESCALE), + broadcast.unique_name(".aac"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; let mut audio_config = hang::catalog::AudioConfig::new( @@ -33,8 +34,12 @@ impl Import { ); audio_config.container = hang::catalog::Container::Legacy; - tracing::debug!(name = ?track.name, config = ?audio_config, "starting track"); - catalog.lock().audio.renditions.insert(track.name.clone(), audio_config); + tracing::debug!(name = ?track.name(), config = ?audio_config, "starting track"); + catalog + .lock() + .audio + .renditions + .insert(track.name().to_string(), audio_config); Ok(Self { catalog, @@ -99,7 +104,7 @@ impl Import { impl Drop for Import { fn drop(&mut self) { - tracing::debug!(name = ?self.track.name, "ending track"); - self.catalog.lock().audio.renditions.remove(&self.track.name); + tracing::debug!(name = ?self.track.name(), "ending track"); + self.catalog.lock().audio.renditions.remove(self.track.name()); } } diff --git a/rs/moq-mux/src/codec/av1/import.rs b/rs/moq-mux/src/codec/av1/import.rs index c80133e631..9dc1f0c98b 100644 --- a/rs/moq-mux/src/codec/av1/import.rs +++ b/rs/moq-mux/src/codec/av1/import.rs @@ -90,19 +90,20 @@ impl Import { } if let Some(track) = &self.track.take() { - tracing::debug!(name = ?track.name, "reinitializing track"); - self.catalog.lock().video.renditions.remove(&track.name); + tracing::debug!(name = ?track.name(), "reinitializing track"); + self.catalog.lock().video.renditions.remove(track.name()); } let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(".av01")).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(".av01"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; - tracing::debug!(name = ?track.name, ?config, "starting track"); + tracing::debug!(name = ?track.name(), ?config, "starting track"); self.catalog .lock() .video .renditions - .insert(track.name.clone(), config.clone()); + .insert(track.name().to_string(), config.clone()); self.config = Some(config); self.track = Some(crate::container::Producer::new( @@ -132,14 +133,15 @@ impl Import { config.container = hang::catalog::Container::Legacy; let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(".av01")).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(".av01"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; - tracing::debug!(name = ?track.name, "starting track with minimal config"); + tracing::debug!(name = ?track.name(), "starting track with minimal config"); self.catalog .lock() .video .renditions - .insert(track.name.clone(), config.clone()); + .insert(track.name().to_string(), config.clone()); self.config = Some(config); self.track = Some(crate::container::Producer::new( @@ -207,17 +209,18 @@ impl Import { } if let Some(track) = &self.track.take() { - self.catalog.lock().video.renditions.remove(&track.name); + self.catalog.lock().video.renditions.remove(track.name()); } let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(".av01")).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(".av01"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; self.catalog .lock() .video .renditions - .insert(track.name.clone(), config.clone()); + .insert(track.name().to_string(), config.clone()); self.config = Some(config); self.track = Some(crate::container::Producer::new( @@ -369,7 +372,7 @@ impl Import { track.write(frame)?; if let Some(jitter) = self.jitter.observe(pts) - && let Some(c) = self.catalog.lock().video.renditions.get_mut(&track.name) + && let Some(c) = self.catalog.lock().video.renditions.get_mut(track.name()) { c.jitter = Some(jitter); } @@ -415,8 +418,8 @@ impl Import { impl Drop for Import { fn drop(&mut self) { if let Some(track) = self.track.take() { - tracing::debug!(name = ?track.name, "ending track"); - self.catalog.lock().video.renditions.remove(&track.name); + tracing::debug!(name = ?track.name(), "ending track"); + self.catalog.lock().video.renditions.remove(track.name()); } } } diff --git a/rs/moq-mux/src/codec/h264/export.rs b/rs/moq-mux/src/codec/h264/export.rs index e8f1ff32e2..dd7c36b7d4 100644 --- a/rs/moq-mux/src/codec/h264/export.rs +++ b/rs/moq-mux/src/codec/h264/export.rs @@ -274,9 +274,12 @@ mod tests { let catalog = avc1_catalog("video.m4s", avcc); // Producer side: publish the broadcast with one length-prefixed video track. - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let mut track = broadcast - .create_track(moq_net::Track::new("video.m4s").with_timescale(hang::container::TIMESCALE)) + .create_track( + "video.m4s", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); // Group 0 (keyframe-starting group): one IDR frame. diff --git a/rs/moq-mux/src/codec/h264/import.rs b/rs/moq-mux/src/codec/h264/import.rs index f3fa3071b3..eba1e295b8 100644 --- a/rs/moq-mux/src/codec/h264/import.rs +++ b/rs/moq-mux/src/codec/h264/import.rs @@ -87,7 +87,8 @@ impl Import { } Mode::Avc3 => { let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(".avc3")).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(".avc3"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; self.track = Some(crate::container::Producer::new( track, @@ -170,7 +171,8 @@ impl Import { }; if self.track.is_none() { let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(".avc3")).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(".avc3"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; self.track = Some(crate::container::Producer::new( track, @@ -249,7 +251,7 @@ impl Import { })?; if let Some(jitter) = self.jitter.observe(pts) - && let Some(c) = self.catalog.lock().video.renditions.get_mut(&track.name) + && let Some(c) = self.catalog.lock().video.renditions.get_mut(track.name()) { c.jitter = Some(jitter); } @@ -378,7 +380,12 @@ impl Import { // The avc3 track was created eagerly in initialize_avc3; just publish // (or republish) the catalog rendition with the latest config. - let track_name = self.track.as_ref().ok_or(Error::Avc3TrackNotCreated)?.name.clone(); + let track_name = self + .track + .as_ref() + .ok_or(Error::Avc3TrackNotCreated)? + .name() + .to_string(); let mut catalog = self.catalog.lock(); catalog.video.renditions.insert(track_name, config.clone()); self.config = Some(config); @@ -409,7 +416,7 @@ impl Import { })?; if let Some(jitter) = self.jitter.observe(pts) - && let Some(c) = self.catalog.lock().video.renditions.get_mut(&track.name) + && let Some(c) = self.catalog.lock().video.renditions.get_mut(track.name()) { c.jitter = Some(jitter); } @@ -427,14 +434,18 @@ impl Import { let mut catalog = self.catalog.lock(); if let Some(track) = self.track.take() { - tracing::debug!(name = ?track.name, "reinitializing H.264 track"); - catalog.video.renditions.remove(&track.name); + tracing::debug!(name = ?track.name(), "reinitializing H.264 track"); + catalog.video.renditions.remove(track.name()); } let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(suffix)).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(suffix), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; - tracing::debug!(name = ?track.name, ?config, "starting H.264 track"); - catalog.video.renditions.insert(track.name.clone(), config.clone()); + tracing::debug!(name = ?track.name(), ?config, "starting H.264 track"); + catalog + .video + .renditions + .insert(track.name().to_string(), config.clone()); self.config = Some(config); self.track = Some(crate::container::Producer::new( @@ -476,8 +487,8 @@ impl Import { impl Drop for Import { fn drop(&mut self) { if let Some(track) = self.track.take() { - tracing::debug!(name = ?track.name, "ending H.264 track"); - self.catalog.lock().video.renditions.remove(&track.name); + tracing::debug!(name = ?track.name(), "ending H.264 track"); + self.catalog.lock().video.renditions.remove(track.name()); } } } @@ -556,7 +567,7 @@ mod tests { avcc.extend_from_slice(&sps_nal); avcc.extend_from_slice(&[0x01, 0x00, 0x04, 0x68, 0xce, 0x3c, 0x80]); // num_pps + pps - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = crate::catalog::hang::Producer::new(&mut producer).unwrap(); @@ -592,7 +603,7 @@ mod tests { annexb.extend_from_slice(&[0, 0, 0, 1]); annexb.extend_from_slice(pps); - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = crate::catalog::hang::Producer::new(&mut producer).unwrap(); diff --git a/rs/moq-mux/src/codec/h265/import.rs b/rs/moq-mux/src/codec/h265/import.rs index 1c1d03f244..0b323b39af 100644 --- a/rs/moq-mux/src/codec/h265/import.rs +++ b/rs/moq-mux/src/codec/h265/import.rs @@ -83,15 +83,19 @@ impl Import { let mut catalog = self.catalog.lock(); if let Some(track) = &self.track.take() { - tracing::debug!(name = ?track.name, "reinitializing track"); - catalog.video.renditions.remove(&track.name); + tracing::debug!(name = ?track.name(), "reinitializing track"); + catalog.video.renditions.remove(track.name()); } let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(".hev1")).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(".hev1"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; - tracing::debug!(name = ?track.name, ?config, "starting track"); - catalog.video.renditions.insert(track.name.clone(), config.clone()); + tracing::debug!(name = ?track.name(), ?config, "starting track"); + catalog + .video + .renditions + .insert(track.name().to_string(), config.clone()); self.config = Some(config); self.track = Some(crate::container::Producer::new( @@ -307,7 +311,7 @@ impl Import { track.write(frame)?; if let Some(jitter) = self.jitter.observe(pts) - && let Some(c) = self.catalog.lock().video.renditions.get_mut(&track.name) + && let Some(c) = self.catalog.lock().video.renditions.get_mut(track.name()) { c.jitter = Some(jitter); } @@ -356,8 +360,8 @@ impl Import { impl Drop for Import { fn drop(&mut self) { if let Some(track) = &self.track { - tracing::debug!(name = ?track.name, "ending track"); - self.catalog.lock().video.renditions.remove(&track.name); + tracing::debug!(name = ?track.name(), "ending track"); + self.catalog.lock().video.renditions.remove(track.name()); } } } diff --git a/rs/moq-mux/src/codec/opus/import.rs b/rs/moq-mux/src/codec/opus/import.rs index cec81d1b67..0d16055962 100644 --- a/rs/moq-mux/src/codec/opus/import.rs +++ b/rs/moq-mux/src/codec/opus/import.rs @@ -21,7 +21,8 @@ impl Import { config: Config, ) -> crate::Result { let track = broadcast.create_track( - moq_net::Track::new(broadcast.unique_name(".opus")).with_timescale(hang::container::TIMESCALE), + broadcast.unique_name(".opus"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; let mut audio_config = hang::catalog::AudioConfig::new( @@ -31,8 +32,12 @@ impl Import { ); audio_config.container = hang::catalog::Container::Legacy; - tracing::debug!(name = ?track.name, config = ?audio_config, "starting track"); - catalog.lock().audio.renditions.insert(track.name.clone(), audio_config); + tracing::debug!(name = ?track.name(), config = ?audio_config, "starting track"); + catalog + .lock() + .audio + .renditions + .insert(track.name().to_string(), audio_config); Ok(Self { catalog, @@ -98,7 +103,7 @@ impl Import { impl Drop for Import { fn drop(&mut self) { - tracing::debug!(name = ?self.track.name, "ending track"); - self.catalog.lock().audio.renditions.remove(&self.track.name); + tracing::debug!(name = ?self.track.name(), "ending track"); + self.catalog.lock().audio.renditions.remove(self.track.name()); } } diff --git a/rs/moq-mux/src/container/consumer.rs b/rs/moq-mux/src/container/consumer.rs index 1a4232513f..333edb5ccb 100644 --- a/rs/moq-mux/src/container/consumer.rs +++ b/rs/moq-mux/src/container/consumer.rs @@ -502,9 +502,10 @@ mod tests { #[tokio::test] async fn read_single_group() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -522,9 +523,10 @@ mod tests { #[tokio::test] async fn read_multiple_frames_single_group() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -542,9 +544,10 @@ mod tests { #[tokio::test] async fn read_multiple_groups_within_latency() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -563,9 +566,10 @@ mod tests { #[tokio::test] async fn latency_skip_delivers_recent_groups() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -607,9 +611,10 @@ mod tests { #[tokio::test] async fn zero_latency_skips_aggressively() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::ZERO); @@ -646,9 +651,10 @@ mod tests { #[tokio::test] async fn latency_skip_correctness() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -691,9 +697,10 @@ mod tests { #[tokio::test] async fn groups_delivered_in_sequence_order() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -729,9 +736,10 @@ mod tests { #[tokio::test] async fn adjacent_group_flushed_immediately() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -749,9 +757,10 @@ mod tests { #[tokio::test] async fn bframes_within_group() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -770,9 +779,10 @@ mod tests { #[tokio::test] async fn empty_track_returns_none() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -790,9 +800,10 @@ mod tests { #[tokio::test] async fn track_closed_with_error() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -814,9 +825,10 @@ mod tests { #[tokio::test] async fn closed_resolves_when_track_ends() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -839,9 +851,10 @@ mod tests { #[tokio::test] async fn gap_in_group_sequence_recovery() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -859,9 +872,10 @@ mod tests { #[tokio::test] async fn gap_at_start_of_sequence() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(80)); @@ -879,9 +893,10 @@ mod tests { #[tokio::test] async fn frame_timestamp_and_index_decoding() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -901,9 +916,10 @@ mod tests { #[tokio::test] async fn frame_payload_preserved() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -941,9 +957,10 @@ mod tests { #[tokio::test] async fn no_infinite_loop_with_buffered_frames() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10)); @@ -989,9 +1006,10 @@ mod tests { #[tokio::test] async fn large_timestamps() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(3700)); @@ -1007,9 +1025,10 @@ mod tests { #[tokio::test] async fn set_latency_changes_behavior() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_secs(10)); @@ -1027,9 +1046,10 @@ mod tests { #[tokio::test] async fn max_timestamp_tracks_through_bframes() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); // latency must exceed (group1_max - group0_min) = 100ms - 0ms = 100ms // to avoid the latency skip and test B-frame timestamp tracking. @@ -1081,9 +1101,10 @@ mod tests { #[tokio::test] async fn startup_selects_earliest_group() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -1136,9 +1157,10 @@ mod tests { #[tokio::test] async fn startup_skips_groups_without_data() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -1161,9 +1183,10 @@ mod tests { #[tokio::test] async fn startup_single_group_mid_stream() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -1177,9 +1200,10 @@ mod tests { #[tokio::test] async fn multiple_sequential_latency_skips() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(50)); @@ -1215,9 +1239,10 @@ mod tests { #[tokio::test] async fn latency_skip_boundary_exact() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -1255,9 +1280,10 @@ mod tests { #[tokio::test] async fn single_newer_group_triggers_skip() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -1295,9 +1321,10 @@ mod tests { #[tokio::test] async fn single_missing_sequence_near_eof_skips() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(100)); @@ -1313,9 +1340,10 @@ mod tests { #[tokio::test] async fn group_error_skips_to_next() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -1332,9 +1360,10 @@ mod tests { #[tokio::test] async fn track_finishes_while_reading() { tokio::time::pause(); - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -1363,9 +1392,10 @@ mod tests { #[tokio::test] async fn empty_group_advances() { - let mut track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -1385,9 +1415,10 @@ mod tests { async fn video_container_legacy() { tokio::time::pause(); - let mut track = moq_net::Track::new("video") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let mut track = moq_net::TrackProducer::new( + "video", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, Container::Legacy).with_latency(Duration::from_millis(500)); @@ -1429,7 +1460,7 @@ mod tests { tokio::time::pause(); // DurationWire is a test-only container that doesn't stamp moq_net frame // timestamps; leave the track untimed so model-layer validation matches. - let mut track = moq_net::Track::new("test").produce(); + let mut track = moq_net::TrackProducer::new("test", None); let consumer_track = track.subscribe(None); // Latency dwarfs the gap, so only duration coverage can trigger the skip. let mut consumer = Consumer::new(consumer_track, DurationWire).with_latency(Duration::from_secs(10)); @@ -1470,7 +1501,7 @@ mod tests { async fn duration_below_gap_does_not_skip() { tokio::time::pause(); // DurationWire is untimed at the moq_net frame layer. - let mut track = moq_net::Track::new("test").produce(); + let mut track = moq_net::TrackProducer::new("test", None); let consumer_track = track.subscribe(None); let mut consumer = Consumer::new(consumer_track, DurationWire).with_latency(Duration::from_secs(10)); diff --git a/rs/moq-mux/src/container/fmp4/export_test.rs b/rs/moq-mux/src/container/fmp4/export_test.rs index 57228958a7..9480faf1f4 100644 --- a/rs/moq-mux/src/container/fmp4/export_test.rs +++ b/rs/moq-mux/src/container/fmp4/export_test.rs @@ -19,13 +19,16 @@ async fn avc3_source_to_cmaf_export_roundtrip() { use hang::catalog::{Container, H264, VideoConfig}; use moq_net::Timestamp; - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut producer).unwrap(); let track = producer - .create_track(moq_net::Track::new(producer.unique_name(".avc3")).with_timescale(hang::container::TIMESCALE)) + .create_track( + producer.unique_name(".avc3"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); let mut config = VideoConfig::new(H264 { profile: 0x42, @@ -36,7 +39,7 @@ async fn avc3_source_to_cmaf_export_roundtrip() { config.coded_width = Some(320); config.coded_height = Some(240); config.container = Container::Legacy; - catalog.lock().video.renditions.insert(track.name.clone(), config); + catalog.lock().video.renditions.insert(track.name().to_string(), config); const SC: &[u8] = &[0, 0, 0, 1]; let sps = &[0x67u8, 0x42, 0xc0, 0x1f, 0xde, 0xad, 0xbe, 0xef][..]; @@ -120,13 +123,16 @@ async fn legacy_aac_source_to_cmaf_export_synthesizes_esds() { use hang::catalog::{AAC, AudioConfig, Container}; use moq_net::Timestamp; - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut producer).unwrap(); let track = producer - .create_track(moq_net::Track::new(producer.unique_name(".aac")).with_timescale(hang::container::TIMESCALE)) + .create_track( + producer.unique_name(".aac"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); // AAC-LC (profile 2), 44100 Hz, stereo. The TS importer sets `description` @@ -140,7 +146,7 @@ async fn legacy_aac_source_to_cmaf_export_synthesizes_esds() { let mut config = AudioConfig::new(AAC { profile: 2 }, 44100, 2); config.description = Some(description.clone()); config.container = Container::Legacy; - catalog.lock().audio.renditions.insert(track.name.clone(), config); + catalog.lock().audio.renditions.insert(track.name().to_string(), config); let mut track_producer = crate::container::Producer::new(track, crate::catalog::hang::Container::Legacy); track_producer @@ -211,7 +217,7 @@ async fn legacy_aac_source_to_cmaf_export_synthesizes_esds() { async fn cmaf_source_to_cmaf_export_passthrough() { let data = include_bytes!("test_data/bbb.mp4"); - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); diff --git a/rs/moq-mux/src/container/fmp4/import.rs b/rs/moq-mux/src/container/fmp4/import.rs index f2e80c0726..169f2cb945 100644 --- a/rs/moq-mux/src/container/fmp4/import.rs +++ b/rs/moq-mux/src/container/fmp4/import.rs @@ -150,18 +150,19 @@ impl Import { let suffix = ".m4s"; let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(suffix)).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(suffix), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; let kind = match handler.as_ref() { b"vide" => { let config = self.init_video(trak, &moov)?; - catalog.video.renditions.insert(track.name.clone(), config); + catalog.video.renditions.insert(track.name().to_string(), config); TrackKind::Video } b"soun" => { let config = self.init_audio(trak, &moov)?; - catalog.audio.renditions.insert(track.name.clone(), config); + catalog.audio.renditions.insert(track.name().to_string(), config); TrackKind::Audio } b"sbtl" => return Err(Error::UnsupportedSubtitle.into()), @@ -614,16 +615,16 @@ impl Import { let config = catalog .video .renditions - .get_mut(&track.track.name) - .ok_or_else(|| Error::MissingVideoTrack(track.track.name.clone()))?; + .get_mut(track.track.name()) + .ok_or_else(|| Error::MissingVideoTrack(track.track.name().to_string()))?; config.jitter = Some(jitter.into()); } TrackKind::Audio => { let config = catalog .audio .renditions - .get_mut(&track.track.name) - .ok_or_else(|| Error::MissingAudioTrack(track.track.name.clone()))?; + .get_mut(track.track.name()) + .ok_or_else(|| Error::MissingAudioTrack(track.track.name().to_string()))?; config.jitter = Some(jitter.into()); } } @@ -669,10 +670,10 @@ impl Drop for Import { for track in self.tracks.values() { match track.kind { TrackKind::Video => { - catalog.video.renditions.remove(&track.track.name); + catalog.video.renditions.remove(track.track.name()); } TrackKind::Audio => { - catalog.audio.renditions.remove(&track.track.name); + catalog.audio.renditions.remove(track.track.name()); } } } diff --git a/rs/moq-mux/src/container/fmp4/import_test.rs b/rs/moq-mux/src/container/fmp4/import_test.rs index b345122ef7..48d7b72381 100644 --- a/rs/moq-mux/src/container/fmp4/import_test.rs +++ b/rs/moq-mux/src/container/fmp4/import_test.rs @@ -14,7 +14,7 @@ fn drain_group_sequences(consumer: &mut moq_net::TrackSubscriber) -> Vec { } fn run_fmp4(data: &[u8]) -> hang::Catalog { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.clone()); @@ -146,7 +146,7 @@ fn test_vp9_catalog() { async fn test_seek_sets_initial_sequence() { use mp4_atom::{Any, DecodeMaybe}; - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let broadcast_consumer = broadcast.consume(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut fmp4 = crate::container::fmp4::Import::new(broadcast, catalog.clone()); @@ -183,8 +183,10 @@ async fn test_seek_sets_initial_sequence() { let snap = catalog.snapshot(); let video_name = snap.video.renditions.keys().next().expect("video track").clone(); let mut video_track = broadcast_consumer - .consume_track(&video_name) + .track(&video_name) + .unwrap() .subscribe(None) + .unwrap() .await .expect("video track should exist"); @@ -210,8 +212,8 @@ async fn test_seek_sets_initial_sequence() { /// exercises the full unified pipeline (hang -> MSF JSON on the wire -> hang). #[tokio::test] async fn test_msf_catalog_roundtrip() { - let mut broadcast = moq_net::Broadcast::new().produce(); - // Take the consumer before adding tracks; consume_track is called after the + let mut broadcast = moq_net::BroadcastInfo::new().produce(); + // Take the consumer before adding tracks; track() is called after the // MSF catalog track has been created by `catalog::Producer::new`. let consumer = broadcast.consume(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); @@ -223,8 +225,10 @@ async fn test_msf_catalog_roundtrip() { let _ = fmp4.decode(&mut buf); let track = consumer - .consume_track(moq_msf::DEFAULT_NAME) + .track(moq_msf::DEFAULT_NAME) + .unwrap() .subscribe(None) + .unwrap() .await .expect("MSF catalog track should exist"); let mut msf = crate::catalog::msf::Consumer::new(track); diff --git a/rs/moq-mux/src/container/hls/import.rs b/rs/moq-mux/src/container/hls/import.rs index 9a93c41159..9d87cefaab 100644 --- a/rs/moq-mux/src/container/hls/import.rs +++ b/rs/moq-mux/src/container/hls/import.rs @@ -615,7 +615,7 @@ mod tests { #[test] fn hls_ingest_starts_without_importers() { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let url = "https://example.com/master.m3u8".to_string(); let cfg = Config::new(url); diff --git a/rs/moq-mux/src/container/mkv/export_test.rs b/rs/moq-mux/src/container/mkv/export_test.rs index 3cb82f65f3..3fa00f9d6f 100644 --- a/rs/moq-mux/src/container/mkv/export_test.rs +++ b/rs/moq-mux/src/container/mkv/export_test.rs @@ -17,7 +17,7 @@ async fn export_header_roundtrip_vp9_opus() { let import_bytes = synth_webm(); // Ingest into a broadcast. - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); @@ -123,7 +123,7 @@ async fn export_header_roundtrip_vp9_opus() { // Verify the round-trip by re-importing the header (a header alone is enough // to populate the catalog). - let mut broadcast2 = moq_net::Broadcast::new().produce(); + let mut broadcast2 = moq_net::BroadcastInfo::new().produce(); let catalog2 = crate::catalog::hang::Producer::new(&mut broadcast2).unwrap(); let mut importer2 = crate::container::mkv::Import::new(broadcast2, catalog2.clone()); let mut hbuf = bytes::BytesMut::from(header.as_ref()); @@ -145,7 +145,7 @@ async fn export_header_roundtrip_vp9_opus() { /// pending until the catalog lands. #[tokio::test(start_paused = true)] async fn export_waits_for_catalog_before_header() { - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); @@ -176,7 +176,7 @@ async fn export_emits_blocks_for_each_frame() { // of SimpleBlock elements with the right track assignments. let import_bytes = synth_webm_with_frames(); - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); @@ -228,7 +228,7 @@ async fn export_emits_blocks_for_each_frame() { // Round-trip verification: feed the exported bytes back through the importer // and check the catalog repopulates with the same codecs. - let mut bcast2 = moq_net::Broadcast::new().produce(); + let mut bcast2 = moq_net::BroadcastInfo::new().produce(); let cat2 = crate::catalog::hang::Producer::new(&mut bcast2).unwrap(); let mut imp2 = crate::container::mkv::Import::new(bcast2, cat2.clone()); let mut rt = bytes::BytesMut::from(exported.as_slice()); @@ -253,13 +253,16 @@ async fn export_rejects_cmaf_track() { // video track. The exporter should bail. use hang::catalog::{Container, H264, VideoConfig}; - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut producer).unwrap(); let track = producer - .create_track(moq_net::Track::new(producer.unique_name(".avc1")).with_timescale(hang::container::TIMESCALE)) + .create_track( + producer.unique_name(".avc1"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); let mut config = VideoConfig::new(H264 { profile: 0x64, @@ -275,7 +278,7 @@ async fn export_rejects_cmaf_track() { timescale: None, track_id: None, }; - catalog.lock().video.renditions.insert(track.name.clone(), config); + catalog.lock().video.renditions.insert(track.name().to_string(), config); let catalog_stream = crate::catalog::Consumer::new(&consumer, crate::catalog::CatalogFormat::Hang) .await @@ -298,13 +301,16 @@ async fn export_avc3_source_synthesizes_avcc_and_length_prefixes() { use hang::catalog::{Container, H264, VideoConfig}; use moq_net::Timestamp; - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut producer).unwrap(); let track = producer - .create_track(moq_net::Track::new(producer.unique_name(".avc3")).with_timescale(hang::container::TIMESCALE)) + .create_track( + producer.unique_name(".avc3"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); let mut config = VideoConfig::new(H264 { profile: 0x42, @@ -315,7 +321,7 @@ async fn export_avc3_source_synthesizes_avcc_and_length_prefixes() { config.coded_width = Some(320); config.coded_height = Some(240); config.container = Container::Legacy; - catalog.lock().video.renditions.insert(track.name.clone(), config); + catalog.lock().video.renditions.insert(track.name().to_string(), config); // Annex-B start code. const SC: &[u8] = &[0, 0, 0, 1]; @@ -461,7 +467,7 @@ async fn export_avc3_source_synthesizes_avcc_and_length_prefixes() { // avcC carried through as `description`. This catches subtle structural // mistakes in the avcC layout that the slot-by-slot check above might // pass even when the record as a whole is malformed. - let mut bcast2 = moq_net::Broadcast::new().produce(); + let mut bcast2 = moq_net::BroadcastInfo::new().produce(); let cat2 = crate::catalog::hang::Producer::new(&mut bcast2).unwrap(); let mut imp2 = crate::container::mkv::Import::new(bcast2, cat2.clone()); let mut rt = bytes::BytesMut::from(exported.as_slice()); @@ -484,7 +490,7 @@ async fn export_fragment_duration_batches_blocks() { // should land in ONE Cluster (vs 5 separate Clusters in per-frame mode). let import_bytes = synth_webm_with_frames(); - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let consumer = producer.consume(); diff --git a/rs/moq-mux/src/container/mkv/import.rs b/rs/moq-mux/src/container/mkv/import.rs index 8c1462d9bf..e41a761887 100644 --- a/rs/moq-mux/src/container/mkv/import.rs +++ b/rs/moq-mux/src/container/mkv/import.rs @@ -286,7 +286,8 @@ impl Import { }; let track = self.broadcast.create_track( - moq_net::Track::new(self.broadcast.unique_name(suffix)).with_timescale(hang::container::TIMESCALE), + self.broadcast.unique_name(suffix), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; let mut catalog = self.catalog.clone(); let mut catalog = catalog.lock(); @@ -294,11 +295,11 @@ impl Import { match kind { TrackKind::Video => { let config = build_video_config(&codec_id, codec_private.as_ref(), video_children.as_deref())?; - catalog.video.renditions.insert(track.name.clone(), config); + catalog.video.renditions.insert(track.name().to_string(), config); } TrackKind::Audio => { let config = build_audio_config(&codec_id, codec_private.as_ref(), audio_children.as_deref())?; - catalog.audio.renditions.insert(track.name.clone(), config); + catalog.audio.renditions.insert(track.name().to_string(), config); } } @@ -426,10 +427,10 @@ impl Drop for Import { for track in self.tracks.values() { match track.kind { TrackKind::Video => { - catalog.video.renditions.remove(&track.track.name); + catalog.video.renditions.remove(track.track.name()); } TrackKind::Audio => { - catalog.audio.renditions.remove(&track.track.name); + catalog.audio.renditions.remove(track.track.name()); } } } diff --git a/rs/moq-mux/src/container/mkv/import_test.rs b/rs/moq-mux/src/container/mkv/import_test.rs index b5ce62ebc8..9b9866f42c 100644 --- a/rs/moq-mux/src/container/mkv/import_test.rs +++ b/rs/moq-mux/src/container/mkv/import_test.rs @@ -135,7 +135,7 @@ fn track_entry_video_vp9(number: u64, width: u64, height: u64) -> MatroskaSpec { } fn run(data: &[u8]) -> hang::Catalog { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut mkv = crate::container::mkv::Import::new(broadcast, catalog.clone()); let mut buf = bytes::BytesMut::from(data); @@ -222,7 +222,7 @@ fn test_chunked_decode_dedup() { .segment_end() .build(); - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut mkv = crate::container::mkv::Import::new(broadcast, catalog.clone()); diff --git a/rs/moq-mux/src/container/producer.rs b/rs/moq-mux/src/container/producer.rs index f65de14483..07c6ac9a52 100644 --- a/rs/moq-mux/src/container/producer.rs +++ b/rs/moq-mux/src/container/producer.rs @@ -230,9 +230,10 @@ mod tests { /// Explicit keyframe closes the current group and starts a new one. #[tokio::test] async fn keyframe_closes_group_immediately() { - let track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer = track.subscribe(None); let mut producer = Producer::new(track, Container::Legacy); @@ -248,9 +249,10 @@ mod tests { /// `finish_group()` flushes the current group immediately; the next write must be a keyframe. #[tokio::test] async fn finish_group_closes_immediately() { - let track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer = track.subscribe(None); let mut producer = Producer::new(track, Container::Legacy); @@ -266,9 +268,10 @@ mod tests { /// Writing a non-keyframe with no open group is a protocol violation. #[test] fn first_frame_must_be_keyframe() { - let track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let mut producer = Producer::new(track, Container::Legacy); let err = producer.write(frame(0, false)).unwrap_err(); @@ -287,9 +290,10 @@ mod tests { /// `seek(n)` opens the next group at sequence `n`. #[tokio::test] async fn seek_uses_explicit_sequence() { - let track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer = track.subscribe(None); let mut producer = Producer::new(track, Container::Legacy); @@ -304,9 +308,10 @@ mod tests { /// `seek` is consumed on the next group creation; subsequent groups auto-increment from there. #[tokio::test] async fn seek_clears_pending_after_use() { - let track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let consumer = track.subscribe(None); let mut producer = Producer::new(track, Container::Legacy); @@ -344,9 +349,10 @@ mod tests { /// group's last frame, without buffering an extra frame. #[tokio::test] async fn keyframe_backfills_last_frame_duration() { - let track = moq_net::Track::new("test") - .with_timescale(hang::container::TIMESCALE) - .produce(); + let track = moq_net::TrackProducer::new( + "test", + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ); let recording = Recording::default(); let mut producer = Producer::new(track, recording.clone()).with_latency(std::time::Duration::from_secs(10)); diff --git a/rs/moq-mux/src/container/source.rs b/rs/moq-mux/src/container/source.rs index f5dbca6a45..04baf9b64d 100644 --- a/rs/moq-mux/src/container/source.rs +++ b/rs/moq-mux/src/container/source.rs @@ -49,7 +49,7 @@ impl VideoTransform { /// A subscription that resolves on first poll, then the live consumer. enum SourceState { /// Waiting for the subscription to resolve (blocks on the publisher's SUBSCRIBE_OK). - Subscribing(moq_net::TrackPending), + Subscribing(moq_net::TrackSubscriberPending), /// The resolved consumer, reading frames. Active(Consumer), } @@ -82,7 +82,7 @@ impl ExportSource { let description = config.description.as_ref().filter(|b| !b.is_empty()).cloned(); Ok(Self { - state: SourceState::Subscribing(broadcast.consume_track(name).subscribe(None)), + state: SourceState::Subscribing(broadcast.track(name)?.subscribe(None)?), media: Some(media), latency, transform, @@ -104,7 +104,7 @@ impl ExportSource { let description = config.description.as_ref().filter(|b| !b.is_empty()).cloned(); Ok(Self { - state: SourceState::Subscribing(broadcast.consume_track(name).subscribe(None)), + state: SourceState::Subscribing(broadcast.track(name)?.subscribe(None)?), media: Some(media), latency, transform: None, @@ -124,7 +124,7 @@ impl ExportSource { let description = config.description.as_ref().filter(|b| !b.is_empty()).cloned(); Ok(Self { - state: SourceState::Subscribing(broadcast.consume_track(name).subscribe(None)), + state: SourceState::Subscribing(broadcast.track(name)?.subscribe(None)?), media: Some(media), latency, transform: None, diff --git a/rs/moq-mux/src/container/ts/export_test.rs b/rs/moq-mux/src/container/ts/export_test.rs index 8c61f18fd0..e2624a5d34 100644 --- a/rs/moq-mux/src/container/ts/export_test.rs +++ b/rs/moq-mux/src/container/ts/export_test.rs @@ -73,14 +73,17 @@ fn assert_packet_aligned(ts: &[u8]) { #[tokio::test(start_paused = true)] async fn export_aac_roundtrip() { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let consumer = broadcast.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let track = broadcast - .create_track(moq_net::Track::new(broadcast.unique_name(".aac")).with_timescale(hang::container::TIMESCALE)) + .create_track( + broadcast.unique_name(".aac"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); - let name = track.name.clone(); + let name = track.name().to_string(); { let mut cfg = AudioConfig::new(AAC { profile: 2 }, 48_000, 2); cfg.container = Container::Legacy; @@ -191,14 +194,17 @@ fn reassemble_video(ts: &[u8], expected_stream_type: StreamType) -> Vec { /// into a synthesized avcC, and the muxer re-injects them on the keyframe. #[tokio::test(start_paused = true)] async fn export_avc3_in_band_reassembles() { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let consumer = broadcast.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let track = broadcast - .create_track(moq_net::Track::new(broadcast.unique_name(".avc3")).with_timescale(hang::container::TIMESCALE)) + .create_track( + broadcast.unique_name(".avc3"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); - let name = track.name.clone(); + let name = track.name().to_string(); { let mut cfg = VideoConfig::new(H264 { profile: 0x64, @@ -240,16 +246,19 @@ async fn export_avc3_in_band_reassembles() { /// length prefixes to start codes. #[tokio::test(start_paused = true)] async fn export_avc1_out_of_band_reassembles() { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let consumer = broadcast.consume(); let mut catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let avcc = crate::codec::h264::build_avcc(SPS, PPS).unwrap(); let track = broadcast - .create_track(moq_net::Track::new(broadcast.unique_name(".avc1")).with_timescale(hang::container::TIMESCALE)) + .create_track( + broadcast.unique_name(".avc1"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), + ) .unwrap(); - let name = track.name.clone(); + let name = track.name().to_string(); { let mut cfg = VideoConfig::new(H264 { profile: 0x64, diff --git a/rs/moq-mux/src/container/ts/import.rs b/rs/moq-mux/src/container/ts/import.rs index 8a60f56f73..190a61995a 100644 --- a/rs/moq-mux/src/container/ts/import.rs +++ b/rs/moq-mux/src/container/ts/import.rs @@ -358,7 +358,7 @@ impl AacStream { // WebCodecs) can configure the decoder. TS itself carries it inline. let description = config.encode(); let import = aac::Import::new(self.broadcast.clone(), self.catalog.clone(), config)?; - let name = import.track().name.clone(); + let name = import.track().name().to_string(); if let Some(rendition) = self.catalog.lock().audio.renditions.get_mut(&name) { rendition.description = Some(description); } @@ -421,7 +421,7 @@ impl AacStream { self.jitter = Some(jitter); if let Some(import) = &self.import { - let name = import.track().name.clone(); + let name = import.track().name().to_string(); if let Some(rendition) = self.catalog.lock().audio.renditions.get_mut(&name) { rendition.jitter = Some(jitter.into()); } diff --git a/rs/moq-mux/src/container/ts/import_test.rs b/rs/moq-mux/src/container/ts/import_test.rs index 5e9967eb75..7fdd89f181 100644 --- a/rs/moq-mux/src/container/ts/import_test.rs +++ b/rs/moq-mux/src/container/ts/import_test.rs @@ -7,7 +7,7 @@ use bytes::BytesMut; /// Decode a whole TS buffer into a fresh broadcast and return the catalog. fn import_ts(data: &[u8]) -> hang::Catalog { - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.clone()); @@ -50,7 +50,7 @@ async fn import_export_import_roundtrip() { let data = include_bytes!("test_data/bbb.ts"); // Import the fixture into a broadcast. - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let consumer = broadcast.consume(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.clone()); @@ -84,7 +84,7 @@ fn import_handles_unaligned_chunks() { // exercising the partial-packet retention across calls. let data = include_bytes!("test_data/bbb.ts"); - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); let catalog = crate::catalog::hang::Producer::new(&mut broadcast).unwrap(); let mut import = crate::container::ts::Import::new(broadcast, catalog.clone()); diff --git a/rs/moq-native/examples/chat.rs b/rs/moq-native/examples/chat.rs index 091f1e6a67..d950d94e1b 100644 --- a/rs/moq-native/examples/chat.rs +++ b/rs/moq-native/examples/chat.rs @@ -37,11 +37,11 @@ async fn run_session(origin: moq_net::OriginProducer) -> anyhow::Result<()> { async fn run_broadcast(origin: moq_net::OriginProducer) -> anyhow::Result<()> { // Create and publish a broadcast to the origin.. // A broadcast is a collection of tracks, but in this example we'll only create one. - let mut broadcast = moq_net::Broadcast::new().produce(); + let mut broadcast = moq_net::BroadcastInfo::new().produce(); // Create a track that we'll insert into the broadcast. // A track is a series of groups representing a live stream. - let mut track = broadcast.create_track(moq_net::Track::new("chat"))?; + let mut track = broadcast.create_track("chat", None)?; // NOTE: The path is empty because we're using the URL to scope the broadcast. // If you put "alice" here, it would be published as "anon/chat-example/alice". diff --git a/rs/moq-native/examples/clock.rs b/rs/moq-native/examples/clock.rs index 921a969dd8..4a978d193f 100644 --- a/rs/moq-native/examples/clock.rs +++ b/rs/moq-native/examples/clock.rs @@ -66,8 +66,8 @@ async fn main() -> anyhow::Result<()> { match config.role { Command::Publish => { - let mut broadcast = moq_net::Broadcast::new().produce(); - let track = broadcast.create_track(moq_net::Track::new(track))?; + let mut broadcast = moq_net::BroadcastInfo::new().produce(); + let track = broadcast.create_track(track, None)?; let clock = Publisher::new(track); origin.publish_broadcast(&config.broadcast, broadcast.consume()); @@ -102,7 +102,7 @@ async fn main() -> anyhow::Result<()> { Some(broadcast) => { tracing::info!(broadcast = %path, "broadcast is online, subscribing to track"); let track = broadcast - .consume_track(&track).subscribe(None) + .track(&track)?.subscribe(None)? .await?; clock = Some(Subscriber::new(track)); } diff --git a/rs/moq-native/tests/backend.rs b/rs/moq-native/tests/backend.rs index 7a2a2e21a7..782a5c37da 100644 --- a/rs/moq-native/tests/backend.rs +++ b/rs/moq-native/tests/backend.rs @@ -4,7 +4,7 @@ //! Each test is gated with `#[cfg(feature = "...")]` so it only compiles when the //! corresponding backend is enabled. Running `cargo test --all-features` exercises all. -use moq_native::moq_net::{Origin, Track}; +use moq_native::moq_net::Origin; use std::time::Duration; const TIMEOUT: Duration = Duration::from_secs(10); @@ -16,9 +16,7 @@ async fn backend_test(scheme: &str, backend: moq_native::QuicBackend) { // ── publisher (server) ────────────────────────────────────────── let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); let mut group = track.append_group().expect("failed to append group"); group.write_frame(b"hello".as_ref()).expect("failed to write frame"); @@ -70,8 +68,10 @@ async fn backend_test(scheme: &str, backend: moq_native::QuicBackend) { let bc = bc.broadcast().expect("expected announce, got unannounce"); let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); @@ -140,9 +140,7 @@ async fn iroh_connect() { // ── publisher (server) ────────────────────────────────────────── let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); let mut group = track.append_group().expect("failed to append group"); group.write_frame(b"hello".as_ref()).expect("failed to write frame"); @@ -224,8 +222,10 @@ async fn iroh_connect() { let bc = bc.broadcast().expect("expected announce, got unannounce"); let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); diff --git a/rs/moq-native/tests/broadcast.rs b/rs/moq-native/tests/broadcast.rs index e4a001fd7d..32abfb1a6f 100644 --- a/rs/moq-native/tests/broadcast.rs +++ b/rs/moq-native/tests/broadcast.rs @@ -8,7 +8,7 @@ //! This covers raw QUIC (moqt://) and WebTransport (https://) transports, //! exercising every protocol version the library supports. -use moq_native::moq_net::{self, Origin, Track}; +use moq_native::moq_net::{self, Origin}; use std::time::Duration; const TIMEOUT: Duration = Duration::from_secs(10); @@ -25,9 +25,7 @@ async fn broadcast_test(scheme: &str, client_version: Option<&str>, server_versi // ── publisher (server) ────────────────────────────────────────── let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); // Write a group containing a single frame. let mut group = track.append_group().expect("failed to append group"); @@ -88,8 +86,10 @@ async fn broadcast_test(scheme: &str, client_version: Option<&str>, server_versi // Subscribe to the track. let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); @@ -128,7 +128,7 @@ async fn lite05_timestamp_roundtrip(scheme: &str) { // Track with an advertised microsecond timescale. Without it, Lite05 publish // fails with ProtocolViolation. let mut track = broadcast - .create_track(Track::new("video").with_timescale(Timescale::MICRO)) + .create_track("video", moq_net::TrackInfo::default().with_timescale(Timescale::MICRO)) .expect("failed to create track"); // Three frames where the middle PTS goes backwards (B-frame decode order) @@ -188,8 +188,10 @@ async fn lite05_timestamp_roundtrip(scheme: &str) { let bc = bc.broadcast().expect("expected announce, got unannounce"); let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); @@ -235,7 +237,7 @@ async fn broadcast_moq_lite_05_timestamps_webtransport() { async fn broadcast_moq_lite_05_without_timescale() { let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); - let mut track = broadcast.create_track(Track::new("video")).expect("create track"); + let mut track = broadcast.create_track("video", None).expect("create track"); let mut group = track.append_group().expect("append group"); group.write_frame(b"hello".as_ref()).expect("write frame"); @@ -279,8 +281,10 @@ async fn broadcast_moq_lite_05_without_timescale() { let bc = bc.broadcast().expect("expected announce"); let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); @@ -623,14 +627,12 @@ async fn broadcast_webtransport_negotiate_client_all_server_transport_18() { #[tracing_test::traced_test] #[tokio::test] async fn broadcast_websocket() { - use moq_native::moq_net::{Origin, Track}; + use moq_native::moq_net::Origin; // ── publisher (server) ────────────────────────────────────────── let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); let mut group = track.append_group().expect("failed to append group"); group.write_frame(b"hello".as_ref()).expect("failed to write frame"); @@ -693,8 +695,10 @@ async fn broadcast_websocket() { // Subscribe to the track. let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); @@ -729,14 +733,12 @@ async fn broadcast_websocket() { #[tracing_test::traced_test] #[tokio::test] async fn broadcast_websocket_fallback() { - use moq_native::moq_net::{Origin, Track}; + use moq_native::moq_net::Origin; // ── publisher (server) ────────────────────────────────────────── let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); let mut group = track.append_group().expect("failed to append group"); group.write_frame(b"hello".as_ref()).expect("failed to write frame"); @@ -802,8 +804,10 @@ async fn broadcast_websocket_fallback() { // Subscribe to the track. let mut track_sub = bc - .consume_track("video") + .track("video") + .unwrap() .subscribe(None) + .unwrap() .await .expect("consume_track failed"); @@ -845,9 +849,7 @@ const NEWEST_LITE: &str = "moq-lite-04"; async fn broadcast_websocket_uses_newest_version() { let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); let mut group = track.append_group().expect("failed to append group"); group.write_frame(b"hello".as_ref()).expect("failed to write frame"); group.finish().expect("failed to finish group"); @@ -913,9 +915,7 @@ async fn broadcast_websocket_uses_newest_version() { async fn broadcast_race_quic_wins() { let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast"); - let mut track = broadcast - .create_track(Track::new("video")) - .expect("failed to create track"); + let mut track = broadcast.create_track("video", None).expect("failed to create track"); let mut group = track.append_group().expect("failed to append group"); group.write_frame(b"hello".as_ref()).expect("failed to write frame"); group.finish().expect("failed to finish group"); @@ -1001,7 +1001,7 @@ async fn broadcast_race_quic_wins() { async fn linger_resubscribe_keeps_flowing_moq_lite_03() { let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); - let mut track = broadcast.create_track(Track::new("video")).expect("create track"); + let mut track = broadcast.create_track("video", None).expect("create track"); let mut group0 = track.append_group().expect("append group 0"); group0.write_frame(b"a".as_ref()).expect("write frame 0"); @@ -1044,7 +1044,13 @@ async fn linger_resubscribe_keeps_flowing_moq_lite_03() { let bc = bc.broadcast().expect("expected announce"); // First subscription: receive group 0. - let mut sub1 = bc.consume_track("video").subscribe(None).await.expect("subscribe1"); + let mut sub1 = bc + .track("video") + .unwrap() + .subscribe(None) + .unwrap() + .await + .expect("subscribe1"); let mut g = tokio::time::timeout(TIMEOUT, sub1.recv_group()) .await .expect("recv group 0 timeout") @@ -1068,7 +1074,13 @@ async fn linger_resubscribe_keeps_flowing_moq_lite_03() { tokio::time::sleep(Duration::from_millis(20)).await; // Resubscribe well inside the 5s linger window. - let mut sub2 = bc.consume_track("video").subscribe(None).await.expect("subscribe2"); + let mut sub2 = bc + .track("video") + .unwrap() + .subscribe(None) + .unwrap() + .await + .expect("subscribe2"); // A new group published after the resubscribe must reach the consumer // regardless of which linger branch fired. diff --git a/rs/moq-net/src/ietf/publisher.rs b/rs/moq-net/src/ietf/publisher.rs index d947303b62..a976a9d6f2 100644 --- a/rs/moq-net/src/ietf/publisher.rs +++ b/rs/moq-net/src/ietf/publisher.rs @@ -132,7 +132,7 @@ impl Publisher { ..Default::default() }; - let track = match broadcast.consume_track(&msg.track_name).subscribe(subscription).await { + let track = match async { broadcast.track(&msg.track_name)?.subscribe(subscription)?.await }.await { Ok(track) => track, Err(err) => { self.write_subscribe_error(&mut stream.writer, request_id, 404, &err.to_string()) @@ -255,7 +255,7 @@ impl Publisher { }?; let sequence = group.sequence; - tracing::debug!(subscribe = %request_id, track = %track.name, sequence, "serving group"); + tracing::debug!(subscribe = %request_id, track = %track.name(), sequence, "serving group"); let msg = ietf::GroupHeader { track_alias: request_id.0, diff --git a/rs/moq-net/src/ietf/subscriber.rs b/rs/moq-net/src/ietf/subscriber.rs index d167c8ae2e..df8df7ea44 100644 --- a/rs/moq-net/src/ietf/subscriber.rs +++ b/rs/moq-net/src/ietf/subscriber.rs @@ -3,8 +3,8 @@ use std::collections::{HashMap, hash_map::Entry}; use std::sync::Arc; use crate::{ - Broadcast, BroadcastDynamic, Error, Frame, FrameProducer, Group, GroupProducer, MAX_FRAME_SIZE, OriginProducer, - Path, PathOwned, StatsHandle, SubscriberStats, SubscriberTrack, Track, TrackProducer, TrackRequest, + BroadcastDynamic, BroadcastInfo, Error, Frame, FrameProducer, Group, GroupProducer, MAX_FRAME_SIZE, OriginProducer, + Path, PathOwned, StatsHandle, SubscriberStats, SubscriberTrack, TrackProducer, TrackRequest, coding::{Reader, Stream}, ietf::{self, Control, FilterType, GroupOrder, RequestId}, model::BroadcastProducer, @@ -430,7 +430,7 @@ impl Subscriber { return Ok(entry.get().producer.clone()); } Entry::Vacant(entry) => { - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); self.origin.publish_broadcast(path.clone(), broadcast.consume()); entry.insert(BroadcastState { producer: broadcast.clone(), @@ -476,7 +476,7 @@ impl Subscriber { fn start_publish(&mut self, msg: &ietf::Publish<'_>) -> Result<(), Error> { let request_id = msg.request_id; - let track = Track::new(msg.track_name.to_string()).produce(); + let track = TrackProducer::new(msg.track_name.to_string(), None); let abs = self.origin.absolute(&msg.track_namespace).to_owned(); let track_stats = Arc::new(self.stats.broadcast(&abs).subscriber_track(&msg.track_name)); @@ -506,7 +506,7 @@ impl Subscriber { drop(state); let mut broadcast = self.start_announce(msg.track_namespace.to_owned())?; - broadcast.insert_track(track.subscribe(None))?; + broadcast.insert_track(track.consume())?; Ok(()) } @@ -540,14 +540,7 @@ impl Subscriber { // Accept right away: IETF group data can arrive before SubscribeOk, so we // need the producer in place to route it. This also unblocks the // downstream subscriber's `consume_track`. - let name = request.name().to_string(); - let mut track = match request.accept(Track::new(name)) { - Ok(track) => track, - Err(err) => { - tracing::debug!(%err, "failed to accept track request"); - return; - } - }; + let mut track = request.accept(None); let request_id = match self.control.next_request_id().await { Ok(id) => id, @@ -567,7 +560,7 @@ impl Subscriber { }; let abs = self.origin.absolute(&broadcast_path).to_owned(); - let track_stats = Arc::new(self.stats.broadcast(&abs).subscriber_track(&track.name)); + let track_stats = Arc::new(self.stats.broadcast(&abs).subscriber_track(track.name())); // Pre-register the track so group data arriving before SubscribeOk can be routed. // The publisher uses request_id.0 as track_alias, and recv_group falls back to @@ -595,7 +588,7 @@ impl Subscriber { return; } - tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name, "subscribe started"); + tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name(), "subscribe started"); // Read the response and register the alias mapping let track_alias = match self.read_subscribe_response(&mut stream).await { @@ -624,17 +617,17 @@ impl Subscriber { tokio::select! { _ = track.unused() => { - tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name, "subscribe cancelled"); + tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name(), "subscribe cancelled"); let _ = track.abort(Error::Cancel); } err = broadcast.closed() => { - tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name, "broadcast closed"); + tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name(), "broadcast closed"); let _ = track.abort(err); } res = stream.reader.closed() => { match res { Ok(()) => { - tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name, "subscribe complete"); + tracing::info!(broadcast = %self.origin.absolute(&broadcast_path), track = %track.name(), "subscribe complete"); let _ = track.finish(); } Err(err) => { @@ -667,7 +660,7 @@ impl Subscriber { .encode(&ietf::Subscribe { request_id, track_namespace: broadcast.to_owned(), - track_name: (&track.name).into(), + track_name: track.name().into(), subscriber_priority: track.subscription().map(|s| s.priority).unwrap_or(0), group_order: GroupOrder::Descending, filter_type: FilterType::LargestObject, diff --git a/rs/moq-net/src/lib.rs b/rs/moq-net/src/lib.rs index 57a236d806..08cc3a8068 100644 --- a/rs/moq-net/src/lib.rs +++ b/rs/moq-net/src/lib.rs @@ -7,9 +7,9 @@ //! //! ## API //! The API is built around Producer/Consumer pairs, with the hierarchy: -//! - [Origin]: A collection of [Broadcast]s, produced by one or more [Session]s. -//! - [Broadcast]: A collection of [Track]s, produced by a single publisher. -//! - [Track]: A collection of [Group]s, delivered out-of-order until expired. +//! - [Origin]: A collection of [BroadcastConsumer]s, produced by one or more [Session]s. +//! - [BroadcastConsumer]: A collection of [TrackConsumer]s, produced by a single publisher. +//! - [TrackConsumer]: A collection of [Group]s, delivered out-of-order until expired. //! - [Group]: A collection of [Frame]s, delivered in order until cancelled. //! - [Frame]: Chunks of data with an upfront size. //! diff --git a/rs/moq-net/src/lite/publisher.rs b/rs/moq-net/src/lite/publisher.rs index 9a020fa75f..5d2b24b84a 100644 --- a/rs/moq-net/src/lite/publisher.rs +++ b/rs/moq-net/src/lite/publisher.rs @@ -249,7 +249,7 @@ impl Publisher { match event.broadcast() { Some(broadcast) => { - let hops = &broadcast.hops; + let hops = &broadcast.info().hops; // Apply the same exclude_hop and reflected-announce skips as the live // loop so the count matches exactly what we send (minus the self push). if exclude_hop != 0 && hops.iter().any(|h| h.id == exclude_hop) { @@ -306,7 +306,7 @@ impl Publisher { match event { crate::Announced::Active(active) => { - let Some(hops) = Self::prepare_active_hops(&active.hops, self_origin, exclude_hop, version, &absolute) else { + let Some(hops) = Self::prepare_active_hops(&active.info().hops, self_origin, exclude_hop, version, &absolute) else { continue; }; tracing::debug!(broadcast = %absolute, "announce"); @@ -319,7 +319,7 @@ impl Publisher { // On lite-05+ a restart travels as a duplicate ANNOUNCE (a second // `Active` for an already-announced path). Older versions never defined // that, so split it into an unannounce followed by a fresh announce. - match Self::prepare_active_hops(&active.hops, self_origin, exclude_hop, version, &absolute) { + match Self::prepare_active_hops(&active.info().hops, self_origin, exclude_hop, version, &absolute) { Some(hops) => { tracing::debug!(broadcast = %absolute, "restart"); // Continuity: keep the existing stats guard (no close + reopen). @@ -455,10 +455,7 @@ impl Publisher { }; let broadcast = consumer.ok_or(Error::NotFound)?; - let track = broadcast - .consume_track(&subscribe.track) - .subscribe(subscription) - .await?; + let track = broadcast.track(&subscribe.track)?.subscribe(subscription)?.await?; // Compress only when the producer marked the track worth it and the // negotiated draft understands the SUBSCRIBE_OK codec field. Older drafts @@ -467,7 +464,7 @@ impl Publisher { version, Version::Lite01 | Version::Lite02 | Version::Lite03 | Version::Lite04 ); - let compression = if track.compress && supports_compression { + let compression = if track.info().compress && supports_compression { Compression::Deflate } else { Compression::None @@ -478,7 +475,7 @@ impl Publisher { // (the field never lands on the wire) and stream untimed frames; Lite05+ // honors `Some(_)` and skips the timestamp byte for `None`. let timescale = if version.has_timestamps() { - track.timescale + track.info().timescale } else { None }; @@ -497,7 +494,7 @@ impl Publisher { timescale, // Announce the publisher's cache window so the subscriber (a relay) // re-serves with the same eviction window. Pre-lite-05 peers ignore it. - cache: track.cache, + cache: track.info().cache, }; stream.writer.encode(&lite::SubscribeResponse::Ok(info)).await?; @@ -512,7 +509,7 @@ impl Publisher { let sub = Subscription { session, id: subscribe.id, - track_name: Arc::from(track.name.as_str()), + track_name: Arc::from(track.name()), track_stats: Arc::new(track_stats), priority, track_priority: track_priority_rx, diff --git a/rs/moq-net/src/lite/subscriber.rs b/rs/moq-net/src/lite/subscriber.rs index 3e5ee4e6da..069f363095 100644 --- a/rs/moq-net/src/lite/subscriber.rs +++ b/rs/moq-net/src/lite/subscriber.rs @@ -8,9 +8,9 @@ use std::{ use futures::{StreamExt, stream::FuturesUnordered}; use crate::{ - AsPath, BandwidthProducer, Broadcast, BroadcastDynamic, Compression, Error, Frame, FrameProducer, Group, + AsPath, BandwidthProducer, BroadcastDynamic, BroadcastInfo, Compression, Error, Frame, FrameProducer, Group, GroupProducer, MAX_FRAME_SIZE, OriginProducer, Path, PathOwned, StatsHandle, SubscriberStats, SubscriberTrack, - Timescale, Timestamp, Track, TrackProducer, TrackRequest, + Timescale, Timestamp, TrackInfo, TrackProducer, TrackRequest, coding::{Reader, Stream}, lite, model::BroadcastProducer, @@ -381,7 +381,7 @@ impl Subscriber { tracing::debug!(broadcast = %self.log_path(&path), hops = hops.len(), "announce"); - let broadcast = Broadcast { hops }.produce(); + let broadcast = BroadcastInfo { hops }.produce(); // Make sure the peer doesn't double announce. match producers.entry(path.to_owned()) { @@ -434,7 +434,7 @@ impl Subscriber { tracing::debug!(broadcast = %self.log_path(&path), hops = hops.len(), "restart"); - let broadcast = Broadcast { hops }.produce(); + let broadcast = BroadcastInfo { hops }.produce(); let dynamic = broadcast.dynamic(); // Publish the replacement first so the origin restarts atomically; the old broadcast is @@ -485,7 +485,7 @@ impl Subscriber { /// to uncap. The stream stays open across the whole lifecycle — only a timeout /// or a publisher-side close ends it. This avoids the stream-churn / duplicate-fetch /// race that an unsubscribe-and-reissue approach would have. - async fn run_subscribe(&mut self, path: PathOwned, broadcast: BroadcastDynamic, request: TrackRequest) { + async fn run_subscribe(&mut self, path: PathOwned, broadcast: BroadcastDynamic, mut request: TrackRequest) { // Subscriber-side track stats; counters bump as frames/bytes/groups arrive. // Drop on subscription end records `subscriber.subscriptions_closed`. We use // subscriber_track to avoid double-counting broadcasts: the broadcast lifetime @@ -500,7 +500,8 @@ impl Subscriber { let id = self.next_id.fetch_add(1, atomic::Ordering::Relaxed); // Forward the aggregate of every downstream subscriber's preferences upstream. - let subscription = request.subscription().clone(); + // Blocks until the first downstream subscriber registers its preferences. + let subscription = request.subscription_changed().await.unwrap_or_default(); let msg = lite::Subscribe { id, broadcast: path.as_path(), @@ -515,7 +516,7 @@ impl Subscriber { tracing::info!(id, broadcast = %self.log_path(&path), track = %name, "subscribe started"); let result = self - .run_subscribe_session(id, &name, request, track_stats, &broadcast, msg) + .run_subscribe_session(id, request, track_stats, &broadcast, msg) .await; self.subscribes.lock().remove(&id); @@ -543,7 +544,6 @@ impl Subscriber { async fn run_subscribe_session( &self, id: u64, - name: &str, request: TrackRequest, track_stats: Arc, broadcast: &BroadcastDynamic, @@ -563,33 +563,33 @@ impl Subscriber { let mut stream = match Stream::open(&self.session, self.version).await { Ok(s) => s, Err(err) => { - request.deny(err.clone()); + request.reject(err.clone()); return SessionOutcome::Error(err); } }; if let Err(err) = stream.writer.encode(&lite::ControlType::Subscribe).await { - request.deny(err.clone()); + request.reject(err.clone()); return SessionOutcome::Error(err); } if let Err(err) = stream.writer.encode(&msg).await { stream.writer.abort(&err); - request.deny(err.clone()); + request.reject(err.clone()); return SessionOutcome::Error(err); } // The first response MUST be a SUBSCRIBE_OK. Bail if the broadcast dies first. let resp = tokio::select! { err = broadcast.closed() => { - request.deny(err.clone()); + request.reject(err.clone()); return SessionOutcome::BroadcastClosed(err); } resp = stream.reader.decode::() => match resp { Ok(r) => r, Err(err) => { stream.writer.abort(&err); - request.deny(err.clone()); + request.reject(err.clone()); return SessionOutcome::Error(err); } } @@ -597,7 +597,7 @@ impl Subscriber { let lite::SubscribeResponse::Ok(info) = resp else { let err = Error::ProtocolViolation; stream.writer.abort(&err); - request.deny(err.clone()); + request.reject(err.clone()); return SessionOutcome::Error(err); }; @@ -617,18 +617,14 @@ impl Subscriber { // Stamp the negotiated timescale onto the local Track so groups inherit // it and downstream consumers (including this subscriber's frame decode // path) can validate per-frame timestamps at the model layer. - let mut local_info = Track::new(name); - local_info.timescale = info.timescale; - // Carry the publisher's cache window so the local producer evicts (and - // clamps downstream stale windows) with the same bound when re-served. - local_info.cache = info.cache; - let mut track = match request.accept(local_info) { - Ok(track) => track, - Err(err) => { - stream.writer.abort(&err); - return SessionOutcome::Error(err); - } + let local_info = TrackInfo { + timescale: info.timescale, + // Carry the publisher's cache window so the local producer evicts (and + // clamps downstream stale windows) with the same bound when re-served. + cache: info.cache, + ..Default::default() }; + let mut track = request.accept(local_info); let subscribe_ok = kio::Producer::new(Some(info)).consume(); self.subscribes.lock().insert( id, @@ -697,7 +693,7 @@ impl Subscriber { break 'lifecycle SessionOutcome::Error(err); } - tracing::info!(track = %track.name, "subscribe resumed"); + tracing::info!(track = %track.name(), "subscribe resumed"); let uncap = lite::SubscribeUpdate { priority: original_priority, diff --git a/rs/moq-net/src/model/broadcast.rs b/rs/moq-net/src/model/broadcast.rs index ab6b53bc54..f8b5da0c11 100644 --- a/rs/moq-net/src/model/broadcast.rs +++ b/rs/moq-net/src/model/broadcast.rs @@ -1,78 +1,50 @@ use std::{ collections::{HashMap, VecDeque, hash_map}, - ops::Deref, + sync::Arc, task::{Poll, ready}, }; -use crate::{Error, Subscription, TrackProducer, TrackSubscriber, model::track::TrackWeak}; +use crate::{Error, TrackConsumer, TrackProducer, TrackRequest, TrackWeak}; -use super::{OriginList, Track}; +use super::{OriginList, TrackInfo}; /// A collection of media tracks that can be published and subscribed to. /// -/// Create via [`Broadcast::produce`] to obtain both [`BroadcastProducer`] and [`BroadcastConsumer`] pair. +/// Create via [`BroadcastInfo::produce`] to obtain both [`BroadcastProducer`] and [`BroadcastConsumer`] pair. #[derive(Clone, Debug, Default)] -pub struct Broadcast { +pub struct BroadcastInfo { /// The chain of origins the broadcast has traversed. Each relay appends its own /// [`crate::Origin`] when forwarding, so the list is used for loop detection and /// shortest-path preference. pub hops: OriginList, } -impl Broadcast { +impl BroadcastInfo { /// Create a new broadcast with an empty hop chain. pub fn new() -> Self { Self::default() } - /// Consume this [Broadcast] to create a producer that carries its metadata + /// Consume this [BroadcastInfo] to create a producer that carries its metadata /// (including the hop chain). pub fn produce(self) -> BroadcastProducer { BroadcastProducer::new(self) } } -/// The slot a pending subscription resolves into: `None` until the publisher -/// accepts (delivering the consumer) or denies (delivering an error). Carried by -/// a [`kio`] channel so subscribers can `poll_ok` it without tokio. The consumer -/// is created at accept time so it counts toward the producer immediately, before -/// the subscriber even polls. -type PendingSlot = Option>; - -/// One waiting subscriber: its preferences and the producer side of its resolver channel. -type Resolver = (Subscription, kio::Producer); - -/// A track that has been subscribed to but not yet served by the dynamic handler. -/// -/// Multiple subscribers to the same name before it is accepted coalesce into one -/// pending request, each adding a resolver channel so they all receive a consumer -/// for the same producer once it is accepted. #[derive(Default)] -struct PendingRequest { - resolvers: Vec, -} - -/// Resolve every waiting subscriber with `err`. -fn fail_resolvers(resolvers: Vec, err: &Error) { - for (_, slot) in resolvers { - if let Ok(mut slot) = slot.write() { - *slot = Some(Err(err.clone())); - } - } -} - -#[derive(Default)] -struct State { +struct BroadcastState { // Weak references for deduplication. Doesn't prevent track auto-close. - tracks: HashMap, + // Keyed by the track's shared `Arc` name (the same Arc the handle holds). + tracks: HashMap, TrackWeak>, // Pending requests keyed by track name, waiting for the dynamic handler to // accept or deny them. - requests: HashMap, + requests: HashMap, TrackRequest>, // Requested names in FIFO order for the dynamic handler to drain. A name // stays in `requests` (but not here) once handed out as a `TrackRequest`. - request_order: VecDeque, + request_order: VecDeque>, // The current number of dynamic producers. // If this is 0, requests must be empty. @@ -82,37 +54,35 @@ struct State { abort: Option, } -fn modify(state: &kio::Producer) -> Result, Error> { - match state.write() { - Ok(state) => Ok(state), - Err(r) => Err(r.abort.clone().unwrap_or(Error::Dropped)), +impl BroadcastState { + fn modify(state: &kio::Producer) -> Result, Error> { + match state.write() { + Ok(state) => Ok(state), + Err(r) => Err(r.abort.clone().unwrap_or(Error::Dropped)), + } } -} -impl State { /// Insert a track weak handle into the lookup, returning an error on duplicate. fn insert_track(&mut self, weak: TrackWeak) -> Result<(), Error> { - let hash_map::Entry::Vacant(entry) = self.tracks.entry(weak.info.name.clone()) else { + let hash_map::Entry::Vacant(entry) = self.tracks.entry(weak.name().clone()) else { return Err(Error::Duplicate); }; entry.insert(weak); Ok(()) } - /// Drop every pending request, notifying all waiting subscribers with `err`. - fn abort_requests(&mut self, err: &Error) { - self.request_order.clear(); - for (_, pending) in self.requests.drain() { - fail_resolvers(pending.resolvers, err); + fn abort(&mut self, err: Error) -> Result<(), Error> { + if let Some(abort) = self.abort.take() { + return Err(abort); } - } - /// Drop a single named pending request, notifying its subscribers with `err`. - fn deny_request(&mut self, name: &str, err: Error) { - self.request_order.retain(|n| n != name); - if let Some(pending) = self.requests.remove(name) { - fail_resolvers(pending.resolvers, &err); + for (_, request) in self.requests.drain() { + request.reject(err.clone()); } + self.request_order.clear(); + self.abort = Some(err); + + Ok(()) } } @@ -122,52 +92,53 @@ impl State { /// or handle on-demand requests via [Self::dynamic]. #[derive(Clone)] pub struct BroadcastProducer { - info: Broadcast, - state: kio::Producer, -} - -impl Deref for BroadcastProducer { - type Target = Broadcast; - - fn deref(&self) -> &Self::Target { - &self.info - } + info: BroadcastInfo, + state: kio::Producer, } impl BroadcastProducer { - /// Create a producer for the given broadcast metadata. Prefer [`Broadcast::produce`]. - pub fn new(info: Broadcast) -> Self { + /// Create a producer for the given broadcast metadata. Prefer [`BroadcastInfo::produce`]. + pub fn new(info: BroadcastInfo) -> Self { Self { info, state: Default::default(), } } + pub fn info(&self) -> &BroadcastInfo { + &self.info + } + /// Insert a track into the lookup, returning an error on duplicate. /// /// Stores a weak handle to the track. The caller (or the owner of the /// track's [`TrackProducer`]) is responsible for keeping the track alive; /// when all producers are dropped, the entry becomes closed and is /// eventually evicted. - pub fn insert_track(&mut self, track: TrackSubscriber) -> Result<(), Error> { - let mut state = modify(&self.state)?; + pub fn insert_track(&mut self, track: TrackConsumer) -> Result<(), Error> { + let mut state = BroadcastState::modify(&self.state)?; state.insert_track(track.weak()) } /// Remove a track from the lookup. pub fn remove_track(&mut self, name: &str) -> Result<(), Error> { - let mut state = modify(&self.state)?; + let mut state = BroadcastState::modify(&self.state)?; state.tracks.remove(name).ok_or(Error::NotFound)?; Ok(()) } /// Produce a new track and insert it into the broadcast. /// - /// Accepts anything that converts into a [`Track`], so a bare name works: - /// `create_track("video")`. - pub fn create_track(&mut self, track: impl Into) -> Result { - let track = TrackProducer::new(track.into()); - let mut state = modify(&self.state)?; + /// Pass a name and an optional [`TrackInfo`], so a bare name works: + /// `create_track("video", None)`. + pub fn create_track( + &mut self, + name: impl Into>, + info: impl Into>, + ) -> Result { + let info = info.into().unwrap_or_default(); + let track = TrackProducer::new(name, info); + let mut state = BroadcastState::modify(&self.state)?; state.insert_track(track.weak())?; drop(state); Ok(track) @@ -177,9 +148,9 @@ impl BroadcastProducer { /// /// Generates names like `0{suffix}`, `1{suffix}`, etc. and picks the first /// one not already used in this broadcast. - pub fn unique_track(&mut self, suffix: &str) -> Result { + pub fn unique_track(&mut self, suffix: &str, info: impl Into>) -> Result { let name = self.unique_name(suffix); - self.create_track(Track::new(name)) + self.create_track(name, info) } /// Generate a unique track name from a suffix without creating the track. @@ -189,10 +160,10 @@ impl BroadcastProducer { /// `with_compress`) before handing the Track to [`Self::create_track`]. pub fn unique_name(&self, suffix: &str) -> String { let state = self.state.read(); - (0u32..) + (0u16..) .map(|i| format!("{i}{suffix}")) - .find(|name| !state.tracks.contains_key(name)) - .expect("u32 namespace exhausted") + .find(|name| !state.tracks.contains_key(name.as_str())) + .expect("u16 namespace exhausted; wow") } /// Create a dynamic producer that handles on-demand track requests from consumers. @@ -216,14 +187,7 @@ impl BroadcastProducer { /// by the broadcast and have no other producer to fulfill them, so they are /// aborted here. pub fn abort(&mut self, err: Error) -> Result<(), Error> { - let mut guard = modify(&self.state)?; - - // Wake any pending subscribers; nothing will ever serve their requests. - guard.abort_requests(&err); - - guard.abort = Some(err); - guard.close(); - Ok(()) + BroadcastState::modify(&self.state)?.abort(err) } /// Return true if this is the same broadcast instance. @@ -234,188 +198,16 @@ impl BroadcastProducer { #[cfg(test)] impl BroadcastProducer { - pub fn assert_create_track(&mut self, track: &Track) -> TrackProducer { - self.create_track(track.clone()).expect("should not have errored") - } - - pub fn assert_insert_track(&mut self, track: &TrackProducer) { - self.insert_track(track.subscribe(None)) - .expect("should not have errored") - } -} - -/// A subscription waiting to be served, handed out by [`BroadcastDynamic::requested_track`]. -/// -/// The publisher inspects [`Self::name`] (and optionally [`Self::subscription`]), -/// then either [`Self::accept`]s it with a concrete [`Track`], which resolves all -/// waiting subscribers, or [`Self::deny`]s it. Dropping without doing either -/// denies with [`Error::Cancel`]. -pub struct TrackRequest { - name: String, - subscription: Subscription, - state: kio::Weak, - /// Set once accepted or denied so [`Drop`] doesn't deny a second time. - completed: bool, -} - -impl TrackRequest { - /// The requested track name. - pub fn name(&self) -> &str { - &self.name - } - - /// The first waiting subscriber's preferences, as a hint for constructing the - /// [`Track`]. The full aggregate is available on the [`TrackProducer`] returned - /// by [`Self::accept`] via [`TrackProducer::subscription`]. - pub fn subscription(&self) -> &Subscription { - &self.subscription - } - - /// Serve the request with the given track, resolving every waiting subscriber. - /// - /// The track's name must match [`Self::name`]. Returns [`Error::NotFound`] on - /// mismatch, or the broadcast's abort error if it closed while pending. - pub fn accept(mut self, track: Track) -> Result { - if track.name != self.name { - return Err(Error::NotFound); - } - self.completed = true; - - let mut state = self - .state - .write() - .map_err(|r| r.abort.clone().unwrap_or(Error::Cancel))?; - - let pending = state.requests.remove(&self.name).ok_or(Error::Cancel)?; - state.request_order.retain(|n| n != &self.name); - - let producer = TrackProducer::new(track); - - // Insert a weak reference so future subscribers dedupe onto this producer. - state.tracks.insert(self.name.clone(), producer.weak()); - - // Hand each waiting subscriber a consumer carrying its own preferences. - // Building it here (not when the subscriber polls) means it counts toward - // the producer immediately, so a publisher checking `unused()` right after - // accept doesn't see zero consumers and tear the track down. - for (subscription, slot) in pending.resolvers { - if let Ok(mut slot) = slot.write() { - *slot = Some(Ok(producer.subscribe(subscription))); - } - } - - drop(state); - - // Evict the lookup entry once the producer closes. The producer owner is - // responsible for closing it (abort/finish/drop); waiting on close rather - // than unused lets a producer linger across transient consumer churn (relay - // subscription churn) without losing the lookup entry to an auto-cleanup race. - let weak = producer.weak(); - let cleanup = self.state.clone(); - web_async::spawn(async move { - weak.closed().await; - - let Some(producer) = cleanup.produce() else { - return; - }; - let Ok(mut state) = producer.write() else { - return; - }; - - // Remove the entry, but reinsert if it was replaced by a newer producer. - if let Some(current) = state.tracks.remove(&weak.info.name) - && !current.is_clone(&weak) - { - state.tracks.insert(current.info.name.clone(), current); - } - }); - - Ok(producer) - } - - /// Reject the request, waking all waiting subscribers with `err`. - pub fn deny(mut self, err: Error) { - self.completed = true; - if let Ok(mut state) = self.state.write() { - state.deny_request(&self.name, err); - } + pub fn assert_create_track( + &mut self, + name: impl Into>, + info: impl Into>, + ) -> TrackProducer { + self.create_track(name, info).expect("should not have errored") } -} -impl Drop for TrackRequest { - fn drop(&mut self) { - if !self.completed - && let Ok(mut state) = self.state.write() - { - state.deny_request(&self.name, Error::Cancel); - } - } -} - -/// A pending subscription returned by [`TrackConsumer::subscribe`]. -/// -/// The subscription isn't live until the publisher accepts it (for the wire, -/// SUBSCRIBE_OK). It implements [`Future`], so `.await` it to get the -/// [`TrackSubscriber`] (or an error). Poll-based callers can instead drive it -/// with [`Self::poll_ok`] inside a `kio` poll loop. -pub struct TrackPending { - inner: TrackPendingInner, - /// Kept alive between `Future::poll` calls so its registration in the - /// resolver channel stays valid until the next poll replaces it. - waiter: Option, -} - -enum TrackPendingInner { - /// Resolved synchronously: the track already existed, or it failed immediately. - Ready(Result), - /// Waiting for the publisher to accept or deny via the dynamic handler. - Waiting(kio::Consumer), -} - -impl TrackPending { - fn ready(result: Result) -> Self { - Self { - inner: TrackPendingInner::Ready(result), - waiter: None, - } - } - - fn waiting(consumer: kio::Consumer) -> Self { - Self { - inner: TrackPendingInner::Waiting(consumer), - waiter: None, - } - } - - /// Poll for the resolved [`TrackSubscriber`], without blocking. - pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll> { - match &self.inner { - TrackPendingInner::Ready(result) => Poll::Ready(result.clone()), - TrackPendingInner::Waiting(consumer) => match consumer.poll(waiter, |slot| match &**slot { - Some(result) => Poll::Ready(result.clone()), - None => Poll::Pending, - }) { - Poll::Ready(Ok(result)) => Poll::Ready(result), - // Channel closed: the resolver may have left the final result behind. - Poll::Ready(Err(closed)) => Poll::Ready(match &*closed { - Some(result) => result.clone(), - None => Err(Error::Cancel), - }), - Poll::Pending => Poll::Pending, - }, - } - } -} - -impl std::future::Future for TrackPending { - type Output = Result; - - fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let this = self.get_mut(); - // Replacing drops the previous waiter, freeing its slot so poll_ok's - // register call can recycle it (see `kio::wait`). - this.waiter = Some(kio::Waiter::new(cx.waker().clone())); - this.poll_ok(this.waiter.as_ref().unwrap()) + pub fn assert_insert_track(&mut self, track: TrackConsumer) { + self.insert_track(track).expect("should not have errored") } } @@ -423,12 +215,12 @@ impl std::future::Future for TrackPending { /// /// When a consumer requests a track that doesn't exist, the dynamic producer /// picks up the request via [`Self::requested_track`] and either -/// [`TrackRequest::accept`]s it with a concrete [`Track`] or -/// [`TrackRequest::deny`]s it. Dropped when no longer needed; pending requests +/// [`TrackRequest::accept`]s it with a concrete [`TrackInfo`] or +/// [`TrackRequest::reject`]s it. Dropped when no longer needed; pending requests /// are automatically aborted. pub struct BroadcastDynamic { - info: Broadcast, - state: kio::Producer, + info: BroadcastInfo, + state: kio::Producer, } impl Clone for BroadcastDynamic { @@ -436,7 +228,7 @@ impl Clone for BroadcastDynamic { // Mirror `new`: bump `state.dynamic` so each live handle is counted. // Without this, deriving Clone would let `Drop` decrement past `new`'s // single increment and prematurely flip `dynamic` to zero, causing - // future `consume_track` calls to return `NotFound`. + // future `track` calls to return `NotFound`. if let Ok(mut state) = self.state.write() { state.dynamic += 1; } @@ -448,16 +240,8 @@ impl Clone for BroadcastDynamic { } } -impl Deref for BroadcastDynamic { - type Target = Broadcast; - - fn deref(&self) -> &Self::Target { - &self.info - } -} - impl BroadcastDynamic { - fn new(info: Broadcast, state: kio::Producer) -> Self { + fn new(info: BroadcastInfo, state: kio::Producer) -> Self { if let Ok(mut state) = state.write() { // If the broadcast is already closed, we can't handle any new requests. state.dynamic += 1; @@ -466,10 +250,14 @@ impl BroadcastDynamic { Self { info, state } } + pub fn info(&self) -> &BroadcastInfo { + &self.info + } + // A helper to automatically apply Dropped if the state is closed without an error. fn poll(&self, waiter: &kio::Waiter, f: F) -> Poll> where - F: FnMut(&mut kio::Mut<'_, State>) -> Poll, + F: FnMut(&mut kio::Mut<'_, BroadcastState>) -> Poll, { Poll::Ready(match ready!(self.state.poll(waiter, f)) { Ok(r) => Ok(r), @@ -479,24 +267,17 @@ impl BroadcastDynamic { /// Poll for the next consumer-requested track, without blocking. pub fn poll_requested_track(&mut self, waiter: &kio::Waiter) -> Poll> { - let weak = self.state.weak(); self.poll(waiter, |state| { let Some(name) = state.request_order.pop_front() else { return Poll::Pending; }; + // The name stays in `requests` so concurrent subscribers can still // coalesce onto it until the publisher accepts or denies. - let pending = state.requests.get(&name).expect("request_order out of sync"); - let subscription = pending.resolvers.first().map(|(s, _)| s.clone()).unwrap_or_default(); - Poll::Ready((name, subscription)) - }) - .map(|res| { - res.map(|(name, subscription)| TrackRequest { - name, - subscription, - state: weak, - completed: false, - }) + let pending = state.requests.remove(&name).expect("request_order out of sync"); + state.tracks.insert(name, pending.weak()); + + Poll::Ready(pending) }) } @@ -526,14 +307,7 @@ impl BroadcastDynamic { /// requests are owned by the broadcast and aborted here so consumers don't /// stay stuck waiting on producers nobody will fulfill. pub fn abort(&mut self, err: Error) -> Result<(), Error> { - let mut guard = modify(&self.state)?; - - // Wake any pending subscribers; nothing will ever serve their requests. - guard.abort_requests(&err); - - guard.abort = Some(err); - guard.close(); - Ok(()) + BroadcastState::modify(&self.state)?.abort(err) } /// Return true if this is the same broadcast instance. @@ -551,8 +325,7 @@ impl Drop for BroadcastDynamic { return; } - // No dynamic producer remains to serve pending requests. - state.abort_requests(&Error::Cancel); + state.abort(Error::Dropped).ok(); } } } @@ -577,79 +350,51 @@ impl BroadcastDynamic { /// Subscribe to arbitrary broadcast/tracks. #[derive(Clone)] pub struct BroadcastConsumer { - info: Broadcast, - state: kio::Consumer, + info: BroadcastInfo, + state: kio::Consumer, } -impl Deref for BroadcastConsumer { - type Target = Broadcast; - - fn deref(&self) -> &Self::Target { +impl BroadcastConsumer { + pub fn info(&self) -> &BroadcastInfo { &self.info } -} -impl BroadcastConsumer { /// Get a handle to a track on this broadcast. - /// - /// This is a cheap, synchronous lookup that returns a [`TrackConsumer`] bound - /// to `name`. Nothing is sent to the publisher yet: call - /// [`TrackConsumer::subscribe`] to open a live subscription (blocking on - /// SUBSCRIBE_OK), or hold the handle and subscribe later. - pub fn consume_track(&self, name: &str) -> TrackConsumer { - TrackConsumer { - broadcast: self.clone(), - name: name.to_string(), - } - } - - /// Register a subscription for `name` and return a [`TrackPending`] that - /// resolves once the publisher accepts it. - /// - /// Reuses a live producer if one is already publishing the track (the pending - /// resolves right away), otherwise queues a dynamic request served via - /// [`BroadcastDynamic::requested_track`] and [`TrackRequest::accept`] (for the - /// wire this is SUBSCRIBE_OK). Resolves to [`Error::NotFound`] if no dynamic - /// producer exists to handle the request. - fn request_subscribe(&self, name: &str, subscription: Subscription) -> TrackPending { + pub fn track(&self, name: &str) -> Result { // Upgrade to a temporary producer so we can modify the state. - let Some(producer) = self.state.produce() else { - let err = self.state.read().abort.clone().unwrap_or(Error::Dropped); - return TrackPending::ready(Err(err)); - }; - let mut state = match modify(&producer) { + let mut state = match self.state.write() { Ok(state) => state, - Err(err) => return TrackPending::ready(Err(err)), + Err(state) => return Err(state.abort.clone().unwrap_or(Error::Dropped)), }; // Reuse a live producer if one is already publishing the track. if let Some(weak) = state.tracks.get(name) { if !weak.is_closed() { - return TrackPending::ready(Ok(weak.subscribe(subscription))); + return Ok(weak.consume()); } // Drop the stale entry and fall through to a fresh request. state.tracks.remove(name); } - let slot = kio::Producer::new(None); - let consumer = slot.consume(); - if let Some(pending) = state.requests.get_mut(name) { // Coalesce onto an in-flight request for the same name. - pending.resolvers.push((subscription, slot)); - } else if state.dynamic == 0 { - return TrackPending::ready(Err(Error::NotFound)); - } else { - state.requests.insert( - name.to_string(), - PendingRequest { - resolvers: vec![(subscription, slot)], - }, - ); - state.request_order.push_back(name.to_string()); + return Ok(pending.consume()); } - TrackPending::waiting(consumer) + if state.dynamic == 0 { + return Err(Error::NotFound); + } + + // Allocate the name once and share the same Arc across the request, the + // requests map, and the FIFO order. + let name: Arc = name.into(); + let request = TrackRequest::new(name.clone()); + let consumer = request.consume(); + + state.requests.insert(name.clone(), request); + state.request_order.push_back(name); + + Ok(consumer) } /// Block until the broadcast is closed and return the cause. @@ -681,42 +426,6 @@ impl BroadcastConsumer { } } -/// A handle to a single track within a broadcast. -/// -/// Obtained from [`BroadcastConsumer::consume_track`]. Holding it sends nothing -/// to the publisher; it just names a track you can [`subscribe`](Self::subscribe) -/// to (a live, ongoing stream of groups) later. The same handle can be subscribed -/// to multiple times, and clones are cheap. -// TODO: add `fetch` for one-shot retrieval of a past group range. -#[derive(Clone)] -pub struct TrackConsumer { - broadcast: BroadcastConsumer, - name: String, -} - -impl TrackConsumer { - /// The track name this handle is bound to. - pub fn name(&self) -> &str { - &self.name - } - - /// Open a live subscription. - /// - /// Returns a [`TrackPending`] that resolves once the publisher accepts the - /// subscription (SUBSCRIBE_OK on the wire). `.await` it for the - /// [`TrackSubscriber`], which carries the publisher's [`Track`] and reads its - /// groups; or drive it with [`TrackPending::poll_ok`] from a poll loop. - /// - /// `subscription` is this subscriber's preferences and feeds the producer's - /// [`TrackProducer::subscription`] aggregate; pass `None` for - /// [`Subscription::default`]. Concurrent subscribers to the same name coalesce - /// onto one request. - pub fn subscribe(&self, subscription: impl Into>) -> TrackPending { - self.broadcast - .request_subscribe(&self.name, subscription.into().unwrap_or_default()) - } -} - #[cfg(test)] impl BroadcastConsumer { pub fn assert_not_closed(&self) { @@ -733,13 +442,13 @@ mod test { use super::*; /// Subscribe and assert the result hasn't resolved yet (it stays pending until - /// a publisher accepts). Returns the [`TrackPending`] to resolve after accepting. + /// a publisher accepts). Returns the [`TrackSubscriberPending`] to resolve after accepting. macro_rules! subscribe_pending { ($consumer:expr, $name:expr) => {{ - let pending = $consumer.consume_track($name).subscribe(None); + let pending = $consumer.track($name).unwrap().subscribe(None).unwrap(); assert!( pending.poll_ok(&kio::Waiter::noop()).is_pending(), - "consume_track should stay pending until the request is accepted" + "subscribe should stay pending until the request is accepted" ); pending }}; @@ -747,24 +456,36 @@ mod test { #[tokio::test] async fn insert() { - let mut producer = Broadcast::new().produce(); - let mut track1 = Track::new("track1").produce(); + let mut producer = BroadcastInfo::new().produce(); + let mut track1 = TrackProducer::new("track1", None); // Make sure we can insert before a consumer is created. - producer.assert_insert_track(&track1); + producer.assert_insert_track(track1.consume()); track1.append_group().unwrap(); let consumer = producer.consume(); // The track already exists, so subscribe resolves immediately. - let mut track1_sub = consumer.consume_track("track1").subscribe(None).await.unwrap(); + let mut track1_sub = consumer + .track("track1") + .unwrap() + .subscribe(None) + .unwrap() + .await + .unwrap(); track1_sub.assert_group(); - let mut track2 = Track::new("track2").produce(); - producer.assert_insert_track(&track2); + let mut track2 = TrackProducer::new("track2", None); + producer.assert_insert_track(track2.consume()); let consumer2 = producer.consume(); - let mut track2_consumer = consumer2.consume_track("track2").subscribe(None).await.unwrap(); + let mut track2_consumer = consumer2 + .track("track2") + .unwrap() + .subscribe(None) + .unwrap() + .await + .unwrap(); track2_consumer.assert_no_group(); track2.append_group().unwrap(); @@ -774,15 +495,21 @@ mod test { #[tokio::test] async fn closed() { - let mut producer = Broadcast::new().produce(); + let mut producer = BroadcastInfo::new().produce(); let _dynamic = producer.dynamic(); let consumer = producer.consume(); consumer.assert_not_closed(); // Create a new track and insert it into the broadcast (resolves immediately). - let track1 = producer.assert_create_track(&Track::new("track1")); - let track1c = consumer.consume_track("track1").subscribe(None).await.unwrap(); + let track1 = producer.assert_create_track("track1", None); + let track1c = consumer + .track("track1") + .unwrap() + .subscribe(None) + .unwrap() + .await + .unwrap(); // A track nobody publishes stays pending until accepted. let track2_fut = subscribe_pending!(consumer, "track2"); @@ -800,7 +527,7 @@ mod test { #[tokio::test] async fn requests() { - let mut producer = Broadcast::new().produce().dynamic(); + let mut producer = BroadcastInfo::new().produce().dynamic(); let consumer = producer.consume(); let consumer2 = consumer.clone(); @@ -815,7 +542,7 @@ mod test { assert_eq!(request.name(), "track1"); // Accept it, which resolves both waiting subscribers. - let mut track3 = request.accept(Track::new("track1")).unwrap(); + let mut track3 = request.accept(None); let mut track1 = track1_fut.await.unwrap(); let mut track2 = track2_fut.await.unwrap(); @@ -833,19 +560,19 @@ mod test { drop(producer); assert!(track4_fut.await.is_err()); - // With no dynamic producer left, new subscribes fail outright. - let track5 = consumer2.consume_track("track3").subscribe(None).await; + // With no dynamic producer left, requesting the handle fails outright. + let track5 = consumer2.track("track3"); assert!(track5.is_err(), "should have errored"); } #[tokio::test] async fn stale_producer() { - let mut broadcast = Broadcast::new().produce().dynamic(); + let mut broadcast = BroadcastInfo::new().produce().dynamic(); let consumer = broadcast.consume(); // Subscribe to a track and serve it. let track1_fut = subscribe_pending!(consumer, "track1"); - let mut producer1 = broadcast.assert_request().accept(Track::new("track1")).unwrap(); + let mut producer1 = broadcast.assert_request().accept(None); let track1 = track1_fut.await.unwrap(); // Close the producer (simulating publisher disconnect). @@ -858,7 +585,7 @@ mod test { // Subscribe again to the same track: should get a NEW producer, not the stale one. let track2_fut = subscribe_pending!(consumer, "track1"); - let mut producer2 = broadcast.assert_request().accept(Track::new("track1")).unwrap(); + let mut producer2 = broadcast.assert_request().accept(None); let mut track2 = track2_fut.await.unwrap(); track2.assert_not_closed(); track2.assert_not_clone(&track1); @@ -870,12 +597,12 @@ mod test { #[tokio::test(start_paused = true)] async fn requested_unused() { - let mut broadcast = Broadcast::new().produce().dynamic(); + let mut broadcast = BroadcastInfo::new().produce().dynamic(); let bc = broadcast.consume(); // Subscribe to a track that doesn't exist yet, then serve it. let c1_fut = subscribe_pending!(bc, "unknown_track"); - let mut producer1 = broadcast.assert_request().accept(Track::new("unknown_track")).unwrap(); + let mut producer1 = broadcast.assert_request().accept(None); let consumer1 = c1_fut.await.unwrap(); // The producer should NOT be unused yet because there's a consumer. @@ -885,7 +612,13 @@ mod test { ); // A second subscriber reuses the live producer (fast path / dedup). - let consumer2 = bc.consume_track("unknown_track").subscribe(None).await.unwrap(); + let consumer2 = bc + .track("unknown_track") + .unwrap() + .subscribe(None) + .unwrap() + .await + .unwrap(); consumer2.assert_is_clone(&consumer1); drop(consumer1); @@ -903,18 +636,23 @@ mod test { // While the producer is still alive, re-subscribing to the same name reuses // it (no new request) — this is what lets the relay linger upstream // subscriptions across transient consumer churn. - let consumer3 = bc.consume_track("unknown_track").subscribe(None).await.unwrap(); + let consumer3 = bc + .track("unknown_track") + .unwrap() + .subscribe(None) + .unwrap() + .await + .unwrap(); consumer3.assert_is_clone(&producer1.subscribe(None)); broadcast.assert_no_request(); drop(consumer3); - // Aborting the producer triggers cleanup; the next subscribe creates a fresh request. + // Aborting the producer closes its lookup entry; the next subscribe sees the + // stale weak, evicts it, and creates a fresh request. producer1.abort(Error::Cancel).unwrap(); - // Yield (paused time) so the spawned cleanup task evicts the lookup entry. - tokio::time::advance(std::time::Duration::from_millis(1)).await; let c4_fut = subscribe_pending!(bc, "unknown_track"); - let producer2 = broadcast.assert_request().accept(Track::new("unknown_track")).unwrap(); + let producer2 = broadcast.assert_request().accept(None); let consumer4 = c4_fut.await.unwrap(); drop(consumer4); assert!( @@ -927,10 +665,10 @@ mod test { // `state.dynamic` to zero. The relay's lite subscriber clones the // dynamic per spawned subscribe; if Clone skipped the increment, the // first finished subscribe would tear down the broadcast and any - // follow-up `consume_track` would return `NotFound`. + // follow-up `track` would return `NotFound`. #[tokio::test] async fn dynamic_clone_keeps_alive() { - let broadcast = Broadcast::new().produce().dynamic(); + let broadcast = BroadcastInfo::new().produce().dynamic(); let consumer = broadcast.consume(); let clone = broadcast.clone(); diff --git a/rs/moq-net/src/model/frame.rs b/rs/moq-net/src/model/frame.rs index cc5505d487..ea056e4372 100644 --- a/rs/moq-net/src/model/frame.rs +++ b/rs/moq-net/src/model/frame.rs @@ -32,7 +32,7 @@ pub struct Frame { /// `None` means no timestamp is attached to this frame, which is the case for /// pre-Lite05 moq-lite streams and IETF moq-transport streams that haven't /// negotiated a timescale. On Lite05+, producers must set `Some(ts)` whose - /// scale matches the track's [`crate::Track::timescale`]; the publisher + /// scale matches the track's [`crate::TrackInfo::timescale`]; the publisher /// surfaces a `ProtocolViolation` otherwise. pub timestamp: Option, } diff --git a/rs/moq-net/src/model/mod.rs b/rs/moq-net/src/model/mod.rs index 1ffbdfef0e..27258d847a 100644 --- a/rs/moq-net/src/model/mod.rs +++ b/rs/moq-net/src/model/mod.rs @@ -4,6 +4,7 @@ mod compression; mod frame; mod group; mod origin; +mod subscription; mod time; mod track; @@ -13,5 +14,6 @@ pub use compression::*; pub use frame::*; pub use group::*; pub use origin::*; +pub use subscription::*; pub use time::*; pub use track::*; diff --git a/rs/moq-net/src/model/origin.rs b/rs/moq-net/src/model/origin.rs index 5e93815627..02ee29ee21 100644 --- a/rs/moq-net/src/model/origin.rs +++ b/rs/moq-net/src/model/origin.rs @@ -10,7 +10,7 @@ use web_async::Lock; use super::BroadcastConsumer; use crate::{ - AsPath, Broadcast, BroadcastProducer, Path, PathOwned, PathPrefixes, + AsPath, BroadcastInfo, BroadcastProducer, Path, PathOwned, PathPrefixes, coding::{Decode, DecodeError, Encode, EncodeError}, }; @@ -483,7 +483,7 @@ impl OriginNode { return; } - if route_key(&full, &broadcast.hops) < route_key(&full, &existing.active.hops) { + if route_key(&full, &broadcast.info().hops) < route_key(&full, &existing.active.info().hops) { let old = existing.active.clone(); existing.active = broadcast.clone(); existing.backup.push_back(old); @@ -577,7 +577,7 @@ impl OriginNode { .backup .iter() .enumerate() - .min_by_key(|(_, b)| route_key(&full, &b.hops)) + .min_by_key(|(_, b)| route_key(&full, &b.info().hops)) .map(|(i, _)| i); if let Some(idx) = best { let active = entry.backup.remove(idx).expect("index in range"); @@ -753,7 +753,7 @@ impl OriginProducer { /// This is a helper method when you only want to publish a broadcast to a single origin. /// Returns [None] if the broadcast is not allowed to be published. pub fn create_broadcast(&self, path: impl AsPath) -> Option { - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); self.publish_broadcast(path, broadcast.consume()).then_some(broadcast) } @@ -772,7 +772,7 @@ impl OriginProducer { let path = path.as_path(); // Loop detection: refuse broadcasts whose hop chain already contains our id. - if broadcast.hops.contains(&self.info) { + if broadcast.info().hops.contains(&self.info) { return false; } @@ -1175,7 +1175,7 @@ impl AnnounceConsumer { #[cfg(test)] mod tests { - use crate::Broadcast; + use crate::BroadcastInfo; use super::*; @@ -1203,8 +1203,8 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); let mut consumer1 = origin.consume().announced(); // Make a new consumer that should get it. @@ -1270,9 +1270,9 @@ mod tests { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); let consumer1 = broadcast1.consume(); let consumer2 = broadcast2.consume(); @@ -1327,11 +1327,11 @@ mod tests { // single atomic restart. let origin = Origin::random().produce(); // `a` carries one hop; `b` has none, so `b` wins the route and replaces it. - let a = Broadcast { + let a = BroadcastInfo { hops: OriginList::try_from(vec![Origin::from(1u64)]).unwrap(), } .produce(); - let b = Broadcast::new().produce(); + let b = BroadcastInfo::new().produce(); let mut announced = origin.consume().announced(); origin.publish_broadcast("test", a.consume()); @@ -1348,11 +1348,11 @@ mod tests { // just swaps in the newer broadcast and is still delivered as a fresh Active. let origin = Origin::random().produce(); // `a` carries one hop; `b` has none, so `b` wins the route and replaces it. - let a = Broadcast { + let a = BroadcastInfo { hops: OriginList::try_from(vec![Origin::from(1u64)]).unwrap(), } .produce(); - let b = Broadcast::new().produce(); + let b = BroadcastInfo::new().produce(); let mut announced = origin.consume().announced(); origin.publish_broadcast("test", a.consume()); @@ -1367,8 +1367,8 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast1.consume()); origin.publish_broadcast("test", broadcast2.consume()); @@ -1395,7 +1395,7 @@ mod tests { // Build a broadcast carrying a specific hop chain. fn route(ids: &[u64]) -> BroadcastProducer { let hops = OriginList::try_from(ids.iter().copied().map(Origin::from).collect::>()).unwrap(); - Broadcast { hops }.produce() + BroadcastInfo { hops }.produce() } // Resolve the active route for "test" after publishing both routes in the given order. @@ -1405,7 +1405,7 @@ mod tests { let b = route(second); origin.publish_broadcast("test", a.consume()); origin.publish_broadcast("test", b.consume()); - let hops = origin.consume().get_broadcast("test").unwrap().hops.clone(); + let hops = origin.consume().get_broadcast("test").unwrap().info().hops.clone(); // Keep the producers alive until after we read the active route. drop((a, b)); hops @@ -1427,7 +1427,7 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Ensure it doesn't crash. origin.publish_broadcast("test", broadcast.consume()); @@ -1448,7 +1448,7 @@ mod tests { #[tokio::test] async fn test_many_announces() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); let mut consumer = origin.consume().announced(); for i in 0..256 { @@ -1464,7 +1464,7 @@ mod tests { #[tokio::test] async fn test_many_announces_try() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); let mut consumer = origin.consume().announced(); for i in 0..256 { @@ -1479,7 +1479,7 @@ mod tests { #[tokio::test] async fn test_with_root_basic() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Create a producer with root "/foo" let foo_producer = origin.with_root("foo").expect("should create root"); @@ -1500,7 +1500,7 @@ mod tests { #[tokio::test] async fn test_with_root_nested() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Create nested roots let foo_producer = origin.with_root("foo").expect("should create foo root"); @@ -1522,7 +1522,7 @@ mod tests { #[tokio::test] async fn test_publish_scope_allows() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Create a producer that can only publish to "allowed" paths let limited_producer = origin @@ -1551,9 +1551,9 @@ mod tests { #[tokio::test] async fn test_consume_scope_filters() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); let mut consumer = origin.consume().announced(); @@ -1583,9 +1583,9 @@ mod tests { #[tokio::test] async fn test_consume_scope_multiple_prefixes() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); origin.publish_broadcast("foo/test", broadcast1.consume()); origin.publish_broadcast("bar/test", broadcast2.consume()); @@ -1607,7 +1607,7 @@ mod tests { #[tokio::test] async fn test_with_root_and_publish_scope() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // User connects to /foo root let foo_producer = origin.with_root("foo").expect("should create foo root"); @@ -1640,9 +1640,9 @@ mod tests { #[tokio::test] async fn test_with_root_and_consume_scope() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); // Publish broadcasts origin.publish_broadcast("foo/bar/test", broadcast1.consume()); @@ -1687,7 +1687,7 @@ mod tests { #[tokio::test] async fn test_wildcard_permission() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Producer with root access (empty string means wildcard) let root_producer = origin.clone(); @@ -1704,8 +1704,8 @@ mod tests { #[tokio::test] async fn test_consume_broadcast_with_permissions() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); origin.publish_broadcast("allowed/test", broadcast1.consume()); origin.publish_broadcast("notallowed/test", broadcast2.consume()); @@ -1733,7 +1733,7 @@ mod tests { #[tokio::test] async fn test_nested_paths_with_permissions() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Create producer limited to "a/b/c" let limited_producer = origin.scope(&["a/b/c".into()]).expect("should create limited producer"); @@ -1752,9 +1752,9 @@ mod tests { #[tokio::test] async fn test_multiple_consumers_with_different_permissions() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); // Publish to different paths origin.publish_broadcast("foo/test", broadcast1.consume()); @@ -1795,8 +1795,8 @@ mod tests { #[tokio::test] async fn test_select_with_empty_prefix() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); // User with root "demo" allowed to subscribe to "worm-node" and "foobar" let demo_producer = origin.with_root("demo").expect("should create demo root"); @@ -1828,9 +1828,9 @@ mod tests { #[tokio::test] async fn test_select_narrowing_scope() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); // User with root "demo" allowed to subscribe to "worm-node" and "foobar" let demo_producer = origin.with_root("demo").expect("should create demo root"); @@ -1869,9 +1869,9 @@ mod tests { #[tokio::test] async fn test_select_multiple_roots_with_empty_prefix() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); // Producer with multiple allowed roots let limited_producer = origin @@ -1900,7 +1900,7 @@ mod tests { #[tokio::test] async fn test_publish_scope_with_empty_prefix() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Producer with specific allowed paths let limited_producer = origin @@ -1922,9 +1922,9 @@ mod tests { #[tokio::test] async fn test_select_narrowing_to_deeper_path() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); - let broadcast3 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); + let broadcast3 = BroadcastInfo::new().produce(); // Producer with broad permission let limited_producer = origin.scope(&["org".into()]).expect("should create limited producer"); @@ -2025,8 +2025,8 @@ mod tests { #[tokio::test] async fn test_select_maintains_access_with_wider_prefix() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); // Setup: user with root "demo" allowed to subscribe to specific paths let demo_producer = origin.with_root("demo").expect("should create demo root"); @@ -2068,7 +2068,7 @@ mod tests { #[tokio::test] async fn test_duplicate_prefixes_deduped() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // scope with duplicate prefixes should work (deduped internally) let producer = origin @@ -2085,7 +2085,7 @@ mod tests { #[tokio::test] async fn test_overlapping_prefixes_deduped() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // "demo" and "demo/foo" — "demo/foo" is redundant, only "demo" should remain let producer = origin @@ -2103,7 +2103,7 @@ mod tests { #[tokio::test] async fn test_overlapping_prefixes_no_duplicate_announcements() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); // Both "demo" and "demo/foo" are requested — should only have one node let producer = origin @@ -2133,7 +2133,7 @@ mod tests { #[tokio::test] async fn test_announced_broadcast_already_announced() { let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast.consume()); @@ -2147,7 +2147,7 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); let consumer = origin.consume(); @@ -2171,8 +2171,8 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let other = Broadcast::new().produce(); - let target = Broadcast::new().produce(); + let other = BroadcastInfo::new().produce(); + let target = BroadcastInfo::new().produce(); let consumer = origin.consume(); @@ -2198,8 +2198,8 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let nested = Broadcast::new().produce(); - let exact = Broadcast::new().produce(); + let nested = BroadcastInfo::new().produce(); + let exact = BroadcastInfo::new().produce(); let consumer = origin.consume(); @@ -2261,7 +2261,7 @@ mod tests { let origin = Origin::random().produce(); let mut announced = origin.consume().announced(); - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast.consume()); drop(broadcast); @@ -2279,8 +2279,8 @@ mod tests { let origin = Origin::random().produce(); let mut announced = origin.consume().announced(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast1.consume()); drop(broadcast1); @@ -2298,7 +2298,7 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast1.consume()); let mut announced = origin.consume().announced(); @@ -2308,7 +2308,7 @@ mod tests { drop(broadcast1); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; - let broadcast2 = Broadcast::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast2.consume()); // The cursor must see the unannounce before the new announce. @@ -2324,7 +2324,7 @@ mod tests { tokio::time::pause(); let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast1.consume()); let mut announced = origin.consume().announced(); @@ -2333,7 +2333,7 @@ mod tests { drop(broadcast1); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; - let broadcast2 = Broadcast::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast2.consume()); drop(broadcast2); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; @@ -2354,7 +2354,7 @@ mod tests { let mut announced = origin.consume().announced(); for _ in 0..1000 { - let broadcast = Broadcast::new().produce(); + let broadcast = BroadcastInfo::new().produce(); origin.publish_broadcast("test", broadcast.consume()); drop(broadcast); } @@ -2381,8 +2381,8 @@ mod tests { #[tokio::test] async fn test_consumer_clone_is_side_effect_free() { let origin = Origin::random().produce(); - let broadcast1 = Broadcast::new().produce(); - let broadcast2 = Broadcast::new().produce(); + let broadcast1 = BroadcastInfo::new().produce(); + let broadcast2 = BroadcastInfo::new().produce(); origin.publish_broadcast("test1", broadcast1.consume()); origin.publish_broadcast("test2", broadcast2.consume()); diff --git a/rs/moq-net/src/model/subscription.rs b/rs/moq-net/src/model/subscription.rs new file mode 100644 index 0000000000..1dff23cc99 --- /dev/null +++ b/rs/moq-net/src/model/subscription.rs @@ -0,0 +1,90 @@ +use std::{task::Poll, time::Duration}; + +/// Subscriber-side preferences for receiving a track. +/// +/// Each subscriber holds its own [`Subscription`]; the publisher observes an +/// aggregate across all live subscribers via [`crate::TrackProducer::subscription`]. +/// A subscriber can change its preferences after the fact with +/// [`crate::TrackSubscriber::update`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Subscription { + /// Delivery priority. Higher values preempt lower ones when bandwidth is constrained. + pub priority: u8, + /// Whether groups should be delivered in sequence order. + pub ordered: bool, + /// How long to wait for a group before skipping it once a newer group has + /// arrived. `Duration::ZERO` skips immediately (e.g. group 8 arriving means + /// group 7 is skipped); a larger value tolerates that much reordering before + /// giving up on the older group. + pub stale: Duration, + /// First group to deliver, or `None` to start at the latest group. + pub group_start: Option, + /// Last group to deliver (inclusive), or `None` for no end. + pub group_end: Option, +} + +impl Default for Subscription { + fn default() -> Self { + Self { + priority: 0, + ordered: true, + stale: Duration::ZERO, + group_start: None, + group_end: None, + } + } +} + +impl Subscription { + /// Set the delivery priority, returning `self` for chaining. + pub fn with_priority(mut self, priority: u8) -> Self { + self.priority = priority; + self + } + + /// Set whether groups are delivered in sequence order, returning `self` for chaining. + pub fn with_ordered(mut self, ordered: bool) -> Self { + self.ordered = ordered; + self + } + + /// Set how long to wait for a group before skipping it, returning `self` for chaining. + pub fn with_stale(mut self, stale: Duration) -> Self { + self.stale = stale; + self + } + + /// Set the first group to deliver, returning `self` for chaining. + pub fn with_group_start(mut self, group_start: impl Into>) -> Self { + self.group_start = group_start.into(); + self + } + + /// Set the last group to deliver (inclusive), returning `self` for chaining. + pub fn with_group_end(mut self, group_end: impl Into>) -> Self { + self.group_end = group_end.into(); + self + } + + // Returns Ready with the new combined subscription, unless this subscription is a subset. + // TODO I don't know if we need to return Pending at all? I'm kind of confused. + pub(super) fn poll_combined(&self, combined: &Option) -> Poll { + let Some(combined) = combined else { + return Poll::Ready(self.clone()); + }; + + let merged = Subscription { + priority: self.priority.max(combined.priority), + ordered: !self.ordered || !combined.ordered, + stale: self.stale.max(combined.stale), + group_start: self.group_start.min(combined.group_start), + group_end: self.group_end.max(combined.group_end), + }; + + if &merged != combined { + return Poll::Ready(merged); + } + + Poll::Pending + } +} diff --git a/rs/moq-net/src/model/track.rs b/rs/moq-net/src/model/track.rs index 4c2f6588c4..8e0bb676e1 100644 --- a/rs/moq-net/src/model/track.rs +++ b/rs/moq-net/src/model/track.rs @@ -8,35 +8,35 @@ //! A [TrackSubscriber] may not receive all streams in order or at all. //! These streams are meant to be transmitted over congested networks and the key to MoQ Transport is to not block on them. //! Streams will be cached for a potentially limited duration added to the unreliable nature. -//! A cloned [TrackSubscriber] will receive a copy of all new streams going forward (fanout). +//! A [TrackConsumer] is a cheap, cloneable handle; subscribing it multiple times fans the same +//! cached streams out to each independent [TrackSubscriber]. //! //! The track is closed with [Error] when all writers or readers are dropped. -use crate::{Error, Result, Timescale, coding}; +use crate::{Error, Result, Subscription, Timescale, coding}; use super::{Group, GroupConsumer, GroupProducer}; use std::{ collections::{HashSet, VecDeque}, - sync::{Arc, Mutex, Weak}, - task::{Poll, ready}, + pin::Pin, + sync::Arc, + task::{Context, Poll, ready}, time::Duration, }; -/// Default [`Track::cache`] age when the publisher doesn't set one. +/// Default [`TrackInfo::cache`] age when the publisher doesn't set one. pub const DEFAULT_CACHE: Duration = Duration::from_secs(5); /// Publisher-side properties of a track. /// /// These are fixed by the publisher when the track is created and don't change /// while the track is alive. A subscriber learns them via -/// [`BroadcastConsumer::consume_track`](crate::BroadcastConsumer::consume_track), -/// which returns the publisher's [`Track`] once the subscription is accepted. +/// [`crate::BroadcastConsumer::track`](crate::BroadcastConsumer::track), +/// which returns the publisher's [`TrackInfo`] once the subscription is accepted. #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Track { - /// Identifier within a broadcast. Unique per [`crate::Broadcast`]. - pub name: String, +pub struct TrackInfo { /// Hint that this track's frames are worth compressing (e.g. a JSON catalog). /// The publisher honors it by negotiating a codec in SUBSCRIBE_OK; codec-less /// peers (older drafts) ignore it and send frames verbatim. @@ -67,12 +67,6 @@ pub struct Track { pub cache: Duration, } -impl Default for Track { - fn default() -> Self { - Self::new(String::new()) - } -} - #[cfg(feature = "serde")] fn default_cache() -> Duration { DEFAULT_CACHE @@ -83,7 +77,7 @@ fn is_default_cache(cache: &Duration) -> bool { *cache == DEFAULT_CACHE } -/// Serialize [`Track::cache`] as a bare integer of milliseconds, matching the +/// Serialize [`TrackInfo::cache`] as a bare integer of milliseconds, matching the /// catalog's other durations (and the wire), rather than serde's `{secs, nanos}`. #[cfg(feature = "serde")] mod cache_millis { @@ -98,18 +92,17 @@ mod cache_millis { Ok(Duration::from_millis(ms)) } } - -impl Track { - /// Create a track with the given name. - pub fn new>(name: T) -> Self { +impl Default for TrackInfo { + fn default() -> Self { Self { - name: name.into(), compress: false, timescale: None, cache: DEFAULT_CACHE, } } +} +impl TrackInfo { /// Mark this track's frames as worth compressing, returning `self` for chaining. pub fn with_compress(mut self, compress: bool) -> Self { self.compress = compress; @@ -136,108 +129,44 @@ impl Track { fn clamp_stale(&self, stale: Duration) -> Duration { stale.min(self.cache) } - - /// Consume this [`Track`] to create a producer that owns its metadata. - pub fn produce(self) -> TrackProducer { - TrackProducer::new(self) - } } -impl From<&str> for Track { - fn from(name: &str) -> Self { - Track::new(name) - } -} - -impl From for Track { - fn from(name: String) -> Self { - Track::new(name) - } -} +#[derive(Default)] +struct TrackState { + // The info for the track; always Some for TrackSubscriber/TrackProducer. + info: Option, -/// Subscriber-side preferences for receiving a track. -/// -/// Each subscriber holds its own [`Subscription`]; the publisher observes an -/// aggregate across all live subscribers via [`TrackProducer::subscription`]. -/// A subscriber can change its preferences after the fact with -/// [`TrackSubscriber::update`]. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Subscription { - /// Delivery priority. Higher values preempt lower ones when bandwidth is constrained. - pub priority: u8, - /// Whether groups should be delivered in sequence order. - pub ordered: bool, - /// How long to wait for a group before skipping it once a newer group has - /// arrived. `Duration::ZERO` skips immediately (e.g. group 8 arriving means - /// group 7 is skipped); a larger value tolerates that much reordering before - /// giving up on the older group. - pub stale: Duration, - /// First group to deliver, or `None` to start at the latest group. - pub group_start: Option, - /// Last group to deliver (inclusive), or `None` for no end. - pub group_end: Option, -} + // Groups in arrival order. `None` entries are tombstones for evicted groups. + groups: VecDeque>, -impl Default for Subscription { - fn default() -> Self { - Self { - priority: 0, - ordered: true, - stale: Duration::ZERO, - group_start: None, - group_end: None, - } - } -} + // TODO Do we need this? + duplicates: HashSet, -impl Subscription { - /// Set the delivery priority, returning `self` for chaining. - pub fn with_priority(mut self, priority: u8) -> Self { - self.priority = priority; - self - } + // We've popped the front of this VecDeque this many times, used to map sequence -> index. + offset: usize, - /// Set whether groups are delivered in sequence order, returning `self` for chaining. - pub fn with_ordered(mut self, ordered: bool) -> Self { - self.ordered = ordered; - self - } + // The highest sequence number successfully appended to the track. + max_sequence: Option, - /// Set how long to wait for a group before skipping it, returning `self` for chaining. - pub fn with_stale(mut self, stale: Duration) -> Self { - self.stale = stale; - self - } + // The sequence number at which the track was finalized. + final_sequence: Option, - /// Set the first group to deliver, returning `self` for chaining. - pub fn with_group_start(mut self, group_start: impl Into>) -> Self { - self.group_start = group_start.into(); - self - } + // The error that caused the track to be aborted, if any. + abort: Option, - /// Set the last group to deliver (inclusive), returning `self` for chaining. - pub fn with_group_end(mut self, group_end: impl Into>) -> Self { - self.group_end = group_end.into(); - self - } + // Active subscriptions. + subscriptions: Vec>, } -#[derive(Default)] -struct State { - /// Groups in arrival order. `None` entries are tombstones for evicted groups. - groups: VecDeque>, - duplicates: HashSet, - offset: usize, - max_sequence: Option, - final_sequence: Option, - abort: Option, - /// Live subscribers' preferences, aggregated by [`TrackProducer::subscription`]. - /// Weak so dropped consumers don't leak across consumer churn while the - /// producer lingers. - subscriptions: Vec>>, -} +impl TrackState { + fn poll_info(&self) -> Poll> { + if let Some(info) = &self.info { + Poll::Ready(Ok(info.clone())) + } else { + Poll::Pending + } + } -impl State { /// Find the next non-tombstoned group at or after `index` in arrival order. /// /// Returns the group and its absolute index so the consumer can advance past it. @@ -431,96 +360,50 @@ impl State { } } - /// Aggregate every live subscriber's preferences into a single [`Subscription`] - /// describing the most demanding request, pruning entries for dropped consumers. - /// - /// Returns `None` when there are no live subscribers. Otherwise the result is - /// what an upstream relay should forward: highest priority, ordered if anyone - /// wants it, the tightest stale deadline, the earliest group start, and the - /// latest group end (unbounded wins). - fn aggregate_subscription(&mut self) -> Option { - self.subscriptions.retain(|s| s.strong_count() > 0); - - let mut agg: Option = None; - for weak in &self.subscriptions { - let Some(arc) = weak.upgrade() else { continue }; - let sub = arc.lock().unwrap().clone(); - agg = Some(match agg { - None => sub, - Some(acc) => Subscription { - priority: acc.priority.max(sub.priority), - ordered: acc.ordered || sub.ordered, - stale: tightest_stale(acc.stale, sub.stale), - group_start: min_option(acc.group_start, sub.group_start), - group_end: max_option(acc.group_end, sub.group_end), - }, - }); - } - - agg - } -} - -/// The tighter of two stale deadlines. `Duration::ZERO` skips immediately, so -/// any real deadline beats it; otherwise the smaller (sooner) deadline wins. -fn tightest_stale(a: Duration, b: Duration) -> Duration { - match (a.is_zero(), b.is_zero()) { - (true, _) => b, - (_, true) => a, - _ => a.min(b), - } -} - -/// The smaller of two optional bounds, treating `None` as "no preference". -fn min_option(a: Option, b: Option) -> Option { - match (a, b) { - (Some(a), Some(b)) => Some(a.min(b)), - (a, b) => a.or(b), - } -} - -/// The later group end. `None` means unbounded, which dominates any bound. -fn max_option(a: Option, b: Option) -> Option { - match (a, b) { - (Some(a), Some(b)) => Some(a.max(b)), - _ => None, + fn modify(producer: &kio::Producer) -> Result> { + producer.write().map_err(|r| r.abort.clone().unwrap_or(Error::Dropped)) } } /// A producer for a track, used to create new groups. +#[derive(Clone)] pub struct TrackProducer { - info: Track, - state: kio::Producer, -} - -impl std::ops::Deref for TrackProducer { - type Target = Track; - - fn deref(&self) -> &Self::Target { - &self.info - } + name: Arc, + state: kio::Producer, + prev_subscription: Option, } impl TrackProducer { - /// Build a producer for the given track metadata. Prefer [`Track::produce`]. - pub fn new(info: Track) -> Self { + /// Build a producer for the given track metadata. + pub fn new(name: impl Into>, info: impl Into>) -> Self { + let info = info.into().unwrap_or_default(); Self { - info, - state: kio::Producer::default(), + name: name.into(), + state: kio::Producer::new(TrackState { + info: Some(info), + ..Default::default() + }), + prev_subscription: None, } } - /// Create a new group with the given sequence number. - pub fn create_group(&mut self, info: Group) -> Result { - let group = GroupProducer::new_with_timescale(info, self.info.timescale); + pub fn name(&self) -> &str { + &self.name + } + /// Create a new group with the given sequence number. + pub fn create_group(&mut self, group: Group) -> Result { let mut state = self.modify()?; if let Some(fin) = state.final_sequence && group.sequence >= fin { return Err(Error::Closed); } + let info = state.info.as_ref().unwrap(); + let timescale = info.timescale; + let cache = info.cache; + let group = GroupProducer::new_with_timescale(group, timescale); if !state.duplicates.insert(group.sequence) { return Err(Error::Duplicate); } @@ -528,7 +411,7 @@ impl TrackProducer { let now = tokio::time::Instant::now(); state.max_sequence = Some(state.max_sequence.unwrap_or(0).max(group.sequence)); state.groups.push_back(Some((group.clone(), now))); - state.evict_expired(now, self.info.cache); + state.evict_expired(now, cache); Ok(group) } @@ -546,13 +429,17 @@ impl TrackProducer { return Err(Error::Closed); } - let group = GroupProducer::new_with_timescale(Group { sequence }, self.info.timescale); + let info = state.info.as_ref().unwrap(); + let timescale = info.timescale; + let cache = info.cache; + + let group = GroupProducer::new_with_timescale(Group { sequence }, timescale); let now = tokio::time::Instant::now(); state.duplicates.insert(sequence); state.max_sequence = Some(sequence); state.groups.push_back(Some((group.clone(), now))); - state.evict_expired(now, self.info.cache); + state.evict_expired(now, cache); Ok(group) } @@ -610,38 +497,6 @@ impl TrackProducer { Ok(()) } - /// Subscribe to the track in-process with the given subscriber preferences. - /// - /// Pass `None` for [`Subscription::default`]. The preferences feed the - /// producer's [`Self::subscription`] aggregate and can be changed later via - /// [`TrackSubscriber::update`]. - pub fn subscribe(&self, subscription: impl Into>) -> TrackSubscriber { - let mut subscription = subscription.into().unwrap_or_default(); - subscription.stale = self.info.clamp_stale(subscription.stale); - let subscription = Arc::new(Mutex::new(subscription)); - if let Ok(mut state) = self.state.write() { - state.subscriptions.push(Arc::downgrade(&subscription)); - } - TrackSubscriber { - info: self.info.clone(), - state: self.state.consume(), - subscription, - index: 0, - min_sequence: 0, - next_sequence: 0, - end_sequence: None, - } - } - - /// The aggregate of every live subscriber's [`Subscription`] (most demanding - /// request across all consumers), or `None` when there are no live subscribers. - pub fn subscription(&self) -> Option { - self.state - .write() - .ok() - .and_then(|mut state| state.aggregate_subscription()) - } - /// Block until there are no active consumers. pub async fn unused(&self) -> Result<()> { self.state @@ -682,38 +537,131 @@ impl TrackProducer { /// Create a weak reference that doesn't prevent auto-close. pub(crate) fn weak(&self) -> TrackWeak { TrackWeak { - info: self.info.clone(), + name: self.name.clone(), state: self.state.weak(), } } - fn modify(&self) -> Result> { - self.state - .write() - .map_err(|r| r.abort.clone().unwrap_or(Error::Dropped)) + /// Get a consumer handle for this in-process track. + /// + /// Unlike a wire subscription, the info is already known, so a subscription + /// opened from this handle resolves immediately. + pub fn consume(&self) -> TrackConsumer { + TrackConsumer { + name: self.name.clone(), + state: self.state.consume(), + } } -} -impl Clone for TrackProducer { - fn clone(&self) -> Self { - Self { - info: self.info.clone(), - state: self.state.clone(), + /// Subscribe to this in-process track, resolving synchronously. + /// + /// The info is fixed at creation, so there's nothing to wait for (no + /// SUBSCRIBE_OK round trip). The subscriber's stale window is clamped to the + /// track's cache. Pass `None` for [`Subscription::default`]. + pub fn subscribe(&self, subscription: impl Into>) -> TrackSubscriber { + let mut preferences = subscription.into().unwrap_or_default(); + + let mut state = self.modify().expect("track producer state is never closed"); + let info = state.info.clone().expect("producer always has info"); + preferences.stale = info.clamp_stale(preferences.stale); + let subscription = kio::Producer::new(preferences); + state.subscriptions.push(subscription.consume()); + drop(state); + + TrackSubscriber { + name: self.name.clone(), + info, + state: self.state.consume(), + subscription, + index: 0, + min_sequence: 0, + next_sequence: 0, + end_sequence: None, } } + + /// Block until the aggregate subscription changes, then return the new value. + /// + /// Yields the most demanding request across all live subscribers, or `None` + /// once the last one drops. Used by relays to forward downstream demand + /// upstream (e.g. SUBSCRIBE_UPDATE). + pub async fn subscription_changed(&mut self) -> Result> { + kio::wait(|waiter| self.poll_subscription_changed(waiter)).await + } + + /// A non-blocking snapshot of the current aggregate subscription, or `None` + /// when there are no live subscribers. Unlike [`Self::subscription`], this + /// doesn't wait for a change or advance the change cursor. + pub fn subscription(&self) -> Option { + let state = self.state.read(); + let mut combined: Option = None; + for sub in &state.subscriptions { + if let Poll::Ready(merged) = sub.read().poll_combined(&combined) { + combined = Some(merged); + } + } + combined + } + + pub fn poll_subscription_changed(&mut self, waiter: &kio::Waiter) -> Poll>> { + let prev = &self.prev_subscription; + let combined = match self + .state + .poll(waiter, |state| poll_combined_subscriptions(state, waiter, prev)) + { + Poll::Ready(Ok(combined)) => combined, + Poll::Ready(Err(state)) => return Poll::Ready(Err(state.abort.clone().unwrap_or(Error::Dropped))), + Poll::Pending => return Poll::Pending, + }; + self.prev_subscription = combined.clone(); + Poll::Ready(Ok(combined)) + } + + fn modify(&self) -> Result> { + TrackState::modify(&self.state) + } } -impl From for TrackProducer { - fn from(info: Track) -> Self { - TrackProducer::new(info) +/// Aggregate every live subscriber's preferences into the most demanding request, +/// returning `Poll::Ready` only when the result differs from `prev`. +/// +/// Iterates `subscriptions` immutably so it never flags the [`TrackState`] as +/// modified on a no-op poll. Marking it modified would drain and wake unrelated +/// waiters on the channel (e.g. a [`TrackSubscriberPending`] parked on track +/// info), which races with [`TrackRequest::accept`] and can drop that wakeup. +/// Closed subscribers are pruned only when one has actually closed, which is a +/// real change that legitimately wakes other waiters. +fn poll_combined_subscriptions( + state: &mut kio::Mut<'_, TrackState>, + waiter: &kio::Waiter, + prev: &Option, +) -> Poll> { + let mut combined = None; + let mut any_closed = false; + for sub in state.subscriptions.iter() { + match sub.poll(waiter, |sub| sub.poll_combined(&combined)) { + Poll::Ready(Ok(sub)) => combined = Some(sub), + Poll::Ready(Err(_)) => any_closed = true, + Poll::Pending => {} + } + } + + if any_closed { + state.subscriptions.retain(|sub| !sub.is_closed()); + } + + if &combined == prev { + Poll::Pending + } else { + Poll::Ready(combined) } } /// A weak reference to a track that doesn't prevent auto-close. #[derive(Clone)] pub(crate) struct TrackWeak { - pub(crate) info: Track, - state: kio::Weak, + name: Arc, + state: kio::Weak, } impl TrackWeak { @@ -721,34 +669,147 @@ impl TrackWeak { self.state.is_closed() } - pub fn subscribe(&self, mut subscription: Subscription) -> TrackSubscriber { - subscription.stale = self.info.clamp_stale(subscription.stale); - let subscription = Arc::new(Mutex::new(subscription)); - // Register the preference if the producer is still alive so it shows up - // in the aggregate; otherwise the consumer will just observe close. - if let Some(producer) = self.state.produce() - && let Ok(mut state) = producer.write() - { - state.subscriptions.push(Arc::downgrade(&subscription)); - } - TrackSubscriber { - info: self.info.clone(), + pub fn consume(&self) -> TrackConsumer { + TrackConsumer { + name: self.name.clone(), state: self.state.consume(), + } + } + + /// The shared name handle, for use as a broadcast lookup key (clone is a + /// refcount bump, and the same `Arc` is shared with the track's handles). + pub(crate) fn name(&self) -> &Arc { + &self.name + } +} + +/// A handle to a single track within a broadcast. +/// +/// Obtained from [`crate::BroadcastConsumer::track`]. Holding it sends nothing +/// to the publisher; it just names a track you can [`subscribe`](Self::subscribe) +/// to (a live, ongoing stream of groups) later. The same handle can be subscribed +/// to multiple times, and clones are cheap. +// TODO: add `fetch` for one-shot retrieval of a past group range. +#[derive(Clone)] +pub struct TrackConsumer { + name: Arc, + state: kio::Consumer, +} + +impl TrackConsumer { + /// The track name this handle is bound to. + pub fn name(&self) -> &str { + &self.name + } + + pub(crate) fn weak(&self) -> TrackWeak { + TrackWeak { + name: self.name.clone(), + state: self.state.weak(), + } + } + + /// Open a live subscription. + pub fn subscribe(&self, subscription: impl Into>) -> Result { + let subscription = kio::Producer::new(subscription.into().unwrap_or_default()); + + match self.state.write() { + Ok(mut state) => state.subscriptions.push(subscription.consume()), + Err(state) => return Err(state.abort.clone().unwrap_or(Error::Dropped)), + }; + + Ok(TrackSubscriberPending { + name: self.name.clone(), + state: self.state.clone(), subscription, + waiter: None, + }) + } + + //pub fn fetch(&self) -> TrackFetchPending { + // TODO + //} + + pub fn info(&self) -> TrackInfoPending { + TrackInfoPending { + state: self.state.clone(), + waiter: None, + } + } +} + +pub struct TrackSubscriberPending { + name: Arc, + state: kio::Consumer, + subscription: kio::Producer, + // Kept alive between `Future::poll` calls so the weak waker kio registered + // stays upgradeable until the next poll replaces it. A temporary would drop + // after poll returns, leaving a dead weak ref and a lost wakeup on accept. + waiter: Option, +} + +impl TrackSubscriberPending { + pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll> { + // Wait until the track info is available + let info = ready!(self.state.poll(waiter, |state| state.poll_info())) + .map_err(|e| e.abort.clone().unwrap_or(Error::Dropped))??; + + Poll::Ready(Ok(TrackSubscriber { + name: self.name.clone(), + info, + state: self.state.clone(), + subscription: self.subscription.clone(), index: 0, min_sequence: 0, next_sequence: 0, end_sequence: None, + })) + } + + pub fn update(&mut self, subscription: Subscription) { + if let Ok(mut state) = self.subscription.write() { + *state = subscription; + } else { + panic!("subscription is closed"); } } +} - /// Wait until the underlying track state closes (producer dropped or aborted). - pub async fn closed(&self) { - self.state.closed().await; +impl Future for TrackSubscriberPending { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + // Replacing drops the previous waiter, freeing its slot so the register + // call below can recycle it (see kio's weak-waker GC). + this.waiter = Some(kio::Waiter::new(cx.waker().clone())); + this.poll_ok(this.waiter.as_ref().unwrap()) } +} - pub fn is_clone(&self, other: &Self) -> bool { - self.state.same_channel(&other.state) +pub struct TrackInfoPending { + state: kio::Consumer, + // See [`TrackSubscriberPending::waiter`]: kept alive so the registered weak + // waker stays upgradeable between polls. + waiter: Option, +} + +impl TrackInfoPending { + pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll> { + // Wait until the track info is available + let info = ready!(self.state.poll(waiter, |state| state.poll_info())) + .map_err(|e| e.abort.clone().unwrap_or(Error::Dropped))??; + Poll::Ready(Ok(info)) + } +} + +impl Future for TrackInfoPending { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + this.waiter = Some(kio::Waiter::new(cx.waker().clone())); + this.poll_ok(this.waiter.as_ref().unwrap()) } } @@ -757,13 +818,12 @@ impl TrackWeak { /// Created via [`TrackConsumer::subscribe`](crate::TrackConsumer::subscribe), or /// directly from a [`TrackProducer`] for an in-process track. Carries this /// subscriber's [`Subscription`] preferences, which feed the producer's aggregate. -#[derive(Clone)] pub struct TrackSubscriber { - info: Track, - state: kio::Consumer, - /// This subscriber's preferences, shared with the producer's aggregate. - /// Cloning a subscriber shares the same preferences (one subscription, fanned out). - subscription: Arc>, + name: Arc, + info: TrackInfo, + state: kio::Consumer, + + subscription: kio::Producer, /// Arrival-order cursor used by [`Self::recv_group`]. index: usize, /// Minimum sequence to return from any `recv` method. Set by [`Self::start_at`]. @@ -778,19 +838,19 @@ pub struct TrackSubscriber { end_sequence: Option, } -impl std::ops::Deref for TrackSubscriber { - type Target = Track; - - fn deref(&self) -> &Self::Target { +impl TrackSubscriber { + pub fn info(&self) -> &TrackInfo { &self.info } -} -impl TrackSubscriber { + pub fn name(&self) -> &str { + &self.name + } + // A helper to automatically apply Dropped if the state is closed without an error. fn poll(&self, waiter: &kio::Waiter, f: F) -> Poll> where - F: Fn(&kio::Ref<'_, State>) -> Poll>, + F: Fn(&kio::Ref<'_, TrackState>) -> Poll>, { Poll::Ready(match ready!(self.state.poll(waiter, f)) { Ok(res) => res, @@ -940,24 +1000,109 @@ impl TrackSubscriber { /// This subscriber's current preferences. pub fn subscription(&self) -> Subscription { - self.subscription.lock().unwrap().clone() + self.subscription.read().clone() } /// Replace this subscriber's preferences, updating the producer's aggregate. - pub fn update(&self, mut subscription: Subscription) { - subscription.stale = self.info.clamp_stale(subscription.stale); - *self.subscription.lock().unwrap() = subscription; + pub fn update(&mut self, subscription: Subscription) { + if let Ok(mut state) = self.subscription.write() { + *state = subscription; + } else { + panic!("subscription is closed"); + } } /// Return the latest sequence number in the track. pub fn latest(&self) -> Option { self.state.read().max_sequence } +} - /// Create a weak reference that doesn't prevent auto-close. - pub(crate) fn weak(&self) -> TrackWeak { +pub struct TrackRequest { + name: Arc, + state: kio::Producer, + + // The previous subscription that was combined, used to detect changes. + prev_subscription: Option, +} + +impl TrackRequest { + pub fn new(name: impl Into>) -> Self { + Self { + name: name.into(), + state: Default::default(), + prev_subscription: None, + } + } + + /// The requested track name. + pub fn name(&self) -> &str { + &self.name + } + + pub fn consume(&self) -> TrackConsumer { + TrackConsumer { + name: self.name.clone(), + state: self.state.consume(), + } + } + + //pub async fn requested(&self) -> TrackDynamic { + // TODO Used to return the next group requested for FETCH + //} + + /// Serve the request with the given track, resolving every waiting subscriber. + /// + /// The track's name must match [`Self::name`]. Returns [`Error::NotFound`] on + /// mismatch, or the broadcast's abort error if it closed while pending. + pub fn accept(self, info: impl Into>) -> TrackProducer { + self.state.write().ok().unwrap().info = Some(info.into().unwrap_or_default()); + TrackProducer { + name: self.name, + state: self.state, + prev_subscription: None, + } + } + + /// Reject the request, waking all waiting subscribers with `err`. + pub fn reject(self, err: Error) { + if let Ok(mut state) = self.state.write() { + state.abort = Some(err); + } + } + + pub fn subscription(&self) -> Option { + let state = self.state.read(); + let mut combined: Option = None; + for sub in &state.subscriptions { + if let Poll::Ready(merged) = sub.read().poll_combined(&combined) { + combined = Some(merged); + } + } + combined + } + + pub async fn subscription_changed(&mut self) -> Option { + kio::wait(|waiter| self.poll_subscription_changed(waiter)).await + } + + pub fn poll_subscription_changed(&mut self, waiter: &kio::Waiter) -> Poll> { + let prev = &self.prev_subscription; + // The request owns the only producer, so the channel can't be closed here. + let combined = match ready!( + self.state + .poll(waiter, |state| poll_combined_subscriptions(state, waiter, prev)) + ) { + Ok(combined) => combined, + Err(_) => unreachable!("a TrackRequest holds the only producer"), + }; + self.prev_subscription = combined.clone(); + Poll::Ready(combined) + } + + pub(super) fn weak(&self) -> TrackWeak { TrackWeak { - info: self.info.clone(), + name: self.name.clone(), state: self.state.weak(), } } @@ -1013,12 +1158,12 @@ mod test { use super::*; /// Helper: count non-tombstoned groups in state. - fn live_groups(state: &State) -> usize { + fn live_groups(state: &TrackState) -> usize { state.groups.iter().flatten().count() } /// Helper: get the sequence number of the first live group. - fn first_live_sequence(state: &State) -> u64 { + fn first_live_sequence(state: &TrackState) -> u64 { state.groups.iter().flatten().next().unwrap().0.sequence } @@ -1026,7 +1171,7 @@ mod test { async fn evict_expired_groups() { tokio::time::pause(); - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); // Create 3 groups at time 0. producer.append_group().unwrap(); // seq 0 @@ -1063,7 +1208,7 @@ mod test { async fn evict_keeps_max_sequence() { tokio::time::pause(); - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.append_group().unwrap(); // seq 0 // Advance time past threshold. @@ -1084,7 +1229,7 @@ mod test { async fn no_eviction_when_fresh() { tokio::time::pause(); - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.append_group().unwrap(); // seq 0 producer.append_group().unwrap(); // seq 1 producer.append_group().unwrap(); // seq 2 @@ -1100,7 +1245,7 @@ mod test { async fn consumer_skips_evicted_groups() { tokio::time::pause(); - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.append_group().unwrap(); // seq 0 let mut consumer = producer.subscribe(None); @@ -1118,7 +1263,7 @@ mod test { tokio::time::pause(); // A shorter cache evicts sooner than the default. - let mut producer = Track::new("test").with_cache(Duration::from_secs(1)).produce(); + let mut producer = TrackProducer::new("test", TrackInfo::default().with_cache(Duration::from_secs(1))); producer.append_group().unwrap(); // seq 0 // Past the custom cache but well within DEFAULT_CACHE. @@ -1133,11 +1278,11 @@ mod test { #[test] fn stale_clamped_to_cache() { - let producer = Track::new("test").with_cache(Duration::from_secs(2)).produce(); + let producer = TrackProducer::new("test", TrackInfo::default().with_cache(Duration::from_secs(2))); // A stale window beyond the cache is capped to the cache; a group can't be // waited for longer than the publisher keeps it. - let subscriber = producer.subscribe(Subscription::default().with_stale(Duration::from_secs(10))); + let mut subscriber = producer.subscribe(Subscription::default().with_stale(Duration::from_secs(10))); assert_eq!(subscriber.subscription().stale, Duration::from_secs(2)); // A window within the cache is left alone, and ZERO (skip immediately) stays ZERO. @@ -1152,7 +1297,7 @@ mod test { async fn out_of_order_max_sequence_at_front() { tokio::time::pause(); - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); // Arrive out of order: seq 5 first, then 3, then 4. producer.create_group(Group { sequence: 5 }).unwrap(); @@ -1188,7 +1333,7 @@ mod test { async fn max_sequence_at_front_blocks_trim() { tokio::time::pause(); - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); // Arrive: seq 5, then seq 3. producer.create_group(Group { sequence: 5 }).unwrap(); @@ -1234,7 +1379,7 @@ mod test { #[test] fn append_finish_cannot_be_rewritten() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); // Finishing an empty track is valid (fin = 0, total groups = 0). assert!(producer.finish().is_ok()); @@ -1244,7 +1389,7 @@ mod test { #[test] fn finish_after_groups() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.append_group().unwrap(); assert!(producer.finish().is_ok()); @@ -1254,7 +1399,7 @@ mod test { #[test] fn insert_finish_validates_sequence_and_freezes_to_max() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.create_group(Group { sequence: 5 }).unwrap(); assert!(producer.finish_at(4).is_err()); @@ -1273,7 +1418,7 @@ mod test { #[tokio::test] async fn recv_group_finishes_without_waiting_for_gaps() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.create_group(Group { sequence: 1 }).unwrap(); producer.finish_at(1).unwrap(); @@ -1290,7 +1435,7 @@ mod test { #[tokio::test] async fn next_group_skips_late_arrivals() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); // Seq 5 arrives first. @@ -1327,7 +1472,7 @@ mod test { #[tokio::test] async fn next_group_returns_arrivals_in_order() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); // Seq 3 arrives first, then seq 5 — both should be returned in arrival order. @@ -1353,7 +1498,7 @@ mod test { #[tokio::test] async fn next_group_and_recv_group_use_independent_cursors() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); // Out-of-order arrivals: seq 5 first, then seq 3. @@ -1377,7 +1522,7 @@ mod test { #[tokio::test] async fn end_at_caps_next_group() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); for s in 0..6 { @@ -1409,7 +1554,7 @@ mod test { #[tokio::test] async fn end_at_release_drains_cached_groups() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); for s in 0..6 { @@ -1454,7 +1599,7 @@ mod test { #[tokio::test] async fn end_at_lower_than_cursor_parks_consumer() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); for s in 0..3 { @@ -1498,7 +1643,7 @@ mod test { #[tokio::test] async fn end_at_toggling_around_late_arrivals() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); consumer.end_at(5); @@ -1541,7 +1686,7 @@ mod test { #[tokio::test] async fn read_frame_returns_single_frame_per_group() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); producer.write_frame(b"hello".as_slice()).unwrap(); @@ -1566,7 +1711,7 @@ mod test { #[tokio::test] async fn read_frame_skips_stalled_group_for_newer_ready_frame() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); // Seq 3: group open, no frame yet (stalled). @@ -1588,7 +1733,7 @@ mod test { #[tokio::test] async fn read_frame_discards_rest_of_multi_frame_group() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); // Group 0 has two frames; only the first is returned. @@ -1622,7 +1767,7 @@ mod test { async fn read_frame_waits_for_pending_group_after_finish() { // finish() sets final_sequence, but groups already created with lower sequences // can still produce frames. read_frame must not return None prematurely. - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); let mut g0 = producer.create_group(Group { sequence: 0 }).unwrap(); @@ -1649,7 +1794,7 @@ mod test { async fn read_frame_respects_start_at() { // start_at sets min_sequence; read_frame must skip groups below it even though // next_sequence is still 0. - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); consumer.start_at(5); @@ -1673,7 +1818,7 @@ mod test { #[tokio::test] async fn read_frame_returns_none_when_finished() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); let mut consumer = producer.subscribe(None); producer.write_frame(b"only".as_slice()).unwrap(); @@ -1697,7 +1842,7 @@ mod test { #[tokio::test] async fn get_group_finishes_without_waiting_for_gaps() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); producer.create_group(Group { sequence: 1 }).unwrap(); producer.finish_at(1).unwrap(); @@ -1720,7 +1865,7 @@ mod test { #[test] fn append_group_returns_bounds_exceeded_on_sequence_overflow() { - let mut producer = Track::new("test").produce(); + let mut producer = TrackProducer::new("test", None); { let mut state = producer.state.write().ok().unwrap(); state.max_sequence = Some(u64::MAX); diff --git a/rs/moq-net/src/stats.rs b/rs/moq-net/src/stats.rs index efaf6e563d..8a18808760 100644 --- a/rs/moq-net/src/stats.rs +++ b/rs/moq-net/src/stats.rs @@ -129,7 +129,7 @@ use std::{ use serde::Serialize; use web_async::{Lock, spawn}; -use crate::{AsPath, Broadcast, OriginProducer, Path, PathOwned, Track, TrackProducer}; +use crate::{AsPath, BroadcastInfo, OriginProducer, Path, PathOwned, TrackProducer}; /// Cumulative atomic counters for a single `(tier, role)` on a broadcast. /// @@ -1061,10 +1061,10 @@ async fn run_publisher(weak: Weak, advertised: PathOwned, interval: return; }; - let mut broadcast = Broadcast::new().produce(); + let mut broadcast = BroadcastInfo::new().produce(); // Create the four per-broadcast tracks and the two session tracks up front. - let create = |broadcast: &mut crate::BroadcastProducer, name: &str| match broadcast.create_track(Track::new(name)) { + let create = |broadcast: &mut crate::BroadcastProducer, name: &str| match broadcast.create_track(name, None) { Ok(t) => Some(t), Err(err) => { tracing::warn!(?err, name, "stats: failed to create track"); @@ -1451,8 +1451,10 @@ mod tests { let (_path, event) = consumer.next().await.expect("expected announce"); let broadcast = event.broadcast().expect("active"); let track = broadcast - .consume_track("publisher.json") + .track("publisher.json") + .unwrap() .subscribe(None) + .unwrap() .await .expect("subscribe"); let frame = read_frame(track).await; @@ -1478,8 +1480,10 @@ mod tests { let (_path, event) = consumer.next().await.expect("announce"); let broadcast = event.broadcast().expect("active"); let track = broadcast - .consume_track("publisher.json") + .track("publisher.json") + .unwrap() .subscribe(None) + .unwrap() .await .expect("subscribe"); let frame = read_frame(track).await; @@ -1513,8 +1517,10 @@ mod tests { let (_path, event) = consumer.next().await.expect("announce"); let broadcast = event.broadcast().expect("active"); let track = broadcast - .consume_track("publisher.json") + .track("publisher.json") + .unwrap() .subscribe(None) + .unwrap() .await .expect("subscribe"); let frame = read_frame(track).await; @@ -1640,8 +1646,10 @@ mod tests { let broadcast = event.broadcast().expect("active"); let track = broadcast - .consume_track("sessions.json") + .track("sessions.json") + .unwrap() .subscribe(None) + .unwrap() .await .expect("subscribe"); let frame = read_session_frame(track).await; @@ -1654,8 +1662,10 @@ mod tests { ); let int_track = broadcast - .consume_track("internal/sessions.json") + .track("internal/sessions.json") + .unwrap() .subscribe(None) + .unwrap() .await .expect("subscribe"); let snap = *read_session_frame(int_track).await.get("peer").expect("internal entry"); @@ -1704,8 +1714,10 @@ mod tests { // External publisher slot SHOULD include foo/bar. let pub_track = broadcast - .consume_track("publisher.json") + .track("publisher.json") + .unwrap() .subscribe(None) + .unwrap() .await .expect("subscribe"); assert!( @@ -1716,7 +1728,13 @@ mod tests { // The other three slots had zero activity. The first frame on // each must be `{}`, not `{"foo/bar": {all zeros}}`. for name in ["subscriber.json", "internal/publisher.json", "internal/subscriber.json"] { - let t = broadcast.consume_track(name).subscribe(None).await.expect("subscribe"); + let t = broadcast + .track(name) + .unwrap() + .subscribe(None) + .unwrap() + .await + .expect("subscribe"); let frame = read_frame(t).await; assert!( frame.is_empty(), diff --git a/rs/moq-relay/src/cluster.rs b/rs/moq-relay/src/cluster.rs index fa4d3c99a0..ddee2e6b9e 100644 --- a/rs/moq-relay/src/cluster.rs +++ b/rs/moq-relay/src/cluster.rs @@ -312,7 +312,7 @@ pub struct ClusterConfig { /// /// Local sessions and remote cluster connections all publish into the same /// origin. Loop prevention and shortest-path preference come from the -/// hop list carried on each broadcast (see [`moq_net::Broadcast::hops`]). +/// hop list carried on each broadcast (see [`moq_net::BroadcastInfo::hops`]). /// /// Construct with [`Cluster::new`], then attach a QUIC client and (optionally) /// a [`Stats`] aggregator with the `with_*` builder methods. A cluster without diff --git a/rs/moq-relay/src/web.rs b/rs/moq-relay/src/web.rs index 6ce742df6b..52e03d18e8 100644 --- a/rs/moq-relay/src/web.rs +++ b/rs/moq-relay/src/web.rs @@ -528,9 +528,7 @@ async fn serve_fetch( .announced_broadcast("") .await .ok_or(StatusCode::NOT_FOUND)?; - let mut track = broadcast - .consume_track(&track) - .subscribe(None) + let mut track = async { broadcast.track(&track)?.subscribe(None)?.await } .await .map_err(|err| match err { moq_net::Error::NotFound => StatusCode::NOT_FOUND, @@ -550,7 +548,7 @@ async fn serve_fetch( Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), }; - tracing::info!(track = %track.name, group = %group.sequence, "serving group"); + tracing::info!(track = %track.name(), group = %group.sequence, "serving group"); match params.frame { FetchFrame::Num(index) => match group.get_frame(index).await { diff --git a/rs/moq-relay/tests/smoke.rs b/rs/moq-relay/tests/smoke.rs index 592714b879..ba96dc3619 100644 --- a/rs/moq-relay/tests/smoke.rs +++ b/rs/moq-relay/tests/smoke.rs @@ -9,7 +9,7 @@ use std::{net::TcpListener, sync::atomic::AtomicU64, time::Duration}; -use moq_native::moq_net::{self, Origin, Track}; +use moq_native::moq_net::{self, Origin}; use moq_relay::{AuthConfig, Cluster, ClusterConfig, PublicConfig, Web, WebConfig, WebState}; const TIMEOUT: Duration = Duration::from_secs(10); @@ -119,7 +119,7 @@ async fn relay_websocket_round_trip_uses_newest_version() { // ── publisher ─────────────────────────────────────────────────── let pub_origin = Origin::random().produce(); let mut broadcast = pub_origin.create_broadcast("test").expect("create broadcast"); - let mut track = broadcast.create_track(Track::new("video")).expect("create track"); + let mut track = broadcast.create_track("video", None).expect("create track"); let mut group = track.append_group().expect("append group"); group.write_frame(b"hello".as_ref()).expect("write frame"); group.finish().expect("finish group"); @@ -160,7 +160,13 @@ async fn relay_websocket_round_trip_uses_newest_version() { assert_eq!(path.as_str(), "test"); let bc = bc.broadcast().expect("expected announce, got unannounce"); - let mut track_sub = bc.consume_track("video").subscribe(None).await.expect("consume_track"); + let mut track_sub = bc + .track("video") + .unwrap() + .subscribe(None) + .unwrap() + .await + .expect("consume_track"); let mut group_sub = tokio::time::timeout(TIMEOUT, track_sub.recv_group()) .await .expect("recv_group timeout") diff --git a/rs/moq-rtc/bin/moq-rtc.rs b/rs/moq-rtc/bin/moq-rtc.rs index b365a4377d..3d3d6d31c8 100644 --- a/rs/moq-rtc/bin/moq-rtc.rs +++ b/rs/moq-rtc/bin/moq-rtc.rs @@ -195,7 +195,7 @@ async fn run_client( match direction { Direction::Subscribe => { // WHEP client: receive remote RTP, publish it as `broadcast_name`. - let broadcast = moq_net::Broadcast::new().produce(); + let broadcast = moq_net::BroadcastInfo::new().produce(); let consumer = broadcast.consume(); if !publisher.publish_broadcast(broadcast_name, consumer) { anyhow::bail!("broadcast path conflict: {}", broadcast_name); diff --git a/rs/moq-rtc/src/codec/mod.rs b/rs/moq-rtc/src/codec/mod.rs index a7b19ff030..d4b70c6ed9 100644 --- a/rs/moq-rtc/src/codec/mod.rs +++ b/rs/moq-rtc/src/codec/mod.rs @@ -79,7 +79,7 @@ impl Track { /// Audio track for an Opus rendition. pub async fn opus(broadcast: &moq_net::BroadcastConsumer, name: &str) -> Result { let container = moq_mux::catalog::hang::Container::Legacy; - let track = broadcast.consume_track(name).subscribe(None).await?; + let track = broadcast.track(name)?.subscribe(None)?.await?; let consumer = moq_mux::container::Consumer::new(track, container); Ok(Self { consumer, @@ -92,7 +92,7 @@ impl Track { /// inferred from `config.description` (avc1 vs avc3). pub async fn video(broadcast: &moq_net::BroadcastConsumer, name: &str, config: &VideoConfig) -> Result { let container: moq_mux::catalog::hang::Container = (&config.container).try_into()?; - let track = broadcast.consume_track(name).subscribe(None).await?; + let track = broadcast.track(name)?.subscribe(None)?.await?; let consumer = moq_mux::container::Consumer::new(track, container); let (codec, convert) = match &config.codec { diff --git a/rs/moq-rtc/src/codec/vp8.rs b/rs/moq-rtc/src/codec/vp8.rs index 9132ac8aac..a3b946b325 100644 --- a/rs/moq-rtc/src/codec/vp8.rs +++ b/rs/moq-rtc/src/codec/vp8.rs @@ -15,7 +15,8 @@ pub struct Bridge { impl Bridge { pub fn new(mut broadcast: moq_net::BroadcastProducer, catalog: moq_mux::catalog::hang::Producer) -> Result { let track = broadcast.create_track( - moq_net::Track::new(broadcast.unique_name(".vp8")).with_timescale(hang::container::TIMESCALE), + broadcast.unique_name(".vp8"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; let producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy); Ok(Self { @@ -35,7 +36,7 @@ impl Bridge { .lock() .video .renditions - .insert(self.track.track().name.clone(), config); + .insert(self.track.track().name().to_string(), config); self.announced = true; } } @@ -61,6 +62,6 @@ impl codec::Bridge for Bridge { impl Drop for Bridge { fn drop(&mut self) { - self.catalog.lock().video.renditions.remove(&self.track.track().name); + self.catalog.lock().video.renditions.remove(self.track.track().name()); } } diff --git a/rs/moq-rtc/src/codec/vp9.rs b/rs/moq-rtc/src/codec/vp9.rs index 40b97663b2..0ce5b87b6a 100644 --- a/rs/moq-rtc/src/codec/vp9.rs +++ b/rs/moq-rtc/src/codec/vp9.rs @@ -14,7 +14,8 @@ pub struct Bridge { impl Bridge { pub fn new(mut broadcast: moq_net::BroadcastProducer, catalog: moq_mux::catalog::hang::Producer) -> Result { let track = broadcast.create_track( - moq_net::Track::new(broadcast.unique_name(".vp9")).with_timescale(hang::container::TIMESCALE), + broadcast.unique_name(".vp9"), + moq_net::TrackInfo::default().with_timescale(hang::container::TIMESCALE), )?; let producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy); Ok(Self { @@ -34,7 +35,7 @@ impl Bridge { .lock() .video .renditions - .insert(self.track.track().name.clone(), config); + .insert(self.track.track().name().to_string(), config); self.announced = true; } } @@ -60,6 +61,6 @@ impl codec::Bridge for Bridge { impl Drop for Bridge { fn drop(&mut self) { - self.catalog.lock().video.renditions.remove(&self.track.track().name); + self.catalog.lock().video.renditions.remove(self.track.track().name()); } } diff --git a/rs/moq-rtc/src/egress.rs b/rs/moq-rtc/src/egress.rs index 7aefc28d9e..68bceb1cd0 100644 --- a/rs/moq-rtc/src/egress.rs +++ b/rs/moq-rtc/src/egress.rs @@ -52,8 +52,8 @@ impl EgressSource { /// which takes the receiver via [`Self::take_writes`]. pub async fn new(broadcast: moq_net::BroadcastConsumer) -> Result { let catalog_track = broadcast - .consume_track(hang::Catalog::DEFAULT_NAME) - .subscribe(hang::Catalog::default_subscription()) + .track(hang::Catalog::DEFAULT_NAME)? + .subscribe(hang::Catalog::default_subscription())? .await?; let mut consumer = moq_mux::catalog::hang::Consumer::new(catalog_track); let catalog = consumer diff --git a/rs/moq-rtc/src/server/whip.rs b/rs/moq-rtc/src/server/whip.rs index d01672c7b3..950eb84f51 100644 --- a/rs/moq-rtc/src/server/whip.rs +++ b/rs/moq-rtc/src/server/whip.rs @@ -48,7 +48,7 @@ async fn accept_offer(server: &Server, path: &str, headers: &HeaderMap, body: By // Register the broadcast on the publish origin before negotiating, so a // fast subscriber doesn't see a 404 in the gap between the SDP answer // and the first RTP packet. - let broadcast = moq_net::Broadcast::new().produce(); + let broadcast = moq_net::BroadcastInfo::new().produce(); let consumer = broadcast.consume(); if !server.publisher().publish_broadcast(path, consumer) { return Err(Error::Other(anyhow::anyhow!("path conflict: {path}"))); diff --git a/rs/moq-rtc/tests/bitstream.rs b/rs/moq-rtc/tests/bitstream.rs index d261e7ebde..81c7641e41 100644 --- a/rs/moq-rtc/tests/bitstream.rs +++ b/rs/moq-rtc/tests/bitstream.rs @@ -32,7 +32,7 @@ async fn h264_annexb_frame_publishes_catalog_entry() { let frame = annexb(&[sps, pps, idr]); - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut producer).expect("catalog"); @@ -62,7 +62,7 @@ async fn h264_annexb_frame_publishes_catalog_entry() { #[tokio::test(start_paused = true)] async fn opus_frame_publishes_catalog_entry() { - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut producer).expect("catalog"); @@ -85,7 +85,7 @@ async fn opus_frame_publishes_catalog_entry() { #[tokio::test(start_paused = true)] async fn vp9_keyframe_flag_from_uncompressed_header() { - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut producer).expect("catalog"); @@ -136,7 +136,7 @@ async fn vp9_keyframe_flag_from_uncompressed_header() { #[tokio::test(start_paused = true)] async fn egress_opus_passthrough() { // Build an opus broadcast via the ingest bridge. - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut producer).expect("catalog"); let mut bridge = moq_rtc::codec::opus::Bridge::new(producer.clone(), catalog.clone(), 48_000, 2).expect("bridge"); @@ -172,7 +172,7 @@ async fn egress_h264_avc3_passthrough() { let pps: &[u8] = &[0x68, 0xce, 0x3c, 0x80]; let idr: &[u8] = &[0x65, 0x88, 0x84, 0x21]; - let broadcast = moq_net::Broadcast::new(); + let broadcast = moq_net::BroadcastInfo::new(); let mut producer = broadcast.produce(); let catalog = moq_mux::catalog::hang::Producer::new(&mut producer).expect("catalog");