mod routes;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use axum::Router;
use crate::export::{Broadcaster, Config};
const RESOLVE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone)]
pub struct Server {
inner: Arc<Inner>,
}
struct Inner {
origin: moq_net::origin::Consumer,
config: Config,
broadcasters: Mutex<HashMap<String, Arc<Broadcaster>>>,
}
impl Server {
pub fn new(origin: moq_net::origin::Consumer, config: Config) -> Self {
Self {
inner: Arc::new(Inner {
origin,
config,
broadcasters: Mutex::new(HashMap::new()),
}),
}
}
pub fn router(&self) -> Router {
routes::router(self.clone())
}
pub async fn broadcaster(&self, name: &str) -> Option<Arc<Broadcaster>> {
{
let mut broadcasters = self.inner.broadcasters.lock().unwrap();
if let Some(existing) = broadcasters.get(name) {
if !existing.is_closed() {
return Some(existing.clone());
}
broadcasters.remove(name);
}
}
tokio::time::timeout(RESOLVE_TIMEOUT, self.inner.origin.announced_broadcast(name))
.await
.ok()
.flatten()?;
let source = moq_mux::Source::new(self.inner.origin.consume(), name);
let broadcaster = Broadcaster::new(source, self.inner.config.clone())
.await
.map_err(|err| tracing::warn!(%err, %name, "failed to resolve broadcast catalog"))
.ok()?;
let mut broadcasters = self.inner.broadcasters.lock().unwrap();
if let Some(existing) = broadcasters.get(name) {
if !existing.is_closed() {
return Some(existing.clone());
}
broadcasters.remove(name);
}
let name = name.to_string();
broadcasters.insert(name.clone(), broadcaster.clone());
tokio::spawn(evict_closed(self.inner.clone(), name, broadcaster.clone()));
Some(broadcaster)
}
}
async fn evict_closed(inner: Arc<Inner>, name: String, broadcaster: Arc<Broadcaster>) {
broadcaster.closed().await;
let mut broadcasters = inner.broadcasters.lock().unwrap();
if broadcasters
.get(&name)
.is_some_and(|current| Arc::ptr_eq(current, &broadcaster))
{
broadcasters.remove(&name);
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn settle() {
for _ in 0..10 {
tokio::task::yield_now().await;
}
}
#[tokio::test(start_paused = true)]
async fn router_middleware_gates_requests() {
use axum::body::Body;
use axum::extract::Request;
use axum::http::{StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::Response;
use tower::ServiceExt;
async fn gate(req: Request, next: Next) -> Result<Response, StatusCode> {
match req.headers().get(header::AUTHORIZATION) {
Some(token) if token == "secret" => Ok(next.run(req).await),
_ => Err(StatusCode::UNAUTHORIZED),
}
}
let origin = moq_net::Origin::random().produce();
let server = Server::new(origin.consume(), Config::default());
let app = server.router().layer(middleware::from_fn(gate));
let denied = app
.clone()
.oneshot(Request::builder().uri("/live/master.m3u8").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(denied.status(), StatusCode::UNAUTHORIZED);
let allowed = app
.oneshot(
Request::builder()
.uri("/live/master.m3u8")
.header(header::AUTHORIZATION, "secret")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(allowed.status(), StatusCode::NOT_FOUND);
assert_eq!(allowed.headers()[header::CACHE_CONTROL], "no-store");
}
async fn closed_broadcaster() -> Arc<Broadcaster> {
let origin = moq_net::Origin::random().produce();
let mut producer = origin
.create_broadcast("gone", moq_net::broadcast::Route::new().with_announce(true))
.expect("publish allowed");
settle().await;
let source = moq_mux::Source::new(origin.consume(), "gone");
let broadcaster = Broadcaster::new(source, Config::default())
.await
.expect("catalog broadcast resolves while announced");
producer.finish();
settle().await;
broadcaster
}
#[tokio::test]
async fn broadcaster_replaces_finished_cached_instance() {
let origin = moq_net::Origin::random().produce();
let server = Server::new(origin.consume(), Config::default());
let stale = closed_broadcaster().await;
server
.inner
.broadcasters
.lock()
.unwrap()
.insert("live".to_string(), stale.clone());
let _producer = origin
.create_broadcast("live", moq_net::broadcast::Route::new().with_announce(true))
.expect("publish allowed");
settle().await;
let fresh = server.broadcaster("live").await.expect("broadcast announced");
assert!(!Arc::ptr_eq(&stale, &fresh));
assert!(server.inner.broadcasters.lock().unwrap().contains_key("live"));
}
#[tokio::test]
async fn eviction_keeps_newer_cached_instance() {
let origin = moq_net::Origin::random().produce();
let server = Server::new(origin.consume(), Config::default());
let old = closed_broadcaster().await;
let mut new_producer = origin
.create_broadcast("live", moq_net::broadcast::Route::new().with_announce(true))
.expect("publish allowed");
settle().await;
let new = Broadcaster::new(moq_mux::Source::new(origin.consume(), "live"), Config::default())
.await
.expect("catalog broadcast resolves while announced");
server
.inner
.broadcasters
.lock()
.unwrap()
.insert("live".to_string(), new.clone());
evict_closed(server.inner.clone(), "live".to_string(), old).await;
let cached = server.inner.broadcasters.lock().unwrap().get("live").cloned();
assert!(cached.is_some_and(|cached| Arc::ptr_eq(&cached, &new)));
new_producer.finish();
}
}