pub mod whep;
pub mod whip;
mod mux;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use axum::Router;
use axum::extract::{Path, State};
use axum::http::{HeaderValue, StatusCode, Uri};
use tokio::sync::{OnceCell, oneshot};
use crate::{Error, Result};
use mux::Mux;
pub struct Response {
pub resource_id: String,
pub answer: String,
session: AcceptedSession,
}
impl Response {
pub async fn run(self) -> Result<()> {
self.session.run().await
}
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Response")
.field("resource_id", &self.resource_id)
.field("answer", &self.answer)
.finish_non_exhaustive()
}
}
struct AcceptedSession {
server: Server,
resource_id: String,
session: Option<crate::session::Session>,
registration: Option<mux::Registration>,
cancel: Option<oneshot::Receiver<()>>,
role: &'static str,
broadcast: Option<moq_net::broadcast::Producer>,
}
impl AcceptedSession {
async fn run(mut self) -> Result<()> {
let session = self.session.take().expect("accepted session missing driver");
let registration = self
.registration
.take()
.expect("accepted session missing mux registration");
let cancel = self.cancel.take().expect("accepted session missing cancel receiver");
let result = {
let _registration = registration;
tokio::select! {
res = session.run() => {
crate::session::log_session_end(self.role, &res);
res
}
_ = cancel => {
tracing::debug!(role = self.role, "webrtc session terminated by DELETE");
if let Some(mut broadcast) = self.broadcast.take() {
broadcast.finish();
}
Ok(())
}
}
};
normalize_session_result(result)
}
}
impl Drop for AcceptedSession {
fn drop(&mut self) {
self.server.unregister_session(&self.resource_id);
}
}
fn normalize_session_result(result: Result<()>) -> Result<()> {
match result {
Ok(()) | Err(Error::SessionClosed) => Ok(()),
Err(err) => Err(err),
}
}
pub(crate) fn session_location(uri: &Uri, resource_id: &str) -> Option<HeaderValue> {
let base = uri.path().trim_end_matches('/');
let path = if base.is_empty() {
format!("/{resource_id}")
} else {
format!("{base}/{resource_id}")
};
HeaderValue::from_str(&path).ok()
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub ice_candidates: Vec<SocketAddr>,
pub udp_bind: SocketAddr,
}
impl Default for Config {
fn default() -> Self {
Self {
ice_candidates: Vec::new(),
udp_bind: SocketAddr::from(([0, 0, 0, 0], 0)),
}
}
}
#[derive(Clone)]
pub struct Server {
inner: Arc<Inner>,
}
struct Inner {
config: Config,
publisher: moq_net::origin::Producer,
subscriber: moq_net::origin::Consumer,
mux: OnceCell<Mux>,
sessions: Mutex<HashMap<String, oneshot::Sender<()>>>,
}
impl Server {
pub fn new(config: Config, publisher: moq_net::origin::Producer, subscriber: moq_net::origin::Consumer) -> Self {
Self {
inner: Arc::new(Inner {
config,
publisher,
subscriber,
mux: OnceCell::new(),
sessions: Mutex::new(HashMap::new()),
}),
}
}
pub(crate) async fn mux(&self) -> Result<&Mux> {
self.inner
.mux
.get_or_try_init(|| Mux::bind(self.inner.config.udp_bind, &self.inner.config.ice_candidates))
.await
}
pub fn publish_router(&self) -> Router {
whip::router(self.clone())
}
pub fn subscribe_router(&self) -> Router {
whep::router(self.clone())
}
pub(crate) fn publisher(&self) -> &moq_net::origin::Producer {
&self.inner.publisher
}
pub(crate) fn subscriber(&self) -> &moq_net::origin::Consumer {
&self.inner.subscriber
}
pub(crate) fn register_session(&self, resource_id: String) -> oneshot::Receiver<()> {
let (tx, rx) = oneshot::channel();
self.inner.sessions.lock().unwrap().insert(resource_id, tx);
rx
}
pub(crate) fn unregister_session(&self, resource_id: &str) {
self.inner.sessions.lock().unwrap().remove(resource_id);
}
pub fn terminate(&self, resource_id: &str) -> bool {
if let Some(cancel) = self.inner.sessions.lock().unwrap().remove(resource_id) {
let _ = cancel.send(());
true
} else {
false
}
}
}
pub(crate) async fn delete(State(server): State<Server>, Path(path): Path<String>) -> StatusCode {
match crate::sdp::parse_resource_id(&path) {
Ok(id) if server.terminate(&id.to_string()) => StatusCode::OK,
Ok(_) => StatusCode::NOT_FOUND,
Err(_) => StatusCode::BAD_REQUEST,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn server() -> Server {
let publisher = moq_net::Origin::random().produce();
let subscriber = moq_net::Origin::random().produce().consume();
Server::new(Config::default(), publisher, subscriber)
}
#[test]
fn terminate_unknown_session_is_false() {
assert!(!server().terminate("00000000-0000-0000-0000-000000000000"));
}
#[test]
fn terminate_registered_session_once() {
let server = server();
let id = "11111111-1111-1111-1111-111111111111";
let _cancel = server.register_session(id.to_string());
assert!(server.terminate(id), "first terminate finds the session");
assert!(!server.terminate(id), "second terminate is a no-op");
}
#[test]
fn unregister_drops_the_entry() {
let server = server();
let id = "22222222-2222-2222-2222-222222222222";
let _cancel = server.register_session(id.to_string());
server.unregister_session(id);
assert!(!server.terminate(id), "unregistered session can't be terminated");
}
#[test]
fn peer_close_is_a_successful_session_result() {
assert!(normalize_session_result(Err(Error::SessionClosed)).is_ok());
}
#[test]
fn session_location_preserves_mount_path() {
let uri: Uri = "/whip/live/cam0?token=secret".parse().unwrap();
let location = session_location(&uri, "session-id").expect("header value");
assert_eq!(location, "/whip/live/cam0/session-id");
}
}