use std::sync::Arc;
use std::time::Duration;
use axum::Router;
use axum::extract::{Path, RawQuery, State};
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use bytes::Bytes;
use super::Server;
use crate::export::{Kind, Rendition};
const M3U8: &str = "application/vnd.apple.mpegurl";
const MP4: &str = "video/mp4";
const READY_TIMEOUT: Duration = Duration::from_secs(5);
pub fn router(server: Server) -> Router {
Router::new()
.route("/{broadcast}/master.m3u8", get(master))
.route("/{broadcast}/{kind}/{rendition}/media.m3u8", get(media))
.route("/{broadcast}/{kind}/{rendition}/init.mp4", get(init))
.route("/{broadcast}/{kind}/{rendition}/seg/{file}", get(segment))
.with_state(server)
}
async fn master(State(server): State<Server>, Path(broadcast): Path<String>, RawQuery(query): RawQuery) -> Response {
let Some(broadcaster) = server.broadcaster(&broadcast).await else {
return not_found();
};
let _ = tokio::time::timeout(READY_TIMEOUT, broadcaster.ready()).await;
if broadcaster.is_empty() {
return not_found();
}
m3u8(broadcaster.master_playlist(query.as_deref()))
}
async fn media(
State(server): State<Server>,
Path((broadcast, kind, rendition)): Path<(String, String, String)>,
RawQuery(query): RawQuery,
) -> Response {
let Some(rendition) = rendition_for(&server, &broadcast, &kind, &rendition).await else {
return not_found();
};
let _ = tokio::time::timeout(READY_TIMEOUT, rendition.playable()).await;
match rendition.init().await {
Ok(Some(_)) => {}
Ok(None) => return not_found(),
Err(err) => return server_error(err),
}
match rendition.media_playlist(query.as_deref()) {
Some(playlist) => m3u8(playlist),
None => not_found(),
}
}
async fn init(
State(server): State<Server>,
Path((broadcast, kind, rendition)): Path<(String, String, String)>,
) -> Response {
let Some(rendition) = rendition_for(&server, &broadcast, &kind, &rendition).await else {
return not_found();
};
match rendition.init().await {
Ok(Some(bytes)) => media_bytes(bytes),
Ok(None) => not_found(),
Err(err) => server_error(err),
}
}
async fn segment(
State(server): State<Server>,
Path((broadcast, kind, rendition, file)): Path<(String, String, String, String)>,
) -> Response {
let Some(sequence) = file.strip_suffix(".m4s").and_then(|s| s.parse::<u64>().ok()) else {
return not_found();
};
let Some(rendition) = rendition_for(&server, &broadcast, &kind, &rendition).await else {
return not_found();
};
match rendition.segment(sequence).await {
Ok(Some(bytes)) => media_bytes(bytes),
Ok(None) => not_found(),
Err(err) => server_error(err),
}
}
async fn rendition_for(server: &Server, broadcast: &str, kind: &str, rendition: &str) -> Option<Arc<Rendition>> {
let kind = kind.parse::<Kind>().ok()?;
let broadcaster = server.broadcaster(broadcast).await?;
let _ = tokio::time::timeout(READY_TIMEOUT, broadcaster.ready()).await;
broadcaster.rendition(kind, rendition)
}
fn m3u8(body: String) -> Response {
(
[(header::CONTENT_TYPE, M3U8), (header::CACHE_CONTROL, "no-cache")],
body,
)
.into_response()
}
fn media_bytes(body: Bytes) -> Response {
(
[
(header::CONTENT_TYPE, MP4),
(header::CACHE_CONTROL, "public, max-age=31536000, immutable"),
],
body,
)
.into_response()
}
fn not_found() -> Response {
(StatusCode::NOT_FOUND, [(header::CACHE_CONTROL, "no-store")]).into_response()
}
fn server_error(err: crate::Error) -> Response {
tracing::warn!(%err, "hls request failed");
(StatusCode::INTERNAL_SERVER_ERROR, [(header::CACHE_CONTROL, "no-store")]).into_response()
}