2026-02-26 19:58:37 +01:00
|
|
|
//! Handler HTTP GET /api/webrenderer/{id}/stream
|
|
|
|
|
//!
|
2026-02-28 14:10:38 +01:00
|
|
|
//! Sert le flux OGG-FLAC d'une instance WebRenderer.
|
2026-02-26 19:58:37 +01:00
|
|
|
//!
|
2026-02-28 14:10:38 +01:00
|
|
|
//! Safari fait systématiquement une requête Range: bytes=0-1 avant de jouer.
|
|
|
|
|
//! On répond 206 avec 2 octets factices pour satisfaire la sonde,
|
|
|
|
|
//! puis la vraie requête (sans Range) reçoit le stream persistant.
|
2026-02-26 19:58:37 +01:00
|
|
|
|
|
|
|
|
use axum::{
|
|
|
|
|
body::Body,
|
|
|
|
|
extract::{Path, State},
|
|
|
|
|
http::{
|
2026-02-28 14:10:38 +01:00
|
|
|
HeaderMap, StatusCode,
|
|
|
|
|
header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE, CONTENT_RANGE, CONTENT_LENGTH, ACCEPT_RANGES},
|
2026-02-26 19:58:37 +01:00
|
|
|
},
|
|
|
|
|
response::{IntoResponse, Response},
|
|
|
|
|
};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio_util::io::ReaderStream;
|
|
|
|
|
use tracing::info;
|
|
|
|
|
|
|
|
|
|
use crate::registry::RendererRegistry;
|
|
|
|
|
|
|
|
|
|
/// GET /api/webrenderer/{id}/stream
|
|
|
|
|
pub async fn stream_handler(
|
|
|
|
|
State(registry): State<Arc<RendererRegistry>>,
|
|
|
|
|
Path(instance_id): Path<String>,
|
2026-02-28 14:10:38 +01:00
|
|
|
headers: HeaderMap,
|
2026-02-26 19:58:37 +01:00
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
info!(instance_id = %instance_id, "FLAC stream client connecting");
|
|
|
|
|
|
2026-02-28 14:10:38 +01:00
|
|
|
// Détecter la sonde Range: bytes=0-1 de Safari
|
|
|
|
|
if let Some(range) = headers.get("range") {
|
|
|
|
|
if range.as_bytes() == b"bytes=0-1" {
|
|
|
|
|
info!(instance_id = %instance_id, "Safari range probe — responding 206");
|
|
|
|
|
return Response::builder()
|
|
|
|
|
.status(StatusCode::PARTIAL_CONTENT)
|
|
|
|
|
.header(CONTENT_TYPE, "audio/ogg; codecs=flac")
|
|
|
|
|
.header(ACCEPT_RANGES, "bytes")
|
|
|
|
|
.header(CONTENT_RANGE, "bytes 0-1/*")
|
|
|
|
|
.header(CONTENT_LENGTH, "2")
|
|
|
|
|
.body(Body::from(vec![0u8, 0u8]))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.into_response();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stream = match registry.get_stream(&instance_id) {
|
|
|
|
|
Some(s) => s,
|
2026-02-26 19:58:37 +01:00
|
|
|
None => {
|
|
|
|
|
return (
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
format!("No WebRenderer instance for id={}", instance_id),
|
|
|
|
|
)
|
|
|
|
|
.into_response()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
info!(instance_id = %instance_id, "FLAC stream started");
|
|
|
|
|
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(CONTENT_TYPE, "audio/ogg; codecs=flac")
|
|
|
|
|
.header(CACHE_CONTROL, "no-store, no-transform")
|
|
|
|
|
.header(CONNECTION, "keep-alive")
|
2026-02-28 14:10:38 +01:00
|
|
|
.header(ACCEPT_RANGES, "bytes")
|
2026-02-26 19:58:37 +01:00
|
|
|
.header("X-Content-Type-Options", "nosniff")
|
|
|
|
|
.body(Body::from_stream(ReaderStream::new(stream)))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.into_response()
|
|
|
|
|
}
|