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 std::task::{Poll, ready};
13
14use moq_net::AsPath;
15
16/// The subscription side of an export: an origin and the path of the broadcast
17/// whose catalog drives it.
18///
19/// The catalog broadcast and every rendition (including ones whose catalog
20/// `broadcast` field references a sibling broadcast) resolve against `origin`,
21/// so a source can always follow a cross-broadcast reference. Build one with
22/// [`Source::new`].
23#[derive(Clone)]
24pub struct Source {
25	origin: moq_net::origin::Consumer,
26	path: moq_net::PathOwned,
27}
28
29impl Source {
30	/// A source rooted at `origin`, driven by the catalog of the broadcast at `path`.
31	///
32	/// `path` names the broadcast whose catalog is exported; a rendition's relative
33	/// `broadcast` reference is resolved against it. Both the catalog broadcast and any
34	/// referenced broadcast are fetched via
35	/// [`origin.request_broadcast`](moq_net::origin::Consumer::request_broadcast), so they
36	/// must be reachable through `origin` (by exact path, or served by a dynamic handler).
37	pub fn new(origin: moq_net::origin::Consumer, path: impl AsPath) -> Self {
38		Self {
39			origin,
40			path: path.as_path().to_owned(),
41		}
42	}
43
44	/// Resolve and subscribe to the catalog broadcast (the one at this source's path).
45	pub async fn broadcast(&self) -> crate::Result<moq_net::broadcast::Consumer> {
46		Ok(self.request_catalog().await?)
47	}
48
49	/// Subscribe to this broadcast's catalog.
50	///
51	/// The stream rejects a catalog carrying a `broadcast` reference that walks above the root
52	/// (see [`Error::EscapingBroadcast`](crate::Error::EscapingBroadcast)), so every snapshot a
53	/// consumer sees is one whose references all name a broadcast it may reach.
54	pub async fn catalog<E: crate::catalog::hang::CatalogExt>(
55		&self,
56		format: crate::catalog::CatalogFormat,
57	) -> crate::Result<crate::catalog::Consumer<E>> {
58		let broadcast = self.broadcast().await?;
59		crate::catalog::Consumer::new(&broadcast, format).await
60	}
61
62	/// Wait for the broadcast to come back after `ended`, the one an export was reading, closes.
63	///
64	/// A broadcast that stays up never returns, so a clean catalog end that leaves it announced
65	/// waits here until the caller's own deadline. Bound the wait with a timeout.
66	pub async fn returned(&self, ended: &moq_net::broadcast::Consumer) -> crate::Result<moq_net::broadcast::Consumer> {
67		ended.closed().await;
68		Ok(self.origin.routed_broadcast(&self.path).await?)
69	}
70
71	/// Begin resolving the catalog broadcast (the one at this source's path).
72	pub(crate) fn request_catalog(&self) -> kio::Pending<moq_net::origin::Requesting> {
73		self.origin.request_broadcast(&self.path)
74	}
75
76	/// Resolve a rendition's optional broadcast reference to an origin path.
77	///
78	/// A missing or empty reference returns the catalog broadcast path. A valid reference
79	/// may return the empty root path, which still names a broadcast. `None` means the
80	/// reference walked above the root and names nothing.
81	pub fn resolve_reference(&self, rel: Option<&moq_net::path::Relative<'_>>) -> Option<moq_net::PathOwned> {
82		match rel.filter(|rel| !rel.is_empty()) {
83			Some(rel) => self.path.try_resolve(rel),
84			None => Some(self.path.clone()),
85		}
86	}
87
88	/// The path of the broadcast a rendition's `broadcast` reference names.
89	///
90	/// The erroring counterpart to [`Self::resolve_reference`], for the consumer side, where
91	/// an escaping reference is a fault to report rather than a rendition to drop.
92	fn target(&self, rel: Option<&moq_net::path::Relative<'_>>) -> crate::Result<moq_net::PathOwned> {
93		self.resolve_reference(rel).ok_or_else(|| {
94			let rel = rel.map_or("", |rel| rel.as_str());
95			tracing::error!(rel, catalog = %self.path, "broadcast reference escapes the root");
96			crate::Error::EscapingBroadcast(rel.to_string())
97		})
98	}
99
100	/// Begin resolving the broadcast that serves a rendition, honoring an optional
101	/// cross-broadcast reference.
102	///
103	/// The broadcast is fetched from the origin, which deduplicates repeat requests for the same
104	/// reachable or dynamically served path so the catalog and every rendition share one upstream
105	/// subscription.
106	///
107	/// Fails with [`Error::EscapingBroadcast`](crate::Error::EscapingBroadcast) if `rel` walks
108	/// above the origin root, naming no broadcast.
109	pub(crate) fn request(
110		&self,
111		rel: Option<&moq_net::path::Relative<'_>>,
112	) -> crate::Result<kio::Pending<moq_net::origin::Requesting>> {
113		Ok(self.origin.request_broadcast(&self.target(rel)?))
114	}
115
116	/// The skipping counterpart to [`Self::request`], returning `None` when `rel` walks above
117	/// the origin root.
118	///
119	/// The exporters use it to drop a single bad rendition rather than fail the whole export.
120	/// [`Self::retain_valid`] already removes those renditions, so this is the second half of
121	/// the same policy rather than a different one.
122	pub(crate) fn try_request(
123		&self,
124		rel: Option<&moq_net::path::Relative<'_>>,
125	) -> Option<kio::Pending<moq_net::origin::Requesting>> {
126		Some(self.origin.request_broadcast(&self.resolve_reference(rel)?))
127	}
128
129	/// Remove renditions whose broadcast reference escapes above the origin root.
130	///
131	/// Every section carrying renditions must be listed here; one left out silently exempts
132	/// its renditions from the containment check, exactly as on the consumer side.
133	pub(crate) fn retain_valid<E: crate::catalog::hang::CatalogExt>(
134		&self,
135		catalog: &mut crate::catalog::hang::Catalog<E>,
136	) {
137		self.retain_valid_references("video", &mut catalog.video.renditions);
138		self.retain_valid_references("audio", &mut catalog.audio.renditions);
139		self.retain_valid_references("text", &mut catalog.text.renditions);
140		self.retain_valid_references("json", &mut catalog.json.tracks);
141		self.retain_valid_references("binary", &mut catalog.binary.tracks);
142	}
143
144	/// Remove media renditions whose broadcast reference escapes above the origin root.
145	pub(crate) fn retain_valid_media(&self, catalog: &mut hang::Catalog) {
146		self.retain_valid_references("video", &mut catalog.video.renditions);
147		self.retain_valid_references("audio", &mut catalog.audio.renditions);
148		self.retain_valid_references("text", &mut catalog.text.renditions);
149	}
150
151	fn retain_valid_references<C: BroadcastConfig>(
152		&self,
153		kind: &'static str,
154		renditions: &mut std::collections::BTreeMap<String, C>,
155	) {
156		renditions.retain(|name, config| {
157			let valid = self.resolve_reference(config.broadcast()).is_some();
158			if !valid {
159				tracing::warn!(
160					rendition = name,
161					kind,
162					"ignoring rendition whose broadcast escapes above the root"
163				);
164			}
165			valid
166		});
167	}
168
169	/// Resolve an optional cross-broadcast reference to its broadcast.
170	///
171	/// `rel` is a rendition's catalog `broadcast` field: `None` (or an empty reference)
172	/// resolves the catalog broadcast itself; anything else fetches the referenced
173	/// broadcast from the origin. Use it when you need the broadcast handle itself
174	/// (e.g. to FETCH individual groups) rather than a subscription.
175	///
176	/// A reference that escapes above the origin root is
177	/// [`Error::EscapingBroadcast`](crate::Error::EscapingBroadcast): the rendition names
178	/// no broadcast, so there is nothing to resolve.
179	pub async fn resolve(
180		&self,
181		rel: Option<&moq_net::path::Relative<'_>>,
182	) -> crate::Result<moq_net::broadcast::Consumer> {
183		Ok(self.request(rel)?.await?)
184	}
185
186	/// Start one broadcast request now and retain its result for repeated reads.
187	///
188	/// Unlike [`resolve`](Self::resolve), this issues the request without awaiting or polling.
189	/// A remote or dynamic handler may answer later. The binding never retries a failed request
190	/// or resolves the path again after a publisher is replaced.
191	///
192	/// This always looks up the path, including for a self-reference. Use [`Binding::new`]
193	/// when the intended broadcast is already in hand.
194	///
195	/// Rejects an escaping reference exactly as [`resolve`](Self::resolve) does.
196	pub fn bind(&self, rel: Option<&moq_net::path::Relative<'_>>) -> crate::Result<Binding> {
197		Ok(Binding(Bound::Requested(self.request(rel)?.into_inner())))
198	}
199
200	/// Resolve an optional cross-broadcast reference and subscribe to track `name`,
201	/// awaiting SUBSCRIBE_OK.
202	///
203	/// `rel` is a rendition's catalog `broadcast` field: `None` (or an empty reference)
204	/// subscribes on the catalog broadcast; anything else fetches the referenced broadcast
205	/// from the origin first. A reference that escapes above the origin root is
206	/// [`Error::EscapingBroadcast`](crate::Error::EscapingBroadcast).
207	///
208	/// This is the async counterpart to the poll-driven container exporters: consumers
209	/// that wrap a raw [`moq_net::track::Subscriber`] themselves (e.g. the WebRTC egress)
210	/// use it to honor cross-broadcast renditions without reimplementing the path math.
211	pub async fn subscribe_track(
212		&self,
213		rel: Option<&moq_net::path::Relative<'_>>,
214		name: &str,
215	) -> crate::Result<moq_net::track::Subscriber> {
216		let broadcast = self.request(rel)?.await?;
217		Ok(broadcast.track(name)?.subscribe(None).await?)
218	}
219}
220
221/// A held broadcast or the retained result of one eagerly issued broadcast request.
222///
223/// Reading the binding never looks up its path again, even after a failure or publisher
224/// replacement. Pending requests follow the origin's routing semantics; issuing a request
225/// does not establish an epoch relationship with a separate catalog broadcast.
226///
227/// Build one with [`Source::bind`], or with [`Binding::new`] for a broadcast already in hand.
228pub struct Binding(Bound);
229
230enum Bound {
231	/// A broadcast the caller already holds.
232	Ready(moq_net::broadcast::Consumer),
233	/// A request issued when the binding was made.
234	Requested(moq_net::origin::Requesting),
235}
236
237impl Binding {
238	/// Bind to a broadcast already in hand, for a reference that named it.
239	pub fn new(broadcast: moq_net::broadcast::Consumer) -> Self {
240		Self(Bound::Ready(broadcast))
241	}
242
243	/// Poll for the bound broadcast without blocking.
244	///
245	/// Concurrent polls and cancelled waits retain the same request and result.
246	pub fn poll_broadcast(&self, waiter: &kio::Waiter) -> Poll<crate::Result<moq_net::broadcast::Consumer>> {
247		match &self.0 {
248			Bound::Ready(broadcast) => Poll::Ready(Ok(broadcast.clone())),
249			Bound::Requested(request) => Poll::Ready(Ok(ready!(request.poll_ok(waiter))?)),
250		}
251	}
252
253	/// The bound broadcast, awaiting the origin's answer when it hasn't arrived yet.
254	///
255	/// Concurrent reads and cancelled waits retain the same request and result.
256	pub async fn broadcast(&self) -> crate::Result<moq_net::broadcast::Consumer> {
257		kio::wait(|waiter| self.poll_broadcast(waiter)).await
258	}
259}
260
261trait BroadcastConfig {
262	fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned>;
263}
264
265impl BroadcastConfig for hang::catalog::VideoConfig {
266	fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
267		self.broadcast.as_ref()
268	}
269}
270
271impl BroadcastConfig for hang::catalog::AudioConfig {
272	fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
273		self.broadcast.as_ref()
274	}
275}
276
277impl BroadcastConfig for hang::catalog::TextConfig {
278	fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
279		self.broadcast.as_ref()
280	}
281}
282
283impl BroadcastConfig for hang::catalog::JsonConfig {
284	fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
285		self.broadcast.as_ref()
286	}
287}
288
289impl BroadcastConfig for hang::catalog::BinaryConfig {
290	fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
291		self.broadcast.as_ref()
292	}
293}
294
295/// Test helper: build an origin producer, spawning its driver on the ambient runtime.
296#[cfg(test)]
297pub(crate) fn produce_origin() -> moq_net::origin::Producer {
298	let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
299	if tokio::runtime::Handle::try_current().is_ok() {
300		tokio::spawn(moq_net::time::run(driver));
301	} else {
302		// A sync test: nothing polls the driver, and dropping it would tear
303		// the origin down, so leak it and rely on the synchronous half.
304		std::mem::forget(driver);
305	}
306	producer
307}
308
309/// Test helper: serve `broadcast` on a throwaway origin's dynamic handler and return a
310/// [`Source`] rooted at it, so exporter tests that build a local broadcast can still resolve
311/// it by path. The origin is leaked so the broadcast stays reachable for the source's
312/// lifetime (harmless in a test binary).
313#[cfg(test)]
314pub(crate) fn announced(broadcast: &moq_net::broadcast::Consumer) -> Source {
315	let origin = produce_origin();
316	let dynamic = origin.dynamic("", Default::default()).unwrap();
317	let served = broadcast.clone();
318	tokio::spawn(async move {
319		while let Ok(request) = dynamic.requested_broadcast().await {
320			request.accept(served.clone());
321		}
322	});
323	let source = Source::new(origin.consume(), "test");
324	Box::leak(Box::new(origin));
325	source
326}
327
328#[cfg(test)]
329mod tests {
330	use super::*;
331	use hang::catalog::{H264, VideoConfig};
332	use moq_net::path::Relative;
333
334	/// Let the origin's driver run the fronts that requests and announcements
335	/// started: they serve asynchronously, shortly after the call returns.
336	async fn settle() {
337		for _ in 0..10 {
338			tokio::task::yield_now().await;
339		}
340	}
341
342	#[tokio::test]
343	async fn binding_retains_a_pending_request_across_cancelled_and_repeated_reads() {
344		let origin = produce_origin();
345		let dynamic = origin.dynamic("", Default::default()).unwrap();
346		let source = Source::new(origin.consume(), "live");
347		let binding = source.bind(None).unwrap();
348		let request = dynamic.requested_broadcast().await.unwrap();
349
350		assert!(
351			binding.poll_broadcast(&kio::Waiter::noop()).is_pending(),
352			"the handler has not answered"
353		);
354
355		tokio::select! {
356			biased;
357			_ = binding.broadcast() => panic!("the handler has not answered"),
358			_ = std::future::ready(()) => {}
359		}
360
361		let producer = moq_net::broadcast::Info::default().produce();
362		let (first, second, ()) = tokio::join!(binding.broadcast(), binding.broadcast(), async {
363			request.accept(producer.consume());
364		});
365		let first = first.unwrap();
366		assert!(!first.is_closed());
367		assert!(first.is_clone(&second.unwrap()));
368		drop((producer, dynamic));
369		first.closed().await;
370		assert!(first.is_clone(&binding.broadcast().await.unwrap()));
371	}
372
373	#[tokio::test]
374	async fn failed_binding_does_not_retry_a_later_publisher() {
375		let origin = produce_origin();
376		let source = Source::new(origin.consume(), "live");
377		let binding = source.bind(None).unwrap();
378		assert!(binding.broadcast().await.is_err());
379		let _publisher = origin.publish("live", Default::default()).unwrap();
380		settle().await;
381		assert!(!source.broadcast().await.unwrap().is_closed());
382		assert!(binding.broadcast().await.is_err());
383	}
384
385	#[tokio::test]
386	async fn returned_waits_for_a_new_broadcast_after_the_old_one_ends() {
387		let origin = produce_origin();
388		let first = origin.publish("live", Default::default()).unwrap();
389		settle().await;
390		let source = Source::new(origin.consume(), "live");
391		let ended = source.broadcast().await.unwrap();
392
393		let returned = source.returned(&ended);
394		tokio::pin!(returned);
395		tokio::select! {
396			biased;
397			_ = &mut returned => panic!("the broadcast is still up"),
398			_ = settle() => {}
399		}
400
401		// Gone, and nothing serves the path yet.
402		drop(first);
403		tokio::select! {
404			biased;
405			_ = &mut returned => panic!("nothing has published the path again"),
406			_ = settle() => {}
407		}
408
409		let _second = origin.publish("live", Default::default()).unwrap();
410		let back = returned.await.unwrap();
411		assert!(!back.is_closed());
412		assert!(!back.is_clone(&ended));
413	}
414
415	#[tokio::test]
416	async fn no_override_targets_catalog_broadcast() {
417		let origin = produce_origin();
418		let _producer = origin.create_broadcast("a/pub").unwrap();
419		_producer.announce(Default::default()).unwrap();
420		settle().await;
421
422		let source = Source::new(origin.consume(), "a/pub");
423
424		// No reference and an empty reference both resolve to the catalog broadcast.
425		source
426			.request(None)
427			.expect("no reference is always resolvable")
428			.await
429			.expect("catalog broadcast should resolve");
430		let empty = Relative::empty();
431		source
432			.request(Some(&empty))
433			.expect("empty reference is always resolvable")
434			.await
435			.expect("empty reference should resolve to the catalog broadcast");
436	}
437
438	#[tokio::test]
439	async fn subscribe_track_resolves_catalog_broadcast() {
440		let origin = produce_origin();
441		let producer = origin.create_broadcast("a/pub").unwrap();
442		producer.announce(Default::default()).unwrap();
443		// The track must exist for the subscription to resolve (SUBSCRIBE_OK).
444		let _video = producer.create_track("video", None).unwrap();
445		settle().await;
446
447		let source = Source::new(origin.consume(), "a/pub");
448		source
449			.subscribe_track(None, "video")
450			.await
451			.expect("catalog track should resolve");
452	}
453
454	#[tokio::test]
455	async fn self_reference_targets_catalog_broadcast() {
456		let origin = produce_origin();
457		let producer = origin.create_broadcast("a/pub").unwrap();
458		producer.announce(Default::default()).unwrap();
459		let _video = producer.create_track("video", None).unwrap();
460		settle().await;
461
462		let source = Source::new(origin.consume(), "a/pub");
463
464		// Names the catalog within its own parent.
465		let rel = Relative::new("./pub");
466		source
467			.subscribe_track(Some(&rel), "video")
468			.await
469			.expect("self-reference should resolve to the catalog broadcast");
470	}
471
472	#[tokio::test]
473	async fn escaping_reference_is_rejected() {
474		let origin = produce_origin();
475
476		let catalog = origin.create_broadcast("a/pub").unwrap();
477		catalog.announce(Default::default()).unwrap();
478		let _catalog_video = catalog.create_track("video", None).unwrap();
479
480		// The broadcast an escaping reference would land on if it clamped at the root.
481		let clamped = origin.create_broadcast("elsewhere").unwrap();
482		clamped.announce(Default::default()).unwrap();
483		let _clamped_video = clamped.create_track("video", None).unwrap();
484		settle().await;
485
486		let source = Source::new(origin.consume(), "a/pub");
487
488		// A reference resolves against the catalog's parent (`a`), so a second `..` walks
489		// above the root. A lone `..` stops at the root, which still names a broadcast, so
490		// it is not in this set.
491		for reference in ["../../elsewhere", "../..", "../../.."] {
492			let rel = Relative::new(reference);
493			assert!(
494				source.resolve_reference(Some(&rel)).is_none(),
495				"{reference} should escape"
496			);
497			assert!(source.request(Some(&rel)).is_err(), "{reference} should be rejected");
498
499			// Neither `elsewhere` nor the catalog broadcast answers it: the rendition names
500			// no broadcast at all.
501			match source.resolve(Some(&rel)).await {
502				Err(crate::Error::EscapingBroadcast(_)) => {}
503				Err(err) => panic!("{reference} failed with the wrong error: {err:?}"),
504				Ok(_) => panic!("{reference} should not resolve to any broadcast"),
505			}
506			match source.subscribe_track(Some(&rel), "video").await {
507				Err(crate::Error::EscapingBroadcast(_)) => {}
508				Err(err) => panic!("{reference} failed with the wrong error: {err:?}"),
509				Ok(_) => panic!("{reference} should not resolve to any broadcast"),
510			}
511		}
512	}
513
514	#[test]
515	fn escaping_rendition_is_removed_while_valid_sibling_remains() {
516		let origin = produce_origin();
517		let source = Source::new(origin.consume(), "a/pub");
518		let mut escaped = VideoConfig::new(H264 {
519			profile: 0x42,
520			constraints: 0,
521			level: 0x1e,
522			inline: false,
523		});
524		escaped.broadcast = Some(Relative::new("../../source").to_owned());
525		let mut sibling = escaped.clone();
526		sibling.broadcast = Some(Relative::new("./source").to_owned());
527
528		let mut catalog = hang::Catalog::default();
529		catalog.video.renditions.insert("escaped".to_string(), escaped);
530		catalog.video.renditions.insert("sibling".to_string(), sibling);
531		source.retain_valid_media(&mut catalog);
532
533		assert!(!catalog.video.renditions.contains_key("escaped"));
534		assert!(catalog.video.renditions.contains_key("sibling"));
535	}
536
537	/// The filter covers every section carrying renditions, not just the media ones. Text
538	/// is the section that only exists on one side of the containment check by default, so
539	/// it is the one that silently goes unchecked when the two sides drift.
540	#[test]
541	fn escaping_text_rendition_is_removed() {
542		let origin = produce_origin();
543		let source = Source::new(origin.consume(), "a/pub");
544
545		let mut escaped = hang::catalog::TextConfig::new(hang::catalog::TextFormat::Vtt);
546		escaped.broadcast = Some(Relative::new("../../source").to_owned());
547		let mut sibling = escaped.clone();
548		sibling.broadcast = Some(Relative::new("./source").to_owned());
549
550		let mut catalog = hang::Catalog::default();
551		catalog.text.renditions.insert("escaped".to_string(), escaped);
552		catalog.text.renditions.insert("sibling".to_string(), sibling);
553		source.retain_valid_media(&mut catalog);
554
555		assert!(!catalog.text.renditions.contains_key("escaped"));
556		assert!(catalog.text.renditions.contains_key("sibling"));
557	}
558
559	#[tokio::test]
560	async fn subscribe_track_resolves_referenced_broadcast() {
561		let origin = produce_origin();
562
563		let _catalog = origin.create_broadcast("a/pub").unwrap();
564		_catalog.announce(Default::default()).unwrap();
565
566		let referenced = origin.create_broadcast("a/source").unwrap();
567		referenced.announce(Default::default()).unwrap();
568		let _video = referenced.create_track("video", None).unwrap();
569		settle().await;
570
571		let source = Source::new(origin.consume(), "a/pub");
572
573		// The reference resolves to `a/source`, whose "video" track answers the subscribe.
574		let rel = Relative::new("./source");
575		source
576			.subscribe_track(Some(&rel), "video")
577			.await
578			.expect("referenced track should resolve");
579	}
580
581	#[tokio::test]
582	async fn dot_resolves_output_parent() {
583		let origin = produce_origin();
584
585		let _catalog = origin.create_broadcast("a/source/transcode").unwrap();
586		_catalog.announce(Default::default()).unwrap();
587
588		let referenced = origin.create_broadcast("a/source").unwrap();
589		referenced.announce(Default::default()).unwrap();
590		let _video = referenced.create_track("video", None).unwrap();
591		settle().await;
592
593		let source = Source::new(origin.consume(), "a/source/transcode");
594		let rel = Relative::new(".");
595		source
596			.subscribe_track(Some(&rel), "video")
597			.await
598			.expect("dot should resolve to the catalog broadcast's parent");
599	}
600
601	#[tokio::test]
602	async fn dot_resolves_one_segment_catalog_to_root() {
603		let origin = produce_origin();
604
605		let _catalog = origin.create_broadcast("top").unwrap();
606		_catalog.announce(Default::default()).unwrap();
607
608		let root = origin.create_broadcast("").unwrap();
609		root.announce(Default::default()).unwrap();
610		let _video = root.create_track("video", None).unwrap();
611		settle().await;
612
613		let source = Source::new(origin.consume(), "top");
614		let rel = Relative::new(".");
615		source
616			.subscribe_track(Some(&rel), "video")
617			.await
618			.expect("dot should resolve to the empty root broadcast");
619	}
620}