mod routes;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use axum::Router;
use crate::export::{Broadcaster, Config, Handle};
const RESOLVE_TIMEOUT: Duration = Duration::from_secs(5);
struct AbortOnDrop(tokio::task::AbortHandle);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}
struct Entry {
id: u64,
handle: Handle,
_driver: AbortOnDrop,
}
#[derive(Clone)]
pub struct Server {
inner: Arc<Inner>,
}
struct Inner {
origin: moq_net::OriginConsumer,
config: Config,
broadcasters: Mutex<HashMap<String, Entry>>,
next_id: AtomicU64,
}
impl Server {
pub fn new(origin: moq_net::OriginConsumer, config: Config) -> Self {
Self {
inner: Arc::new(Inner {
origin,
config,
broadcasters: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(0),
}),
}
}
pub fn router(&self) -> Router {
routes::router(self.clone())
}
pub(crate) async fn handle(&self, name: &str) -> Option<Handle> {
if let Some(handle) = self.cached(name) {
return Some(handle);
}
let broadcast = tokio::time::timeout(RESOLVE_TIMEOUT, self.inner.origin.announced_broadcast(name))
.await
.ok()
.flatten()?;
let mut broadcasters = self.inner.broadcasters.lock().unwrap();
if let Some(entry) = broadcasters.get(name) {
return Some(entry.handle.clone());
}
let mut broadcaster = match Broadcaster::new(broadcast, self.inner.config.clone()) {
Ok(broadcaster) => broadcaster,
Err(err) => {
tracing::warn!(%name, %err, "failed to start HLS export");
return None;
}
};
let handle = broadcaster.handle();
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
let inner = self.inner.clone();
let name = name.to_string();
let driver = tokio::spawn({
let name = name.clone();
async move {
broadcaster.run().await;
evict(&inner, &name, id);
}
});
broadcasters.insert(
name,
Entry {
id,
handle: handle.clone(),
_driver: AbortOnDrop(driver.abort_handle()),
},
);
Some(handle)
}
fn cached(&self, name: &str) -> Option<Handle> {
let broadcasters = self.inner.broadcasters.lock().unwrap();
broadcasters.get(name).map(|entry| entry.handle.clone())
}
}
fn evict(inner: &Inner, name: &str, id: u64) {
let mut broadcasters = inner.broadcasters.lock().unwrap();
if broadcasters.get(name).is_some_and(|entry| entry.id == id) {
broadcasters.remove(name);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn live_broadcast() -> (moq_net::BroadcastProducer, moq_net::TrackProducer) {
let mut producer = moq_net::Broadcast::new().produce();
let catalog = producer
.create_track(moq_net::Track {
name: "catalog.json".to_string(),
priority: 0,
})
.unwrap();
(producer, catalog)
}
fn seed_entry(server: &Server, id: u64) -> (moq_net::BroadcastProducer, moq_net::TrackProducer) {
let (producer, catalog) = live_broadcast();
let mut broadcaster = Broadcaster::new(producer.consume(), Config::default()).unwrap();
let handle = broadcaster.handle();
let driver = tokio::spawn(async move { broadcaster.run().await });
server.inner.broadcasters.lock().unwrap().insert(
"live".to_string(),
Entry {
id,
handle,
_driver: AbortOnDrop(driver.abort_handle()),
},
);
(producer, catalog)
}
#[tokio::test]
async fn resolves_and_caches_handle() {
let origin = moq_net::Origin::random().produce();
let server = Server::new(origin.consume(), Config::default());
let mut producer = origin.create_broadcast("live").expect("publish allowed");
let _catalog = producer
.create_track(moq_net::Track {
name: "catalog.json".to_string(),
priority: 0,
})
.unwrap();
let first = server.handle("live").await.expect("broadcast announced");
assert!(server.inner.broadcasters.lock().unwrap().contains_key("live"));
let _second = server.handle("live").await.expect("cached");
assert_eq!(server.inner.broadcasters.lock().unwrap().len(), 1);
drop(first);
}
#[tokio::test]
async fn finished_driver_evicts_its_own_entry() {
let origin = moq_net::Origin::random().produce();
let server = Server::new(origin.consume(), Config::default());
let _guards = seed_entry(&server, 7);
evict(&server.inner, "live", 7);
assert!(!server.inner.broadcasters.lock().unwrap().contains_key("live"));
}
#[tokio::test]
async fn stale_eviction_keeps_newer_entry() {
let origin = moq_net::Origin::random().produce();
let server = Server::new(origin.consume(), Config::default());
let _guards = seed_entry(&server, 9);
evict(&server.inner, "live", 8);
assert!(server.inner.broadcasters.lock().unwrap().contains_key("live"));
}
}