refactor(handlers): clean up UPnP action handlers and pipeline command logic

- Add documentation comments to all AVTransport, RenderingControl & ConnectionManager handlers
- Simplify play_handler: remove redundant state writes and clarify flow with comments  
- Deduplicate pipeline command logic in registry.rs by introducing send_pipeline_command helper
- Replace direct PlayerCommand usage with unified PipelineControl enum in registry and handlers  
- Fix handler signatures: clone pipeline once before Arc closure (next/previous)—avoid redundant clones
- Add helper macros for UPnP service factory to reduce boilerplate (add_action, add_var)
- Minor formatting fixes: sort imports and align handler assignments
This commit is contained in:
2026-04-05 11:35:10 +02:00
parent 546e8a782f
commit d5b1ed5635
3 changed files with 80 additions and 44 deletions

View File

@@ -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",

View File

@@ -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<PipelineHandle> {
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

View File

@@ -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)))?;