use axum::{
Router,
body::Bytes,
extract::{OriginalUri, Path, State},
http::{HeaderMap, HeaderValue, StatusCode, header},
response::{IntoResponse, Response as HttpResponse},
routing::post,
};
use str0m::Candidate;
use std::time::Duration;
use crate::{Error, Result, egress::EgressSource, sdp, server::Server, session};
pub use crate::server::Response;
#[derive(Clone)]
struct RouterState {
server: Server,
subscriber: moq_net::origin::Consumer,
}
const CATALOG_TIMEOUT: Duration = Duration::from_secs(5);
pub fn router(server: Server, subscriber: moq_net::origin::Consumer) -> Router {
Router::new()
.route("/{*path}", post(handle).delete(delete))
.with_state(RouterState { server, subscriber })
}
async fn handle(
state: State<RouterState>,
path: Path<String>,
OriginalUri(uri): OriginalUri,
headers: HeaderMap,
body: Bytes,
) -> HttpResponse {
let (state, path) = (state.0, path.0);
match accept_offer(&state.server, &state.subscriber, &path, &headers, body).await {
Ok(response) => {
let Response {
resource_id,
answer,
session,
} = response;
let mut response_headers = HeaderMap::new();
response_headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/sdp"));
if let Some(loc) = crate::server::session_location(&uri, &resource_id) {
response_headers.insert(header::LOCATION, loc);
}
tokio::spawn(async move {
let _ = session.run().await;
});
(StatusCode::CREATED, response_headers, answer).into_response()
}
Err(err) => {
tracing::warn!(%err, "whep request failed");
(status_for(&err), err.to_string()).into_response()
}
}
}
async fn accept_offer(
server: &Server,
subscriber: &moq_net::origin::Consumer,
path: &str,
headers: &HeaderMap,
body: Bytes,
) -> Result<Response> {
if !is_sdp(headers) {
return Err(Error::InvalidSdp("expected Content-Type: application/sdp".into()));
}
let offer = std::str::from_utf8(&body).map_err(|err| Error::InvalidSdp(err.to_string()))?;
accept(server, subscriber, path, offer).await
}
async fn delete(State(state): State<RouterState>, Path(path): Path<String>) -> StatusCode {
crate::server::delete(&state.server, &path)
}
pub async fn accept(
server: &Server,
subscriber: &moq_net::origin::Consumer,
broadcast: impl moq_net::AsPath,
offer: &str,
) -> Result<Response> {
let offer = sdp::parse_offer(offer)?;
let broadcast = broadcast.as_path().to_string();
let source = moq_mux::Source::new(subscriber.clone(), &broadcast);
let source = tokio::time::timeout(CATALOG_TIMEOUT, EgressSource::new(source))
.await
.map_err(|_| Error::CatalogTimeout)??;
let codecs = source.catalog_codecs();
if codecs.is_empty() {
return Err(Error::NoRenditions);
}
let mux = server.mux().await?;
let (creds, inbound, registration) = mux.register();
let mut rtc = session::rtc_config_with_codecs(&codecs)
.set_local_ice_credentials(creds)
.build(std::time::Instant::now());
for addr in mux.candidates() {
let cand = Candidate::host(*addr, "udp").map_err(Error::rtc)?;
rtc.add_local_candidate(cand);
}
let answer = rtc.sdp_api().accept_offer(offer).map_err(Error::rtc)?;
let resource_id = sdp::new_resource_id();
let session = session::Session::egress(rtc, mux.socket(), mux.candidates().to_vec(), inbound, source);
let cancel = server.register_session(resource_id.clone());
Ok(Response {
resource_id: resource_id.clone(),
answer: sdp::render_answer(&answer),
session: crate::server::AcceptedSession {
server: server.clone(),
resource_id,
session: Some(session),
registration: Some(registration),
cancel: Some(cancel),
role: "whep server",
broadcast: None,
},
})
}
fn is_sdp(headers: &HeaderMap) -> bool {
headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|v| v.eq_ignore_ascii_case("application/sdp"))
.unwrap_or(false)
}
fn status_for(err: &Error) -> StatusCode {
match err {
Error::InvalidSdp(_) => StatusCode::BAD_REQUEST,
Error::UnsupportedCodec(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
Error::SessionNotFound => StatusCode::NOT_FOUND,
Error::Moq(moq_net::Error::Unauthorized) => StatusCode::UNAUTHORIZED,
Error::Moq(moq_net::Error::NotFound | moq_net::Error::Unroutable) => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}