1use axum::{
7 Router,
8 body::Bytes,
9 extract::{OriginalUri, Path, State},
10 http::{HeaderMap, HeaderValue, StatusCode, header},
11 response::{IntoResponse, Response as HttpResponse},
12 routing::post,
13};
14use str0m::Candidate;
15
16use std::time::Duration;
17
18use crate::{Error, Result, egress::EgressSource, sdp, server::Server, session};
19
20pub use crate::server::Response;
21
22const CATALOG_TIMEOUT: Duration = Duration::from_secs(5);
27
28pub fn router(server: Server) -> Router {
30 Router::new()
31 .route("/{*path}", post(handle).delete(crate::server::delete))
32 .with_state(server)
33}
34
35async fn handle(
36 server: State<Server>,
37 path: Path<String>,
38 OriginalUri(uri): OriginalUri,
39 headers: HeaderMap,
40 body: Bytes,
41) -> HttpResponse {
42 let (server, path) = (server.0, path.0);
43 match accept_offer(&server, &path, &headers, body).await {
44 Ok(response) => {
45 let Response {
46 resource_id,
47 answer,
48 session,
49 } = response;
50 let mut response_headers = HeaderMap::new();
51 response_headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/sdp"));
52 if let Some(loc) = crate::server::session_location(&uri, &resource_id) {
53 response_headers.insert(header::LOCATION, loc);
54 }
55 tokio::spawn(async move {
56 let _ = session.run().await;
57 });
58 (StatusCode::CREATED, response_headers, answer).into_response()
59 }
60 Err(err) => {
61 tracing::warn!(%err, "whep request failed");
62 (status_for(&err), err.to_string()).into_response()
63 }
64 }
65}
66
67async fn accept_offer(server: &Server, path: &str, headers: &HeaderMap, body: Bytes) -> Result<Response> {
70 if !is_sdp(headers) {
71 return Err(Error::InvalidSdp("expected Content-Type: application/sdp".into()));
72 }
73 let offer = std::str::from_utf8(&body).map_err(|err| Error::InvalidSdp(err.to_string()))?;
74 accept(server, server.subscriber(), path, offer).await
75}
76
77pub async fn accept(
98 server: &Server,
99 subscriber: &moq_net::origin::Consumer,
100 broadcast: impl moq_net::AsPath,
101 offer: &str,
102) -> Result<Response> {
103 let offer = sdp::parse_offer(offer)?;
104
105 let broadcast = broadcast.as_path().to_string();
110 let source = moq_mux::Source::new(subscriber.clone(), &broadcast);
111
112 let source = tokio::time::timeout(CATALOG_TIMEOUT, EgressSource::new(source))
115 .await
116 .map_err(|_| {
117 Error::Other(anyhow::anyhow!(
118 "broadcast {broadcast} did not resolve with a catalog within {CATALOG_TIMEOUT:?}"
119 ))
120 })??;
121 let codecs = source.catalog_codecs();
122 if codecs.is_empty() {
123 return Err(Error::Other(anyhow::anyhow!(
124 "catalog has no codecs we can egress (Opus / H.264 / H.265 / VP8 / VP9 / AV1)"
125 )));
126 }
127
128 let mux = server.mux().await?;
133 let (creds, inbound, registration) = mux.register();
134 let mut rtc = session::rtc_config_with_codecs(&codecs)
135 .set_local_ice_credentials(creds)
136 .build(std::time::Instant::now());
137 for addr in mux.candidates() {
138 let cand = Candidate::host(*addr, "udp").map_err(str0m::RtcError::from)?;
139 rtc.add_local_candidate(cand);
140 }
141
142 let answer = rtc.sdp_api().accept_offer(offer).map_err(Error::Rtc)?;
143 let resource_id = sdp::new_resource_id();
144 let session = session::Session::egress(rtc, mux.socket(), mux.candidates().to_vec(), inbound, source);
145
146 let cancel = server.register_session(resource_id.clone());
149
150 Ok(Response {
151 resource_id: resource_id.clone(),
152 answer: sdp::render_answer(&answer),
153 session: crate::server::AcceptedSession {
154 server: server.clone(),
155 resource_id,
156 session: Some(session),
157 registration: Some(registration),
158 cancel: Some(cancel),
159 role: "whep server",
160 broadcast: None,
161 },
162 })
163}
164
165fn is_sdp(headers: &HeaderMap) -> bool {
166 headers
167 .get(header::CONTENT_TYPE)
168 .and_then(|v| v.to_str().ok())
169 .map(|v| v.eq_ignore_ascii_case("application/sdp"))
170 .unwrap_or(false)
171}
172
173fn status_for(err: &Error) -> StatusCode {
174 match err {
175 Error::InvalidSdp(_) => StatusCode::BAD_REQUEST,
176 Error::UnsupportedCodec(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
177 Error::SessionNotFound => StatusCode::NOT_FOUND,
178 _ => StatusCode::INTERNAL_SERVER_ERROR,
179 }
180}