1use std::task::{Poll, ready};
13
14use moq_net::AsPath;
15
16#[derive(Clone)]
24pub struct Source {
25 origin: moq_net::origin::Consumer,
26 path: moq_net::PathOwned,
27}
28
29impl Source {
30 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 pub async fn broadcast(&self) -> crate::Result<moq_net::broadcast::Consumer> {
46 Ok(self.request_catalog().await?)
47 }
48
49 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 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 pub(crate) fn request_catalog(&self) -> kio::Pending<moq_net::origin::Requesting> {
73 self.origin.request_broadcast(&self.path)
74 }
75
76 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 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 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 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 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 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 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 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 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
221pub struct Binding(Bound);
229
230enum Bound {
231 Ready(moq_net::broadcast::Consumer),
233 Requested(moq_net::origin::Requesting),
235}
236
237impl Binding {
238 pub fn new(broadcast: moq_net::broadcast::Consumer) -> Self {
240 Self(Bound::Ready(broadcast))
241 }
242
243 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 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#[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 std::mem::forget(driver);
305 }
306 producer
307}
308
309#[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 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 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 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 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 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 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 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 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 #[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 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}