use std::task::{Poll, ready};
use moq_net::AsPath;
#[derive(Clone)]
pub struct Source {
origin: moq_net::origin::Consumer,
path: moq_net::PathOwned,
}
impl Source {
pub fn new(origin: moq_net::origin::Consumer, path: impl AsPath) -> Self {
Self {
origin,
path: path.as_path().to_owned(),
}
}
pub async fn broadcast(&self) -> crate::Result<moq_net::broadcast::Consumer> {
Ok(self.request_catalog().await?)
}
pub async fn catalog<E: crate::catalog::hang::CatalogExt>(
&self,
format: crate::catalog::CatalogFormat,
) -> crate::Result<crate::catalog::Consumer<E>> {
let broadcast = self.broadcast().await?;
crate::catalog::Consumer::new(&broadcast, format).await
}
pub async fn returned(&self, ended: &moq_net::broadcast::Consumer) -> crate::Result<moq_net::broadcast::Consumer> {
ended.closed().await;
Ok(self.origin.routed_broadcast(&self.path).await?)
}
pub(crate) fn request_catalog(&self) -> kio::Pending<moq_net::origin::Requesting> {
self.origin.request_broadcast(&self.path)
}
pub fn resolve_reference(&self, rel: Option<&moq_net::path::Relative<'_>>) -> Option<moq_net::PathOwned> {
match rel.filter(|rel| !rel.is_empty()) {
Some(rel) => self.path.try_resolve(rel),
None => Some(self.path.clone()),
}
}
fn target(&self, rel: Option<&moq_net::path::Relative<'_>>) -> crate::Result<moq_net::PathOwned> {
self.resolve_reference(rel).ok_or_else(|| {
let rel = rel.map_or("", |rel| rel.as_str());
tracing::error!(rel, catalog = %self.path, "broadcast reference escapes the root");
crate::Error::EscapingBroadcast(rel.to_string())
})
}
pub(crate) fn request(
&self,
rel: Option<&moq_net::path::Relative<'_>>,
) -> crate::Result<kio::Pending<moq_net::origin::Requesting>> {
Ok(self.origin.request_broadcast(&self.target(rel)?))
}
pub(crate) fn try_request(
&self,
rel: Option<&moq_net::path::Relative<'_>>,
) -> Option<kio::Pending<moq_net::origin::Requesting>> {
Some(self.origin.request_broadcast(&self.resolve_reference(rel)?))
}
pub(crate) fn retain_valid<E: crate::catalog::hang::CatalogExt>(
&self,
catalog: &mut crate::catalog::hang::Catalog<E>,
) {
self.retain_valid_references("video", &mut catalog.video.renditions);
self.retain_valid_references("audio", &mut catalog.audio.renditions);
self.retain_valid_references("text", &mut catalog.text.renditions);
self.retain_valid_references("json", &mut catalog.json.tracks);
self.retain_valid_references("binary", &mut catalog.binary.tracks);
}
pub(crate) fn retain_valid_media(&self, catalog: &mut hang::Catalog) {
self.retain_valid_references("video", &mut catalog.video.renditions);
self.retain_valid_references("audio", &mut catalog.audio.renditions);
self.retain_valid_references("text", &mut catalog.text.renditions);
}
fn retain_valid_references<C: BroadcastConfig>(
&self,
kind: &'static str,
renditions: &mut std::collections::BTreeMap<String, C>,
) {
renditions.retain(|name, config| {
let valid = self.resolve_reference(config.broadcast()).is_some();
if !valid {
tracing::warn!(
rendition = name,
kind,
"ignoring rendition whose broadcast escapes above the root"
);
}
valid
});
}
pub async fn resolve(
&self,
rel: Option<&moq_net::path::Relative<'_>>,
) -> crate::Result<moq_net::broadcast::Consumer> {
Ok(self.request(rel)?.await?)
}
pub fn bind(&self, rel: Option<&moq_net::path::Relative<'_>>) -> crate::Result<Binding> {
Ok(Binding(Bound::Requested(self.request(rel)?.into_inner())))
}
pub async fn subscribe_track(
&self,
rel: Option<&moq_net::path::Relative<'_>>,
name: &str,
) -> crate::Result<moq_net::track::Subscriber> {
let broadcast = self.request(rel)?.await?;
Ok(broadcast.track(name)?.subscribe(None).await?)
}
}
pub struct Binding(Bound);
enum Bound {
Ready(moq_net::broadcast::Consumer),
Requested(moq_net::origin::Requesting),
}
impl Binding {
pub fn new(broadcast: moq_net::broadcast::Consumer) -> Self {
Self(Bound::Ready(broadcast))
}
pub fn poll_broadcast(&self, waiter: &kio::Waiter) -> Poll<crate::Result<moq_net::broadcast::Consumer>> {
match &self.0 {
Bound::Ready(broadcast) => Poll::Ready(Ok(broadcast.clone())),
Bound::Requested(request) => Poll::Ready(Ok(ready!(request.poll_ok(waiter))?)),
}
}
pub async fn broadcast(&self) -> crate::Result<moq_net::broadcast::Consumer> {
kio::wait(|waiter| self.poll_broadcast(waiter)).await
}
}
trait BroadcastConfig {
fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned>;
}
impl BroadcastConfig for hang::catalog::VideoConfig {
fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
self.broadcast.as_ref()
}
}
impl BroadcastConfig for hang::catalog::AudioConfig {
fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
self.broadcast.as_ref()
}
}
impl BroadcastConfig for hang::catalog::TextConfig {
fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
self.broadcast.as_ref()
}
}
impl BroadcastConfig for hang::catalog::JsonConfig {
fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
self.broadcast.as_ref()
}
}
impl BroadcastConfig for hang::catalog::BinaryConfig {
fn broadcast(&self) -> Option<&moq_net::path::RelativeOwned> {
self.broadcast.as_ref()
}
}
#[cfg(test)]
pub(crate) fn produce_origin() -> moq_net::origin::Producer {
let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
if tokio::runtime::Handle::try_current().is_ok() {
tokio::spawn(moq_net::time::run(driver));
} else {
std::mem::forget(driver);
}
producer
}
#[cfg(test)]
pub(crate) fn announced(broadcast: &moq_net::broadcast::Consumer) -> Source {
let origin = produce_origin();
let dynamic = origin.dynamic("", Default::default()).unwrap();
let served = broadcast.clone();
tokio::spawn(async move {
while let Ok(request) = dynamic.requested_broadcast().await {
request.accept(served.clone());
}
});
let source = Source::new(origin.consume(), "test");
Box::leak(Box::new(origin));
source
}
#[cfg(test)]
mod tests {
use super::*;
use hang::catalog::{H264, VideoConfig};
use moq_net::path::Relative;
async fn settle() {
for _ in 0..10 {
tokio::task::yield_now().await;
}
}
#[tokio::test]
async fn binding_retains_a_pending_request_across_cancelled_and_repeated_reads() {
let origin = produce_origin();
let dynamic = origin.dynamic("", Default::default()).unwrap();
let source = Source::new(origin.consume(), "live");
let binding = source.bind(None).unwrap();
let request = dynamic.requested_broadcast().await.unwrap();
assert!(
binding.poll_broadcast(&kio::Waiter::noop()).is_pending(),
"the handler has not answered"
);
tokio::select! {
biased;
_ = binding.broadcast() => panic!("the handler has not answered"),
_ = std::future::ready(()) => {}
}
let producer = moq_net::broadcast::Info::default().produce();
let (first, second, ()) = tokio::join!(binding.broadcast(), binding.broadcast(), async {
request.accept(producer.consume());
});
let first = first.unwrap();
assert!(!first.is_closed());
assert!(first.is_clone(&second.unwrap()));
drop((producer, dynamic));
first.closed().await;
assert!(first.is_clone(&binding.broadcast().await.unwrap()));
}
#[tokio::test]
async fn failed_binding_does_not_retry_a_later_publisher() {
let origin = produce_origin();
let source = Source::new(origin.consume(), "live");
let binding = source.bind(None).unwrap();
assert!(binding.broadcast().await.is_err());
let _publisher = origin.publish("live", Default::default()).unwrap();
settle().await;
assert!(!source.broadcast().await.unwrap().is_closed());
assert!(binding.broadcast().await.is_err());
}
#[tokio::test]
async fn returned_waits_for_a_new_broadcast_after_the_old_one_ends() {
let origin = produce_origin();
let first = origin.publish("live", Default::default()).unwrap();
settle().await;
let source = Source::new(origin.consume(), "live");
let ended = source.broadcast().await.unwrap();
let returned = source.returned(&ended);
tokio::pin!(returned);
tokio::select! {
biased;
_ = &mut returned => panic!("the broadcast is still up"),
_ = settle() => {}
}
drop(first);
tokio::select! {
biased;
_ = &mut returned => panic!("nothing has published the path again"),
_ = settle() => {}
}
let _second = origin.publish("live", Default::default()).unwrap();
let back = returned.await.unwrap();
assert!(!back.is_closed());
assert!(!back.is_clone(&ended));
}
#[tokio::test]
async fn no_override_targets_catalog_broadcast() {
let origin = produce_origin();
let _producer = origin.create_broadcast("a/pub").unwrap();
_producer.announce(Default::default()).unwrap();
settle().await;
let source = Source::new(origin.consume(), "a/pub");
source
.request(None)
.expect("no reference is always resolvable")
.await
.expect("catalog broadcast should resolve");
let empty = Relative::empty();
source
.request(Some(&empty))
.expect("empty reference is always resolvable")
.await
.expect("empty reference should resolve to the catalog broadcast");
}
#[tokio::test]
async fn subscribe_track_resolves_catalog_broadcast() {
let origin = produce_origin();
let producer = origin.create_broadcast("a/pub").unwrap();
producer.announce(Default::default()).unwrap();
let _video = producer.create_track("video", None).unwrap();
settle().await;
let source = Source::new(origin.consume(), "a/pub");
source
.subscribe_track(None, "video")
.await
.expect("catalog track should resolve");
}
#[tokio::test]
async fn self_reference_targets_catalog_broadcast() {
let origin = produce_origin();
let producer = origin.create_broadcast("a/pub").unwrap();
producer.announce(Default::default()).unwrap();
let _video = producer.create_track("video", None).unwrap();
settle().await;
let source = Source::new(origin.consume(), "a/pub");
let rel = Relative::new("./pub");
source
.subscribe_track(Some(&rel), "video")
.await
.expect("self-reference should resolve to the catalog broadcast");
}
#[tokio::test]
async fn escaping_reference_is_rejected() {
let origin = produce_origin();
let catalog = origin.create_broadcast("a/pub").unwrap();
catalog.announce(Default::default()).unwrap();
let _catalog_video = catalog.create_track("video", None).unwrap();
let clamped = origin.create_broadcast("elsewhere").unwrap();
clamped.announce(Default::default()).unwrap();
let _clamped_video = clamped.create_track("video", None).unwrap();
settle().await;
let source = Source::new(origin.consume(), "a/pub");
for reference in ["../../elsewhere", "../..", "../../.."] {
let rel = Relative::new(reference);
assert!(
source.resolve_reference(Some(&rel)).is_none(),
"{reference} should escape"
);
assert!(source.request(Some(&rel)).is_err(), "{reference} should be rejected");
match source.resolve(Some(&rel)).await {
Err(crate::Error::EscapingBroadcast(_)) => {}
Err(err) => panic!("{reference} failed with the wrong error: {err:?}"),
Ok(_) => panic!("{reference} should not resolve to any broadcast"),
}
match source.subscribe_track(Some(&rel), "video").await {
Err(crate::Error::EscapingBroadcast(_)) => {}
Err(err) => panic!("{reference} failed with the wrong error: {err:?}"),
Ok(_) => panic!("{reference} should not resolve to any broadcast"),
}
}
}
#[test]
fn escaping_rendition_is_removed_while_valid_sibling_remains() {
let origin = produce_origin();
let source = Source::new(origin.consume(), "a/pub");
let mut escaped = VideoConfig::new(H264 {
profile: 0x42,
constraints: 0,
level: 0x1e,
inline: false,
});
escaped.broadcast = Some(Relative::new("../../source").to_owned());
let mut sibling = escaped.clone();
sibling.broadcast = Some(Relative::new("./source").to_owned());
let mut catalog = hang::Catalog::default();
catalog.video.renditions.insert("escaped".to_string(), escaped);
catalog.video.renditions.insert("sibling".to_string(), sibling);
source.retain_valid_media(&mut catalog);
assert!(!catalog.video.renditions.contains_key("escaped"));
assert!(catalog.video.renditions.contains_key("sibling"));
}
#[test]
fn escaping_text_rendition_is_removed() {
let origin = produce_origin();
let source = Source::new(origin.consume(), "a/pub");
let mut escaped = hang::catalog::TextConfig::new(hang::catalog::TextFormat::Vtt);
escaped.broadcast = Some(Relative::new("../../source").to_owned());
let mut sibling = escaped.clone();
sibling.broadcast = Some(Relative::new("./source").to_owned());
let mut catalog = hang::Catalog::default();
catalog.text.renditions.insert("escaped".to_string(), escaped);
catalog.text.renditions.insert("sibling".to_string(), sibling);
source.retain_valid_media(&mut catalog);
assert!(!catalog.text.renditions.contains_key("escaped"));
assert!(catalog.text.renditions.contains_key("sibling"));
}
#[tokio::test]
async fn subscribe_track_resolves_referenced_broadcast() {
let origin = produce_origin();
let _catalog = origin.create_broadcast("a/pub").unwrap();
_catalog.announce(Default::default()).unwrap();
let referenced = origin.create_broadcast("a/source").unwrap();
referenced.announce(Default::default()).unwrap();
let _video = referenced.create_track("video", None).unwrap();
settle().await;
let source = Source::new(origin.consume(), "a/pub");
let rel = Relative::new("./source");
source
.subscribe_track(Some(&rel), "video")
.await
.expect("referenced track should resolve");
}
#[tokio::test]
async fn dot_resolves_output_parent() {
let origin = produce_origin();
let _catalog = origin.create_broadcast("a/source/transcode").unwrap();
_catalog.announce(Default::default()).unwrap();
let referenced = origin.create_broadcast("a/source").unwrap();
referenced.announce(Default::default()).unwrap();
let _video = referenced.create_track("video", None).unwrap();
settle().await;
let source = Source::new(origin.consume(), "a/source/transcode");
let rel = Relative::new(".");
source
.subscribe_track(Some(&rel), "video")
.await
.expect("dot should resolve to the catalog broadcast's parent");
}
#[tokio::test]
async fn dot_resolves_one_segment_catalog_to_root() {
let origin = produce_origin();
let _catalog = origin.create_broadcast("top").unwrap();
_catalog.announce(Default::default()).unwrap();
let root = origin.create_broadcast("").unwrap();
root.announce(Default::default()).unwrap();
let _video = root.create_track("video", None).unwrap();
settle().await;
let source = Source::new(origin.consume(), "top");
let rel = Relative::new(".");
source
.subscribe_track(Some(&rel), "video")
.await
.expect("dot should resolve to the empty root broadcast");
}
}