1mod routes;
56
57pub use routes::{Resource, Route};
58
59use std::collections::HashMap;
60use std::sync::{Arc, Mutex};
61use std::time::Duration;
62
63use axum::Router;
64
65use crate::export::{Broadcaster, Config};
66
67const RESOLVE_TIMEOUT: Duration = Duration::from_secs(5);
69
70#[derive(Clone)]
72pub struct Server {
73 inner: Arc<Inner>,
74}
75
76struct Inner {
77 origin: moq_net::origin::Consumer,
78 config: Config,
79 broadcasters: Mutex<HashMap<String, Arc<Broadcaster>>>,
80}
81
82impl Server {
83 pub fn new(origin: moq_net::origin::Consumer, config: Config) -> Self {
86 Self {
87 inner: Arc::new(Inner {
88 origin,
89 config,
90 broadcasters: Mutex::new(HashMap::new()),
91 }),
92 }
93 }
94
95 pub fn router(&self) -> Router {
97 routes::router(self.clone())
98 }
99
100 pub async fn broadcaster(&self, name: &str) -> Option<Arc<Broadcaster>> {
104 {
105 let mut broadcasters = self.inner.broadcasters.lock().unwrap();
106 if let Some(existing) = broadcasters.get(name) {
107 if !existing.is_closed() {
108 return Some(existing.clone());
109 }
110 broadcasters.remove(name);
111 }
112 }
113
114 tokio::time::timeout(RESOLVE_TIMEOUT, self.inner.origin.routed(name))
118 .await
119 .ok()
120 .flatten()?;
121
122 let source = moq_mux::Source::new(self.inner.origin.consume(), name);
123 let broadcaster = Broadcaster::new(source, self.inner.config.clone())
124 .await
125 .map_err(|err| tracing::warn!(%err, %name, "failed to resolve broadcast catalog"))
126 .ok()?;
127
128 let mut broadcasters = self.inner.broadcasters.lock().unwrap();
129 if let Some(existing) = broadcasters.get(name) {
130 if !existing.is_closed() {
131 return Some(existing.clone());
132 }
133 broadcasters.remove(name);
134 }
135
136 let name = name.to_string();
137 broadcasters.insert(name.clone(), broadcaster.clone());
138 tokio::spawn(evict_closed(self.inner.clone(), name, broadcaster.clone()));
139 Some(broadcaster)
140 }
141}
142
143async fn evict_closed(inner: Arc<Inner>, name: String, broadcaster: Arc<Broadcaster>) {
144 broadcaster.closed().await;
145
146 let mut broadcasters = inner.broadcasters.lock().unwrap();
147 if broadcasters
148 .get(&name)
149 .is_some_and(|current| Arc::ptr_eq(current, &broadcaster))
150 {
151 broadcasters.remove(&name);
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 fn produce_origin() -> moq_net::origin::Producer {
159 let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
160 if tokio::runtime::Handle::try_current().is_ok() {
161 tokio::spawn(moq_net::time::run(driver));
162 } else {
163 std::mem::forget(driver);
166 }
167 producer
168 }
169
170 use super::*;
171
172 async fn settle() {
175 for _ in 0..10 {
176 tokio::task::yield_now().await;
177 }
178 }
179
180 #[tokio::test(start_paused = true)]
184 async fn router_middleware_gates_requests() {
185 use axum::body::Body;
186 use axum::extract::Request;
187 use axum::http::{StatusCode, header};
188 use axum::middleware::{self, Next};
189 use axum::response::Response;
190 use tower::ServiceExt;
191
192 async fn gate(req: Request, next: Next) -> Result<Response, StatusCode> {
193 match req.headers().get(header::AUTHORIZATION) {
194 Some(token) if token == "secret" => Ok(next.run(req).await),
195 _ => Err(StatusCode::UNAUTHORIZED),
196 }
197 }
198
199 let origin = produce_origin();
202 let server = Server::new(origin.consume(), Config::default());
203 let app = server.router().layer(middleware::from_fn(gate));
204
205 let denied = app
206 .clone()
207 .oneshot(Request::builder().uri("/live/master.m3u8").body(Body::empty()).unwrap())
208 .await
209 .unwrap();
210 assert_eq!(denied.status(), StatusCode::UNAUTHORIZED);
211
212 let allowed = app
213 .oneshot(
214 Request::builder()
215 .uri("/live/master.m3u8")
216 .header(header::AUTHORIZATION, "secret")
217 .body(Body::empty())
218 .unwrap(),
219 )
220 .await
221 .unwrap();
222 assert_eq!(allowed.status(), StatusCode::NOT_FOUND);
223 assert_eq!(allowed.headers()[header::CACHE_CONTROL], "no-store");
226 }
227
228 #[tokio::test(start_paused = true)]
229 async fn router_matches_multisegment_broadcast_when_nested() {
230 use axum::body::Body;
231 use axum::extract::Request;
232 use axum::http::{StatusCode, header};
233 use tower::ServiceExt;
234
235 let origin = produce_origin();
236 let server = Server::new(origin.consume(), Config::default());
237 let app = Router::new().nest("/hls", server.router());
238
239 let response = app
240 .oneshot(
241 Request::builder()
242 .uri("/hls/project/live/master.m3u8")
243 .body(Body::empty())
244 .unwrap(),
245 )
246 .await
247 .unwrap();
248
249 assert_eq!(response.status(), StatusCode::NOT_FOUND);
250 assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
252 }
253
254 async fn closed_broadcaster() -> Arc<Broadcaster> {
255 let origin = produce_origin();
256 let producer = origin.create_broadcast("gone").expect("publish allowed");
257 producer.announce(Default::default()).expect("publish allowed");
258 settle().await;
259 let source = moq_mux::Source::new(origin.consume(), "gone");
260 let broadcaster = Broadcaster::new(source, Config::default())
261 .await
262 .expect("catalog broadcast resolves while announced");
263 producer.close();
265 settle().await;
266 broadcaster
267 }
268
269 #[tokio::test]
270 async fn broadcaster_replaces_finished_cached_instance() {
271 let origin = produce_origin();
272 let server = Server::new(origin.consume(), Config::default());
273 let stale = closed_broadcaster().await;
274
275 server
276 .inner
277 .broadcasters
278 .lock()
279 .unwrap()
280 .insert("live".to_string(), stale.clone());
281 let _producer = origin.create_broadcast("live").expect("publish allowed");
282 _producer.announce(Default::default()).expect("publish allowed");
283 settle().await;
284
285 let fresh = server.broadcaster("live").await.expect("broadcast announced");
286
287 assert!(!Arc::ptr_eq(&stale, &fresh));
288 assert!(server.inner.broadcasters.lock().unwrap().contains_key("live"));
289 }
290
291 #[tokio::test]
292 async fn eviction_keeps_newer_cached_instance() {
293 let origin = produce_origin();
294 let server = Server::new(origin.consume(), Config::default());
295 let old = closed_broadcaster().await;
296 let new_producer = origin.create_broadcast("live").expect("publish allowed");
297 new_producer.announce(Default::default()).expect("publish allowed");
298 settle().await;
299 let new = Broadcaster::new(moq_mux::Source::new(origin.consume(), "live"), Config::default())
300 .await
301 .expect("catalog broadcast resolves while announced");
302
303 server
304 .inner
305 .broadcasters
306 .lock()
307 .unwrap()
308 .insert("live".to_string(), new.clone());
309
310 evict_closed(server.inner.clone(), "live".to_string(), old).await;
311
312 let cached = server.inner.broadcasters.lock().unwrap().get("live").cloned();
313 assert!(cached.is_some_and(|cached| Arc::ptr_eq(&cached, &new)));
314 new_producer.close();
315 }
316}