1pub mod whep;
10pub mod whip;
11
12mod mux;
13
14use std::collections::HashMap;
15use std::net::SocketAddr;
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18
19#[cfg(feature = "server")]
20use axum::Router;
21#[cfg(feature = "server")]
22use axum::http::{HeaderValue, StatusCode, Uri};
23use tokio::sync::{OnceCell, oneshot};
24
25use crate::{Error, Result};
26use mux::Mux;
27
28pub struct Response {
32 pub resource_id: String,
34 pub answer: String,
36 session: AcceptedSession,
37}
38
39impl Response {
40 pub async fn run(self) -> Result<()> {
42 self.session.run().await
43 }
44}
45
46impl std::fmt::Debug for Response {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("Response")
49 .field("resource_id", &self.resource_id)
50 .field("answer", &self.answer)
51 .finish_non_exhaustive()
52 }
53}
54
55struct AcceptedSession {
60 server: Server,
61 resource_id: String,
62 session: Option<crate::session::Session>,
63 registration: Option<mux::Registration>,
64 cancel: Option<oneshot::Receiver<()>>,
65 role: &'static str,
66 broadcast: Option<moq_net::broadcast::Producer>,
69}
70
71impl AcceptedSession {
72 async fn run(mut self) -> Result<()> {
73 let session = self.session.take().expect("accepted session missing driver");
74 let registration = self
75 .registration
76 .take()
77 .expect("accepted session missing mux registration");
78 let cancel = self.cancel.take().expect("accepted session missing cancel receiver");
79
80 let result = {
81 let _registration = registration;
84 tokio::select! {
85 res = session.run() => {
86 crate::session::log_session_end(self.role, &res);
87 res
88 }
89 _ = cancel => {
90 tracing::debug!(role = self.role, "webrtc session terminated by DELETE");
91 if let Some(broadcast) = self.broadcast.take() {
94 broadcast.finish();
95 }
96 Ok(())
97 }
98 }
99 };
100 normalize_session_result(result)
101 }
102}
103
104impl Drop for AcceptedSession {
105 fn drop(&mut self) {
106 self.server.unregister_session(&self.resource_id);
107 }
108}
109
110fn normalize_session_result(result: Result<()>) -> Result<()> {
113 match result {
114 Ok(()) | Err(Error::SessionClosed) => Ok(()),
115 Err(err) => Err(err),
116 }
117}
118
119#[cfg(feature = "server")]
123pub(crate) fn session_location(uri: &Uri, resource_id: &str) -> Option<HeaderValue> {
124 let base = uri.path().trim_end_matches('/');
125 let path = if base.is_empty() {
126 format!("/{resource_id}")
127 } else {
128 format!("{base}/{resource_id}")
129 };
130 HeaderValue::from_str(&path).ok()
131}
132
133#[derive(Clone, Debug)]
135#[non_exhaustive]
136pub struct Config {
137 pub ice_candidates: Vec<SocketAddr>,
145
146 pub udp_bind: SocketAddr,
151
152 pub max_age: Option<Duration>,
163
164 pub bandwidth: moq_net::bandwidth::Allocator,
167}
168
169impl Default for Config {
170 fn default() -> Self {
171 Self {
172 ice_candidates: Vec::new(),
173 udp_bind: SocketAddr::from(([0, 0, 0, 0], 0)),
174 max_age: None,
175 bandwidth: moq_net::bandwidth::Allocator::unlimited(),
176 }
177 }
178}
179
180#[derive(Clone)]
182pub struct Server {
183 inner: Arc<Inner>,
184}
185
186struct Inner {
187 config: Config,
188 mux: OnceCell<Mux>,
191 sessions: Mutex<HashMap<String, oneshot::Sender<()>>>,
195}
196
197impl Server {
198 pub fn new(config: Config) -> Self {
200 Self {
201 inner: Arc::new(Inner {
202 config,
203 mux: OnceCell::new(),
204 sessions: Mutex::new(HashMap::new()),
205 }),
206 }
207 }
208
209 pub(crate) async fn mux(&self) -> Result<&Mux> {
211 self.inner
212 .mux
213 .get_or_try_init(|| Mux::bind(self.inner.config.udp_bind, &self.inner.config.ice_candidates))
214 .await
215 }
216
217 #[cfg(feature = "server")]
225 pub fn publish_router(&self, publisher: moq_net::origin::Producer) -> Router {
226 whip::router(self.clone(), publisher)
227 }
228
229 #[cfg(feature = "server")]
237 pub fn subscribe_router(&self, subscriber: moq_net::origin::Consumer) -> Router {
238 whep::router(self.clone(), subscriber)
239 }
240
241 pub(crate) fn config(&self) -> &Config {
242 &self.inner.config
243 }
244
245 pub(crate) fn register_session(&self, resource_id: String) -> oneshot::Receiver<()> {
249 let (tx, rx) = oneshot::channel();
250 self.inner.sessions.lock().unwrap().insert(resource_id, tx);
251 rx
252 }
253
254 pub(crate) fn unregister_session(&self, resource_id: &str) {
256 self.inner.sessions.lock().unwrap().remove(resource_id);
257 }
258
259 pub fn terminate(&self, resource_id: &str) -> bool {
266 if let Some(cancel) = self.inner.sessions.lock().unwrap().remove(resource_id) {
267 let _ = cancel.send(());
268 true
269 } else {
270 false
271 }
272 }
273}
274
275#[cfg(feature = "server")]
278pub(crate) fn delete(server: &Server, path: &str) -> StatusCode {
279 match crate::sdp::parse_resource_id(path) {
280 Ok(id) if server.terminate(&id.to_string()) => StatusCode::OK,
281 Ok(_) => StatusCode::NOT_FOUND,
282 Err(_) => StatusCode::BAD_REQUEST,
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn server() -> Server {
291 Server::new(Config::default())
292 }
293
294 #[test]
295 fn terminate_unknown_session_is_false() {
296 assert!(!server().terminate("00000000-0000-0000-0000-000000000000"));
297 }
298
299 #[test]
300 fn terminate_registered_session_once() {
301 let server = server();
302 let id = "11111111-1111-1111-1111-111111111111";
303 let _cancel = server.register_session(id.to_string());
304 assert!(server.terminate(id), "first terminate finds the session");
305 assert!(!server.terminate(id), "second terminate is a no-op");
306 }
307
308 #[test]
309 fn unregister_drops_the_entry() {
310 let server = server();
311 let id = "22222222-2222-2222-2222-222222222222";
312 let _cancel = server.register_session(id.to_string());
313 server.unregister_session(id);
314 assert!(!server.terminate(id), "unregistered session can't be terminated");
315 }
316
317 #[test]
318 fn peer_close_is_a_successful_session_result() {
319 assert!(normalize_session_result(Err(Error::SessionClosed)).is_ok());
320 }
321
322 #[cfg(feature = "server")]
323 #[test]
324 fn session_location_preserves_mount_path() {
325 let uri: Uri = "/whip/live/cam0?token=secret".parse().unwrap();
326 let location = session_location(&uri, "session-id").expect("header value");
327 assert_eq!(location, "/whip/live/cam0/session-id");
328 }
329}