Skip to main content

moq_mux/
source.rs

1//! Export input: an origin plus the path of the broadcast whose catalog drives the export.
2//!
3//! A hang catalog rendition may reference a track published in *another*
4//! broadcast via its `broadcast` field (a path relative to the catalog's
5//! broadcast, e.g. `../source`). Resolving that reference needs the catalog
6//! broadcast's own path and an [`moq_net::origin::Consumer`] to fetch the
7//! referenced broadcast from. [`Source`] bundles the two, and resolves both the
8//! catalog broadcast and any referenced broadcast through the same origin so
9//! [`request_broadcast`](moq_net::origin::Consumer::request_broadcast) deduplicates
10//! shared subscriptions.
11
12use moq_net::AsPath;
13
14/// The subscription side of an export: an origin and the path of the broadcast
15/// whose catalog drives it.
16///
17/// The catalog broadcast and every rendition (including ones whose catalog
18/// `broadcast` field references a sibling broadcast) resolve against `origin`,
19/// so a source can always follow a cross-broadcast reference. Build one with
20/// [`Source::new`].
21#[derive(Clone)]
22pub struct Source {
23	origin: moq_net::origin::Consumer,
24	path: moq_net::PathOwned,
25}
26
27impl Source {
28	/// A source rooted at `origin`, driven by the catalog of the broadcast at `path`.
29	///
30	/// `path` names the broadcast whose catalog is exported; a rendition's relative
31	/// `broadcast` reference is resolved against it. Both the catalog broadcast and any
32	/// referenced broadcast are fetched via
33	/// [`origin.request_broadcast`](moq_net::origin::Consumer::request_broadcast), so they
34	/// must be reachable through `origin` (announced, or served by a dynamic handler).
35	pub fn new(origin: moq_net::origin::Consumer, path: impl AsPath) -> Self {
36		Self {
37			origin,
38			path: path.as_path().to_owned(),
39		}
40	}
41
42	/// Resolve and subscribe to the catalog broadcast (the one at this source's path).
43	pub async fn broadcast(&self) -> crate::Result<moq_net::broadcast::Consumer> {
44		Ok(self.origin.request_broadcast(&self.path).await?)
45	}
46
47	/// Begin resolving the broadcast that serves rendition track `name`, honoring an
48	/// optional cross-broadcast reference.
49	///
50	/// A missing/empty `rel`, or one that resolves back to the catalog's own path (or
51	/// walks past the origin root), targets the catalog broadcast; anything else targets
52	/// the resolved sibling broadcast. Either way the broadcast is fetched from the origin,
53	/// which deduplicates repeat requests for the same live path (announced or dynamically
54	/// served) so the catalog and every rendition share one upstream subscription.
55	pub(crate) fn request(&self, rel: Option<&moq_net::PathRelative<'_>>) -> kio::Pending<moq_net::origin::Requesting> {
56		let target = match rel.filter(|rel| !rel.is_empty()) {
57			// Excess `..` clamps to the (empty) origin root, which is not a broadcast; treat
58			// it as a self-reference and use the catalog broadcast instead.
59			Some(rel) => match self.path.resolve(rel) {
60				resolved if resolved.is_empty() => self.path.clone(),
61				resolved => resolved,
62			},
63			None => self.path.clone(),
64		};
65
66		self.origin.request_broadcast(&target)
67	}
68
69	/// Resolve an optional cross-broadcast reference to its broadcast.
70	///
71	/// `rel` is a rendition's catalog `broadcast` field: `None` (or an empty / self
72	/// reference) resolves the catalog broadcast itself; anything else fetches the
73	/// referenced sibling broadcast from the origin. Use it when you need the broadcast
74	/// handle itself (e.g. to FETCH individual groups) rather than a subscription.
75	pub async fn resolve(
76		&self,
77		rel: Option<&moq_net::PathRelative<'_>>,
78	) -> crate::Result<moq_net::broadcast::Consumer> {
79		Ok(self.request(rel).await?)
80	}
81
82	/// Resolve an optional cross-broadcast reference and subscribe to track `name`,
83	/// awaiting SUBSCRIBE_OK.
84	///
85	/// `rel` is a rendition's catalog `broadcast` field: `None` (or an empty / self
86	/// reference) subscribes on the catalog broadcast; anything else fetches the
87	/// referenced broadcast from the origin first.
88	///
89	/// This is the async counterpart to the poll-driven container exporters: consumers
90	/// that wrap a raw [`moq_net::track::Subscriber`] themselves (e.g. the WebRTC egress)
91	/// use it to honor cross-broadcast renditions without reimplementing the path math.
92	pub async fn subscribe_track(
93		&self,
94		rel: Option<&moq_net::PathRelative<'_>>,
95		name: &str,
96	) -> crate::Result<moq_net::track::Subscriber> {
97		let broadcast = self.request(rel).await?;
98		Ok(broadcast.track(name)?.subscribe(None).await?)
99	}
100}
101
102/// Test helper: serve `broadcast` on a throwaway origin's dynamic handler and return a
103/// [`Source`] rooted at it, so exporter tests that build a local broadcast can still resolve
104/// it by path. The origin is leaked so the broadcast stays reachable for the source's
105/// lifetime (harmless in a test binary).
106#[cfg(test)]
107pub(crate) fn announced(broadcast: &moq_net::broadcast::Consumer) -> Source {
108	let origin = moq_net::Origin::random().produce();
109	let mut dynamic = origin.dynamic();
110	let served = broadcast.clone();
111	tokio::spawn(async move {
112		while let Ok(request) = dynamic.requested_broadcast().await {
113			request.accept(served.clone());
114		}
115	});
116	let source = Source::new(origin.consume(), "test");
117	Box::leak(Box::new(origin));
118	source
119}
120
121#[cfg(test)]
122mod tests {
123	use super::*;
124	use moq_net::{Origin, PathRelative};
125
126	/// Let the origin's spawned attach task run: a created broadcast becomes
127	/// routable asynchronously, shortly after `create_broadcast` returns.
128	async fn settle() {
129		for _ in 0..10 {
130			tokio::task::yield_now().await;
131		}
132	}
133
134	#[tokio::test]
135	async fn no_override_targets_catalog_broadcast() {
136		let origin = Origin::random().produce();
137		let _producer = origin
138			.create_broadcast("a/pub", moq_net::broadcast::Route::new().with_announce(true))
139			.unwrap();
140		settle().await;
141
142		let source = Source::new(origin.consume(), "a/pub");
143
144		// No reference and an empty reference both resolve to the catalog broadcast.
145		source.request(None).await.expect("catalog broadcast should resolve");
146		let empty = PathRelative::empty();
147		source
148			.request(Some(&empty))
149			.await
150			.expect("empty reference should resolve to the catalog broadcast");
151	}
152
153	#[tokio::test]
154	async fn subscribe_track_resolves_catalog_broadcast() {
155		let origin = Origin::random().produce();
156		let mut producer = origin
157			.create_broadcast("a/pub", moq_net::broadcast::Route::new().with_announce(true))
158			.unwrap();
159		// The track must exist for the subscription to resolve (SUBSCRIBE_OK).
160		let _video = producer.create_track("video", None).unwrap();
161		settle().await;
162
163		let source = Source::new(origin.consume(), "a/pub");
164		source
165			.subscribe_track(None, "video")
166			.await
167			.expect("catalog track should resolve");
168	}
169
170	#[tokio::test]
171	async fn self_reference_targets_catalog_broadcast() {
172		let origin = Origin::random().produce();
173		let mut producer = origin
174			.create_broadcast("a/pub", moq_net::broadcast::Route::new().with_announce(true))
175			.unwrap();
176		let _video = producer.create_track("video", None).unwrap();
177		settle().await;
178
179		let source = Source::new(origin.consume(), "a/pub");
180
181		// Walks back to the catalog's own path.
182		let rel = PathRelative::new("../pub");
183		source
184			.subscribe_track(Some(&rel), "video")
185			.await
186			.expect("self-reference should resolve to the catalog broadcast");
187
188		// Excess `..` walks past the (empty) origin root, treated as a self-reference.
189		let rel = PathRelative::new("../../..");
190		source
191			.subscribe_track(Some(&rel), "video")
192			.await
193			.expect("excess `..` should resolve to the catalog broadcast");
194	}
195
196	#[tokio::test]
197	async fn subscribe_track_resolves_referenced_broadcast() {
198		let origin = Origin::random().produce();
199
200		let _catalog = origin
201			.create_broadcast("a/pub", moq_net::broadcast::Route::new().with_announce(true))
202			.unwrap();
203
204		let mut referenced = origin
205			.create_broadcast("a/source", moq_net::broadcast::Route::new().with_announce(true))
206			.unwrap();
207		let _video = referenced.create_track("video", None).unwrap();
208		settle().await;
209
210		let source = Source::new(origin.consume(), "a/pub");
211
212		// The reference resolves to `a/source`, whose "video" track answers the subscribe.
213		let rel = PathRelative::new("../source");
214		source
215			.subscribe_track(Some(&rel), "video")
216			.await
217			.expect("referenced track should resolve");
218	}
219}