diff --git a/pmowebrenderer/src/handlers.rs b/pmowebrenderer/src/handlers.rs index 9277c626..f0ac0616 100644 --- a/pmowebrenderer/src/handlers.rs +++ b/pmowebrenderer/src/handlers.rs @@ -19,41 +19,32 @@ type ActionFuture = // ─── AVTransport Handlers ─────────────────────────────────────────────────── +/// Handler pour l'action UPnP "Play" - lance la lecture du flux audio pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { 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. - - // 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 + "url": "/api/webrenderer/stream" })); tracing::info!("UPnP Play: stored stream command for frontend polling"); } - pipeline.send(PipelineControl::Play).await; Ok(data) }) }) } +/// Handler pour l'action UPnP "Stop" - arrête la lecture pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); @@ -66,6 +57,7 @@ pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandl }) } +/// Handler pour l'action UPnP "Pause" - met en pause la lecture pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); @@ -78,7 +70,9 @@ pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHand }) } +/// Handler pour l'action UPnP "Next" - passe à la piste suivante pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler { + let pipeline = pipeline.clone(); Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); Box::pin(async move { @@ -88,7 +82,9 @@ pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler { }) } +/// Handler pour l'action UPnP "Previous" - retourne au début de la piste actuelle pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler { + let pipeline = pipeline.clone(); Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); Box::pin(async move { @@ -98,6 +94,7 @@ pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler { }) } +/// Handler pour l'action UPnP "Seek" - seek à une position donnée pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); @@ -110,6 +107,7 @@ pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler { }) } +/// Handler pour l'action UPnP "SetAVTransportURI" - définit l'URI à jouer pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); @@ -125,8 +123,6 @@ 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; { @@ -140,6 +136,7 @@ pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHa }) } +/// Handler pour l'action UPnP "SetNextAVTransportURI" - définit l'URI suivante (gapless) pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let pipeline = pipeline.clone(); @@ -165,6 +162,7 @@ pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> Act }) } +/// Handler pour l'action UPnP "GetPositionInfo" - retourne la position actuelle pub fn get_position_info_handler(state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -206,6 +204,7 @@ pub fn get_position_info_handler(state: SharedState) -> ActionHandler { }) } +/// Handler pour l'action UPnP "GetTransportInfo" - retourne l'état du transport pub fn get_transport_info_handler(state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -227,6 +226,7 @@ pub fn get_transport_info_handler(state: SharedState) -> ActionHandler { }) } +/// Handler pour l'action UPnP "GetMediaInfo" - retourne les infos du média pub fn get_media_info_handler(state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -249,6 +249,7 @@ pub fn get_media_info_handler(state: SharedState) -> ActionHandler { // ─── RenderingControl Handlers ────────────────────────────────────────────── +/// Handler pour l'action UPnP "SetVolume" - définit le volume pub fn set_volume_handler(_pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -260,6 +261,7 @@ pub fn set_volume_handler(_pipeline: PipelineHandle, state: SharedState) -> Acti }) } +/// Handler pour l'action UPnP "GetVolume" - retourne le volume actuel pub fn get_volume_handler(state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -272,6 +274,7 @@ pub fn get_volume_handler(state: SharedState) -> ActionHandler { }) } +/// Handler pour l'action UPnP "SetMute" - définit le mute pub fn set_mute_handler(_pipeline: PipelineHandle, state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -283,6 +286,7 @@ pub fn set_mute_handler(_pipeline: PipelineHandle, state: SharedState) -> Action }) } +/// Handler pour l'action UPnP "GetMute" - retourne l'état mute pub fn get_mute_handler(state: SharedState) -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { let state = state.clone(); @@ -297,12 +301,12 @@ pub fn get_mute_handler(state: SharedState) -> ActionHandler { // ─── ConnectionManager Handlers ───────────────────────────────────────────── +/// Handler pour l'action UPnP "GetProtocolInfo" - retourne les protocoles supportés pub fn get_protocol_info_handler() -> ActionHandler { Arc::new(move |data: ActionData| -> ActionFuture { Box::pin(async move { let mut data = data; set!(&mut data, "Source", String::new()); - // Serveur-side streaming : on produit du FLAC uniquement set!( &mut data, "Sink", diff --git a/pmowebrenderer/src/registry.rs b/pmowebrenderer/src/registry.rs index 3c2c8466..7d10f650 100644 --- a/pmowebrenderer/src/registry.rs +++ b/pmowebrenderer/src/registry.rs @@ -16,7 +16,7 @@ use std::time::SystemTime; use pmoupnp::devices::DeviceInstance; use crate::error::WebRendererError; -use crate::pipeline::{InstancePipeline, PipelineHandle}; +use crate::pipeline::{InstancePipeline, PipelineControl, PipelineHandle}; use crate::renderer::WebRendererFactory; use crate::state::{RendererState, SharedState}; @@ -273,41 +273,36 @@ impl RendererRegistry { } } - /// 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()); + /// Consume and send a command to the pipeline + async fn send_pipeline_command(&self, instance_id: &str, cmd: PipelineControl) { + let pipeline = self.get_pipeline(instance_id); 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"); + pipeline.send(cmd).await; + } else { + tracing::error!(instance_id = %instance_id, "Instance not found for pipeline command"); } } + + fn get_pipeline(&self, instance_id: &str) -> Option { + self.instances.read().get(instance_id).map(|i| i.pipeline.clone()) + } + + /// Charge une URI dans le pipeline et lance la lecture + pub async fn load_uri(&self, instance_id: &str, uri: String) { + self.send_pipeline_command(instance_id, PipelineControl::LoadUri(uri.clone())).await; + self.send_pipeline_command(instance_id, PipelineControl::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!"); - } + self.send_pipeline_command(instance_id, PipelineControl::Play).await; } /// 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; - } + self.send_pipeline_command(instance_id, PipelineControl::Pause).await; } /// Check if the instance has a current URI loaded diff --git a/pmowebrenderer/src/renderer.rs b/pmowebrenderer/src/renderer.rs index ad0b77f5..e0be39d0 100644 --- a/pmowebrenderer/src/renderer.rs +++ b/pmowebrenderer/src/renderer.rs @@ -3,11 +3,11 @@ //! Construit des Device/Service models UPnP dynamiques avec des action handlers //! qui relaient les commandes SOAP vers le navigateur via WebSocket. -use std::sync::Arc; -use thiserror::Error; use pmoupnp::actions::{Action, Argument}; use pmoupnp::devices::Device; use pmoupnp::services::Service; +use std::sync::Arc; +use thiserror::Error; use crate::handlers; use crate::pipeline::PipelineHandle; @@ -46,6 +46,37 @@ pub enum FactoryError { VariableError(String), } +macro_rules! add_action_arg { + ($action:ident, $name:expr, $var:ident, $direction:ident) => {{ + $action + .add_argument(Arc::new(Argument::new_$direction( + $name.to_string(), + Arc::clone(&$var), + ))) + .map_err(|e| FactoryError::ActionError(e.to_string())) + }}; + ($action:ident, $name:expr, $var:ident, in) => { + add_action_arg!($action, $name, $var, in) + }; + ($action:ident, $name:expr, $var:ident, out) => { + add_action_arg!($action, $name, $var, out) + }; +} + +macro_rules! add_action { + ($svc:ident, $action:ident) => {{ + $svc.add_action(Arc::new($action)) + .map_err(|e| FactoryError::ActionError(e.to_string())) + }}; +} + +macro_rules! add_var { + ($svc:ident, $var:ident) => {{ + $svc.add_variable(Arc::clone(&$var)) + .map_err(|e| FactoryError::VariableError(e.to_string())) + }}; +} + /// Extrait un nom de navigateur court depuis un User-Agent complet. fn extract_browser_name(ua: &str) -> &str { if ua.contains("Edg/") || ua.contains("EdgA/") { @@ -261,7 +292,10 @@ impl WebRendererFactory { Arc::clone(&AVTRANSPORTNEXTURIMETADATA), ))) .map_err(|e| FactoryError::ActionError(format!("{:?}", e)))?; - set_next_uri.set_handler(handlers::set_next_uri_handler(pipeline.clone(), state.clone())); + set_next_uri.set_handler(handlers::set_next_uri_handler( + pipeline.clone(), + state.clone(), + )); add_action(&mut svc, Arc::new(set_next_uri))?; // GetPositionInfo @@ -453,7 +487,10 @@ impl WebRendererFactory { Arc::clone(&VOLUME), ))) .map_err(|e| FactoryError::ActionError(format!("{:?}", e)))?; - set_vol.set_handler(handlers::set_volume_handler(pipeline.clone(), state.clone())); + set_vol.set_handler(handlers::set_volume_handler( + pipeline.clone(), + state.clone(), + )); svc.add_action(Arc::new(set_vol)) .map_err(|e| FactoryError::ActionError(format!("{:?}", e)))?;