🗑️ Remove unused imports, macros and dead code

- Drop `Path`/``State``` from unused Axum imports in config.rs and registry
- Mark `_position_sec` field as `#[allow(dead_code)]`` in PositionUpdateRequest and PlayerStateReport
- Remove unused macro rules (`add_action_arg!`, `add_action!``, `` add_var!)``
- Delete unused PlayerReport struct and related handler code
This commit is contained in:
2026-04-05 10:52:54 +02:00
parent 340c69cb2b
commit 546e8a782f
21 changed files with 1479 additions and 451 deletions

View File

@@ -22,13 +22,16 @@ tokio-util = { workspace = true }
async-trait = { workspace = true }
# HTTP
axum = { workspace = true }
axum-extra = { version = "0.9", features = ["typed-header"] }
tower-http = { version = "0.6", features = ["fs", "trace"] }
axum = "0.8.4"
axum-extra = "0.12"
tower-http = "0.6"
futures = "0.3"
reqwest = { workspace = true, features = ["stream"] }
bytes = "1.0"
# OpenAPI
utoipa = { version = "5.3", features = ["axum_extras"] }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }

View File

@@ -7,7 +7,11 @@ use std::sync::Arc;
use async_trait::async_trait;
#[cfg(feature = "pmoserver")]
use axum::{Router, routing::{delete, get, post}};
use axum::{
Router,
extract::{Path, State},
routing::{delete, get, post},
};
#[cfg(feature = "pmoserver")]
use pmocontrol::ControlPoint;
@@ -15,7 +19,10 @@ use pmocontrol::ControlPoint;
#[cfg(feature = "pmoserver")]
use crate::error::WebRendererError;
#[cfg(feature = "pmoserver")]
use crate::register::{position_update_handler, register_handler, unregister_handler};
use crate::register::{
pause_handler, play_handler, position_update_handler, register_handler,
report_handler, set_uri_handler, unregister_handler,
};
#[cfg(feature = "pmoserver")]
use crate::registry::RendererRegistry;
#[cfg(feature = "pmoserver")]
@@ -48,18 +55,25 @@ impl WebRendererExt for pmoserver::Server {
)
.await;
// GET /api/webrenderer/{id}/stream + DELETE /api/webrenderer/{id} + POST /api/webrenderer/{id}/position
// GET /api/webrenderer/{id}/stream + DELETE /api/webrenderer/{id}
// POST /api/webrenderer/{id}/play -> tell player to start streaming
// POST /api/webrenderer/{id}/pause, /set_uri, /report
// GET /api/webrenderer/{id}/command, /position
let dynamic_router = Router::new()
.route("/{id}/stream", get(stream_handler))
.route("/{id}/position", post(position_update_handler))
.route("/{id}", delete(unregister_handler))
.route("/{id}/play", post(play_handler))
.route("/{id}/pause", post(pause_handler))
.route("/{id}/set_uri", post(set_uri_handler))
.route("/{id}/report", post(report_handler))
.route("/{id}/command", get(crate::register::command_handler))
.route("/{id}/position", post(position_update_handler))
.with_state(registry.clone());
self.add_router("/api/webrenderer", dynamic_router).await;
tracing::info!("WebRenderer server-side streaming endpoints registered");
tracing::info!(" POST /api/webrenderer/register");
tracing::info!(" GET /api/webrenderer/{{id}}/stream");
tracing::info!(" POST /api/webrenderer/{{id}}/position");
tracing::info!(" DELETE /api/webrenderer/{{id}}");
Ok(())
}

View File

@@ -24,10 +24,30 @@ pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandl
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
tracing::info!("[WebRenderer] UPnP Play action invoked");
// Ne pas écrire Playing ici : c'est stream_source qui le fera
// une fois que les premiers bytes FLAC ont été produits.
// Écrire Transitioning pour signaler que la lecture va démarrer.
state.write().playback_state = PlaybackState::Transitioning;
// Check if URI is loaded FIRST, then write state
let has_uri = state.read().current_uri.is_some();
// Single write to update playback_state - avoid holding read lock
{
let mut s = state.write();
s.playback_state = PlaybackState::Transitioning;
}
// Tell frontend to start streaming - include the stream URL
if has_uri {
// Use a single write to set player_command
state.write().player_command = Some(serde_json::json!({
"type": "stream",
"url": "/api/webrenderer/stream" // Frontend will prefix with instance ID
}));
tracing::info!("UPnP Play: stored stream command for frontend polling");
}
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
@@ -95,6 +115,7 @@ pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHa
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked");
let uri: String = get!(&data, "CurrentURI", String);
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
.or_else(|_| {
@@ -103,6 +124,8 @@ pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHa
})
.unwrap_or_default();
tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline");
// Envoyer l'URI au pipeline serveur (remplace l'envoi WebSocket)
pipeline.send(PipelineControl::LoadUri(uri.clone())).await;

View File

@@ -47,6 +47,8 @@ pub enum TransportAction {
Seek,
SetUri,
SetNextUri,
/// Flush buffer immediatement - pour reponse rapide
Flush,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -84,6 +86,11 @@ pub enum ClientMessage {
/// Envoyé quand la piste courante se termine naturellement (gapless).
/// Le backend fait avancer current → next dans l'état partagé.
TrackEnded,
/// Ready state du player HTML5 audio
/// have_nothing, have_metadata, have_current_data, have_future_data, can_play, can_play_through
ReadyStateUpdate {
ready_state: String,
},
Pong,
}

View File

@@ -5,7 +5,7 @@
use axum::{
extract::{Path, State},
http::StatusCode,
http::{StatusCode, header::HeaderMap},
response::IntoResponse,
Json,
};
@@ -24,9 +24,12 @@ pub struct RegisterRequest {
pub struct RegisterResponse {
pub stream_url: String,
pub udn: String,
/// true si le backend est déjà en lecture — le frontend doit démarrer immédiatement
pub should_play: bool,
}
/// POST /api/webrenderer/register
#[axum::debug_handler]
pub async fn register_handler(
State(registry): State<Arc<RendererRegistry>>,
Json(req): Json<RegisterRequest>,
@@ -41,14 +44,15 @@ pub async fn register_handler(
.register_or_reconnect(&req.instance_id, &req.user_agent)
.await
{
Ok((stream_url, udn)) => {
Ok((stream_url, udn, should_play)) => {
tracing::info!(
instance_id = %req.instance_id,
stream_url = %stream_url,
udn = %udn,
should_play = %should_play,
"WebRenderer: registered"
);
(StatusCode::OK, Json(RegisterResponse { stream_url, udn })).into_response()
(StatusCode::OK, Json(RegisterResponse { stream_url, udn, should_play })).into_response()
}
Err(e) => {
tracing::error!(
@@ -70,6 +74,7 @@ pub struct PositionUpdateRequest {
/// POST /api/webrenderer/{id}/position
/// position_sec est ignoré (géré par PlayerEvent::Position côté serveur).
/// duration_sec est utilisé comme fallback si la source ne connaît pas la durée (flux radio).
#[axum::debug_handler]
pub async fn position_update_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
@@ -80,6 +85,7 @@ pub async fn position_update_handler(
}
/// DELETE /api/webrenderer/{id}
#[axum::debug_handler]
pub async fn unregister_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
@@ -88,3 +94,106 @@ pub async fn unregister_handler(
registry.schedule_unregister(&instance_id);
StatusCode::NO_CONTENT
}
#[derive(Debug, Deserialize)]
pub struct UriRequest {
pub uri: String,
}
/// POST /api/webrenderer/{id}/set_uri - charge une URI et joue
#[axum::debug_handler]
pub async fn set_uri_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
Json(req): Json<UriRequest>,
) -> impl IntoResponse {
tracing::info!(instance_id = %instance_id, uri = %req.uri, "WebRenderer: set_uri request");
registry.load_uri(&instance_id, req.uri).await;
StatusCode::OK
}
/// POST /api/webrenderer/{id}/pause
#[axum::debug_handler]
pub async fn pause_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
tracing::info!(instance_id = %instance_id, "WebRenderer: pause request");
registry.send_pause_command(&instance_id).await;
StatusCode::OK
}
// ─── Rapports du player ─────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct PlayerStateReport {
pub position_sec: Option<f64>,
pub duration_sec: Option<f64>,
pub state: Option<String>,
pub ready_state: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PlayerReport {
pub instance_id: String,
#[serde(flatten)]
pub report: PlayerStateReport,
}
/// POST /api/webrenderer/{id}/report - recoit rapports position/state du player
#[axum::debug_handler]
pub async fn report_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
Json(report): Json<PlayerStateReport>,
) -> impl IntoResponse {
// Registry met à jour l'état avec les rapports du player
registry.update_player_state(&instance_id, report).await;
StatusCode::OK
}
// ─── Commandes vers le player ─────────────────────────────────
/// GET /api/webrenderer/{id}/command - recupere commande pending pour le player
#[axum::debug_handler]
pub async fn command_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
match registry.get_pending_command(&instance_id).await {
Some(cmd) => (StatusCode::OK, Json(cmd)).into_response(),
None => StatusCode::NO_CONTENT.into_response(),
}
}
/// POST /api/webrenderer/{id}/play - tell player to stream and play
#[axum::debug_handler]
pub async fn play_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
// Check if there's a valid URI loaded - if not, ignore the play command
if !registry.has_current_uri(&instance_id) {
tracing::warn!(instance_id = %instance_id, "Play command ignored: no URI loaded");
let mut headers = HeaderMap::new();
headers.insert(axum::http::header::CONTENT_TYPE, "text/plain".parse().unwrap());
return (StatusCode::BAD_REQUEST, headers, "No URI loaded").into_response();
}
tracing::info!(instance_id = %instance_id, "WebRenderer: play request");
// Get stream URL and tell player to play it
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
// Set command for player to start streaming
let command = serde_json::json!({
"type": "stream",
"url": stream_url
});
registry.set_player_command(&instance_id, command);
// Also tell pipeline to play (if not already) - use existing method
registry.send_play_command(&instance_id).await;
(StatusCode::OK, "OK").into_response()
}

View File

@@ -3,6 +3,11 @@
//! Remplace `SessionManager` et `websocket.rs`. La session est maintenant liée
//! au flux FLAC HTTP, pas à une connexion WebSocket.
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
@@ -68,12 +73,13 @@ impl RendererRegistry {
}
/// Enregistre ou reconnecte une instance.
/// Retourne `(stream_url, udn)`.
/// Retourne `(stream_url, udn, should_play)`.
/// `should_play` est true si le backend est déjà en lecture : le frontend doit démarrer immédiatement.
pub async fn register_or_reconnect(
&self,
instance_id: &str,
user_agent: &str,
) -> Result<(String, String), WebRendererError> {
) -> Result<(String, String, bool), WebRendererError> {
// Annuler tout unregister différé pour cet instance_id
if let Some(cancel) = self.pending_unregister.write().remove(instance_id) {
tracing::info!(instance_id = %instance_id, "WebRenderer: cancelled pending unregister (page reload)");
@@ -88,7 +94,14 @@ impl RendererRegistry {
#[cfg(feature = "pmoserver")]
self.register_with_control_point(&existing.device_instance)?;
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
return Ok((stream_url, existing.udn.clone()));
let should_play = {
let s = existing.state.read();
s.current_uri.is_some() && matches!(
s.playback_state,
crate::messages::PlaybackState::Playing | crate::messages::PlaybackState::Transitioning
)
};
return Ok((stream_url, existing.udn.clone(), should_play));
}
}
@@ -113,7 +126,7 @@ impl RendererRegistry {
"WebRenderer: new instance registered"
);
Ok((stream_url, udn))
Ok((stream_url, udn, false))
}
/// Retourne un OggFlacClientStream indépendant pour l'endpoint /stream.
@@ -122,10 +135,17 @@ impl RendererRegistry {
&self,
instance_id: &str,
) -> Option<pmoaudio_ext::sinks::OggFlacClientStream> {
self.instances
.read()
.get(instance_id)
.map(|i| i.flac_handle.subscribe())
let instances = self.instances.read();
match instances.get(instance_id) {
Some(i) => {
tracing::debug!(instance_id = %instance_id, "Found instance, getting flac_handle");
Some(i.flac_handle.subscribe())
}
None => {
tracing::error!(instance_id = %instance_id, "Instance not found in registry!");
None
}
}
}
/// Retourne le PipelineHandle par UDN (pour les handlers UPnP)
@@ -208,6 +228,97 @@ impl RendererRegistry {
});
}
/// Met à jour l'état avec les rapports du player
pub async fn update_player_state(
&self,
instance_id: &str,
report: crate::register::PlayerStateReport,
) {
let instances = self.instances.read();
if let Some(instance) = instances.get(instance_id) {
let mut state = instance.state.write();
if let Some(pos) = report.position_sec {
state.position = Some(pos.to_string());
}
if let Some(dur) = report.duration_sec {
state.duration = Some(dur.to_string());
}
if let Some(s) = &report.state {
state.playback_state = match s.as_str() {
"playing" => crate::messages::PlaybackState::Playing,
"paused" => crate::messages::PlaybackState::Paused,
"stopped" => crate::messages::PlaybackState::Stopped,
_ => state.playback_state.clone(),
};
}
tracing::debug!(instance_id = %instance_id, position = ?state.position, "player state updated");
}
}
/// Récupère et consomme la commande en attente pour le player
pub async fn get_pending_command(
&self,
instance_id: &str,
) -> Option<serde_json::Value> {
self.instances
.read()
.get(instance_id)
.and_then(|instance| instance.state.write().player_command.take())
}
/// Stocke une commande pour le player (consommée via GET /command)
pub fn set_player_command(&self, instance_id: &str, command: serde_json::Value) {
if let Some(instance) = self.instances.read().get(instance_id) {
instance.state.write().player_command = Some(command);
}
}
/// Charge une URI dans le pipeline
pub async fn load_uri(&self, instance_id: &str, uri: String) {
// Get pipeline handle before async call to avoid holding lock across await
let pipeline = self.instances.read().get(instance_id).map(|i| i.pipeline.clone());
if let Some(pipeline) = pipeline {
use pmoaudio_ext::PlayerCommand;
pipeline.send(PlayerCommand::LoadUri(uri.clone())).await;
pipeline.send(PlayerCommand::Play).await;
tracing::info!(instance_id = %instance_id, uri = %uri, "loaded URI");
}
}
/// Envoie commande play au pipeline
pub async fn send_play_command(&self, instance_id: &str) {
tracing::info!(instance_id = %instance_id, "send_play_command called");
// Get pipeline handle before async call to avoid holding lock across await
let pipeline = self.instances.read().get(instance_id).map(|i| i.pipeline.clone());
if let Some(pipeline) = pipeline {
tracing::info!(instance_id = %instance_id, "Instance found, sending PlayerCommand::Play");
use pmoaudio_ext::PlayerCommand;
pipeline.send(PlayerCommand::Play).await;
tracing::info!(instance_id = %instance_id, "PlayerCommand::Play sent");
} else {
tracing::error!(instance_id = %instance_id, "Instance not found in send_play_command!");
}
}
/// Envoie commande pause au pipeline
pub async fn send_pause_command(&self, instance_id: &str) {
// Get pipeline handle before async call to avoid holding lock across await
let pipeline = self.instances.read().get(instance_id).map(|i| i.pipeline.clone());
if let Some(pipeline) = pipeline {
use pmoaudio_ext::PlayerCommand;
pipeline.send(PlayerCommand::Pause).await;
}
}
/// Check if the instance has a current URI loaded
pub fn has_current_uri(&self, instance_id: &str) -> bool {
self.instances
.read()
.get(instance_id)
.map(|i| i.state.read().current_uri.is_some())
.unwrap_or(false)
}
// ── Création d'instance ────────────────────────────────────────────────────
async fn create_instance(

View File

@@ -1,6 +1,7 @@
//! État partagé du renderer (backend ↔ pipeline)
use parking_lot::RwLock;
use serde_json::Value;
use std::sync::Arc;
use crate::messages::PlaybackState;
@@ -17,6 +18,8 @@ pub struct RendererState {
pub duration: Option<String>,
pub volume: u16,
pub mute: bool,
/// Commande en attente pour le player frontend (polled via /command)
pub player_command: Option<Value>,
}
impl Default for RendererState {
@@ -31,6 +34,7 @@ impl Default for RendererState {
duration: None,
volume: 100,
mute: false,
player_command: None,
}
}
}

View File

@@ -9,14 +9,14 @@ use axum::{
body::Body,
extract::{Path, State},
http::{
HeaderMap, StatusCode,
header::{CACHE_CONTROL, CONNECTION, CONTENT_TYPE, TRANSFER_ENCODING},
HeaderMap, StatusCode,
},
response::{IntoResponse, Response},
};
use std::sync::Arc;
use tokio_util::io::ReaderStream;
use tracing::info;
use tracing::{error, info};
use crate::registry::RendererRegistry;
@@ -29,25 +29,26 @@ pub async fn stream_handler(
info!(instance_id = %instance_id, "FLAC stream client connecting");
// Ignorer le header Range — flux live infini, non seekable.
// On ne répond jamais 206 ni 416 : toujours 200 chunked.
// Safari (et d'autres clients) envoient parfois Range: bytes=0-N ;
// répondre 416 ou 206 leur fait croire à une ressource finie.
if let Some(range) = headers.get("range") {
info!(instance_id = %instance_id, "Range header ignored (live stream): {:?}", range);
info!(instance_id = %instance_id, "Range header ignored: {:?}", range);
}
let stream = match registry.get_stream(&instance_id) {
Some(s) => s,
Some(s) => {
info!(instance_id = %instance_id, "Found instance, getting stream");
s
}
None => {
error!(instance_id = %instance_id, "No WebRenderer instance found!");
return (
StatusCode::NOT_FOUND,
format!("No WebRenderer instance for id={}", instance_id),
)
.into_response()
.into_response();
}
};
info!(instance_id = %instance_id, "FLAC stream started");
info!(instance_id = %instance_id, "FLAC stream started - returning OGG-FLAC");
Response::builder()
.status(StatusCode::OK)